Repository navigation
feat(PayPalCheckoutRequest): source editBillingAgreementJwt from client token for PayPal view/Edit FI flow - #1710
Conversation
…tToken instead of raw field editBillingAgreement is now a boolean opt-in on PayPalCheckoutRequest, and the payment method ID JWT is decoded by Core from the client token (Authorization.paymentMethodIdJwt) rather than passed in as a raw string by the merchant.
…ernal write access editBillingAgreement was a public constructor property, letting any caller opt into the View/Edit FI flow. It's now internally settable only, via a dedicated PayPalClient.createPaymentAuthRequestForEditFi entry point
|
Hi! I noticed some of the steps from our Inner Sourcing process (there at the bottom of the comment template) haven't been completed yet. Pleas take a look and let us know if you have any questions |
|
/inner source |
| * @param callback [PayPalPaymentAuthCallback] | ||
| */ | ||
| @OptIn(ExperimentalBetaApi::class) | ||
| fun createPaymentAuthRequestForEditFi( |
There was a problem hiding this comment.
The PR says this flow isn't exposed publicly. What stops a merchant from calling this method today?
There was a problem hiding this comment.
Nothing originally , it was public. Now @RestrictTo(LIBRARY_GROUP) on createPaymentAuthRequestForEditFi + internal set/@get:RestrictTo on editBillingAgreement, so only internal SDK modules can invoke it (Lint-enforced).
| * The payment method ID JWT extracted from the client token, if present. | ||
| * @suppress | ||
| */ | ||
| open val paymentMethodIdJwt: String? get() = null |
There was a problem hiding this comment.
paymentMethodIdJwt lives on a class annotated @RestrictTo(LIBRARY_GROUP) and @suppress, so it's library-group-only and hidden from docs/merchants.
| callback: PayPalPaymentAuthCallback, | ||
| ) { | ||
| payPalCheckoutRequest.editBillingAgreement = true | ||
| createPaymentAuthRequest(context, payPalCheckoutRequest, callback) |
There was a problem hiding this comment.
What happens if the client token has no JWT, or the merchant uses a tokenization key? What does the user experience after tapping Edit?
There was a problem hiding this comment.
-
No JWT in client token: enters the ClientToken branch but
paymentMethodIdJwt?.let{}is null, soedit_billing_agreement_jwtis omitted — proceeds as a normal checkout, no crash. -
Tokenization key: authorization is ClientToken is false, so the edit block is skipped entirely . JWT never sent, proceeds as a normal checkout.
There was a problem hiding this comment.
Right, and that's the concern: the user tapped Edit and silently gets a normal checkout, possibly creating a new billing agreement. Should the Edit entry point proceed at all without a JWT, or return a Failure via the callback, like the PayPal-disabled case? Either way, let's have a test that pins the behavior.
There was a problem hiding this comment.
If the merchant passes a client token that doesn't carry the JWT, the guarding happens upstream in PayPalSavedPaymentMethodClient:
- If the authorization is not a client token, the flow fails with an
InvalidAuthorizationexception. - If the client token is valid but lacks the payment method ID JWT, the flow fails with a
MissingPaymentMethodIdJwtexception.
In both cases the View/Edit Funding Instrument call fails, and the component falls back state - rendering only the brand logo.
iOS handling ref : https://github.com/braintree/braintree_ios/pull/1850/changes#diff-9a18a6836dbd89b5d427d4d00026297eef69975645c4a164a6c0c49c5022d03eR55
| payPalCheckoutRequest: PayPalCheckoutRequest, | ||
| callback: PayPalPaymentAuthCallback, | ||
| ) { | ||
| payPalCheckoutRequest.editBillingAgreement = true |
There was a problem hiding this comment.
What happens if the caller reuses this same request for a normal checkout afterwards?
There was a problem hiding this comment.
editBillingAgreement is reset to false right after emitting the JWT, so reusing the same request won't resend edit_billing_agreement_jwt
There was a problem hiding this comment.
Reset only runs in the ClientToken path. What about tokenization-key calls, or early failures like PayPal disabled?
There was a problem hiding this comment.
Fixed , reset will happen both ClientToken and tokenizationKey paths.
rvmondeti-svg
left a comment
There was a problem hiding this comment.
Good test coverage. Left a few questions on API exposure, the missing-JWT path, and request state; Also please link the gateway contract for the JWT key name, confirm parity with iOS #1844, and complete the checklist.
| */ | ||
| @IgnoredOnParcel | ||
| @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) | ||
| var editBillingAgreement: Boolean? = null |
There was a problem hiding this comment.
What is the benefit of having this nullable?
There was a problem hiding this comment.
None , Changed to a non-null Boolean = false, since there's no tri-state need - null and false both meant "not opted in".
…pt-in - Add @RestrictTo(LIBRARY_GROUP) to createPaymentAuthRequestForEditFi and editBillingAgreement getter so merchants cannot invoke the edit-FI flow - Reset editBillingAgreement after emitting the JWT so reusing the request for a normal checkout does not resend edit_billing_agreement_jwt - Add regression test for request reuse
a04a05b to
0e13381
Compare
|
Heads up: PayPalClientUnitTest still has assertNull(editBillingAgreement) |
Read-and-clear editBillingAgreement at the top of createRequestBody so the one-shot opt-in is reset regardless of auth type (including tokenization-key calls), preventing a later ClientToken reuse from resending the JWT. Add a regression test for the tokenization-key reuse case.
|
LGTM |
Update stale assertNull to assertFalse now that editBillingAgreement is a non-null Boolean defaulting to false.
Added |
|
Approved looks good |
|
/ready |
|
Approving. Entry point is library-internal, opt-in resets on every request-build path with tests, and the missing-JWT guard upstream matches iOS. Non-blocking: KDoc on editBillingAgreement still says "Defaults to null (opted out)". It's now a non-null Boolean, so please update it to "Defaults to false (opted out)". |
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
updated the KDoc now says "Defaults to false (opted out)" to match the non-null Boolean type. |
|
Looks like there are some lint errors here. Could you please address those? |
| payPalCheckoutRequest: PayPalCheckoutRequest, | ||
| callback: PayPalPaymentAuthCallback, | ||
| ) { | ||
| payPalCheckoutRequest.editBillingAgreement = true |
There was a problem hiding this comment.
What happens to this value if something throws in between when this is set to true and then set back to false in createRequestBody? It seems like there are several situations where this could get stuck as true
| bearer = authorizationFingerprint | ||
| customerId = parseCustomerId(authorizationFingerprint) | ||
| paymentMethodIdJwt = jsonObject.takeIf { it.has(PAYMENT_METHOD_ID_JWT_KEY) } | ||
| ?.getString(PAYMENT_METHOD_ID_JWT_KEY) |
There was a problem hiding this comment.
This will throw a JSONException which is caught and thrown as "Client token was invalid" if the key is present but the value is null. Could there be a situation where that could happen? If so, I don't know if we want to fail init for an optional field being null
| * The payment method ID JWT extracted from the client token, if present. | ||
| * @suppress | ||
| */ | ||
| open val paymentMethodIdJwt: String? get() = null |
There was a problem hiding this comment.
Why is this needed in this class? Isn't this only relevant for client tokens?
| */ | ||
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) | ||
| @OptIn(ExperimentalBetaApi::class) | ||
| fun createPaymentAuthRequestForEditFi( |
There was a problem hiding this comment.
We have moved to a suspend primary with callback wrapper model. Please fit this functionality into that. Additionally, if this is not exposed publicly, how is this intended to be used? Will those changes be coming in a different PR?
|
It also appears these changes have also broken the local payments tests. Please take a look at those |
|
Non-blocking: LLD corrections Reviewing against the Edit FI LLD, this PR's changes don't match the doc in a few places:
Separately, for the follow-up PR: the §7.3 Android |
Source the
editBillingAgreementJwtfor the PayPal Edit FI flow from the client token and thread it intocreate_payment_resource. The JWT is never merchant-supplied, and the flow is enabled only through a dedicated internal entry point rather than exposed on the public API.Summary of changes
paymentMethodIdJwttoAuthorizationas an open property defaulting tonull;ClientTokenoverrides it and parses the value from thepaymentMethodIdJwtkey in the client token JSON — otherAuthorizationsubtypes (e.g. tokenization key) keep the defaultnulleditBillingAgreementflag onPayPalCheckoutRequest— a non-nullBooleandefaulting tofalse, not exposed on either public constructor, withinternal setand a@get:RestrictTo(LIBRARY_GROUP)getter so it is neither settable nor readable by merchantsPayPalClient.createPaymentAuthRequestForEditFi(context, payPalCheckoutRequest, callback), annotated@RestrictTo(LIBRARY_GROUP), as the sole entry point that setseditBillingAgreement = trueand delegates tocreatePaymentAuthRequest, so the JWT is included in thecreate_payment_resourcecall only when the SDK is initialized with a client token carrying iteditBillingAgreementis reset tofalse, so reusing the same request instance for a subsequent normal checkout does not resendedit_billing_agreement_jwtPayPalRequest/PayPalVaultRequestare unchanged — the JWT and flag are checkout-onlyClientTokenparsing with/without the claim, and request reuse after an edit (JWT omitted on the second call)AI Usage
Which AI Agent Was Used?
Estimated AI Code Contribution
Checklist
Authors
Inner Source Process
Internal to PayPal contributors should fill out this section. All others can delete.
PR should follow these steps before codeowners review will begin:
/inner sourceon this PR — this will automatically add theinner sourceandtech lead review requiredlabels. Open the PR in a draft state./readyon this PR — this will automatically remove thetech lead review requiredlabel. Move the PR to ready to review.Inner Source Checklist