-
Notifications
You must be signed in to change notification settings - Fork 19
AuthTab Support Feature #124
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
Conversation
* remove duplicate intent-filter * - Update androidx.browser to 1.9.0 - Update AGP version to 8.9.1 -- requires update to gradle wrapper 8.11.1 - Update compile and target sdk to 36
* remove duplicate intent-filter * - Update androidx.browser to 1.9.0 - Update AGP version to 8.9.1 -- requires update to gradle wrapper 8.11.1 - Update compile and target sdk to 36 * first iteration on AuthTab Setup: added AuthTabInternalClient, removed ChromeCustomTabsInternalClient, modified ComposeActivity & DemoActivitySingleTop to accept the above changes, added toast visuals to see whether or not the AuthTab is supported * Rename .java to .kt * second iteration on AuthTab Setup: rewrote AuthTabInternalClient to Kotlin and added a ClearTop Flag, wrote new AuthTabInternalClientUnitTest, removed ChromeCustomTabsInternalClientUnitTest, modified BrowserSwitchClientUnitTest to accept the AuthTab changes * third iteration on AuthTab Setup: removed CustomTabIntentBuilder, removed tests associated with CustomTabs from AuthTabInternalClientUnitTest,, modified BrowserSwitchClientUnitTest to accept the AuthTab launchURL signature * EOD commit: -Testing the authtab flow on older devices without intents * Revert "EOD commit:" This reverts commit e27cfa1. * Revert "third iteration on AuthTab Setup:" This reverts commit 9803e43. Branching still needed to provide support for older devices. * Completed implementation of AuthTab Support: - added AuthTabInternalClient, - removed ChromeCustomTabsInternalClient, - modified ComposeActivity & DemoActivitySingleTop to accept the above changes, - added toast visuals to see whether or not the AuthTab is supported * Address PR suggestions * Address PR suggestions --------- Co-authored-by: saperi <[email protected]>
* remove duplicate intent-filter * - Update androidx.browser to 1.9.0 - Update AGP version to 8.9.1 -- requires update to gradle wrapper 8.11.1 - Update compile and target sdk to 36 * first iteration on AuthTab Setup: added AuthTabInternalClient, removed ChromeCustomTabsInternalClient, modified ComposeActivity & DemoActivitySingleTop to accept the above changes, added toast visuals to see whether or not the AuthTab is supported * Rename .java to .kt * second iteration on AuthTab Setup: rewrote AuthTabInternalClient to Kotlin and added a ClearTop Flag, wrote new AuthTabInternalClientUnitTest, removed ChromeCustomTabsInternalClientUnitTest, modified BrowserSwitchClientUnitTest to accept the AuthTab changes * third iteration on AuthTab Setup: removed CustomTabIntentBuilder, removed tests associated with CustomTabs from AuthTabInternalClientUnitTest,, modified BrowserSwitchClientUnitTest to accept the AuthTab launchURL signature * EOD commit: -Testing the authtab flow on older devices without intents * Revert "EOD commit:" This reverts commit e27cfa1. * Revert "third iteration on AuthTab Setup:" This reverts commit 9803e43. Branching still needed to provide support for older devices. * Completed implementation of AuthTab Support: - added AuthTabInternalClient, - removed ChromeCustomTabsInternalClient, - modified ComposeActivity & DemoActivitySingleTop to accept the above changes, - added toast visuals to see whether or not the AuthTab is supported * Address PR suggestions * Address PR suggestions * Add support for parametrized constructor to give a merchant a choice to use AuthTab or not * -added the ability to store a callback internally without exposing it -modified BrowserSwitchClient's constructor to only accept Activity as a parameter -rerouted the flow to always call completeRequest and check for the callback result withing the function -added a call to completeRequest in DemoActivitySingleTop.java onResume -added the tests to support the new flow * -addressed PR suggestions -removed toast pop-ups * updated javadoc for parametrized BrowserSwitchClient constructor and completeRequest() function --------- Co-authored-by: saperi <[email protected]>
# Conflicts: # CHANGELOG.md # browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java # build.gradle
| private final BrowserSwitchInspector browserSwitchInspector; | ||
| private final AuthTabInternalClient authTabInternalClient; | ||
| private ActivityResultLauncher<Intent> authTabLauncher; | ||
| private BrowserSwitchRequest pendingAuthTabRequest; |
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.
This is the only part I can see that may not survive a process kill. I wish there was an easy way to access the pending request state that we ask the merchant to hold on to.
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.
Yep. During development, there didn't seem to be a possible way to handle process kill. Since an ActivityResultLauncher<Intent> is used by the Auth Tab API, in the event of a process kill, the merchant app would pass in a new instance of ActivityResultLauncher<Intent> which would not get invoked properly when the merchant app Fragment/Activity is recreated.
But we're open to ideas if there's a good way to handle process kill!
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 like we encode the request and send it to the merchant: return new BrowserSwitchStartResult.Started(request.toBase64EncodedJSON());.
In case of a process kill, maybe we can have a constructor that takes the pending request?! In that case, we'd expect them to call completeRequest() next? Would that work in your opinion?
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 it could work. On PPCP we have balancing call pattern for start<FEATURE>() and finish<FEATURE>() to allow merchants to pass pending state back into the SDK. The idea for having pending state be a Base64 encoded string is to:
- discourage merchants from tampering with the string
- strings are supported by virtually all Android persistence mechanisms (
Parcelable, etc.) which allows merchants to use their preferred persistence mechanism out-of-the-box for browser switch pending state
It's worth getting creative to come up with a complete solution. Ideally it's good for us to provide a way for merchants to recover from the rare event of a process kill.
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.
Hi all, Sai's suggestion seems to work, I pushed the change and it is visible now at 742b29c
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.
abstracted process kill recovery into a method restorePendingRequest at 387e96c
| BrowserSwitchFinalResult finalResult; | ||
| switch (result.resultCode) { | ||
| case AuthTabIntent.RESULT_OK: | ||
| if (result.resultUri != null && pendingAuthTabRequest != 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.
On a cold start, this value will be null. A false .NoResult may occur here when the host application is recovering from a process kill.
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 don't believe the ActivityResultCallback<AuthResult> will get invoked when the host app is recovering from a process kill since the authTabLauncher is not used until the host app/sdk calls start().
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 it might. It's definitely worth trying with developer settings to see if the callback is invoked. I'm looking at the ActivityResult API docs that has this somewhat cryptic message:
It's a tough use case to handle, because the ActivityResult API also places the responsibility on the host application for state restoration, and since we wrap their API that responsibility gets forwarded to the merchant.
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 tried testing with Don't keep activities and the callback itself is called, but as you mentioned, pendingAuthTabRequest is 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.
I tested the newest change with Don't keep activities, and it seems to work as expected 742b29c
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.
abstracted process kill recovery into a method restorePendingRequest at 387e96c
demo/src/main/java/com/braintreepayments/api/browserswitch/demo/MainActivity.java
Outdated
Show resolved
Hide resolved
browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java
Show resolved
Hide resolved
browser-switch/src/main/java/com/braintreepayments/api/BrowserSwitchClient.java
Outdated
Show resolved
Hide resolved
| * @throws BrowserSwitchException if the pending request cannot be parsed | ||
| */ | ||
| public void restorePendingRequest(@NonNull String pendingRequest) throws BrowserSwitchException { | ||
| this.pendingAuthTabRequest = BrowserSwitchRequest.fromBase64EncodedJSON(pendingRequest); |
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.
did we want to validate that the pendingRequest sent is non-null?
@nonnull annotation on java is just for compiler, it doesn't give any guarantees, AFAIK.
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.
hey, that's a great suggestion, I added it to befcdd1
saralvasquez
left a comment
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.
This looks great! 🎉 I had a few non-blocking comments and a question but this looks ready to go to me!
CHANGELOG.md
Outdated
| * Upgrade `compileSdkVersion` and `targetSdkVersion` to API 36 | ||
| * Replace `ChromeCustomTabsInternalClient.java` with `AuthTabInternalClient.kt` | ||
| * Add parameterized constructor `BrowserSwitchClient(ActivityResultCaller)` to initialize AuthTab support | ||
| * Maintain default constructor `BrowserSwitchClient()` (without AuthTab support) for backward compatibility |
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.
Tiny tiny nit: not sure if the (without AuthTab support) is necessary and it make the entry a bit long
| LaunchType.ACTIVITY_CLEAR_TOP -> { | ||
| customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) | ||
| } | ||
| 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.
I think I'm missing some kotlin knowledge. Why is the null case necessary?
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.
See this. It is likely that the compiler complained about a non-exhaustive when statement.
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, this was exactly the case, i had to add it for a compiler. I suppose there must be a more elegant way to deal with it though...
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.
Ah makes so much sense! Thanks for the resource Sai!
|
|
||
| } catch (ActivityNotFoundException e) { | ||
| this.pendingAuthTabRequest = null; | ||
| return new BrowserSwitchStartResult.Failure( |
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.
Nit: Nested function calls and object instantiation is a bit hard to read. Especially when it's also a return. It might be easier to read if the exception creation was broken out onto a separate line. The same goes for line 214
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.
this was another pattern that I took from legacy code, but i agree that readability can be improved!
| import android.os.Bundle; | ||
| import android.view.View; | ||
| import android.widget.Button; | ||
|
|
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.
Nit: Cleanup is nice, but no-ops can make commit history for a file a bit cluttered. I would recommend leaving this file alone
|
|
||
| @Before | ||
| fun setUp() { | ||
| clearAllMocks() |
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.
This definitely feels like best practice! Love it. Out of curiosity, did you notice that this call was necessary to the tests passing?
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.
This was a part of the ChromeCustomTabClientUnitTest that was removed because the new AuthTabClient was introduced. Since it was used in the old test, I decided to add it to this one as well. However, the tests DO pass without that line, because we are not reusing the mocked objects, I opted in to keeping it because it is was in the old test.
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.
Ah makes sense! Thanks for the info!
| every { CustomTabsClient.getPackageName(context, null) } returns packageName | ||
| every { CustomTabsClient.isAuthTabSupported(context, packageName) } returns false | ||
|
|
||
| val client = AuthTabInternalClient(authTabBuilder, customTabsBuilder) |
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.
Nit: I think in most of the unit test classes we use sut for the variable name if it's the object we testing
| browserSwitchClient = new BrowserSwitchClient(); | ||
|
|
||
| browserSwitchClient = new BrowserSwitchClient(this); | ||
| // Check if there is a preserved pending request after the process kill |
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.
Nit: The code here seems pretty self explanatory so I'm not sure if the comments here add much
Thank you for your contribution to Braintree.
Summary of changes
Checklist
Authors