KEMBAR78
[link] Web auth fallback by lng-stripe · Pull Request #11571 · stripe/stripe-android · GitHub
Skip to content

Conversation

@lng-stripe
Copy link
Contributor

@lng-stripe lng-stripe commented Sep 12, 2025

Summary

Fall back to web auth when required. High-level summary of how it works:

  1. Lookup returns a web URL if the SDK does not support a required verification type.
  2. SDK opens the URL in a Chrome custom tab
  3. Upon successful auth, web will redirect to a link-popup://... URI that the SDK will intercept
  4. SDK will close the custom tab, refresh the consumer session, and proceed with the next step
  5. For LAI consent, we will always do natively for now. In the future, consent may happen in the web, which the SDK should be able to support. To handle that, we inspect the LAI status returned by the refresh response.

Motivation

It won't be SMS only forever.

Testing

  • Added tests
  • Modified tests
  • Manually verified

Screenshots

Recordings of various flows below

Screen.Recording.2025-09-17.at.12.51.33.PM-compressed.mov
Screen.Recording.2025-09-17.at.12.28.09.PM-compressed.mov
Screen.Recording.2025-09-17.at.12.23.21.PM-compressed.mov
Screen.Recording.2025-09-17.at.12.22.16.PM-compressed.mov

@github-actions
Copy link
Contributor

github-actions bot commented Sep 12, 2025

Diffuse output:

OLD: paymentsheet-example-release-master.apk (signature: V1, V2)
NEW: paymentsheet-example-release-pr.apk (signature: V1, V2)

          │            compressed             │           uncompressed            
          ├───────────┬───────────┬───────────┼───────────┬───────────┬───────────
 APK      │ old       │ new       │ diff      │ old       │ new       │ diff      
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────
      dex │   4.8 MiB │   4.8 MiB │ +15.5 KiB │  10.6 MiB │  10.7 MiB │ +32.6 KiB 
     arsc │   2.6 MiB │   2.6 MiB │       0 B │   2.6 MiB │   2.6 MiB │       0 B 
 manifest │   5.8 KiB │   5.8 KiB │       0 B │  30.4 KiB │  30.4 KiB │       0 B 
      res │ 927.6 KiB │ 927.6 KiB │       0 B │   1.5 MiB │   1.5 MiB │       0 B 
   native │   3.5 MiB │   3.5 MiB │       0 B │   8.5 MiB │   8.5 MiB │       0 B 
    asset │   1.6 MiB │   1.6 MiB │  +1.5 KiB │   1.6 MiB │   1.6 MiB │  +1.5 KiB 
    other │ 198.7 KiB │ 198.7 KiB │      -2 B │ 375.4 KiB │ 375.4 KiB │       0 B 
──────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────
    total │  13.6 MiB │  13.7 MiB │   +17 KiB │  25.2 MiB │  25.2 MiB │ +34.1 KiB 

         │         raw          │               unique               
         ├───────┬───────┬──────┼───────┬───────┬────────────────────
 DEX     │ old   │ new   │ diff │ old   │ new   │ diff               
─────────┼───────┼───────┼──────┼───────┼───────┼────────────────────
   files │     2 │     2 │    0 │       │       │                    
 strings │ 54236 │ 54709 │ +473 │ 49976 │ 50046 │  +70 (+194 -124)   
   types │ 19519 │ 19730 │ +211 │ 17422 │ 17460 │  +38 (+155 -117)   
 classes │ 14710 │ 14743 │  +33 │ 14710 │ 14743 │  +33 (+46 -13)     
 methods │ 75252 │ 75686 │ +434 │ 71940 │ 72056 │ +116 (+6262 -6146) 
  fields │ 49106 │ 49325 │ +219 │ 47852 │ 47947 │  +95 (+4600 -4505) 

 ARSC    │ old  │ new  │ diff 
─────────┼──────┼──────┼──────
 configs │  242 │  242 │  0   
 entries │ 6362 │ 6362 │  0
APK
      compressed       │     uncompressed      │                                           
───────────┬───────────┼───────────┬───────────┤                                           
 size      │ diff      │ size      │ diff      │ path                                      
