diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 314be529..ea641f31 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -1,61 +1,61 @@ name: PUBLISH DOCS on: - workflow_dispatch: - workflow_call: - # or set up your own custom triggers + workflow_dispatch: + workflow_call: + # or set up your own custom triggers permissions: - contents: write # allows the 'Commit' step without tokens + contents: write # allows the 'Commit' step without tokens jobs: - get_history: # create an artifact from the existing documentation builds - runs-on: ubuntu-latest - steps: - - name: get the gh-pages repo - uses: actions/checkout@v5 - with: - ref: gh-pages - - - name: remove all symbolic links from root if present - run: | - find . -maxdepth 1 -type l -delete - - - name: tar the existing docs from root - run: | - tar -cvf documentation.tar ./ - - - name: create a document artifact - uses: actions/upload-artifact@v5 - with: - name: documentation - path: documentation.tar - - build: # builds the distribution and then the documentation - needs: get_history - runs-on: ubuntu-latest - steps: - - name: Checkout src - uses: actions/checkout@v5 - - - name: Download the existing documents artifact - uses: actions/download-artifact@v6 - with: - name: documentation - - run: rm -rf ./docs # delete previous docs folder present - - run: mkdir ./docs # create an empty docs folder - - run: tar -xf documentation.tar -C ./docs - - run: rm -f documentation.tar - - - name: Setup - uses: ./.github/actions/setup - - - name: Build documents - run: yarn docs #set up 'docs' build script in your package.json - - - name: Remove all the symbolic links from docs folder - run: find ./docs -type l -delete - - - name: Run cleanup and manage document versions - run: node scripts/manage-doc-versions.js - - - name: Deploy to github pages using gh-pages - run: npx gh-pages -d docs + get_history: # create an artifact from the existing documentation builds + runs-on: ubuntu-latest + steps: + - name: get the gh-pages repo + uses: actions/checkout@v5 + with: + ref: gh-pages + + - name: remove all symbolic links from root if present + run: | + find . -maxdepth 1 -type l -delete + + - name: tar the existing docs from root + run: | + tar -cvf documentation.tar ./ + + - name: create a document artifact + uses: actions/upload-artifact@v5 + with: + name: documentation + path: documentation.tar + + build: # builds the distribution and then the documentation + needs: get_history + runs-on: ubuntu-latest + steps: + - name: Checkout src + uses: actions/checkout@v5 + + - name: Download the existing documents artifact + uses: actions/download-artifact@v6 + with: + name: documentation + - run: rm -rf ./docs # delete previous docs folder present + - run: mkdir ./docs # create an empty docs folder + - run: tar -xf documentation.tar -C ./docs + - run: rm -f documentation.tar + + - name: Setup + uses: ./.github/actions/setup + + - name: Build documents + run: yarn docs #set up 'docs' build script in your package.json + + - name: Remove all the symbolic links from docs folder + run: find ./docs -type l -delete + + - name: Run cleanup and manage document versions + run: node scripts/manage-doc-versions.js + + - name: Deploy to github pages using gh-pages + run: npx gh-pages -d docs diff --git a/README.md b/README.md index 6b48b604..07c0ba14 100644 --- a/README.md +++ b/README.md @@ -212,9 +212,9 @@ To use the SDK with Expo, configure the app at build time by providing the `doma > :info: If you want to switch between multiple domains in your app, refer [here](https://github.com/auth0/react-native-auth0/blob/master/EXAMPLES.md#domain-switching) -| API | Description | -| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| domain | Mandatory: Provide the Auth0 domain that can be found at the [Application Settings](https://manage.auth0.com/#/applications) | +| API | Description | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| domain | Mandatory: Provide the Auth0 domain that can be found at the [Application Settings](https://manage.auth0.com/#/applications) | | customScheme | Optional: Custom scheme to build the callback URL with. The value provided here should be passed to the `customScheme` option parameter of the `authorize` and `clearSession` methods. The custom scheme should be a unique, all lowercase value with no special characters. To use Android App Links, set this value to `"https"`. | **Note:** When using `customScheme: "https"` for Android App Links, the plugin will automatically add `android:autoVerify="true"` to the intent-filter in your Android manifest to enable automatic verification of App Links. diff --git a/android/src/main/java/com/auth0/react/A0Auth0Module.kt b/android/src/main/java/com/auth0/react/A0Auth0Module.kt index 79f2dde7..cd686cf3 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.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.authentication.storage.CredentialsManagerException @@ -290,6 +291,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 58c764ae..9f8ed786 100644 --- a/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt +++ b/android/src/main/oldarch/com/auth0/react/A0Auth0Spec.kt @@ -51,6 +51,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 ( - -
- - -