Skip to content

Commit

Permalink
feat: sign in with Google (#2579)
Browse files Browse the repository at this point in the history
Closes #2515
  • Loading branch information
kueda authored Dec 19, 2024
1 parent 1d340eb commit ebba764
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 85 deletions.
4 changes: 4 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ IOS_PROVISIONING_PROFILE_NAME="provisioning profile name"
IOS_SHARE_BUNDLE_ID="share bundle ID"
IOS_SHARE_PROVISIONING_PROFILE_NAME="share provisioning profile name"
SLACK_URL="Slack webhook URL"

# Third-party Sign in
GOOGLE_WEB_CLIENT_ID=your-google-web-client-id
GOOGLE_IOS_CLIENT_ID=your-google-ios-client-id
31 changes: 30 additions & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
PODS:
- AppAuth (1.7.6):
- AppAuth/Core (= 1.7.6)
- AppAuth/ExternalUserAgent (= 1.7.6)
- AppAuth/Core (1.7.6)
- AppAuth/ExternalUserAgent (1.7.6):
- AppAuth/Core
- boost (1.83.0)
- BVLinearGradient (2.8.3):
- React-Core
Expand All @@ -21,6 +27,14 @@ PODS:
- ReactCommon/turbomodule/core (= 0.73.7)
- fmt (6.2.1)
- glog (0.3.5)
- GoogleSignIn (7.1.0):
- AppAuth (< 2.0, >= 1.7.3)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GTMAppAuth (4.1.1):
- AppAuth/Core (~> 1.7)
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher/Core (3.5.0)
- hermes-engine (0.73.7):
- hermes-engine/Pre-built (= 0.73.7)
- hermes-engine/Pre-built (0.73.7)
Expand Down Expand Up @@ -1151,6 +1165,9 @@ PODS:
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- RNGoogleSignin (13.1.0):
- GoogleSignIn (~> 7.1)
- React-Core
- RNLocalize (3.1.0):
- React-Core
- RNPermissions (4.1.5):
Expand Down Expand Up @@ -1276,6 +1293,7 @@ DEPENDENCIES:
- "RNFlashList (from `../node_modules/@shopify/flash-list`)"
- RNFS (from `../node_modules/react-native-fs`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- "RNGoogleSignin (from `../node_modules/@react-native-google-signin/google-signin`)"
- RNLocalize (from `../node_modules/react-native-localize`)
- RNPermissions (from `../node_modules/react-native-permissions`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
Expand All @@ -1290,7 +1308,11 @@ DEPENDENCIES:

SPEC REPOS:
trunk:
- AppAuth
- fmt
- GoogleSignIn
- GTMAppAuth
- GTMSessionFetcher
- libevent
- MMKV
- MMKVCore
Expand Down Expand Up @@ -1461,6 +1483,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-fs"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNGoogleSignin:
:path: "../node_modules/@react-native-google-signin/google-signin"
RNLocalize:
:path: "../node_modules/react-native-localize"
RNPermissions:
Expand All @@ -1485,6 +1509,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
Expand All @@ -1493,6 +1518,9 @@ SPEC CHECKSUMS:
FBReactNativeSpec: 40b791f4a1df779e7e4aa12c000319f4f216d40a
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801
Expand Down Expand Up @@ -1571,6 +1599,7 @@ SPEC CHECKSUMS:
RNFlashList: 818cb6cff1f47cabe1acd6298c98af1a39b9b18c
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNGestureHandler: e262eeb792addec0705a116456f210ee1be0dcd0
RNGoogleSignin: ba93c1137f8d5cebdd39b04f493fd212ddf5ecd6
RNLocalize: 080849cb8a824d9f759b8a5ae00c8321d46dbed0
RNPermissions: f14c20f4eb7a20fff611ad9f467da7bb5872ac4f
RNReanimated: b158619f02f1384a5be9e1203b58e0e4a80407d7
Expand All @@ -1586,4 +1615,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: eff4b75123af5d6680139a78c055b44ad37c269b

COCOAPODS: 1.15.2
COCOAPODS: 1.14.3
12 changes: 10 additions & 2 deletions ios/iNaturalistReactNative/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@
<string>inaturalistmobile</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.796686868523-4m5ne09fa17p5me04s1qdlsnci9e8qeo</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>132</string>
Expand All @@ -55,6 +63,8 @@
<string>Add existing sounds to your observations.</string>
<key>NSCameraUsageDescription</key>
<string>iNaturalist Next uses the camera to add photos to your observations of nature.</string>
<key>NSHumanReadableCopyright</key>
<string>© iNaturalist</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We do not intentionally request this permission. If you are seeing this, please take a screenshot and email it to help@inaturalist.org</string>
<key>NSLocationWhenInUseUsageDescription</key>
Expand Down Expand Up @@ -94,7 +104,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSHumanReadableCopyright</key>
<string>© iNaturalist</string>
</dict>
</plist>
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const config: Config = {
preset: "react-native",
setupFiles: [
"./node_modules/react-native-gesture-handler/jestSetup.js",
"./node_modules/@react-native-google-signin/google-signin/jest/build/jest/setup.js",
"<rootDir>/tests/jest.setup.js"
],
globalSetup: "<rootDir>/tests/jest.globalSetup.js",
Expand Down
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@react-native-community/hooks": "^3.0.0",
"@react-native-community/netinfo": "^11.3.1",
"@react-native-community/slider": "^4.5.0",
"@react-native-google-signin/google-signin": "^13.1.0",
"@react-native-picker/picker": "^2.7.2",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/drawer": "^6.6.15",
Expand Down
90 changes: 9 additions & 81 deletions src/components/LoginSignUp/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { appleAuth, AppleButton, AppleError } from "@invertase/react-native-apple-authentication";
import { AppleButton } from "@invertase/react-native-apple-authentication";
import { RouteProp, useNavigation, useRoute } from "@react-navigation/native";
import classnames from "classnames";
import {
Expand All @@ -9,21 +9,16 @@ import { t } from "i18next";
import { RealmContext } from "providers/contexts.ts";
import React, { useEffect, useRef, useState } from "react";
import {
Alert,
Platform,
TextInput,
TouchableWithoutFeedback
} from "react-native";
import Realm from "realm";
import { log } from "sharedHelpers/logger";
import useKeyboardInfo from "sharedHooks/useKeyboardInfo";
import colors from "styles/tailwindColors";

import {
authenticateUser,
authenticateUserByAssertion
} from "./AuthenticationService";
import { authenticateUser } from "./AuthenticationService";
import Error from "./Error";
import { signInWithApple, signInWithGoogle } from "./loginFormHelpers";
import LoginSignUpInputField from "./LoginSignUpInputField";

const { useRealm } = RealmContext;
Expand All @@ -42,85 +37,12 @@ type ParamList = {
LoginFormParams: LoginFormParams
}

interface AppleAuthError {
code: AppleError;
}

const APPLE_BUTTON_STYLE = {
maxWidth: 500,
height: 45, // You must specify a height
marginTop: 10
};

const logger = log.extend( "LoginForm" );

function showSignInWithAppleFailed() {
Alert.alert(
t( "Sign-in-with-Apple-Failed" ),
t( "If-you-have-an-existing-account-try-sign-in-reset" )
);
}

async function signInWithApple( realm: Realm ) {
// Request sign in w/ apple. This should pop up some system UI for signing
// in
let appleAuthRequestResponse;
try {
appleAuthRequestResponse = await appleAuth.performRequest( {
requestedOperation: appleAuth.Operation.LOGIN,
// Note: it appears putting FULL_NAME first is important, see issue #293
requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL]
} );
} catch ( appleAuthRequestError ) {
if ( ( appleAuthRequestError as AppleAuthError ).code === appleAuth.Error.CANCELED ) {
// The user canceled sign in, no need to log
return false;
}
logger.error( "Apple auth request failed", appleAuthRequestError );
showSignInWithAppleFailed();
return false;
}

// Check if auth was successful
const credentialState = await appleAuth.getCredentialStateForUser(
appleAuthRequestResponse.user
);

// If it was, send the identity token to iNat for verification and iNat
// auth
if ( credentialState === appleAuth.State.AUTHORIZED ) {
// Note that we're supporting an irregular assertion that is a JSON object
// w/ the actual identityToken (which is itself a JSON Web Token), and
// the user's name, which we only get from Apple the *first* time that
// grant permission, so the server cannot access it when it verifies the
// token
const assertion = JSON.stringify( {
id_token: appleAuthRequestResponse.identityToken,
name: t( "apple-full-name", {
namePrefix: appleAuthRequestResponse?.fullName?.namePrefix,
givenName: appleAuthRequestResponse?.fullName?.givenName,
middleName: appleAuthRequestResponse?.fullName?.middleName,
nickname: appleAuthRequestResponse?.fullName?.nickname,
familyName: appleAuthRequestResponse?.fullName?.familyName,
nameSuffix: appleAuthRequestResponse?.fullName?.nameSuffix
} )
} );
try {
await authenticateUserByAssertion( "apple", assertion, realm );
} catch ( authenticateUserByAssertionError ) {
logger.error( "Assertion with Apple token failed", authenticateUserByAssertionError );
showSignInWithAppleFailed();
return false;
}
return true;
}
// We only get here if the user does not grant access... I think, so no need
// to log an error
logger.info( "Apple auth failed, credentialState: ", credentialState );
showSignInWithAppleFailed();
return false;
}

const LoginForm = ( {
hideFooter
}: Props ) => {
Expand Down Expand Up @@ -276,6 +198,12 @@ const LoginForm = ( {
onPress={() => logIn( async ( ) => signInWithApple( realm ) )}
/>
) }
<Button
text={t( "Sign-in-with-Google" )}
onPress={() => logIn( async ( ) => signInWithGoogle( realm ) )}
disabled={loading}
className="mt-3"
/>
{!hideFooter && (
<Body1
className={classnames(
Expand Down
Loading

0 comments on commit ebba764

Please sign in to comment.