Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions android/src/main/java/com/auth0/react/A0Auth0Module.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {

Expand Down Expand Up @@ -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<String, String>()
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<APICredentials, CredentialsManagerException> {
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<String, String> {
return mapOf("bundleIdentifier" to reactContext.applicationInfo.packageName)
}
Expand Down
22 changes: 22 additions & 0 deletions android/src/main/java/com/auth0/react/ApiCredentialsParser.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
14 changes: 14 additions & 0 deletions android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions example/src/navigation/MainTabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<MainTabParamList>();
Expand All @@ -31,6 +33,7 @@ const MainTabNavigator = () => {
component={ProfileScreen}
// You can add icons here if desired
/>
<Tab.Screen name="Credentials" component={CredentialsScreen} />
<Tab.Screen name="Api" component={ApiScreen} />
<Tab.Screen name="More" component={MoreScreen} />
</Tab.Navigator>
Expand Down
215 changes: 162 additions & 53 deletions example/src/screens/class-based/ClassProfile.tsx
Original file line number Diff line number Diff line change
@@ -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<ClassDemoStackParamList, 'ClassProfile'>;
type NavigationProp = StackNavigationProp<
ClassDemoStackParamList,
'ClassProfile'
>;

type Props = {
route: ProfileRouteProp;
navigation: NavigationProp<ClassDemoStackParamList, 'ClassProfile'>;
};

const ClassProfileScreen = ({ route }: Props) => {
const navigation = useNavigation<NavigationProp>();
const { credentials } = route.params;
interface State {
user: User | null;
result: Credentials | ApiCredentials | object | boolean | null;
error: Error | null;
audience: string;
}

const user = useMemo<User | null>(() => {
class ClassProfileScreen extends Component<Props, State> {
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<User>(credentials.idToken);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return jwtDecode<User>(idToken);
} catch {
return null;
}
}, [credentials.idToken]);
};

const onLogout = async () => {
runTest = async (testFn: () => Promise<any>, 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 (
<SafeAreaView style={styles.container}>
<Header title="Class-Based Profile" />
<ScrollView contentContainerStyle={styles.content}>
<UserInfo user={user} />
<Button
onPress={onLogout}
title="Log Out"
style={styles.logoutButton}
/>
<View style={styles.spacer} />
<Button onPress={onNavigateToApiTests} title="Go to API Tests" />
</ScrollView>
</SafeAreaView>
);
};
return (
<SafeAreaView style={styles.container}>
<Header title="Class-Based Profile & Credentials" />
<ScrollView contentContainerStyle={styles.content}>
<UserInfo user={user} />
<Result title="Last Action Result" result={result} error={error} />

<Section title="Primary Credentials">
<Button
onPress={() =>
this.runTest(
() => auth0.credentialsManager.getCredentials(),
'Get Credentials'
)
}
title="credentialsManager.getCredentials()"
/>
<Button
onPress={() =>
this.runTest(
() => auth0.credentialsManager.hasValidCredentials(),
'Check Valid Credentials'
)
}
title="credentialsManager.hasValidCredentials()"
/>
<Button
onPress={() =>
this.runTest(
() => auth0.credentialsManager.clearCredentials(),
'Clear Credentials'
)
}
title="credentialsManager.clearCredentials()"
style={styles.destructiveButton}
/>
</Section>

<Section title="API Credentials (MRRT)">
<LabeledInput
label="API Audience"
value={audience}
onChangeText={(text) => this.setState({ audience: text })}
autoCapitalize="none"
/>
<Button
onPress={() =>
this.runTest(
() => auth0.credentialsManager.getApiCredentials(audience),
'Get API Credentials'
)
}
title="credentialsManager.getApiCredentials()"
/>
<Button
onPress={() =>
this.runTest(
() => auth0.credentialsManager.clearApiCredentials(audience),
'Clear API Credentials'
)
}
title="credentialsManager.clearApiCredentials()"
style={styles.secondaryButton}
/>
</Section>

<Section title="Navigation & Logout">
<Button
onPress={() =>
this.props.navigation.navigate('ClassApiTests', { accessToken })
}
title="Go to API Tests"
/>
<Button
onPress={this.onLogout}
title="Log Out"
style={styles.destructiveButton}
/>
</Section>
</ScrollView>
</SafeAreaView>
);
}
}

const Section = ({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) => (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.buttonGroup}>{children}</View>
</View>
);

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
content: {
container: { flex: 1, backgroundColor: '#FFFFFF' },
content: { padding: 16, paddingBottom: 50, alignItems: 'center' },
section: {
width: '100%',
marginBottom: 20,
borderWidth: 1,
borderColor: '#E0E0E0',
borderRadius: 8,
padding: 16,
alignItems: 'center',
},
spacer: {
height: 16,
},
logoutButton: {
backgroundColor: '#424242',
},
sectionTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 12 },
buttonGroup: { gap: 10 },
destructiveButton: { backgroundColor: '#424242' },
secondaryButton: { backgroundColor: '#FF9800' },
});

export default ClassProfileScreen;
Loading