-
Notifications
You must be signed in to change notification settings - Fork 119
Feat: V6 Context Provider #688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat: V6 Context Provider #688
Conversation
* initial package.json and rollup updates, changes tsconfig to bundler and adds v6 specific tsconfig * upgrade react-paypal-js typescript package to v5.3.3 * update tsconfig for v6 namespace * chore: add changeset * align rollup version and update playwright yml * change playwright.yml download deps command * update actions checkout and setup-node to v4 * update .nvmrc * try different method for installing dependencies * fix package dependency issues related to rollup and terser * revert change to download deps step * update package-lock * test fix for rollup option deps bug * update build script * test adding cache to setup node action, update install command, and remove rollup force install in github workflows * re-add force rollup install in github workflows * run npm install to update package-lock with new package versions from typescript upgrade * revert changes to download deps and setup node steps in github workflows * install with update to package.json * leverage fresh package-lock and remove all artifactory package resolution paths * fix check annotations warnings * add new line at the end of gitignore
🦋 Changeset detectedLatest commit: 1f7404c The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
packages/react-paypal-js/src/v6/components/PayPalInstanceProvider.tsx
Outdated
Show resolved
Hide resolved
scriptOptions: LoadCoreSdkScriptOptions; | ||
} | ||
|
||
export const PayPalInstanceProvider: React.FC<PayPalInstanceProviderProps> = ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a fit of spontaneity I used "instance" as a key term in the names for the provider and related files, context state, etc.
I am not attached to this and am willing to make any changes. Originally I was calling this v6PayPalScriptProvider
but felt that the provider was really providing the SDK instance. Any thoughts here would be appreciated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this name makes sense, but what about adding sdk
to make it clear what PayPal thing is being provided, i.e. PayPalSdkInstanceProvider
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that @kand I'll update and see what other folks have to say.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a v6-specific utils file. Only the folders/files within the src/v6/
path get built for V6 and importing utils from the V5 utils file was causing some issues. There is probably a workaround but this was the most straightforward solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
scriptOptions: LoadCoreSdkScriptOptions; | ||
} | ||
|
||
export const PayPalInstanceProvider: React.FC<PayPalInstanceProviderProps> = ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this name makes sense, but what about adding sdk
to make it clear what PayPal thing is being provided, i.e. PayPalSdkInstanceProvider
?
packages/paypal-js/src/v6/index.ts
Outdated
validateArguments(options); | ||
|
||
// SSR safeguard | ||
if (typeof document === "undefined") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to me, but is this the standard way to check for SSR? I don't have much SSR experience so I'm curious.
Also, what's the difference between this check and isServer
checking if typeof window === "undefined"
? Would it make sense to use the same strategy in both places?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shoot I meant to replace this with isServer
.
I want to standardize on one isServer
check, which currently is typeof window === "undefined"
. From looking at a few different packages (stripe, square, adyen-web) there are a few ways to do this, and typeof window === "undefined"
is just one of those.
value: INSTANCE_LOADING_STATE.PENDING, | ||
}); | ||
} | ||
}, []); // Run once on mount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Purposefully using an empty array here in order to run this effect only on mount.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like github / react is unhappy that state.loadingStatus
is missing from the dependency array. I think we'll need it here because state
is technically state that should cause this useEffect
to update. What about putting a check at the top of the effect with a ref
that checks if it's been run already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, going to have to add a useRef
to track this. At first I thought that since loadingStatus !== Initial
after the first mount that a useRef
wasn't necessary since the condition will never be fulfilled.
Massive but here...but it is safer and more explicit to leverage useRef
. Let me push a change
state.scriptOptions, | ||
]); | ||
|
||
// Separate effect for eligibility - runs after instance is created |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently going with calling eligibility for all use cases. Originally we'd discussed passing the eligibility response to the provider and then conditionally calling eligibility. I'd like to update this portion of the provider when we implement the eligibility hook.
packages/paypal-js/src/utils.ts
Outdated
return newScript; | ||
} | ||
|
||
export const isServer = typeof window === "undefined"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two minor things:
- should we make this a function so its callable in different contexts?
- Can we also check
document
? When doing server-side rendering, I think some environments may setwindow
so its safer to checkdocument
. Might as well check for both being defined.
export const isServer = typeof window === "undefined"; | |
export function isServer() { | |
return typeof window === "undefined" && typeof document === "undefined"; | |
} |
d41303e
to
a142270
Compare
…l/paypal-js into feature/DTPPCPSDK-3479-provider
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple small things, but looks great otherwise!
value: INSTANCE_LOADING_STATE.PENDING, | ||
}); | ||
} | ||
}, []); // Run once on mount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like github / react is unhappy that state.loadingStatus
is missing from the dependency array. I think we'll need it here because state
is technically state that should cause this useEffect
to update. What about putting a check at the top of the effect with a ref
that checks if it's been run already?
if (isServer()) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the note on line 101:
SDK loading effect - only runs on client (useEffect doesn't run during SSR)
Do you need this isServer
check here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job @EvanReinstein! Left a few comments
* {children} | ||
* </PayPalSdkInstanceProvider> | ||
*/ | ||
export const PayPalSdkInstanceProvider: React.FC< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would actually vote name this more generic. Something like <PayPalProvider />
or <PayPalCheckoutProvider />
. I think that the word SdkInstance
doesn't hold a lot of meaning for the merchant since the only consumers of this provider will be our hooks/components. Stripe has something similar I believe:
<CheckoutProvider stripe={stripePromise} options={{clientSecret: promise}}>
<CheckoutForm />
</CheckoutProvider>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Point taken!
value: INSTANCE_LOADING_STATE.PENDING, | ||
}); | ||
} | ||
}, []); // Run once on mount |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
* | ||
* @example | ||
* <PayPalSdkInstanceProvider | ||
* createInstanceOptions={{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once again I don't think createInstanceOptions
will ring a bell to anyone who hasn't used the v6 SDK before. I would vote for a more abstracted flattened provider like this one:
<PayPalCheckoutProvider
components: ["paypal-payments"],
clientToken: token,
environment: "sandbox"
>
{children}
</PayPalSdkInstanceProvider>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I considered this and tbh it would be helpful in terms of inline objects passed as props causing rerenders, which the provider then needs to manage.
If other folks don't have strong opinions against this I'll go down this path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hamzanasir Alrighty, I updated the naming convention across the board. Lmk what you think
packages/react-paypal-js/src/v6/components/PayPalSdkInstanceProvider.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work @EvanReinstein!
packages/paypal-js/src/v6/index.ts
Outdated
url: url.toString(), | ||
onSuccess: () => { | ||
if (!window.paypal) { | ||
if (isServerEnv || !window.paypal) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we even get to this line of code if isServerEnv
is true
? I would assume the above SSR safeguard does an early return
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh shoot, correct. Let me push that change
* You can pass options directly without manual memoization. | ||
* | ||
* @example | ||
* <PayPalSdkInstanceProvider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* <PayPalSdkInstanceProvider | |
* <PayPalProvider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch!
memoizedCreateOptions, | ||
memoizedScriptOptions, | ||
state.createInstanceOptions, | ||
state.scriptOptions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little confused by this logic. Why are we listening to both options for changes?
return; | ||
} | ||
|
||
let isSubscribed = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain what this does exactly? To my knowledge once the hook calls the cleanup function this useEffect will be lost so it shouldn't run anymore. Do we need this isSubscribed
logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an attempt to prevent loadSdk
from continuing to run with an update happening in the background. It's async, and its steps are also async. It probably would benefit from a check before await loadCoreSdkScript
though, since we're already in deferred operation at that point.
I think if you wanted to clean it up, you could use an AbortController
instead. Haven't looked at what APIs are used in loadCoreSdkScript
to see if any of them actually take an AbortSignal
, but you could write your own, if this is going to be a common pattern.
|
||
// SSR safeguard | ||
if (isServerEnv) { | ||
return Promise.resolve(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want a noop
here or a warning? Presumably this would only be called in a useEffect
, which won't be run until the client. Seems like this being called on the server is probably more indicative of an integration problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point on adding a warning!
return; | ||
} | ||
|
||
let isSubscribed = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's an attempt to prevent loadSdk
from continuing to run with an update happening in the background. It's async, and its steps are also async. It probably would benefit from a check before await loadCoreSdkScript
though, since we're already in deferred operation at that point.
I think if you wanted to clean it up, you could use an AbortController
instead. Haven't looked at what APIs are used in loadCoreSdkScript
to see if any of them actually take an AbortSignal
, but you could write your own, if this is going to be a common pattern.
const loadEligibility = async () => { | ||
try { | ||
const eligiblePaymentMethods = | ||
await state.sdkInstance?.findEligibleMethods({}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does there need to be a mechanism for the merchant to override the necessity of this call? Can the merchant provide their own server-side eligibility determination?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that work is forthcoming 👍 and this is more of a placeholder.
…l/paypal-js into feature/DTPPCPSDK-3479-provider
This PR adds a Provider wrapper that can be used to access SDK instance context. This is an initial set of basic changes that leverage
paypal-js
'sloadCoreSdkScript
and adds SSR related conditions to prevent/guard against running client-side code.There is a corresponding PR here that implements the Provider and renders a PayPal and Venmo button. In order to test the Provider make sure to have cloned both repos and leverage
npm link
to create a symlink between the changes on this branch inreact-paypal-js
and the pre-built React one time payment example.See the PR linked above for more specific step-by-step instructions.