From 7120cafe2f5789955494e1494a8d598d89063f66 Mon Sep 17 00:00:00 2001 From: Subhankar Maiti Date: Thu, 9 Jul 2026 01:47:22 +0530 Subject: [PATCH 1/3] feat: add useTrustedWebActivity option for Android Web Auth --- EXAMPLES.md | 62 +++++++++++++++++ .../java/com/auth0/react/A0Auth0Module.kt | 10 ++- .../oldarch/com/auth0/react/A0Auth0Spec.kt | 3 +- example/ios/Podfile.lock | 4 +- .../src/screens/class-based/ClassLogin.tsx | 26 ++++++-- example/src/screens/hooks/Home.tsx | 40 +++++++---- ios/A0Auth0.mm | 4 +- .../native/bridge/NativeBridgeManager.ts | 6 +- .../__tests__/NativeBridgeManager.spec.ts | 66 +++++++++++++++++-- src/specs/NativeA0Auth0.ts | 6 +- src/types/platform-specific.ts | 18 +++++ 11 files changed, 213 insertions(+), 32 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index eb25c733..b5da6ad7 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -2214,6 +2214,68 @@ await auth0.webAuth.authorize( The same `allowedBrowserPackages` option is also accepted by `clearSession` to restrict which browser handles the logout flow. +## Trusted Web Activity (Android) + +On Android, web authentication defaults to a Custom Tab, which shows a read-only URL bar at the top of the browser. A **Trusted Web Activity (TWA)** renders the login page full-screen with no address bar, giving a more integrated, app-like experience. Pass `useTrustedWebActivity: true` in the options object to opt in. + +**Behaviour:** + +- When enabled, the login (and logout) page opens in a Trusted Web Activity instead of a Custom Tab. +- TWA relies on **Digital Asset Links** verification between your app and your Auth0 domain. If verification fails — for example because the setup below is missing — it automatically **falls back to a Custom Tab**, so login still completes (just with the URL bar visible). + +> **Platform Support:** Android only. This option is ignored on iOS and web. + +### Required setup + +TWA will only render full-screen if your app's signing certificate is registered with your Auth0 tenant. Without this, Digital Asset Links verification fails and the flow falls back to a Custom Tab. + +1. Get your app's **SHA-256 certificate fingerprint**. For a debug build: + + ```bash + keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android + ``` + + For a release build, run the same command against your release keystore. Copy the `SHA256` fingerprint. + +2. In the **Auth0 Dashboard**, go to **Application → Settings → Advanced → Device Settings → Key Hashes** and add: + - Your app's **package name** (e.g. `com.myapp`). + - The **SHA-256 fingerprint** from step 1. + +3. Save. Digital Asset Links verification now succeeds and the login page renders full-screen. + +> Register the fingerprint for **every** signing config you ship (debug, release, and Play App Signing if you use it), otherwise TWA silently falls back to a Custom Tab for the unregistered builds. + +### Using with Hooks + +```typescript +import { useAuth0 } from 'react-native-auth0'; + +const { authorize } = useAuth0(); + +await authorize( + { scope: 'openid profile email' }, + { useTrustedWebActivity: true } +); +``` + +### Using with Auth0 Class + +```typescript +import Auth0 from 'react-native-auth0'; + +const auth0 = new Auth0({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', +}); + +await auth0.webAuth.authorize( + { scope: 'openid profile email' }, + { useTrustedWebActivity: true } +); +``` + +The same `useTrustedWebActivity` option is also accepted by `clearSession` so the logout flow opens in a Trusted Web Activity as well. + ## Recovering Login After Process Death (Android) On Android, the OS can kill your app's process while the user is completing login in the browser — this is common on devices with aggressive memory management (e.g. Samsung One UI, Xiaomi MIUI), especially during MFA when the user switches apps to fetch a code. When the user finishes and the browser redirects back, the app cold-starts and, without recovery, the in-flight login is lost and the user lands back on the login screen. diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index dbd67c9a..9bd075f1 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -134,6 +134,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 safariViewControllerPresentationStyle: Double?, additionalParameters: ReadableMap?, allowedBrowserPackages: ReadableArray?, + useTrustedWebActivity: Boolean?, promise: Promise ) { if(this.useDPoP) { @@ -172,8 +173,9 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 .build() ) } + if (useTrustedWebActivity == true) { withTrustedWebActivity() } } - + builder.withParameters(cleanedParameters) val activity = reactContext.currentActivity @@ -458,13 +460,17 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 override fun getName(): String = NAME @ReactMethod - override fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, allowedBrowserPackages: ReadableArray?, promise: Promise) { + override fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, allowedBrowserPackages: ReadableArray?, useTrustedWebActivity: Boolean?, promise: Promise) { val builder = WebAuthProvider.logout(auth0!!).withScheme(scheme) if (federated) { builder.withFederated() } + if (useTrustedWebActivity == true) { + builder.withTrustedWebActivity() + } + redirectUri?.let { builder.withReturnToUrl(it) } allowedBrowserPackages?.let { packages -> diff --git a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt index cbe3f429..2bb59e57 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -86,12 +86,13 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ safariViewControllerPresentationStyle: Double?, additionalParameters: ReadableMap?, allowedBrowserPackages: ReadableArray?, + useTrustedWebActivity: Boolean?, promise: Promise ) @ReactMethod @DoNotStrip - abstract fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, allowedBrowserPackages: ReadableArray?, promise: Promise) + abstract fun webAuthLogout(scheme: String, federated: Boolean, redirectUri: String?, allowedBrowserPackages: ReadableArray?, useTrustedWebActivity: Boolean?, promise: Promise) @ReactMethod @DoNotStrip diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index cc13fb45..1c282e27 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - A0Auth0 (5.8.0): + - A0Auth0 (5.9.0): - Auth0 (= 2.23.0) - hermes-engine - RCTRequired @@ -2242,7 +2242,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - A0Auth0: e14195294c028c4ebce21e45a17ee11749e7a943 + A0Auth0: 260f4197bc27923b13525663cc625cbb01ac4e4a Auth0: dd46c309079f995e91dfd7af38e8fefd1a43bf4e FBLazyVector: b3e7ad108f0d882e30445c5527d774e3fd432f3d hermes-engine: 4ba8c4848d46c6a441e09d1c62f883ba69bc5b76 diff --git a/example/src/screens/class-based/ClassLogin.tsx b/example/src/screens/class-based/ClassLogin.tsx index ba9ed046..9d72c9ec 100644 --- a/example/src/screens/class-based/ClassLogin.tsx +++ b/example/src/screens/class-based/ClassLogin.tsx @@ -38,15 +38,19 @@ const ClassLoginScreen = () => { const [challenge, setChallenge] = useState( null ); + const [useTrustedWebActivity, setUseTrustedWebActivity] = useState(false); const onLogin = async () => { setLoading(true); setError(null); try { - const credentials = await auth0.webAuth.authorize({ - scope: 'openid profile email offline_access', - audience: `https://${config.domain}/api/v2/`, - }); + const credentials = await auth0.webAuth.authorize( + { + scope: 'openid profile email offline_access', + audience: `https://${config.domain}/api/v2/`, + }, + { useTrustedWebActivity } + ); // On success, we save the credentials and navigate to the profile screen. await auth0.credentialsManager.saveCredentials(credentials); navigation.replace('ClassProfile', { credentials }); @@ -118,6 +122,20 @@ const ClassLoginScreen = () => { Web Auth