From 69df62ac128b2af969ca086dafc7b0bff9ee28eb Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Thu, 13 Aug 2026 09:31:25 +0530 Subject: [PATCH 1/6] feat: allow configuring Auth0.Android's native networking client Adds `androidNetworkingOptions` to `Auth0Options`, letting apps tune the OkHttp-based `DefaultClient` (connect/read/write/call timeouts, default headers, and debug logging) that Auth0.Android uses for every native request. Android only; accepted and ignored on iOS for API compatibility. Ref: SDK-10614 --- EXAMPLES.md | 63 +++++++++++++++ android/build.gradle | 7 ++ .../java/com/auth0/react/A0Auth0Module.kt | 21 +++++ .../A0Auth0ModuleNetworkingOptionsTest.kt | 76 +++++++++++++++++++ ios/A0Auth0.mm | 2 + .../utils/__tests__/configSignature.spec.ts | 26 +++++++ src/core/utils/configSignature.ts | 1 + .../native/adapters/NativeAuth0Client.ts | 4 +- .../__tests__/NativeAuth0Client.spec.ts | 34 ++++++++- src/platforms/native/bridge/NativeBridge.ts | 5 +- .../native/bridge/NativeBridgeManager.ts | 7 +- .../__tests__/NativeBridgeManager.spec.ts | 36 ++++++++- src/specs/NativeA0Auth0.ts | 3 +- src/types/common.ts | 39 ++++++++++ 14 files changed, 314 insertions(+), 10 deletions(-) create mode 100644 android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt diff --git a/EXAMPLES.md b/EXAMPLES.md index 36a6b54cb..511b00c08 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -21,6 +21,9 @@ - [Using Retry with Auth0 Class](#using-retry-with-auth0-class) - [Platform Support](#platform-support) - [Error Handling](#error-handling) +- [Android Networking Configuration](#android-networking-configuration) + - [Using Networking Options with Hooks](#using-networking-options-with-hooks) + - [Using Networking Options with Auth0 Class](#using-networking-options-with-auth0-class) - [IPSIE Session Expiry](#ipsie-session-expiry) - [Biometric Authentication](#biometric-authentication) - [Biometric Policy Types](#biometric-policy-types) @@ -637,6 +640,66 @@ function MyComponent() { 2. **Configure adequate overlap period**: Ensure your Auth0 tenant has at least 180 seconds token overlap configured 3. **Test on real devices**: Simulate network instability during testing to validate retry behavior +## Android Networking Configuration + +> **Platform Support:** Android only. Accepted on iOS for API compatibility but has no effect. + +The `androidNetworkingOptions` configuration option lets you tune the native networking client (`DefaultClient` from Auth0.Android's OkHttp-based stack) used for every request the native SDK makes on your behalf — web auth token exchange, credential renewal, MFA, passkeys, and My Account API calls. + +```ts +androidNetworkingOptions?: { + connectTimeout?: number; // seconds, default 10 + readTimeout?: number; // seconds, default 10 + writeTimeout?: number; // seconds, default 10 + callTimeout?: number; // seconds, default 0 (no limit) + defaultHeaders?: Record; // sent on every request, default {} + enableLogging?: boolean; // default false +}; +``` + +Any option you omit falls back to Auth0.Android's own default. + +> [!WARNING] +> `enableLogging` is **debug-only**. When enabled, Auth0.Android logs full HTTP request and response bodies to Logcat — including access, refresh, and ID tokens returned from token-endpoint calls, in plaintext. Never enable it in a production build. + +### Using Networking Options with Hooks + +```jsx +import React from 'react'; +import { Auth0Provider } from 'react-native-auth0'; + +function App() { + return ( + + + + ); +} +``` + +### Using Networking Options with Auth0 Class + +```js +import Auth0 from 'react-native-auth0'; + +const auth0 = new Auth0({ + domain: 'YOUR_AUTH0_DOMAIN', + clientId: 'YOUR_AUTH0_CLIENT_ID', + androidNetworkingOptions: { + connectTimeout: 30, + readTimeout: 30, + }, +}); +``` + ## IPSIE Session Expiry > **Platform Support:** iOS, Android, and Web. diff --git a/android/build.gradle b/android/build.gradle index 6d4e8d75b..5823da3e5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -51,6 +51,10 @@ android { abortOnError false } + testOptions { + unitTests.returnDefaultValues = true + } + compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 @@ -79,6 +83,9 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.browser:browser:1.10.0" implementation 'com.auth0.android:auth0:4.0.1' + + testImplementation 'junit:junit:4.13.2' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' } react { diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index 90acf9231..d4bcd0a40 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -20,6 +20,7 @@ import com.auth0.android.dpop.DPoPException import com.auth0.android.provider.BrowserPicker import com.auth0.android.provider.CustomTabsOptions import com.auth0.android.provider.WebAuthProvider +import com.auth0.android.request.DefaultClient import com.auth0.android.request.PublicKeyCredentials import com.auth0.android.request.UserData import com.auth0.android.result.APICredentials @@ -64,6 +65,24 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 private const val DPOP_INVALID_TOKEN_TYPE_CODE = "DPOP_INVALID_TOKEN_TYPE" private const val DPOP_MISSING_PARAMETER_CODE = "DPOP_MISSING_PARAMETER" private const val DPOP_CLEAR_KEY_FAILED_CODE = "DPOP_CLEAR_KEY_FAILED" + + // Builds the DefaultClient Auth0.Android uses for every request it makes (web auth + // token exchange, credential renewal, MFA, passkeys, etc.). Unset keys fall through to + // Auth0.Android's own Builder defaults. `enableLogging` is debug-only: Auth0.Android logs + // full request/response bodies (including tokens) at that level, so we never call + // `logger(...)` ourselves and never expose the raw HttpLoggingInterceptor.Logger to JS. + internal fun buildNetworkingClient(options: ReadableMap): DefaultClient { + val builder = DefaultClient.Builder() + if (options.hasKey("connectTimeout")) builder.connectTimeout(options.getInt("connectTimeout")) + if (options.hasKey("readTimeout")) builder.readTimeout(options.getInt("readTimeout")) + if (options.hasKey("writeTimeout")) builder.writeTimeout(options.getInt("writeTimeout")) + if (options.hasKey("callTimeout")) builder.callTimeout(options.getInt("callTimeout")) + options.getMap("defaultHeaders")?.let { headers -> + builder.defaultHeaders(headers.toHashMap().mapValues { it.value?.toString() ?: "" }) + } + if (options.hasKey("enableLogging")) builder.enableLogging(options.getBoolean("enableLogging")) + return builder.build() + } } private val errorCodeMap = mapOf( @@ -282,6 +301,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 useDPoP: Boolean?, maxRetries: Double, credentialsManagerStorageKey: String?, + androidNetworkingOptions: ReadableMap?, promise: Promise ) { // Note: maxRetries parameter is ignored on Android as the Auth0.Android SDK @@ -290,6 +310,7 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 this.useDPoP = useDPoP ?: false auth0 = Auth0.getInstance(clientId, domain) + androidNetworkingOptions?.let { auth0!!.networkingClient = buildNetworkingClient(it) } mfaClient = MfaClient(auth0!!, this.useDPoP, reactContext) myAccount = MyAccount(auth0!!, this.useDPoP, reactContext) passwordless = Passwordless(auth0!!, this.useDPoP, reactContext) diff --git a/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt b/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt new file mode 100644 index 000000000..30ba456b6 --- /dev/null +++ b/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt @@ -0,0 +1,76 @@ +package com.auth0.react + +import com.auth0.android.request.HttpMethod +import com.auth0.android.request.RequestOptions +import com.facebook.react.bridge.JavaOnlyMap +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Test +import java.io.IOException +import java.util.concurrent.TimeUnit +import kotlin.system.measureTimeMillis + +// Proves that A0Auth0Module.buildNetworkingClient() genuinely threads androidNetworkingOptions +// into the DefaultClient it builds, rather than just compiling. Exercises the client against a +// real (local) server so the OkHttp timeout machinery actually runs. +class A0Auth0ModuleNetworkingOptionsTest { + + private lateinit var server: MockWebServer + + @Before + fun setUp() { + server = MockWebServer() + server.start() + } + + @After + fun tearDown() { + server.shutdown() + } + + @Test + fun `readTimeout from androidNetworkingOptions is applied to the built DefaultClient`() { + val configuredTimeoutSeconds = 1 + // Stall the response well past the configured timeout. + server.enqueue(MockResponse().setHeadersDelay(3, TimeUnit.SECONDS).setBody("{}")) + + val client = A0Auth0Module.buildNetworkingClient( + JavaOnlyMap.of("readTimeout", configuredTimeoutSeconds) + ) + + var threw = false + val elapsedMillis = measureTimeMillis { + try { + client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + fail("Expected the configured read timeout to fire") + } catch (e: IOException) { + threw = true + } + } + + assertTrue("Expected an IOException from the read timeout", threw) + // The server stalls for 3s; a working 1s readTimeout must fire well before that. + assertTrue( + "Expected the call to fail near the configured ${configuredTimeoutSeconds}s timeout, took ${elapsedMillis}ms", + elapsedMillis < TimeUnit.SECONDS.toMillis(2) + ) + } + + @Test + fun `defaultHeaders from androidNetworkingOptions are sent on every request`() { + server.enqueue(MockResponse().setBody("{}")) + + val client = A0Auth0Module.buildNetworkingClient( + JavaOnlyMap.of("defaultHeaders", JavaOnlyMap.of("X-Custom-Header", "custom-value")) + ) + + client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + + val recordedRequest = server.takeRequest() + assertTrue(recordedRequest.getHeader("X-Custom-Header") == "custom-value") + } +} diff --git a/ios/A0Auth0.mm b/ios/A0Auth0.mm index 99024107f..fbe3bd9ae 100644 --- a/ios/A0Auth0.mm +++ b/ios/A0Auth0.mm @@ -100,8 +100,10 @@ - (dispatch_queue_t)methodQueue useDPoP:(nonnull NSNumber *)useDPoP maxRetries:(double)maxRetries credentialsManagerStorageKey:(NSString * _Nullable)credentialsManagerStorageKey + androidNetworkingOptions:(NSDictionary * _Nullable)androidNetworkingOptions resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { + // androidNetworkingOptions is Android-only; intentionally not forwarded to NativeBridge. [self tryAndInitializeNativeBridge:clientId domain:domain withLocalAuthenticationOptions:localAuthenticationOptions useDPoP:useDPoP maxRetries:(NSInteger)maxRetries credentialsManagerStorageKey:credentialsManagerStorageKey resolve:resolve reject:reject]; } diff --git a/src/core/utils/__tests__/configSignature.spec.ts b/src/core/utils/__tests__/configSignature.spec.ts index 5369f8384..611b408e5 100644 --- a/src/core/utils/__tests__/configSignature.spec.ts +++ b/src/core/utils/__tests__/configSignature.spec.ts @@ -84,4 +84,30 @@ describe('getConfigSignature', () => { getConfigSignature({ ...base, maxRetries: 3 }) ); }); + + it('differs when androidNetworkingOptions changes', () => { + expect( + getConfigSignature({ + ...base, + androidNetworkingOptions: { connectTimeout: 10 }, + }) + ).not.toBe( + getConfigSignature({ + ...base, + androidNetworkingOptions: { connectTimeout: 30 }, + }) + ); + }); + + it('is insensitive to androidNetworkingOptions key order', () => { + const a = getConfigSignature({ + ...base, + androidNetworkingOptions: { connectTimeout: 10, readTimeout: 20 }, + }); + const b = getConfigSignature({ + ...base, + androidNetworkingOptions: { readTimeout: 20, connectTimeout: 10 }, + }); + expect(a).toBe(b); + }); }); diff --git a/src/core/utils/configSignature.ts b/src/core/utils/configSignature.ts index acd6f4262..ef0683fe1 100644 --- a/src/core/utils/configSignature.ts +++ b/src/core/utils/configSignature.ts @@ -9,6 +9,7 @@ const SIGNIFICANT_KEYS = [ 'useDPoP', 'maxRetries', 'credentialsManagerStorageKey', + 'androidNetworkingOptions', ] as const satisfies ReadonlyArray; // Stable, order-independent identity string for a config: keys the factory cache, the provider memo, and the native re-init decision. Object values are sorted so key order doesn't matter. diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index cdd63eaf8..d5a6a1a69 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -109,6 +109,7 @@ export class NativeAuth0Client implements Auth0Client { useDPoP = false, maxRetries, credentialsManagerStorageKey, + androidNetworkingOptions, } = options; // Re-init when domain/clientId differ (hasValidInstance) or any other // identity option drifted from what was last applied to the native side. @@ -125,7 +126,8 @@ export class NativeAuth0Client implements Auth0Client { localAuthenticationOptions, useDPoP, maxRetries, - credentialsManagerStorageKey + credentialsManagerStorageKey, + androidNetworkingOptions ); } // Record even on the skip path so siblings differing only in a diff --git a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts index 8b0c97deb..7c5845ad1 100644 --- a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts @@ -120,7 +120,8 @@ describe('NativeAuth0Client', () => { undefined, // No local auth options provided in this test false, // useDPoP defaults to false undefined, // maxRetries not provided - undefined // credentialsManagerStorageKey not provided + undefined, // credentialsManagerStorageKey not provided + undefined // androidNetworkingOptions not provided ); // Use client to avoid unused variable warning @@ -142,7 +143,8 @@ describe('NativeAuth0Client', () => { undefined, false, undefined, - 'tenant-b' + 'tenant-b', + undefined ); expect(client).toBeDefined(); }); @@ -163,13 +165,37 @@ describe('NativeAuth0Client', () => { localAuthOptions, false, // useDPoP defaults to false undefined, // maxRetries not provided - undefined // credentialsManagerStorageKey not provided + undefined, // credentialsManagerStorageKey not provided + undefined // androidNetworkingOptions not provided ); // Use client to avoid unused variable warning expect(client).toBeDefined(); }); + it('should pass androidNetworkingOptions to initialize when provided', async () => { + mockBridgeInstance.hasValidInstance.mockResolvedValue(false); + const androidNetworkingOptions = { connectTimeout: 30, readTimeout: 30 }; + + const client = new NativeAuth0Client({ + ...options, + androidNetworkingOptions, + }); + await new Promise(process.nextTick); + + expect(mockBridgeInstance.initialize).toHaveBeenCalledWith( + options.clientId, + options.domain, + undefined, + false, + undefined, + undefined, + androidNetworkingOptions + ); + + expect(client).toBeDefined(); + }); + it('should ensure initialization is complete before calling a bridge method', async () => { let resolveInitialization: () => void; const initializationPromise = new Promise((resolve) => { @@ -686,6 +712,7 @@ describe('NativeAuth0Client', () => { undefined, false, undefined, + undefined, undefined ); expect(mockBridgeInstance.authorize).toHaveBeenCalledTimes(1); @@ -729,6 +756,7 @@ describe('NativeAuth0Client', () => { undefined, false, // useDPoP flipped to false undefined, + undefined, undefined ); }); diff --git a/src/platforms/native/bridge/NativeBridge.ts b/src/platforms/native/bridge/NativeBridge.ts index ee1ec9f79..6ff50c611 100644 --- a/src/platforms/native/bridge/NativeBridge.ts +++ b/src/platforms/native/bridge/NativeBridge.ts @@ -9,6 +9,7 @@ import type { MfaEnrollmentChallenge, MfaChallengeResult, PasskeyChallengeResponse, + AndroidNetworkingOptions, } from '../../../types'; import type { LocalAuthenticationOptions, @@ -39,6 +40,7 @@ export interface NativeBridge { * @param useDPoP Whether to enable DPoP (Demonstrating Proof-of-Possession) for token requests. * @param maxRetries The maximum number of retry attempts for transient errors during credential renewal. **iOS only** - ignored on Android. Defaults to 0. * @param credentialsManagerStorageKey Namespaces the credentials store. **Android only** SharedPreferences file name. **iOS only** Keychain service name. Defaults to the shared store when omitted. + * @param androidNetworkingOptions Configures the native networking client. **Android only** - ignored on iOS. */ initialize( clientId: string, @@ -46,7 +48,8 @@ export interface NativeBridge { localAuthenticationOptions?: LocalAuthenticationOptions, useDPoP?: boolean, maxRetries?: number, - credentialsManagerStorageKey?: string + credentialsManagerStorageKey?: string, + androidNetworkingOptions?: AndroidNetworkingOptions ): Promise; /** diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index b05efc4c1..9b226467e 100644 --- a/src/platforms/native/bridge/NativeBridgeManager.ts +++ b/src/platforms/native/bridge/NativeBridgeManager.ts @@ -11,6 +11,7 @@ import type { MfaEnrollmentChallenge, MfaChallengeResult, PasskeyChallengeResponse, + AndroidNetworkingOptions, } from '../../../types'; import { SafariViewControllerPresentationStyle, @@ -60,7 +61,8 @@ export class NativeBridgeManager implements NativeBridge { localAuthenticationOptions?: LocalAuthenticationOptions, useDPoP: boolean = false, maxRetries: number = 0, - credentialsManagerStorageKey?: string + credentialsManagerStorageKey?: string, + androidNetworkingOptions?: AndroidNetworkingOptions ): Promise { // This is a new method we'd add to the native side to ensure the // underlying Auth0.swift/Auth0.android SDKs are configured. @@ -73,7 +75,8 @@ export class NativeBridgeManager implements NativeBridge { localAuthenticationOptions, useDPoP, maxRetries, - credentialsManagerStorageKey + credentialsManagerStorageKey, + androidNetworkingOptions ); } diff --git a/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts b/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts index 1ac8c9f44..e8aca9fb9 100644 --- a/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts +++ b/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts @@ -267,7 +267,8 @@ describe('NativeBridgeManager', () => { undefined, true, 0, - 'tenant-b' + 'tenant-b', + undefined ); }); @@ -282,7 +283,38 @@ describe('NativeBridgeManager', () => { undefined, // localAuthenticationOptions false, // useDPoP default 0, // maxRetries default - undefined // credentialsManagerStorageKey + undefined, // credentialsManagerStorageKey + undefined // androidNetworkingOptions + ); + }); + + it('forwards androidNetworkingOptions to the native module when provided', async () => { + const androidNetworkingOptions = { + connectTimeout: 30, + readTimeout: 30, + defaultHeaders: { 'X-Custom': 'value' }, + }; + + await bridge.initialize( + 'client-id', + 'tenant-c.auth0.com', + undefined, + false, + 0, + undefined, + androidNetworkingOptions + ); + + expect( + MockedAuth0NativeModule.initializeAuth0WithConfiguration + ).toHaveBeenCalledWith( + 'client-id', + 'tenant-c.auth0.com', + undefined, + false, + 0, + undefined, + androidNetworkingOptions ); }); diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts index db9ba29c1..39dd5a840 100644 --- a/src/specs/NativeA0Auth0.ts +++ b/src/specs/NativeA0Auth0.ts @@ -26,7 +26,8 @@ export interface Spec extends TurboModule { { [key: string]: string | Int32 | boolean } | undefined, useDPoP: boolean | undefined, maxRetries: Int32, - credentialsManagerStorageKey: string | undefined + credentialsManagerStorageKey: string | undefined, + androidNetworkingOptions: Object | undefined ): Promise; /** diff --git a/src/types/common.ts b/src/types/common.ts index 8c386ddde..f72c12d68 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -214,9 +214,48 @@ export interface Auth0Options { * @remarks Native only (iOS/Android). Has no effect on the web platform. */ credentialsManagerStorageKey?: string; + /** + * Configures the native networking client (OkHttp) that Auth0.Android uses for every + * request it makes (web auth token exchange, credential renewal, MFA, passkeys, etc.). + * @remarks Android only. Accepted on iOS for API compatibility but has no effect. + */ + androidNetworkingOptions?: AndroidNetworkingOptions; // Telemetry and localAuthenticationOptions are platform-specific extensions } +/** + * Configuration for the native networking client used by Auth0.Android. + * Mirrors `DefaultClient.Builder` from the Auth0.Android SDK. + * + * @remarks Android only. Has no effect on iOS or web. + */ +export interface AndroidNetworkingOptions { + /** Connection timeout, in seconds. @default 10 */ + connectTimeout?: number; + /** Read timeout, in seconds. @default 10 */ + readTimeout?: number; + /** Write timeout, in seconds. @default 10 */ + writeTimeout?: number; + /** Overall timeout for the entire call, in seconds. `0` means no timeout. @default 0 */ + callTimeout?: number; + /** + * Headers sent on every request made by the native networking client. If a specific + * request sets a header with the same name, the request-level header takes precedence. + * @default {} + */ + defaultHeaders?: Record; + /** + * Enables verbose HTTP request/response logging to Logcat. + * + * @remarks + * **Debug-only.** Auth0.Android logs full request and response bodies at this level, + * which includes access, refresh, and ID tokens in plaintext for token-endpoint calls. + * Never enable this in production. + * @default false + */ + enableLogging?: boolean; +} + // ========= MFA Flexible Factors Grant Types ========= /** Represents an enrolled MFA authenticator. */ From 1aaee83ed3a954d9f5011d462a46e9572855e3c9 Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Fri, 14 Aug 2026 09:14:09 +0530 Subject: [PATCH 2/6] fix: remove unnecessary unitTests.returnDefaultValues from android/build.gradle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified the new A0Auth0ModuleNetworkingOptionsTest suite passes identically with or without this option — nothing in buildNetworkingClient()'s path touches an unstubbed Android framework API, so the fallback is dead config. --- android/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 5823da3e5..2a7f3ab7e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -51,10 +51,6 @@ android { abortOnError false } - testOptions { - unitTests.returnDefaultValues = true - } - compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 From 3d287e1362b5b3a81702abae451c820d4719128e Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Tue, 18 Aug 2026 18:23:00 +0530 Subject: [PATCH 3/6] fix: gate enableLogging behind debug builds and stop leaking networking config across clients DefaultClient.Builder.enableLogging logs full request/response bodies, including tokens, at the token endpoint. Forwarding androidNetworkingOptions.enableLogging unconditionally meant a release build could log tokens if a consumer set it to true. It's now only honored when the host app is debuggable. Auth0.getInstance(clientId, domain) returns a shared singleton, so a client that omits androidNetworkingOptions could silently inherit another client's timeouts and defaultHeaders (via re-init or a sibling client with the same clientId). networkingClient is now always set explicitly, defaulting to DefaultClient() when no options are given. --- .../java/com/auth0/react/A0Auth0Module.kt | 18 ++++++++++--- .../A0Auth0ModuleNetworkingOptionsTest.kt | 25 +++++++++++++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index d4bcd0a40..e6660f731 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -2,6 +2,7 @@ package com.auth0.react import android.app.Activity import android.content.Intent +import android.content.pm.ApplicationInfo import android.os.Build import android.os.Handler import android.os.Looper @@ -70,8 +71,9 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 // token exchange, credential renewal, MFA, passkeys, etc.). Unset keys fall through to // Auth0.Android's own Builder defaults. `enableLogging` is debug-only: Auth0.Android logs // full request/response bodies (including tokens) at that level, so we never call - // `logger(...)` ourselves and never expose the raw HttpLoggingInterceptor.Logger to JS. - internal fun buildNetworkingClient(options: ReadableMap): DefaultClient { + // `logger(...)` ourselves, never expose the raw HttpLoggingInterceptor.Logger to JS, and + // ignore the option entirely unless the host app is a debug build. + internal fun buildNetworkingClient(options: ReadableMap, isDebuggable: Boolean): DefaultClient { val builder = DefaultClient.Builder() if (options.hasKey("connectTimeout")) builder.connectTimeout(options.getInt("connectTimeout")) if (options.hasKey("readTimeout")) builder.readTimeout(options.getInt("readTimeout")) @@ -80,7 +82,9 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 options.getMap("defaultHeaders")?.let { headers -> builder.defaultHeaders(headers.toHashMap().mapValues { it.value?.toString() ?: "" }) } - if (options.hasKey("enableLogging")) builder.enableLogging(options.getBoolean("enableLogging")) + if (isDebuggable && options.hasKey("enableLogging")) { + builder.enableLogging(options.getBoolean("enableLogging")) + } return builder.build() } } @@ -310,7 +314,13 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 this.useDPoP = useDPoP ?: false auth0 = Auth0.getInstance(clientId, domain) - androidNetworkingOptions?.let { auth0!!.networkingClient = buildNetworkingClient(it) } + // Auth0.getInstance() returns a shared singleton per clientId/domain: a sibling client + // (or this same client on re-init) must not inherit another initialization's networking + // config, so always set networkingClient rather than only when options are present. + val isDebuggable = (reactContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 + auth0!!.networkingClient = androidNetworkingOptions?.let { + buildNetworkingClient(it, isDebuggable) + } ?: DefaultClient() mfaClient = MfaClient(auth0!!, this.useDPoP, reactContext) myAccount = MyAccount(auth0!!, this.useDPoP, reactContext) passwordless = Passwordless(auth0!!, this.useDPoP, reactContext) diff --git a/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt b/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt index 30ba456b6..f687c6765 100644 --- a/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt +++ b/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt @@ -39,7 +39,8 @@ class A0Auth0ModuleNetworkingOptionsTest { server.enqueue(MockResponse().setHeadersDelay(3, TimeUnit.SECONDS).setBody("{}")) val client = A0Auth0Module.buildNetworkingClient( - JavaOnlyMap.of("readTimeout", configuredTimeoutSeconds) + JavaOnlyMap.of("readTimeout", configuredTimeoutSeconds), + isDebuggable = true ) var threw = false @@ -65,7 +66,8 @@ class A0Auth0ModuleNetworkingOptionsTest { server.enqueue(MockResponse().setBody("{}")) val client = A0Auth0Module.buildNetworkingClient( - JavaOnlyMap.of("defaultHeaders", JavaOnlyMap.of("X-Custom-Header", "custom-value")) + JavaOnlyMap.of("defaultHeaders", JavaOnlyMap.of("X-Custom-Header", "custom-value")), + isDebuggable = true ) client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) @@ -73,4 +75,23 @@ class A0Auth0ModuleNetworkingOptionsTest { val recordedRequest = server.takeRequest() assertTrue(recordedRequest.getHeader("X-Custom-Header") == "custom-value") } + + @Test + fun `enableLogging is ignored on a non-debuggable build even when requested`() { + server.enqueue(MockResponse().setBody("{}")) + + // If the debuggable gate is ever removed, Auth0.Android attaches its logging + // interceptor and this request crashes ("Method ... not mocked") because + // android.util.Log isn't stubbed in this unit test environment - that crash is + // exactly the regression this test is meant to catch. + val client = A0Auth0Module.buildNetworkingClient( + JavaOnlyMap.of("enableLogging", true), + isDebuggable = false + ) + + client.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + + val recordedRequest = server.takeRequest() + assertTrue(recordedRequest.method == "GET") + } } From 6f81aa7c60fc491a2a88797e082ee97216a0b5b9 Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Tue, 18 Aug 2026 18:38:12 +0530 Subject: [PATCH 4/6] refactor: avoid the not-null assertion operator in initializeAuth0WithConfiguration auth0!! could theoretically crash if a caller raced this method against a concurrent reset. Capturing Auth0.getInstance()'s result in a local val before assigning the auth0 field lets Kotlin's type system guarantee non-null for the rest of this method without asserting it. --- .../java/com/auth0/react/A0Auth0Module.kt | 30 +++++++++++-------- .../A0Auth0ModuleNetworkingOptionsTest.kt | 27 +++++++++++++++++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index e6660f731..ca20217b1 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -87,6 +87,16 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 } return builder.build() } + + // Auth0.getInstance() returns a shared singleton per clientId/domain: a sibling client + // (or this same client on re-init) must not inherit another initialization's networking + // config, so this always resolves to a fresh DefaultClient() when options are absent + // rather than leaving the previous networkingClient in place. + internal fun resolveNetworkingClient( + androidNetworkingOptions: ReadableMap?, + isDebuggable: Boolean + ): DefaultClient = + androidNetworkingOptions?.let { buildNetworkingClient(it, isDebuggable) } ?: DefaultClient() } private val errorCodeMap = mapOf( @@ -313,19 +323,15 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 // This parameter is accepted for API compatibility with iOS. this.useDPoP = useDPoP ?: false - auth0 = Auth0.getInstance(clientId, domain) - // Auth0.getInstance() returns a shared singleton per clientId/domain: a sibling client - // (or this same client on re-init) must not inherit another initialization's networking - // config, so always set networkingClient rather than only when options are present. + val auth0Instance = Auth0.getInstance(clientId, domain) + auth0 = auth0Instance val isDebuggable = (reactContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 - auth0!!.networkingClient = androidNetworkingOptions?.let { - buildNetworkingClient(it, isDebuggable) - } ?: DefaultClient() - mfaClient = MfaClient(auth0!!, this.useDPoP, reactContext) - myAccount = MyAccount(auth0!!, this.useDPoP, reactContext) - passwordless = Passwordless(auth0!!, this.useDPoP, reactContext) - - val authAPI = AuthenticationAPIClient(auth0!!) + auth0Instance.networkingClient = resolveNetworkingClient(androidNetworkingOptions, isDebuggable) + mfaClient = MfaClient(auth0Instance, this.useDPoP, reactContext) + myAccount = MyAccount(auth0Instance, this.useDPoP, reactContext) + passwordless = Passwordless(auth0Instance, this.useDPoP, reactContext) + + val authAPI = AuthenticationAPIClient(auth0Instance) if (this.useDPoP) { authAPI.useDPoP(reactContext) } diff --git a/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt b/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt index f687c6765..c9cb852c7 100644 --- a/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt +++ b/android/src/test/java/com/auth0/react/A0Auth0ModuleNetworkingOptionsTest.kt @@ -94,4 +94,31 @@ class A0Auth0ModuleNetworkingOptionsTest { val recordedRequest = server.takeRequest() assertTrue(recordedRequest.method == "GET") } + + @Test + fun `re-initializing with the same configuration but no options restores the default client`() { + // First "initialization": custom options give a 1s readTimeout. + val customized = A0Auth0Module.resolveNetworkingClient( + JavaOnlyMap.of("readTimeout", 1), + isDebuggable = true + ) + + // Re-initialization with the same clientId/domain but androidNetworkingOptions omitted + // must not carry the previous readTimeout forward - it should behave like a fresh + // DefaultClient() (10s default readTimeout). + val reset = A0Auth0Module.resolveNetworkingClient(null, isDebuggable = true) + + server.enqueue(MockResponse().setHeadersDelay(3, TimeUnit.SECONDS).setBody("{}")) + var threw = false + try { + customized.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + } catch (e: IOException) { + threw = true + } + assertTrue("Expected the 1s readTimeout to fire on the customized client", threw) + + server.enqueue(MockResponse().setHeadersDelay(3, TimeUnit.SECONDS).setBody("{}")) + // Should comfortably survive the 3s delay under the restored 10s default readTimeout. + reset.load(server.url("/").toString(), RequestOptions(HttpMethod.GET)) + } } From b4c4e9f968f0362ac61a115648959140b019e561 Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Tue, 18 Aug 2026 18:47:22 +0530 Subject: [PATCH 5/6] refactor!: rename androidNetworkingOptions to networkingOptions Every other platform-specific option on Auth0Options (maxRetries, credentialsManagerStorageKey) documents its platform restriction via @remarks rather than baking the platform name into the identifier. androidNetworkingOptions was the one exception. Since v6 hasn't shipped yet, rename it now while it's free, ahead of adding iOS networking config under its own umbrella later. --- EXAMPLES.md | 8 ++++---- .../src/main/java/com/auth0/react/A0Auth0Module.kt | 8 ++++---- .../react/A0Auth0ModuleNetworkingOptionsTest.kt | 8 ++++---- ios/A0Auth0.mm | 4 ++-- src/core/utils/__tests__/configSignature.spec.ts | 12 ++++++------ src/core/utils/configSignature.ts | 2 +- src/platforms/native/adapters/NativeAuth0Client.ts | 4 ++-- .../adapters/__tests__/NativeAuth0Client.spec.ts | 12 ++++++------ src/platforms/native/bridge/NativeBridge.ts | 6 +++--- src/platforms/native/bridge/NativeBridgeManager.ts | 6 +++--- .../bridge/__tests__/NativeBridgeManager.spec.ts | 10 +++++----- src/specs/NativeA0Auth0.ts | 2 +- src/types/common.ts | 4 ++-- 13 files changed, 43 insertions(+), 43 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 511b00c08..46c5cbc10 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -644,10 +644,10 @@ function MyComponent() { > **Platform Support:** Android only. Accepted on iOS for API compatibility but has no effect. -The `androidNetworkingOptions` configuration option lets you tune the native networking client (`DefaultClient` from Auth0.Android's OkHttp-based stack) used for every request the native SDK makes on your behalf — web auth token exchange, credential renewal, MFA, passkeys, and My Account API calls. +The `networkingOptions` configuration option lets you tune the native networking client (`DefaultClient` from Auth0.Android's OkHttp-based stack) used for every request the native SDK makes on your behalf — web auth token exchange, credential renewal, MFA, passkeys, and My Account API calls. ```ts -androidNetworkingOptions?: { +networkingOptions?: { connectTimeout?: number; // seconds, default 10 readTimeout?: number; // seconds, default 10 writeTimeout?: number; // seconds, default 10 @@ -673,7 +673,7 @@ function App() { { ); }); - it('differs when androidNetworkingOptions changes', () => { + it('differs when networkingOptions changes', () => { expect( getConfigSignature({ ...base, - androidNetworkingOptions: { connectTimeout: 10 }, + networkingOptions: { connectTimeout: 10 }, }) ).not.toBe( getConfigSignature({ ...base, - androidNetworkingOptions: { connectTimeout: 30 }, + networkingOptions: { connectTimeout: 30 }, }) ); }); - it('is insensitive to androidNetworkingOptions key order', () => { + it('is insensitive to networkingOptions key order', () => { const a = getConfigSignature({ ...base, - androidNetworkingOptions: { connectTimeout: 10, readTimeout: 20 }, + networkingOptions: { connectTimeout: 10, readTimeout: 20 }, }); const b = getConfigSignature({ ...base, - androidNetworkingOptions: { readTimeout: 20, connectTimeout: 10 }, + networkingOptions: { readTimeout: 20, connectTimeout: 10 }, }); expect(a).toBe(b); }); diff --git a/src/core/utils/configSignature.ts b/src/core/utils/configSignature.ts index ef0683fe1..69f37b9cc 100644 --- a/src/core/utils/configSignature.ts +++ b/src/core/utils/configSignature.ts @@ -9,7 +9,7 @@ const SIGNIFICANT_KEYS = [ 'useDPoP', 'maxRetries', 'credentialsManagerStorageKey', - 'androidNetworkingOptions', + 'networkingOptions', ] as const satisfies ReadonlyArray; // Stable, order-independent identity string for a config: keys the factory cache, the provider memo, and the native re-init decision. Object values are sorted so key order doesn't matter. diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts index d5a6a1a69..e0947e393 100644 --- a/src/platforms/native/adapters/NativeAuth0Client.ts +++ b/src/platforms/native/adapters/NativeAuth0Client.ts @@ -109,7 +109,7 @@ export class NativeAuth0Client implements Auth0Client { useDPoP = false, maxRetries, credentialsManagerStorageKey, - androidNetworkingOptions, + networkingOptions, } = options; // Re-init when domain/clientId differ (hasValidInstance) or any other // identity option drifted from what was last applied to the native side. @@ -127,7 +127,7 @@ export class NativeAuth0Client implements Auth0Client { useDPoP, maxRetries, credentialsManagerStorageKey, - androidNetworkingOptions + networkingOptions ); } // Record even on the skip path so siblings differing only in a diff --git a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts index 7c5845ad1..762ff8f19 100644 --- a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts +++ b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts @@ -121,7 +121,7 @@ describe('NativeAuth0Client', () => { false, // useDPoP defaults to false undefined, // maxRetries not provided undefined, // credentialsManagerStorageKey not provided - undefined // androidNetworkingOptions not provided + undefined // networkingOptions not provided ); // Use client to avoid unused variable warning @@ -166,20 +166,20 @@ describe('NativeAuth0Client', () => { false, // useDPoP defaults to false undefined, // maxRetries not provided undefined, // credentialsManagerStorageKey not provided - undefined // androidNetworkingOptions not provided + undefined // networkingOptions not provided ); // Use client to avoid unused variable warning expect(client).toBeDefined(); }); - it('should pass androidNetworkingOptions to initialize when provided', async () => { + it('should pass networkingOptions to initialize when provided', async () => { mockBridgeInstance.hasValidInstance.mockResolvedValue(false); - const androidNetworkingOptions = { connectTimeout: 30, readTimeout: 30 }; + const networkingOptions = { connectTimeout: 30, readTimeout: 30 }; const client = new NativeAuth0Client({ ...options, - androidNetworkingOptions, + networkingOptions, }); await new Promise(process.nextTick); @@ -190,7 +190,7 @@ describe('NativeAuth0Client', () => { false, undefined, undefined, - androidNetworkingOptions + networkingOptions ); expect(client).toBeDefined(); diff --git a/src/platforms/native/bridge/NativeBridge.ts b/src/platforms/native/bridge/NativeBridge.ts index 6ff50c611..f4879163e 100644 --- a/src/platforms/native/bridge/NativeBridge.ts +++ b/src/platforms/native/bridge/NativeBridge.ts @@ -9,7 +9,7 @@ import type { MfaEnrollmentChallenge, MfaChallengeResult, PasskeyChallengeResponse, - AndroidNetworkingOptions, + NetworkingOptions, } from '../../../types'; import type { LocalAuthenticationOptions, @@ -40,7 +40,7 @@ export interface NativeBridge { * @param useDPoP Whether to enable DPoP (Demonstrating Proof-of-Possession) for token requests. * @param maxRetries The maximum number of retry attempts for transient errors during credential renewal. **iOS only** - ignored on Android. Defaults to 0. * @param credentialsManagerStorageKey Namespaces the credentials store. **Android only** SharedPreferences file name. **iOS only** Keychain service name. Defaults to the shared store when omitted. - * @param androidNetworkingOptions Configures the native networking client. **Android only** - ignored on iOS. + * @param networkingOptions Configures the native networking client. **Android only** - ignored on iOS. */ initialize( clientId: string, @@ -49,7 +49,7 @@ export interface NativeBridge { useDPoP?: boolean, maxRetries?: number, credentialsManagerStorageKey?: string, - androidNetworkingOptions?: AndroidNetworkingOptions + networkingOptions?: NetworkingOptions ): Promise; /** diff --git a/src/platforms/native/bridge/NativeBridgeManager.ts b/src/platforms/native/bridge/NativeBridgeManager.ts index 9b226467e..47f8b855b 100644 --- a/src/platforms/native/bridge/NativeBridgeManager.ts +++ b/src/platforms/native/bridge/NativeBridgeManager.ts @@ -11,7 +11,7 @@ import type { MfaEnrollmentChallenge, MfaChallengeResult, PasskeyChallengeResponse, - AndroidNetworkingOptions, + NetworkingOptions, } from '../../../types'; import { SafariViewControllerPresentationStyle, @@ -62,7 +62,7 @@ export class NativeBridgeManager implements NativeBridge { useDPoP: boolean = false, maxRetries: number = 0, credentialsManagerStorageKey?: string, - androidNetworkingOptions?: AndroidNetworkingOptions + networkingOptions?: NetworkingOptions ): Promise { // This is a new method we'd add to the native side to ensure the // underlying Auth0.swift/Auth0.android SDKs are configured. @@ -76,7 +76,7 @@ export class NativeBridgeManager implements NativeBridge { useDPoP, maxRetries, credentialsManagerStorageKey, - androidNetworkingOptions + networkingOptions ); } diff --git a/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts b/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts index e8aca9fb9..c77366f90 100644 --- a/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts +++ b/src/platforms/native/bridge/__tests__/NativeBridgeManager.spec.ts @@ -284,12 +284,12 @@ describe('NativeBridgeManager', () => { false, // useDPoP default 0, // maxRetries default undefined, // credentialsManagerStorageKey - undefined // androidNetworkingOptions + undefined // networkingOptions ); }); - it('forwards androidNetworkingOptions to the native module when provided', async () => { - const androidNetworkingOptions = { + it('forwards networkingOptions to the native module when provided', async () => { + const networkingOptions = { connectTimeout: 30, readTimeout: 30, defaultHeaders: { 'X-Custom': 'value' }, @@ -302,7 +302,7 @@ describe('NativeBridgeManager', () => { false, 0, undefined, - androidNetworkingOptions + networkingOptions ); expect( @@ -314,7 +314,7 @@ describe('NativeBridgeManager', () => { false, 0, undefined, - androidNetworkingOptions + networkingOptions ); }); diff --git a/src/specs/NativeA0Auth0.ts b/src/specs/NativeA0Auth0.ts index 39dd5a840..1ac2ec682 100644 --- a/src/specs/NativeA0Auth0.ts +++ b/src/specs/NativeA0Auth0.ts @@ -27,7 +27,7 @@ export interface Spec extends TurboModule { useDPoP: boolean | undefined, maxRetries: Int32, credentialsManagerStorageKey: string | undefined, - androidNetworkingOptions: Object | undefined + networkingOptions: Object | undefined ): Promise; /** diff --git a/src/types/common.ts b/src/types/common.ts index f72c12d68..8d4103cec 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -219,7 +219,7 @@ export interface Auth0Options { * request it makes (web auth token exchange, credential renewal, MFA, passkeys, etc.). * @remarks Android only. Accepted on iOS for API compatibility but has no effect. */ - androidNetworkingOptions?: AndroidNetworkingOptions; + networkingOptions?: NetworkingOptions; // Telemetry and localAuthenticationOptions are platform-specific extensions } @@ -229,7 +229,7 @@ export interface Auth0Options { * * @remarks Android only. Has no effect on iOS or web. */ -export interface AndroidNetworkingOptions { +export interface NetworkingOptions { /** Connection timeout, in seconds. @default 10 */ connectTimeout?: number; /** Read timeout, in seconds. @default 10 */ From 83cb09233a458034834fd3b72da43682304b2b52 Mon Sep 17 00:00:00 2001 From: Nandan Prabhu Date: Tue, 18 Aug 2026 18:57:41 +0530 Subject: [PATCH 6/6] added android tests to github action workflow --- .github/workflows/main.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 230a1805f..27060e075 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,3 +31,27 @@ jobs: uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f with: directory: coverage + + android-test: + name: Run Android tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + + - name: Setup + uses: ./.github/actions/setup + + - name: Set up JDK 17 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 + with: + distribution: temurin + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5 + + - name: Run Android unit tests + working-directory: example/android + run: ./gradlew :react-native-auth0:testDebugUnitTest