diff --git a/README.md b/README.md index 1b1ed8f8..6180b9e9 100644 --- a/README.md +++ b/README.md @@ -228,49 +228,114 @@ This library works on MacOS 10.15+ if using in conjunction with [react-native-ma ### Web (not react-native-web, but that may come as a follow-on, this is pure web at the moment) +### WebView + #### 1. Initial set-up -- Ensure you follow the android steps above. -- Install the [web counterpart](https://github.com/A-Tokyo/react-apple-signin-auth) `yarn add react-apple-signin-auth` in your web project. +- Make sure to correctly configure your Apple developer account to allow for proper web based authentication. +- Install the [React Native WebView](https://github.com/react-native-webview/react-native-webview) `yarn add react-native-webview` (or) `npm i react-native-webview` in your project. [Link native dependencies](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md#2-link-native-dependencies). +- Your backend needs to implement web based authentification -#### 2. Implement the login process on web +#### 2. Implement the login process ```js -import AppleSignin from 'react-apple-signin-auth'; - -/** Apple Signin button */ -const MyAppleSigninButton = ({ ...rest }) => ( - { - console.log(response); - // { - // "authorization": { - // "state": "[STATE]", - // "code": "[CODE]", - // "id_token": "[ID_TOKEN]" - // }, - // "user": { - // "email": "[EMAIL]", - // "name": { - // "firstName": "[FIRST_NAME]", - // "lastName": "[LAST_NAME]" - // } - // } - // } - }} - /> -); - -export default MyAppleSigninButton; + +// App.js +import React from "react"; +import { + View, + TouchableWithoutFeedback, + Text +} from "react-native"; +import { + appleAuth, + appleAuthAndroid, + AppleAuthWebView // Internaly using WebView +} from "@invertase/react-native-apple-authentication"; + +function onAppleLoginWebViewButtonPress() { + + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms + const appleAuthConfig = { + // The Service ID you registered with Apple + clientId: "com.example.client-web", + + // Return URL added to your Apple dev console. It must still match the URL you provided to Apple. + redirectUri: "https://example.com/auth/callback", + + // The type of response requested - code, id_token, or both. + responseType: "code id_token", + + // The amount of user information requested from Apple. + scope: "name email" + + // Random nonce value that will be SHA256 hashed before sending to Apple. + // nonce: nonce, + + // Unique state value used to prevent CSRF attacks. A UUID will be generated if nothing is provided. + // state: state + }; + + this.setState( + { + appleAuthConfig: appleAuthConfig + } + ); +} + +function onAppleAuthResponse(responseContent) { + + // Handle your server response (after login - apple redirects to your server url) + console.log("onAppleAuthResponse responseContent", responseContent); +} + +// If no iOS or Android is supported than we use webView fallback with custom button +function App() { + + render() { + const appleAuthConfig = this.state.appleAuthConfig; + + if (appleAuthConfig) { + return ( + { + // return ( + // + // ); + // } + // } + onResponse={this.onAppleAuthResponse} + /> + ); + } + } + + return ( + + + + { + // It makes sense to show the native buttons in a real app, + // something like the code below, but we are just demonstrating web login here so it is commented out + + // (appleAuth.isSupported || appleAuthAndroid.isSupported) ? ( + // + // ) // else add webView view button + } + + + + Sign in with Apple + + + + ); +} ``` #### 3. Verify serverside @@ -278,6 +343,9 @@ export default MyAppleSigninButton; - See [Serverside Verification](#serverside-verification) - Ensure that you pass the clientID as the web service ID, not the native app bundle. Since the project utilizes the service ID for authenticating web and android. +### WebView Config +- https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms + ## Serverside verification #### Nonce diff --git a/example/app.android.js b/example/app.android.js index faff15f7..d941dd10 100644 --- a/example/app.android.js +++ b/example/app.android.js @@ -16,21 +16,22 @@ * */ -import React from 'react'; -import { StyleSheet, View, Image, Text } from 'react-native'; -import { AppleButton, appleAuthAndroid } from '@invertase/react-native-apple-authentication'; +import React, { useState } from 'react'; +import { StyleSheet, View, Image, TouchableOpacity, Text } from 'react-native'; +import { AppleButton, appleAuthAndroid, AppleAuthWebView } from '@invertase/react-native-apple-authentication'; import 'react-native-get-random-values'; import { v4 as uuid } from 'uuid' import appleLogoWhite from './images/apple_logo_white.png'; import appleLogoBlack from './images/apple_logo_black.png'; +import { getAppleAuthConfig, parseAppleAuthResponse } from './app.shared'; - -export default RootComponent = () => { +export default function RootComponent() { + const [appleAuthConfig, setAppleAuthConfig] = useState(null); const doAppleLogin = async () => { // Generate secure, random values for state and nonce const rawNonce = uuid(); - const state = uuid(); + const rawState = uuid(); try { // Initialize the module @@ -63,7 +64,7 @@ export default RootComponent = () => { // [OPTIONAL] // Unique state value used to prevent CSRF attacks. A UUID will be generated if nothing is provided. - state, + state: rawState, }); const response = await appleAuthAndroid.signIn(); @@ -96,70 +97,85 @@ export default RootComponent = () => { } }; + if (appleAuthConfig) { + return ( + { + setAppleAuthConfig(null); + parseAppleAuthResponse(responseContent); + } + } + /> + ); + } + return ( - {appleAuthAndroid.isSupported && ( - - Buttons + { + appleAuthAndroid.isSupported && ( + + Buttons - Continue Styles - doAppleLogin()} - leftView={( - - )} - /> - doAppleLogin()} - leftView={( - - )} - /> - doAppleLogin()} - leftView={( - - )} - /> + Continue Styles + doAppleLogin()} + leftView={( + + )} + /> + doAppleLogin()} + leftView={( + + )} + /> + doAppleLogin()} + leftView={( + + )} + /> Sign-in Styles { )} - {!appleAuthAndroid.isSupported && ( - Sign In with Apple requires Android 4.4 (API 19) or higher. - )} + { + !appleAuthAndroid.isSupported && ( + + + Sign In with Apple requires Android 4.4 (API 19) or higher. + + + But in such cases you can always use apple webView sign in! + + + ) + } + + { + { + setAppleAuthConfig(getAppleAuthConfig()); + } + }> + + + WebView sign in with Apple + + + + } ); } diff --git a/example/app.ios.js b/example/app.ios.js index 38e84210..de9bcbf9 100644 --- a/example/app.ios.js +++ b/example/app.ios.js @@ -17,8 +17,9 @@ */ import React, { useState, useEffect } from 'react'; -import { StyleSheet, View, Text } from 'react-native'; -import { appleAuth, AppleButton } from '@invertase/react-native-apple-authentication'; +import { StyleSheet, View, Text, TouchableOpacity} from 'react-native'; +import { appleAuth, AppleButton, AppleAuthWebView } from '@invertase/react-native-apple-authentication'; +import { getAppleAuthConfig, parseAppleAuthResponse } from './app.shared'; /** * You'd technically persist this somewhere for later use. @@ -93,6 +94,8 @@ async function onAppleButtonPress(updateCredentialStateForUser) { export default function RootComponent() { const [credentialStateForUser, updateCredentialStateForUser] = useState(-1); + const [appleAuthConfig, setAppleAuthConfig] = useState(null); + useEffect(() => { if (!appleAuth.isSupported) return; @@ -112,64 +115,103 @@ export default function RootComponent() { }); }, []); - if (!appleAuth.isSupported) { + if (appleAuthConfig) { return ( - - Apple Authentication is not supported on this device. - + { + setAppleAuthConfig(null); + parseAppleAuthResponse(responseContent); + } + } + /> ); } return ( - Credential State - {credentialStateForUser} - - Buttons - Continue Styles - onAppleButtonPress(updateCredentialStateForUser)} - /> - onAppleButtonPress(updateCredentialStateForUser)} - /> - onAppleButtonPress(updateCredentialStateForUser)} - /> - Sign-in Styles - onAppleButtonPress(updateCredentialStateForUser)} - /> - onAppleButtonPress(updateCredentialStateForUser)} - /> - onAppleButtonPress(updateCredentialStateForUser)} - /> + { + appleAuth.isSupported && ( + + Credential State + {credentialStateForUser} + + Buttons + Continue Styles + onAppleButtonPress(updateCredentialStateForUser)} + /> + onAppleButtonPress(updateCredentialStateForUser)} + /> + onAppleButtonPress(updateCredentialStateForUser)} + /> + Sign-in Styles + onAppleButtonPress(updateCredentialStateForUser)} + /> + onAppleButtonPress(updateCredentialStateForUser)} + /> + onAppleButtonPress(updateCredentialStateForUser)} + /> + + ) + } + + { + !appleAuth.isSupported && ( + + + Apple Authentication is not supported on this device. + + + But in such cases you can always use apple webView sign in! + + + ) + } + + { + { + setAppleAuthConfig(getAppleAuthConfig()); + } + }> + + + WebView sign in with Apple + + + + } ); } diff --git a/example/app.shared.js b/example/app.shared.js new file mode 100644 index 00000000..09efccac --- /dev/null +++ b/example/app.shared.js @@ -0,0 +1,60 @@ +import "react-native-get-random-values"; +import { v4 as uuid } from "uuid"; + +export function getAppleAuthConfig() { + + // Generate secure, random values for state and nonce + const nonce = uuid(); + const state = uuid(); + + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms + const appleAuthConfig = { + // The Service ID you registered with Apple + clientId: "com.example.client-web", + + // Return URL added to your Apple dev console. It must still match the URL you provided to Apple. + redirectUri: "https://example.com/auth/callback", + + // The type of response requested - code, id_token, or both. + responseType: "code id_token", + + // [OPTIONAL] + // The amount of user information requested from Apple - code id_token, code, id_token + scope: "name email", + + // [OPTIONAL] + // Random nonce value that will be SHA256 hashed before sending to Apple. + nonce: nonce, + + // [OPTIONAL] + // Unique state value used to prevent CSRF attacks. A UUID will be generated if nothing is provided. + state: state + }; + + return appleAuthConfig; +} + +export function parseAppleAuthResponse(responseContent) { + + // Handle your server response (after login - apple redirects to your server url) + console.log("parseAppleAuthResponse responseContent", responseContent); + + try { + + // We expecting json response from a server + const serverResponse = JSON.parse(responseContent); + + // Next code depents what your server returns on error. + // If it is json error pbject then check it. + // On success my server return a json with {jwt: "some value"} + const jwt = serverResponse.jwt; + + if (jwt) { + console.log("parseAppleAuthResponse responseContent jwt", jwt); + } else { + console.error("parseAppleAuthResponse responseContent does not contain expected jwt"); + } + } catch (error) { + console.error("parseAppleAuthResponse responseContent is not a valid json", error); + } +} diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a6859dcc..0281c8c6 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -284,6 +284,8 @@ PODS: - glog - react-native-get-random-values (1.9.0): - React-Core + - react-native-webview (13.6.2): + - React-Core - React-NativeModulesApple (0.72.6): - React-callinvoker - React-Core @@ -397,7 +399,7 @@ PODS: - React-Core - React-jsi - ReactTestApp-Resources (1.0.0-dev) - - RNAppleAuthentication (2.2.2): + - RNAppleAuthentication (2.3.0): - React-Core - SocketRocket (0.6.1) - Yoga (1.14.0) @@ -425,6 +427,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) + - react-native-webview (from `../node_modules/react-native-webview`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -496,6 +499,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" react-native-get-random-values: :path: "../node_modules/react-native-get-random-values" + react-native-webview: + :path: "../node_modules/react-native-webview" React-NativeModulesApple: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" React-perflogger: @@ -564,6 +569,7 @@ SPEC CHECKSUMS: React-jsinspector: 194e32c6aab382d88713ad3dd0025c5f5c4ee072 React-logger: cebf22b6cf43434e471dc561e5911b40ac01d289 react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb + react-native-webview: 8fc09f66a1a5b16bbe37c3878fda27d5982bb776 React-NativeModulesApple: 63505fb94b71e2469cab35bdaf36cca813cb5bfd React-perflogger: e3596db7e753f51766bceadc061936ef1472edc3 React-RCTActionSheet: 17ab132c748b4471012abbcdcf5befe860660485 @@ -584,7 +590,7 @@ SPEC CHECKSUMS: ReactNativeHost: 5caf8c9381f26c453fabbe8c3b87f6a013a3c459 ReactTestApp-DevSupport: b1c7adc1c49c50d3bc1cbe7f23191170bdac0319 ReactTestApp-Resources: 1f512f66574607bcfa614e9c0d30e7a990fecf30 - RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6 + RNAppleAuthentication: e99eaf3c4c01ad8ecb6125dd6f0cfd98871685b5 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: b76f1acfda8212aa16b7e26bcce3983230c82603 diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 98635c33..861e216b 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -284,6 +284,8 @@ PODS: - glog - react-native-get-random-values (1.9.0): - React-Core + - react-native-webview (13.6.2): + - React-Core - React-NativeModulesApple (0.72.5): - React-callinvoker - React-Core @@ -397,7 +399,7 @@ PODS: - React-Core - React-jsi - ReactTestApp-Resources (1.0.0-dev) - - RNAppleAuthentication (2.2.2): + - RNAppleAuthentication (2.3.0): - React-Core - SocketRocket (0.7.0) - Yoga (1.14.0) @@ -425,6 +427,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native-macos/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native-macos/ReactCommon/logger`) - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) + - react-native-webview (from `../node_modules/react-native-webview`) - React-NativeModulesApple (from `../node_modules/react-native-macos/ReactCommon/react/nativemodule/core/platform/ios`) - React-perflogger (from `../node_modules/react-native-macos/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native-macos/Libraries/ActionSheetIOS`) @@ -496,6 +499,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-macos/ReactCommon/logger" react-native-get-random-values: :path: "../node_modules/react-native-get-random-values" + react-native-webview: + :path: "../node_modules/react-native-webview" React-NativeModulesApple: :path: "../node_modules/react-native-macos/ReactCommon/react/nativemodule/core/platform/ios" React-perflogger: @@ -564,6 +569,7 @@ SPEC CHECKSUMS: React-jsinspector: c25519b0c5b03eaedbb8332f9a42b30a0a3d05fe React-logger: 2f13e5bdf0bfe2edc6b0473c4749a96a3a7ac048 react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb + react-native-webview: 8fc09f66a1a5b16bbe37c3878fda27d5982bb776 React-NativeModulesApple: 91f44f048ff46a6df8c2e9018e3ff79497a5438c React-perflogger: 6ccd6df1be652005b9e635df8fae345618466bde React-RCTActionSheet: 4adbdcae93457b2b0d146d9fa8ab7c835f661fb6 @@ -584,7 +590,7 @@ SPEC CHECKSUMS: ReactNativeHost: 5caf8c9381f26c453fabbe8c3b87f6a013a3c459 ReactTestApp-DevSupport: b1c7adc1c49c50d3bc1cbe7f23191170bdac0319 ReactTestApp-Resources: 8539dac0f8d2ef3821827a537e37812104c6ff78 - RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6 + RNAppleAuthentication: e99eaf3c4c01ad8ecb6125dd6f0cfd98871685b5 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: eb938eb0ef4d510b93617486cf6cbe1ad6a94436 diff --git a/example/package.json b/example/package.json index 4bceadf3..7365338f 100644 --- a/example/package.json +++ b/example/package.json @@ -18,7 +18,9 @@ "react": "18.2.0", "react-native": "0.72.6", "react-native-get-random-values": "^1.9.0", - "react-native-macos": "^0.72.0" + "react-native-macos": "^0.72.0", + "react-native-webview": "^13.6.2", + "uuid": "^9.0.1" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/yarn.lock b/example/yarn.lock index c1493b30..6451e3d1 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1215,7 +1215,7 @@ integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== "@invertase/react-native-apple-authentication@../": - version "2.2.2" + version "2.3.0" "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -3022,16 +3022,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -3727,7 +3727,7 @@ internal-slot@^1.0.5: hasown "^2.0.0" side-channel "^1.0.4" -invariant@*, invariant@^2.2.4: +invariant@*, invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -5564,6 +5564,14 @@ react-native-test-app@^2.5.32: semver "^7.3.5" uuid "^8.3.2" +react-native-webview@^13.6.2: + version "13.6.2" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.6.2.tgz#0a9b18793e915add5b5dbdbf32509d7751b49167" + integrity sha512-QzhQ5JCU+Nf2W285DtvCZOVQy/MkJXMwNDYPZvOWQbAOgxJMSSO+BtqXTMA1UPugDsko6PxJ0TxSlUwIwJijDg== + dependencies: + escape-string-regexp "2.0.0" + invariant "2.2.4" + react-native@0.72.6: version "0.72.6" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.72.6.tgz#9f8d090694907e2f83af22e115cc0e4a3d5fa626" @@ -6473,6 +6481,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-to-istanbul@^9.0.1: version "9.1.3" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz#ea456604101cd18005ac2cae3cdd1aa058a6306b" diff --git a/lib/AppleAuthWebView.js b/lib/AppleAuthWebView.js new file mode 100644 index 00000000..48d91e79 --- /dev/null +++ b/lib/AppleAuthWebView.js @@ -0,0 +1,282 @@ +import React from "react"; +import { + View, + ActivityIndicator +} from "react-native"; +import { + WebView +} from "react-native-webview"; + +export default class AppleAuthWebView extends React.PureComponent { + constructor(props) { + super(props); + + const me = this; + const onResponse = props.onResponse; + const config = props.config; // || (props.route && props.route.params && props.route.params.config); + const loadingIndicator = props.loadingIndicator; + const clientId = config.clientId; + const redirectUri = config.redirectUri; + const scope = config.scope; + const responseType = config.responseType; + const state = config.state; + const rawNonce = config.rawNonce; + const nonce = config.nonce; + + // Input data validating + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113 + if (typeof onResponse !== "function") { + throw new Error( + "AppleAuthWebView.constructor 'onResponse' is required and must be a function." + ); + } + + if (loadingIndicator && typeof loadingIndicator !== "function") { + throw new Error( + "AppleAuthWebView.constructor 'loadingIndicator' required as a function, if provided." + ); + } + + if (typeof config !== "object") { + throw new Error( + "AppleAuthWebView.constructor 'config' is required and must be an object." + ); + } else { + if (typeof clientId !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'clientId' is required and must be a string." + ); + } + + if (typeof redirectUri !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'redirectUri' is required and must be a string." + ); + } + + if (typeof scope !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'scope' is required and must be a string." + ); + } else if ( + scope !== "name" && + scope !== "email" && + scope !== "name email" + ) { + throw new Error( + "AppleAuthWebView.constructor 'scope' is invalid. Possible values 'name', 'email', 'name email'" + ); + } + + if (responseType && typeof responseType !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'responseType' required as a string, if provided." + ); + } + + if ( + responseType && + responseType !== "code" && + responseType !== "id_token" && + responseType !== "code id_token" + ) { + throw new Error( + "AppleAuthWebView.constructor 'responseType' is invalid. Possible values 'code', 'id_token', 'code id_token'" + ); + } + + if (state && typeof state !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'state' required as a string, if provided." + ); + } + + if (rawNonce && typeof rawNonce !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'rawNonce' required as a string, if provided." + ); + } + + if (nonce && typeof nonce !== "string") { + throw new Error( + "AppleAuthWebView.constructor 'nonce' required as a string, if provided." + ); + } + } + + me.state = { + config: config, + isAppleInitialWebPageLoading: false, + isRedirectResultContentLoading: false + }; + + me.appleDirectAuthorizationUrl = "https://appleid.apple.com/auth/authorize"; + } + + onNavigationStateChange = (navigationState) => { + const me = this; + const url = navigationState.url; + const loading = navigationState.loading; + const appleAuthUrl = me.appleDirectAuthorizationUrl; + + if ( + url.substr(0, appleAuthUrl.length) === appleAuthUrl && + loading !== me.state.isAppleInitialWebPageLoading + ) { + me.setState( + { + isAppleInitialWebPageLoading: loading + } + ); + + return; + } + + // Loading redirected page + const redirectUri = me.state.config.redirectUri; + + if ( + url.substr(0, redirectUri.length) === redirectUri && + !me.state.isAppleInitialWebPageLoading + ) { + me.setState( + { + isRedirectResultContentLoading: true // To hide our server response + } + ); + + me.webview.injectJavaScript( + ` + function checkContent() { + if (document && document.documentElement) { + window.ReactNativeWebView.postMessage( + JSON.stringify( + { + href: window.location.href, + pageContent: document.documentElement.innerText + } + ) + ); + } else { + setTimeout( + function () { + checkContent(); + }, + 300 + ); + } + } + + checkContent(); + true; + ` + ); + } + } + + onMessageFromRedirectedPage = (event) => { + const me = this; + const message = event.nativeEvent.data; + + try { + const data = JSON.parse(message); + const redirectUri = me.state.config.redirectUri; + + if (data.href.substr(0, redirectUri.length) === redirectUri) { + me.props.onResponse(data.pageContent); + } + } catch (error) { + console.error("AppleAuthWebView.onMessageFromRedirectedPage Cannot parse web page message.", message); + } + } + + /** + * Build an Apple URL that supports `form_post` + * Incorporating Sign in with Apple into Other Platforms + * https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms. + */ + getDirectAuthorizationSourceUri = (config) => { + const me = this; + const scope = config.scope; + const state = config.state; + const nonce = config.nonce; + + return { + uri: me.appleDirectAuthorizationUrl + + "?client_id=" + encodeURIComponent(config.clientId) + + "&redirect_uri=" + encodeURIComponent(config.redirectUri) + + "&response_type=" + encodeURIComponent(config.responseType) + + (scope ? "&scope=" + encodeURIComponent(scope) : "") + + "&response_mode=form_post" + + (state ? "&state=" + encodeURIComponent(state) : "") + + (nonce ? "&nonce=" + encodeURIComponent(nonce) : "") + }; + } + + onComponentRefenceReady = (ref) => { + const me = this; + + me.webview = ref; + // me.webview.stopLoading(); + // me.webview.injectJavaScript(redirectTo); + } + + render() { + const me = this; + const state = me.state; + const isLoading = state.isAppleInitialWebPageLoading || state.isRedirectResultContentLoading; + + return ( + + { + ( + + ) + } + + { + isLoading && ( + me.props.loadingIndicator ? ( + + { + me.props.loadingIndicator() + } + + ) : ( + + ) + ) + } + + ); + } +} diff --git a/lib/index.js b/lib/index.js index aae53935..5287ea64 100644 --- a/lib/index.js +++ b/lib/index.js @@ -18,6 +18,7 @@ import version from './version'; import { NativeModules } from 'react-native'; import AppleAuthModule from './AppleAuthModule'; +import AppleAuthWebView from './AppleAuthWebView'; const { RNAppleAuthModule, RNAppleAuthModuleAndroid } = NativeModules; @@ -45,3 +46,8 @@ export const appleAuthAndroid = RNAppleAuthModuleAndroid ? { Scope: RNAppleAuthModuleAndroid.Scope, ResponseType: RNAppleAuthModuleAndroid.ResponseType, } : {}; + +/** + * WebView + */ +export { default as AppleAuthWebView } from './AppleAuthWebView';