diff --git a/docs/docs/introduction.md b/docs/docs/introduction.md index fb0b17ff..9a6359f7 100644 --- a/docs/docs/introduction.md +++ b/docs/docs/introduction.md @@ -10,7 +10,9 @@ Get started by installing the dependencies in your application ```sh yarn add react-native-app-auth ``` + Or + ```sh npm install react-native-app-auth --save ``` @@ -115,9 +117,79 @@ your `Info.plist` as follows: You need to retain the auth session, in order to continue the authorization flow from the redirect. Follow these steps: -`RNAppAuth` will call on the given app's delegate via `[UIApplication sharedApplication].delegate`. -Furthermore, `RNAppAuth` expects the delegate instance to conform to the protocol `RNAppAuthAuthorizationFlowManager`. -Make `AppDelegate` conform to `RNAppAuthAuthorizationFlowManager` with the following changes to `AppDelegate.h`: +###### For react-native >= 0.77 + +As of `react-native@0.77`, the `AppDelegate` template is now written in Swift. + +In order to bridge to the existing Objective-C code that this package utilizes, you need to create a bridging header file. To do so: + +1. Create a new file in your project called `AppDelegate+RNAppAuth.h`. (It can be called anything, but it must end with `.h`) +2. Add the following code to the file: + +``` +#import "RNAppAuthAuthorizationFlowManager.h" +``` + +3. Ensure that your XCode "Build Settings" has the following `Objective-C Bridging Header` path set to the file you just created. For example, it make look something like: `$(SRCROOT)/AppDelegate+RNAppAuth.h` + +4. Add the following code to `AppDelegate.swift` to support React Navigation deep linking and overriding browser behavior in the authorization process + +```swift +@main +class AppDelegate: UIResponder, UIApplicationDelegate, + RNAppAuthAuthorizationFlowManager { + //... existing code... + // Required by RNAppAuthAuthorizationFlowManager protocol + public weak var authorizationFlowManagerDelegate: + RNAppAuthAuthorizationFlowManagerDelegate? + //... existing code... + + // Handle OAuth redirect URL + func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + if let authorizationFlowManagerDelegate = self + .authorizationFlowManagerDelegate + { + if authorizationFlowManagerDelegate.resumeExternalUserAgentFlow(with: url) + { + return true + } + } + return false + } +} +``` + +5. Add the following code to `AppDelegate.swift` to support universal links: + +```swift + + + func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + + // Handle Universal-Link–style OAuth redirects first + if userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let delegate = authorizationFlowManagerDelegate, + delegate.resumeExternalUserAgentFlow(with: userActivity.webpageURL) + { + return true + } + + // Fall back to React Native’s own Linking logic + return RCTLinkingManager.application( + application, + continue: userActivity, + restorationHandler: restorationHandler + ) + } +``` ##### For react-native >= 0.68 @@ -199,45 +271,8 @@ If you want to support universal links, add the following to `AppDelegate.m` und + } ``` -#### Integration of the library with a Swift iOS project - -The approach mentioned should work with Swift. In this case one should make `AppDelegate` conform to `RNAppAuthAuthorizationFlowManager`. Note that this is not tested/guaranteed by the maintainers. - -Steps: - -1. `swift-Bridging-Header.h` should include a reference to `#import "RNAppAuthAuthorizationFlowManager.h`, like so: - -```h -#import -#import -#import -#import -#import "RNAppAuthAuthorizationFlowManager.h" // <-- Add this header -#if DEBUG -#import -// etc... -``` - -2. `AppDelegate.swift` should implement the `RNAppAuthAuthorizationFlowManager` protocol and have a handler for url deep linking. The result should look something like this: - -```swift -@UIApplicationMain -class AppDelegate: UIApplicationDelegate, RNAppAuthAuthorizationFlowManager { //<-- note the additional RNAppAuthAuthorizationFlowManager protocol - public weak var authorizationFlowManagerDelegate: RNAppAuthAuthorizationFlowManagerDelegate? // <-- this property is required by the protocol - //"open url" delegate function for managing deep linking needs to call the resumeExternalUserAgentFlowWithURL method - func application( - _ app: UIApplication, - open url: URL, - options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool { - return authorizationFlowManagerDelegate?.resumeExternalUserAgentFlow(with: url) ?? false - } -} -``` - ### Android Setup -**Note:** for RN >= 0.57, you will get a warning about compile being obsolete. To get rid of this warning, use [patch-package](https://github.com/ds300/patch-package) to replace compile with implementation [as in this PR](https://github.com/FormidableLabs/react-native-app-auth/pull/242) - we're not deploying this right now, because it would break the build for RN < 57. - To setup the Android project, you need to add redirect scheme manifest placeholder: To [capture the authorization redirect](https://github.com/openid/AppAuth-android#capturing-the-authorization-redirect), diff --git a/examples/demo/_bundle/config b/examples/demo/.bundle/config similarity index 100% rename from examples/demo/_bundle/config rename to examples/demo/.bundle/config diff --git a/examples/demo/.gitignore b/examples/demo/.gitignore index 0cab2ac6..de999559 100644 --- a/examples/demo/.gitignore +++ b/examples/demo/.gitignore @@ -20,7 +20,7 @@ DerivedData *.hmap *.ipa *.xcuserstate -ios/.xcode.env.local +**/.xcode.env.local # Android/IntelliJ # @@ -33,6 +33,7 @@ local.properties .cxx/ *.keystore !debug.keystore +.kotlin/ # node.js # @@ -56,7 +57,7 @@ yarn-error.log *.jsbundle # Ruby / CocoaPods -/ios/Pods/ +**/Pods/ /vendor/bundle/ # Temporary files created by Metro to check the health of the file watcher @@ -64,3 +65,11 @@ yarn-error.log # testing /coverage + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/examples/demo/App.js b/examples/demo/App.js deleted file mode 100644 index 3ba5f365..00000000 --- a/examples/demo/App.js +++ /dev/null @@ -1,176 +0,0 @@ -import React, {useState, useCallback, useMemo} from 'react'; -import {Alert} from 'react-native'; -import { - authorize, - refresh, - revoke, - prefetchConfiguration, -} from 'react-native-app-auth'; -import { - Page, - Button, - ButtonContainer, - Form, - FormLabel, - FormValue, - Heading, -} from './components'; - -const configs = { - identityserver: { - issuer: 'https://demo.duendesoftware.com', - clientId: 'interactive.public', - redirectUrl: 'io.identityserver.demo:/oauthredirect', - additionalParameters: {}, - scopes: ['openid', 'profile', 'email', 'offline_access'], - - // serviceConfiguration: { - // authorizationEndpoint: 'https://demo.duendesoftware.com/connect/authorize', - // tokenEndpoint: 'https://demo.duendesoftware.com/connect/token', - // revocationEndpoint: 'https://demo.duendesoftware.com/connect/revoke' - // } - }, - auth0: { - issuer: 'https://rnaa-demo.eu.auth0.com', - clientId: 'VtXdAoGFcYzZ3IJaNy4UIS5RNHhdbKbU', - redirectUrl: 'rnaa-demo://oauthredirect', - additionalParameters: {}, - scopes: ['openid', 'profile', 'email', 'offline_access'], - - // serviceConfiguration: { - // authorizationEndpoint: 'https://samples.auth0.com/authorize', - // tokenEndpoint: 'https://samples.auth0.com/oauth/token', - // revocationEndpoint: 'https://samples.auth0.com/oauth/revoke' - // } - }, -}; - -const defaultAuthState = { - hasLoggedInOnce: false, - provider: '', - accessToken: '', - accessTokenExpirationDate: '', - refreshToken: '', -}; - -const App = () => { - const [authState, setAuthState] = useState(defaultAuthState); - React.useEffect(() => { - prefetchConfiguration({ - warmAndPrefetchChrome: true, - connectionTimeoutSeconds: 5, - ...configs.auth0, - }); - }, []); - - const handleAuthorize = useCallback(async provider => { - try { - const config = configs[provider]; - const newAuthState = await authorize({ - ...config, - connectionTimeoutSeconds: 5, - iosPrefersEphemeralSession: true, - }); - - setAuthState({ - hasLoggedInOnce: true, - provider: provider, - ...newAuthState, - }); - } catch (error) { - Alert.alert('Failed to log in', error.message); - } - }, []); - - const handleRefresh = useCallback(async () => { - try { - const config = configs[authState.provider]; - const newAuthState = await refresh(config, { - refreshToken: authState.refreshToken, - }); - - setAuthState(current => ({ - ...current, - ...newAuthState, - refreshToken: newAuthState.refreshToken || current.refreshToken, - })); - } catch (error) { - Alert.alert('Failed to refresh token', error.message); - } - }, [authState]); - - const handleRevoke = useCallback(async () => { - try { - const config = configs[authState.provider]; - await revoke(config, { - tokenToRevoke: authState.accessToken, - sendClientId: true, - }); - - setAuthState({ - provider: '', - accessToken: '', - accessTokenExpirationDate: '', - refreshToken: '', - }); - } catch (error) { - Alert.alert('Failed to revoke token', error.message); - } - }, [authState]); - - const showRevoke = useMemo(() => { - if (authState.accessToken) { - const config = configs[authState.provider]; - if (config.issuer || config.serviceConfiguration.revocationEndpoint) { - return true; - } - } - return false; - }, [authState]); - - return ( - - {authState.accessToken ? ( -
- accessToken - {authState.accessToken} - accessTokenExpirationDate - {authState.accessTokenExpirationDate} - refreshToken - {authState.refreshToken} - scopes - {authState.scopes.join(', ')} -
- ) : ( - - {authState.hasLoggedInOnce ? 'Goodbye.' : 'Hello, stranger.'} - - )} - - - {!authState.accessToken ? ( - <> -