Skip to content
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

Google pay single result #842

Merged
merged 25 commits into from
Dec 18, 2023
Merged

Google pay single result #842

merged 25 commits into from
Dec 18, 2023

Conversation

sarahkoop
Copy link
Contributor

Summary of changes

  • Add single result object patter to all public Google Pay APIs to align with result handling pattern across payment modules

Checklist

  • Added a changelog entry

Authors

@sarahkoop sarahkoop requested a review from a team as a code owner December 5, 2023 21:21
* The Google Pay API is supported or not set up on this device, or there was an [error]
* determining readiness.
*/
class NotReadyToPay(val error: Exception?) : GooglePayReadinessResult()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class NotReadyToPay(val error: Exception?) : GooglePayReadinessResult()
class NotReadyToPay(val cause: Exception?) : GooglePayReadinessResult()

nit: There is an Exception constructor public Exception(Throwable cause). We could borrow the name cause to signal to merchants that the exception was produced by an underlying cause.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ The cause property also allows us to chain stack traces. It'll be super useful for GitHub inbounds if we leverage that constructor internally when creating BraintreeException instances.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated - do you think we should change to cause for all Failure results or just this one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think wherever it feels right. That may be a separate PR, but sometimes the OG stack trace can provide a lot of context to get to the root issue, especially with everything being asynchronous.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've created another ticket to look into where else in these result objects it would make sense to return throwable. For the GooglePayResult object in this PR, we only ever return Exception objects (stack trace included where possible), so I'm not sure there is a benefit to making it the more generic Throwable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep it as Exception yeah, Exception extends Throwable so we shouldn't have to change any types. There may not always be a cause too, and that's fine cause == null in that case.

If we do have an exception that signifies the root cause, then a merchant can inspect braintreeException?.cause to give us more info. Also, the toString() implementation for Exception prints out the combined stack trace of the exception chain.

new BraintreeException("Error with Config", ioExceptionCausedByConfigFetching) as an example would allow us to chain the exception returned to the merchant by the SDK to the original root cause exception, like in this example the user's WiFi could be disabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok actually I think the cause wording here is confusing - we aren't returning the reason why the user isn't ready to pay (we are just returning NotReadyToPay in those cases). We are only returning an error if there was an error determining if the user was ready to pay (in which case it's not really the cause of why they aren't ready to pay, it's that we can't determine if they are ready to pay or not). I'm going to put it back to error but I still have a ticket to investigate using cause/throwables

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also document this design decision to include cause where we think it's applicable, and some loose guidelines around what "applicable" means somewhere internally.

I can see one of us in the future taking a look at the code and thinking "now why did we do this here but that there?", or even an external dev wondering why we did it 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this back to error, which aligns with all other Failure results because I think it is more descriptive of what we are actually returning - so no additional design decision doc needed 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah cause is mostly useful for acquiring stack traces from merchants when possible.

Copy link
Contributor

@hollabaq86 hollabaq86 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work on some meaningful cleanup. Got a few Qs and things I'd like to confirm before we merge

if (paymentAuthRequest instanceof GooglePayPaymentAuthRequest.ReadyToLaunch) {
googlePayLauncher.launch(
((GooglePayPaymentAuthRequest.ReadyToLaunch) paymentAuthRequest).getRequestParams());
} else if (paymentAuthRequest instanceof GooglePayPaymentAuthRequest.Failure) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any other possible values for GooglePayPaymentAuthRequest? Curious if we need a fallback for scenarios we don't catch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the only options - the benefit of defining GooglePayPaymentAuthRequest as a sealed class is that it is exhaustive, and when we migrate this file to Kotlin, we can use the when switch statement without a default/fallback scenario.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if we need a fallback for scenarios we don't catch.

This is one thing that keeps me up at night lol. Adding a new case to an existing enum can totally break a merchant app. The tradeoff is reduced compiler enforcement in exchange for not breaking a merchant integration.

I do think it's worth calling out in our docs to encourage merchants to catch "UNKNOWN" results so that their integrations are forward compatible.

handleError(((GooglePayResult.Failure) googlePayResult).getError());
} else if (googlePayResult instanceof GooglePayResult.Success){
handleGooglePayActivityResult(((GooglePayResult.Success) googlePayResult).getNonce());
} else if (googlePayResult instanceof GooglePayResult.Cancel) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add an else block for other values, or are these the only options from google?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We define the GooglePayResult object as a sealed class so these are the only options.

"manifest, or did not have a theme of R.style.bt_transparent_activity"));
callback.onGooglePayPaymentAuthRequest(new GooglePayPaymentAuthRequest.Failure(new BraintreeException(
"GooglePayActivity was not found in the Android " +
"manifest, or did not have a theme of R.style.bt_transparent_activity")));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is oddly specific heh. No action, just commenting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... it is getting pretty verbose open to alternative callback naming! The previous pattern was always naming it onResult which is a little confusing when the result we are returning is actually a request object

callback.onResult(false, e);
paymentsClient.isReadyToPay(isReadyToPayRequest).addOnCompleteListener(task -> {
try {
Boolean isReady = task.getResult(ApiException.class);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is some android syntax nuance that I'm missing, but this currently reads like:

"isReady = is there an API Exception?"
if yes, provide readiness result (which can be a failure?) <-- can I get a confirmation of this expectation?
otherwise not ready

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is saying isReady = the result of the task to fetch readiness status (which will be true or false if successfully fetched), but if there is an error executing this task, then throw an APIException.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

randomtwocents: The Google Pay API is legit weird IMO. Merchant wins when we wrap! 🌯

* The Google Pay API is supported or not set up on this device, or there was an [error]
* determining readiness.
*/
class NotReadyToPay(val error: Exception?) : GooglePayReadinessResult()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also document this design decision to include cause where we think it's applicable, and some loose guidelines around what "applicable" means somewhere internally.

I can see one of us in the future taking a look at the code and thinking "now why did we do this here but that there?", or even an external dev wondering why we did it 😄

@sshropshire
Copy link
Contributor

Let's also document this design decision to include cause where we think it's applicable, and some loose guidelines around what "applicable" means somewhere internally.

It's mostly objective, the standard Exception type in Java takes a cause property in its constructor, and BraintreeException extends Exception. If the exception we're reporting to the merchant has a root cause e.g. CardinalException, BrowserSwitchException, then we can still provide our generic BraintreeException message to the merchant like 3D Secure verification failed., and then error.cause == CardinalException.

Also BraintreeException.printStackTrace() will include the OG cardinal exception as well, and we can take that to them to help diagnose the cause of an inbound.

v5_MIGRATION_GUIDE.md Outdated Show resolved Hide resolved
v5_MIGRATION_GUIDE.md Outdated Show resolved Hide resolved
@sarahkoop sarahkoop merged commit 54f7f5d into v5 Dec 18, 2023
2 checks passed
@sarahkoop sarahkoop deleted the google_pay_single_result branch December 18, 2023 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants