diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index f786cf62..4144f603 100644 --- a/android/src/main/java/com/auth0/react/A0Auth0Module.kt +++ b/android/src/main/java/com/auth0/react/A0Auth0Module.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import androidx.fragment.app.FragmentActivity import com.auth0.android.Auth0 +import com.auth0.android.result.APICredentials import com.auth0.android.authentication.AuthenticationException import com.auth0.android.authentication.storage.CredentialsManagerException import com.auth0.android.authentication.storage.LocalAuthenticationOptions @@ -17,8 +18,6 @@ import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil -import java.net.MalformedURLException -import java.net.URL class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0Spec(reactContext), ActivityEventListener { @@ -249,6 +248,46 @@ class A0Auth0Module(private val reactContext: ReactApplicationContext) : A0Auth0 promise.resolve(secureCredentialsManager.hasValidCredentials(minTtl.toLong())) } + @ReactMethod + override fun getApiCredentials( + audience: String, + scope: String?, + minTtl: Double, + parameters: ReadableMap, + promise: Promise + ) { + val cleanedParameters = mutableMapOf() + parameters.toHashMap().forEach { (key, value) -> + value?.let { cleanedParameters[key] = it.toString() } + } + + UiThreadUtil.runOnUiThread { + secureCredentialsManager.getApiCredentials( + audience, + scope, + minTtl.toInt(), + cleanedParameters, + emptyMap(), // headers not supported from JS yet + object : com.auth0.android.callback.Callback { + override fun onSuccess(credentials: APICredentials) { + val map = ApiCredentialsParser.toMap(credentials) + promise.resolve(map) + } + + override fun onFailure(e: CredentialsManagerException) { + val errorCode = deduceErrorCode(e) + promise.reject(errorCode, e.message, e) + } + } + ) + } + } + + @ReactMethod + override fun clearApiCredentials(audience: String, promise: Promise) { + secureCredentialsManager.clearApiCredentials(audience) + promise.resolve(true) + } override fun getConstants(): Map { return mapOf("bundleIdentifier" to reactContext.applicationInfo.packageName) } diff --git a/android/src/main/java/com/auth0/react/ApiCredentialsParser.kt b/android/src/main/java/com/auth0/react/ApiCredentialsParser.kt new file mode 100644 index 00000000..9174ae48 --- /dev/null +++ b/android/src/main/java/com/auth0/react/ApiCredentialsParser.kt @@ -0,0 +1,22 @@ +package com.auth0.react + +import com.auth0.android.result.APICredentials +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReadableMap + +object ApiCredentialsParser { + + private const val ACCESS_TOKEN_KEY = "accessToken" + private const val EXPIRES_AT_KEY = "expiresAt" + private const val SCOPE_KEY = "scope" + private const val TOKEN_TYPE_KEY = "tokenType" + + fun toMap(credentials: APICredentials): ReadableMap { + val map = Arguments.createMap() + map.putString(ACCESS_TOKEN_KEY, credentials.accessToken) + map.putDouble(EXPIRES_AT_KEY, credentials.expiresAt.time / 1000.0) + map.putString(SCOPE_KEY, credentials.scope) + map.putString(TOKEN_TYPE_KEY, credentials.type) + return map + } +} \ No newline at end of file diff --git a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt index 88021156..aba690e1 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -50,6 +50,20 @@ abstract class A0Auth0Spec(context: ReactApplicationContext) : ReactContextBaseJ @DoNotStrip abstract fun clearCredentials(promise: Promise) + @ReactMethod + @DoNotStrip + abstract fun getApiCredentials( + audience: String, + scope: String?, + minTTL: Double, + parameters: ReadableMap, + promise: Promise + ) + + @ReactMethod + @DoNotStrip + abstract fun clearApiCredentials(audience: String, promise: Promise) + @ReactMethod @DoNotStrip abstract fun webAuth( diff --git a/example/src/navigation/MainTabNavigator.tsx b/example/src/navigation/MainTabNavigator.tsx index fe465ed5..0a9c866f 100644 --- a/example/src/navigation/MainTabNavigator.tsx +++ b/example/src/navigation/MainTabNavigator.tsx @@ -5,11 +5,13 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import ProfileScreen from '../screens/hooks/Profile'; import ApiScreen from '../screens/hooks/Api'; import MoreScreen from '../screens/hooks/More'; +import CredentialsScreen from '../screens/hooks/CredentialsScreen'; export type MainTabParamList = { Profile: undefined; Api: undefined; More: undefined; + Credentials: undefined; }; const Tab = createBottomTabNavigator(); @@ -31,6 +33,7 @@ const MainTabNavigator = () => { component={ProfileScreen} // You can add icons here if desired /> + diff --git a/example/src/screens/class-based/ClassProfile.tsx b/example/src/screens/class-based/ClassProfile.tsx index d41b07af..744c6cc8 100644 --- a/example/src/screens/class-based/ClassProfile.tsx +++ b/example/src/screens/class-based/ClassProfile.tsx @@ -1,86 +1,195 @@ -import React, { useMemo } from 'react'; -import { SafeAreaView, ScrollView, View, StyleSheet } from 'react-native'; -import { useNavigation, RouteProp } from '@react-navigation/native'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import React, { Component } from 'react'; +import { + SafeAreaView, + ScrollView, + View, + StyleSheet, + Text, + Alert, +} from 'react-native'; +import { RouteProp, NavigationProp } from '@react-navigation/native'; import { jwtDecode } from 'jwt-decode'; import auth0 from '../../api/auth0'; import Button from '../../components/Button'; import Header from '../../components/Header'; import UserInfo from '../../components/UserInfo'; -import { User } from 'react-native-auth0'; +import { User, Credentials, ApiCredentials } from 'react-native-auth0'; import type { ClassDemoStackParamList } from '../../navigation/ClassDemoNavigator'; +import LabeledInput from '../../components/LabeledInput'; +import config from '../../auth0-configuration'; +import Result from '../../components/Result'; type ProfileRouteProp = RouteProp; -type NavigationProp = StackNavigationProp< - ClassDemoStackParamList, - 'ClassProfile' ->; type Props = { route: ProfileRouteProp; + navigation: NavigationProp; }; -const ClassProfileScreen = ({ route }: Props) => { - const navigation = useNavigation(); - const { credentials } = route.params; +interface State { + user: User | null; + result: Credentials | ApiCredentials | object | boolean | null; + error: Error | null; + audience: string; +} - const user = useMemo(() => { +class ClassProfileScreen extends Component { + constructor(props: Props) { + super(props); + const user = this.decodeIdToken(props.route.params.credentials.idToken); + this.state = { + user, + result: null, + error: null, + audience: config.audience, + }; + } + + decodeIdToken = (idToken: string): User | null => { try { - return jwtDecode(credentials.idToken); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + return jwtDecode(idToken); + } catch { return null; } - }, [credentials.idToken]); + }; - const onLogout = async () => { + runTest = async (testFn: () => Promise, title: string) => { + this.setState({ error: null, result: null }); + try { + const res = await testFn(); + this.setState({ result: res ?? { success: `${title} completed` } }); + } catch (e) { + this.setState({ error: e as Error }); + } + }; + + onLogout = async () => { try { await auth0.webAuth.clearSession(); await auth0.credentialsManager.clearCredentials(); - navigation.goBack(); + this.props.navigation.goBack(); } catch (e) { - console.log('Logout error: ', e); + Alert.alert('Error', (e as Error).message); } }; - const onNavigateToApiTests = () => { - navigation.navigate('ClassApiTests', { - accessToken: credentials.accessToken, - }); - }; + render() { + const { user, result, error, audience } = this.state; + const { accessToken } = this.props.route.params.credentials; - return ( - -
- - -