───────────┼───────────┼───────────┼───────────┼───────────────────────────────────────────
 560.6 KiB │ +34.8 KiB │   1.3 MiB │ +74.1 KiB │ ∆ classes2.dex                            
   4.3 MiB │ -19.2 KiB │   9.4 MiB │ -41.5 KiB │ ∆ classes.dex                             
   9.5 KiB │  +1.5 KiB │   9.4 KiB │  +1.5 KiB │ ∆ assets/dexopt/baseline.prof             
   1.2 KiB │      +9 B │     1 KiB │      +9 B │ ∆ assets/dexopt/baseline.profm            
  51.7 KiB │      -3 B │ 122.4 KiB │       0 B │ ∆ META-INF/MANIFEST.MF                    
     272 B │      +2 B │     120 B │       0 B │ ∆ META-INF/version-control-info.textproto 
  55.2 KiB │      -2 B │ 122.4 KiB │       0 B │ ∆ META-INF/CERT.SF                        
   1.2 KiB │      +1 B │   1.2 KiB │       0 B │ ∆ META-INF/CERT.RSA                       
───────────┼───────────┼───────────┼───────────┼───────────────────────────────────────────
   4.9 MiB │   +17 KiB │  10.9 MiB │ +34.1 KiB │ (total)
DEX
STRINGS:

   old   │ new   │ diff            
  ───────┼───────┼─────────────────
   49976 │ 50046 │ +70 (+194 -124) 
  
  + , isProcessingWebAuth=
  + , linkAuthIntent=
  + , mobileFallbackWebviewParams=
  + , viewedWebviewOpenUrl=
  + , webviewOpenUrl=
  + Authenticated
  + ConsumerSessionRefresh(consumerSession=
  + LD7/w;
  + LD7/x;
  + LD7/y;
  + LD7/z;
  + LK1/s;
  + LLLLLLLLLL
  + LO7/A4;
  + LO7/B4;
  + LO7/C4;
  + LO7/D4;
  + LO7/E4;
  + LO7/t4;
  + LO7/u4;
  + LO7/v4;
  + LO7/w4;
  + LO7/x4;
  + LO7/y4;
  + LO7/z4;
  + LP7/g;
  + LP;
  + LQ7/j;
  + LU6/m;
  + LV5/e;
  + LY9/i;
  + LZLZL
  + La7/s;
  + Lcom/stripe/android/link/WebLinkAuthActivityContract;
  + Link: Force enable payment selection hint
  + Link: Force web auth
  + LinkAuthIntent(status=
  + Lio/sentry/android/replay/util/c;
  + Lj7/F1;
  + Lj7/G1;
  + Lj7/H1;
  + Lj7/I1;
  + Lj7/J1;
  + Lj7/K1;
  + Lj7/L1;
  + Lj7/M1;
  + Lj7/N1;
  + Lja/r;
  + Lk7/Q;
  + Lk7/S;
  + Lk7/T;
  + Lm9/R2;
  + Ls7/I;
  + Ls7/J;
  + Ls9/z;
  + Lw/n0;
  + Lw7/b;
  + Ly8/y0;
  + MobileFallbackWebviewParams(webViewRequirementType=
  + NeedsVerification(webviewOpenUrl=
  + Rejected
  + Unexpected LAI status when account is verified: 
  + VLLLLLLLZLLLL
  + VZZLZZLLZZLLZ
  + [LD7/z;
  + [LL8/Z0;
  + [LL8/b1;
  + [LO7/A0;
  + [LO7/A4;
  + [LO7/B4;
  + [LO7/C0;
  + [LO7/C4;
  + [LO7/D4;
  + [LO7/E4;
  + [LO7/F1;
  + [LO7/H3;
  + [LO7/I0;
  + [LO7/L2;
  + [LO7/N0;
  + [LO7/O1;
  + [LO7/Q1;
  + [LO7/S3;
  + [LO7/U1;
  + [LO7/V2;
  + [LO7/Y2;
  + [LO7/Y3;
  + [LO7/a1;
  + [LO7/d3;
  + [LO7/d4;
  + [LO7/f1;
  + [LO7/h1;
  + [LO7/h2;
  + [LO7/i3;
  + [LO7/i4;
  + [LO7/j4;
  + [LO7/l3;
  + [LO7/l4;
  + [LO7/n0;
  + [LO7/n1;
  + [LO7/n2;
  + [LO7/o0;
  + [LO7/p0;
  + [LO7/t4;
  + [LO7/u1;
  + [LO7/v0;
  + [LO7/w2;
  + [LO7/y4;
  + [LO7/z4;
  + [LP;
  + [LQ7/i;
  + [LR3/u;
  + [LR6/A2;
  + [LR6/E2;
  + [LR6/Q;
  + [LR6/U;
  + [LR6/X;
  + [LV5/e;
  + [LV7/c;
  + [LY5/E;
  + [LY5/I;
  + [LY5/g1;
  + [La7/r;
  + [La8/b;
  + [Lb9/M;
  + [Li8/v;
  + [Lj7/N;
  + [Lj7/O;
  + [Lj7/P;
  + [Lj7/Q;
  + [Lj7/X0;
  + [Lj7/a1;
  + [Lj7/b0;
  + [Lj7/g1;
  + [Lj7/h1;
  + [Lj7/i0;
  + [Lj7/j1;
  + [Lj7/q0;
  + [Lj7/r;
  + [Lj7/t;
  + [Lj7/u1;
  + [Lj7/u;
  + [Lj7/v;
  + [Lj7/z0;
  + [Lk8/w;
  + [Ll9/E;
  + [Ll9/b;
  + [Lm9/O0;
  + [Lm9/p2;
  + [Lm9/t2;
  + [Lm9/w0;
  + [Lp8/e;
  + [Lp8/g;
  + [Lt9/u;
  + [Lw/a0;
  + [Lw8/y;
  + [Lx8/C0;
  + [Lx8/E0;
  + [Lx8/F1;
  + [Lx8/J0;
  + [Lx8/L0;
  + [Lx8/M;
  + [Lx8/P1;
  + [Lx8/S0;
  + [Lx8/S1;
  + [Lx8/V;
  + [Lx8/X0;
  + [Lx8/b1;
  + [Lx8/c0;
  + [Lx8/o0;
  + [Lx8/r1;
  + [Lx8/v0;
  + [Lx8/v;
  + [Ly3/q;
  + com.stripe.android.model.ConsumerSessionRefresh
  + com.stripe.android.model.LinkAuthIntent
  + com.stripe.android.model.LinkAuthIntent.Status
  + com.stripe.android.model.MobileFallbackWebviewParams
  + com.stripe.android.model.MobileFallbackWebviewParams.WebviewRequirementType
  + consumers/sessions/refresh
  + handleWebActivityResult
  + handleWebActivityResult(Lcom/stripe/android/link/LinkActivityResult;)V
  + link.account_refresh.failure
  + link_auth_intent
  + mobile_fallback_webview_params
  + notrequired
  + onVerificationSucceeded(Lcom/stripe/android/model/ConsumerSessionRefresh;)V
  + supported_verification_types
  + verifyDuringSignUp
  + verifyDuringSignUp()V
  + webLinkAuthChannel
  + webViewRequirementType
  + webview_open_url
  + webview_requirement_type
  + ~~R8{"backend":"dex","compilation-mode":"release","has-checksums":false,"min-api":21,"pg-map-id":"fd79808","r8-mode":"full","version":"8.8.34"}
  
  - Force enable payment selection hint
  - LL8/e1;
  - LR3/w;
  - LV7/d;
  - LW8/l;
  - LY5/w1;
  - La8/d;
  - Lio/sentry/android/core/internal/util/p;
  - Lk8/D;
  - Lsa/o;
  - Lv4/l;
  - Lw8/E;
  - Lz6/r;
  - Lz6/s;
  - VLLLLLLLLLLLLLLLLLLI
  - VLLLLLZLLLL
  - VZZLZZLLZZLL
  - [LD7/v;
  - [LL8/a1;
  - [LL8/d1;
  - [LO7/B1;
  - [LO7/F0;
  - [LO7/G0;
  - [LO7/G2;
  - [LO7/I1;
  - [LO7/J1;
  - [LO7/O0;
  - [LO7/R1;
  - [LO7/S0;
  - [LO7/T3;
  - [LO7/V1;
  - [LO7/X1;
  - [LO7/X2;
  - [LO7/b4;
  - [LO7/c2;
  - [LO7/d1;
  - [LO7/g2;
  - [LO7/h3;
  - [LO7/k0;
  - [LO7/k1;
  - [LO7/k3;
  - [LO7/l0;
  - [LO7/l1;
  - [LO7/p3;
  - [LO7/p4;
  - [LO7/q1;
  - [LO7/q3;
  - [LO7/r0;
  - [LO7/s0;
  - [LO7/s1;
  - [LO7/t0;
  - [LO7/t1;
  - [LO7/t2;
  - [LO7/x3;
  - [LO7/y0;
  - [LO7/z2;
  - [LO;
  - [LQ7/h;
  - [LR3/w;
  - [LR6/D2;
  - [LR6/P;
  - [LR6/T;
  - [LR6/W;
  - [LR6/y2;
  - [LV5/a;
  - [LV7/d;
  - [LY5/G;
  - [LY5/J;
  - [LY5/j1;
  - [La7/o
...✂

@Parcelize
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Serializable
data class DisplayablePaymentDetails(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👀 Moved to its own file


@Suppress("CyclomaticComplexMethod", "ComplexCondition")
private suspend fun updateScreenState(withAnimationDelay: Boolean) {
private suspend fun updateScreenState(
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 gave this code a significant makeover in order to improve readability. It might still be hard to follow because of the inherent complexity, but hopefully it's better. Most of the existing behavior shouldn't have changed, which the tests cover.

Copy link
Collaborator

Choose a reason for hiding this comment

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

this is amazing, very needed.

}

@VisibleForTesting
internal fun getScreenStateForAuthorizationAfterRefresh(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👀

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add docs for this function? just to clarify that it can either return a next screen, or just return null and dismiss internally.

Comment on lines 406 to 422
// Launch web auth flow if web auth URL is available.
// Next steps will happen in `handleWebAuthActivityResult`.
if (accountStatus.webviewOpenUrl != null) {
launchWebAuthFlow?.invoke(accountStatus.webviewOpenUrl)
return
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👀

@lng-stripe lng-stripe marked this pull request as ready for review September 12, 2025 17:25
@lng-stripe lng-stripe requested review from a team as code owners September 12, 2025 17:25
@lng-stripe lng-stripe marked this pull request as draft September 12, 2025 20:09
Copy link
Collaborator

@carlosmuvi-stripe carlosmuvi-stripe left a comment

Choose a reason for hiding this comment

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

Left a couple minor comments! doing some testing now.

consumerPublishableKey = linkAccount.consumerPublishableKey
)
.onFailure { error ->
linkEventsReporter.onAccountLookupFailure(error)
Copy link
Collaborator

Choose a reason for hiding this comment

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

[no action] This is not a lookup call right? we might want to have a specific event for this (can be a follow-up)

}

@VisibleForTesting
internal fun getScreenStateForAuthorizationAfterRefresh(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add docs for this function? just to clarify that it can either return a next screen, or just return null and dismiss internally.

@lng-stripe lng-stripe force-pushed the lng/link-web-auth-fallback branch 2 times, most recently from 5579982 to 1939cea Compare September 16, 2025 18:42
@lng-stripe
Copy link
Contributor Author

I updated it so web auth is mostly handled in Verification instead. This has the benefits of:

  1. consolidating verification logic to VerificationViewModel, et. al.
  2. improving UX by adding loading states while refreshing the consumer session.

@lng-stripe lng-stripe marked this pull request as ready for review September 16, 2025 18:44
@lng-stripe lng-stripe marked this pull request as draft September 16, 2025 18:45
@lng-stripe lng-stripe force-pushed the lng/link-web-auth-fallback branch from 1939cea to 5228e09 Compare September 16, 2025 19:34
@lng-stripe
Copy link
Contributor Author

Still in draft because it's blocked on changes to the /refresh API

@lng-stripe
Copy link
Contributor Author

Backend changes were deployed. Pushed some minor-ish changes and updated the PR description with videos from manual testing.

@lng-stripe lng-stripe marked this pull request as ready for review September 17, 2025 16:56
@lng-stripe lng-stripe requested a review from a team as a code owner September 17, 2025 16:56
Copy link
Collaborator

@carlosmuvi-stripe carlosmuvi-stripe 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 here - code looks good, doing some testing now.

private val dismissWithResult: (LinkActivityResult) -> Unit,
) : ViewModel() {

private val isProcessingWebAuth = linkAccount.webviewOpenUrl != null
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - should we keep this only on the state to avoid confusion?


private fun startWebVerification() {
viewModelScope.launch {
// If the web auth URL has already been consumed, perform lookup again to get a fresh one.
Copy link
Collaborator

Choose a reason for hiding this comment

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

we can maybe mention that auth web urls cannot be reused, for context


@Suppress("CyclomaticComplexMethod", "ComplexCondition")
private suspend fun updateScreenState(withAnimationDelay: Boolean) {
private suspend fun updateScreenState(
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is amazing, very needed.

@lng-stripe lng-stripe enabled auto-merge (squash) September 17, 2025 18:31
@lng-stripe lng-stripe merged commit c67fadd into master Sep 17, 2025
16 checks passed
@lng-stripe lng-stripe deleted the lng/link-web-auth-fallback branch September 17, 2025 18:46
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.

3 participants