Site icon May's Notes

React Native 使用 Amplify 進行身份驗證(2) – Google 登入

React Native logo

這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。https://ithelp.ithome.com.tw/users/20136637/ironman/6408

Google 登入

https://docs.amplify.aws/lib/auth/social/q/platform/react-native

創建 OAuth 用戶端 ID

首先需要在 Google Console 新增 OAuth 用戶端 ID:

創建完會獲得 OAuth Client ID 和 OAuth Client Secret,這兩個等等會需要用到:

如果沒有 domain 的話就點擊右側的 Actions – Create Cognito domain 新建一個:

可以自定義 domain:

創建 Identity provider

為了能夠使用 Google 登入,需要要在 user pool 中新建 Identity provider

Amazon Cognito -> User pool -> Sign-in experience -> Add Identity provider

選擇 Google

設置 Google federation

全部都設置完成之後記得要在專案中將最新的設置拉下來:

amplify pull --appId <app_id> --envName <env_name>

完成 Google 登入

使用 Amplify 提供的第三方登入 API Auth.federatedSignIn,provider 設為 CognitoHostedUIIdentityProvider.Google

import { CognitoHostedUIIdentityProvider } from '@aws-amplify/auth'

Auth.federatedSignIn({
 provider: CognitoHostedUIIdentityProvider.Google
})}

完整寫法如下:

import React, { memo, useState, useEffect } from 'react'
import { Button } from 'react-native-paper'
import { Auth, Hub } from 'aws-amplify'
import { CognitoHostedUIIdentityProvider } from '@aws-amplify/auth'
import { UserData } from 'amazon-cognito-identity-js'

export const LoginScreen = ({ navigation }: LoginScreenProps) => {
  const [user, setUser] = useState<UserData | null>(null)

  useEffect(() => {
    const unsubscribe = Hub.listen('auth', ({ payload: { event, data } }) => {
      switch (event) {
        case 'signIn':
          getUser()
          break
        case 'signOut':
          setUser(null)
          break
        case 'signIn_failure':
          console.log('Sign in failure', data)
          break
      }
    });

    getUser()

    return unsubscribe
  }, [])

  const getUser = async () => {
    try {
      const currentUser = await Auth.currentAuthenticatedUser()
      setUser(currentUser)
    } catch (error) {
      console.error(error)
      console.log("Not signed in")
    }
  }

  return (
    <>
      <StyleText>User: {user ? JSON.stringify(user?.attributes) : 'None'}</StyleText>

      <Button mode="contained" onPress={() => Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google })}>
        Login with Google
      </Button>
    </>
  )
}

登入後無法自動跳轉回應用

現在使用 Google 登入成功後依然無法跳轉回 App,這是因為還需要設置 Linking module

iOS

打開 xcodeproj – Build Settings – 搜索 Header Search Paths 雙擊點選新增:

$(PODS_ROOT)/../../node_modules/react-native/Libraries/LinkingIOS

新增完之後打開 AppDelegate.mm 加入以下內容

#import <React/RCTLinkingManager.h>

- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

重新打開 APP 就可以了。

Android

android/app/src/main/AndroidManifest.xml 添加下面這段,還有 android:launchMode 要設為 singleTask

<intent-filter android:label="filter_react_native">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="myapp" />
</intent-filter>

完整的身份驗證流程範例

可以使用 Context + Provider 的方式在應用中共享用戶資料、登入狀態以及登入、登出方法:

<em>// _types_/auth.ts</em>
import { UserData } from 'amazon-cognito-identity-js'
import { LoginSchemaType } from '@/helpers/validation'

export type AuthContextData = {
  user: UserData | null
  isAuthenticated: boolean
  onLogin: (data: LoginSchemaType) => Promise<void>
  onLogout: () => Promise<void>
  onSocialLogin: (type: 'Google') => void
}
<em>// context/AuthContext.tsx</em>
import { createContext } from 'react'
import { AuthContextData } from '@/_types_'

export const AuthContext = createContext<AuthContextData>({} as AuthContextData)
<em>// provider/AuthProvider.tsx</em>
import { useEffect, useState } from 'react'
import { Auth, Hub } from 'aws-amplify'
import { CognitoHostedUIIdentityProvider } from '@aws-amplify/auth'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AuthContext } from '@/context'
import { UserData } from 'amazon-cognito-identity-js'
import { LoginSchemaType } from '@/helpers/validation'

interface AuthProviderProps {
  children: React.ReactNode
}

export const AuthProvider = ({ children }: AuthProviderProps) => {
  const [user, setUser] = useState<UserData | null>(null)

  <em>// 監聽身份驗證事件</em>
  useEffect(() => {
    const unsubscribe = Hub.listen('auth', ({ payload: { event, data } }) => {
      switch (event) {
        case 'signIn':
          getUser()
          break
        case 'signOut':
          setUser(null)
          break
        case 'signIn_failure':
          console.log('Sign in failure', data)
          break
      }
    })
    getUser()
    return unsubscribe
  }, [])

  <em>// 獲取當前用戶資料</em>
  const getUser = async () => {
    try {
      const currentUser = await Auth.currentAuthenticatedUser()
      setUser(currentUser)
    } catch (error) {
      console.error(error)
      setUser(null)
    }
  }

  <em>// 使用第三方登入</em>
  const onSocialLogin = (type: 'Google') => {
    Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider[type] })
  }

  <em>// 普通登入</em>
  const onLogin = async (data: LoginSchemaType) => {
    try {
      const { email, password } = data
      await Auth.signIn(email, password)
    } catch (error: any) {
      console.log(error)
    }
  }

  <em>// 登出</em>
  const onLogout = async () => {
    await Auth.signOut()
  }

  const contextValue = {
    user,
    isAuthenticated: !!user,
    onLogin,
    onLogout,
    onSocialLogin
  }

  return (
    <AuthContext.Provider value={contextValue}>
      {children}
    </AuthContext.Provider>
  )
}

新建一個 useAuth hook:

<em>// hooks/useAuth.tsx</em>
import { useContext } from 'react'
import { AuthContext } from '@/context'

export const useAuth = () => {
  const context = useContext(AuthContext)

  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider')
  }

  return context
}

接著將 Provider 包在應用路由的外層:

<em>// App.tsx</em>
import { Amplify } from 'aws-amplify'
import { createStackNavigator } from '@react-navigation/stack'
import { AuthStackNavigator } from '@/navigation/AuthStackNavigator'
import { LoginStackNavigator } from '@/navigation/LoginStackNavigator'
import { useAuth } from '@/hooks'
import { AuthProvider } from '@/provider'
import awsconfig from './aws-exports'

const Stack = createStackNavigator()

Amplify.configure(awsconfig)

const App = () => {
  const { isAuthenticated } = useAuth()

  return (
    <AuthProvider>
      <Stack.Navigator>
        {isAuthenticated ? (
          <Stack.Screen
            name="AuthStack"
            component={AuthStackNavigator}
            options={{ headerShown: false }}
          />
        ) : (
          <Stack.Screen
            name="LoginStack"
            component={LoginStackNavigator}
            options={{ headerShown: false }}
          />
        )}
      </Stack.Navigator>
    </AuthProvider>
  )
}

export default App

完成以上工作之後就可以在登入、註冊等組件中使用 useAuth hook 來獲取與身份驗證相關的 data 和 function。比如我需要點按鈕登入,那麽就從 useAuth 中獲取 login function 調用:

import { useState } from 'react'
import { Button } from 'react-native'
import { useAuth } from '@/hooks'
<em>// ...</em>

export const LoginScreen = () => {
  const { login } = useAuth()
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  <em>// ...</em>

  return (
    <em>// ...</em>
    <Button
      style={styles.loginButton}
      title={t('Login.SignIn')}
      onPress={() => login({ email, password })}
    />
  )
}

這只是一個簡單的身份驗證方法,還有很多細節待完善,僅供參考。

忘記密碼&重設密碼

amplify 有提供 Auth.forgotPassword(email) 的 API 可以寄出重設密碼的驗證碼,獲取驗證碼之後使用 Auth.forgotPasswordSubmit(email, code, newPassword) 即可重設密碼。

import { useState, useEffect } from 'react'
import { TextInput, Button } from 'react-native'
import { Auth } from 'aws-amplify'
<em>// ...</em>

export const ForgotPasswordScreen = ({ navigation }: ForgotPasswordScreenProps) => {
  const { t } = useTranslation()
  const [isSent, setIsSent] = useState(false)
  const [canResend, setCanResend] = useState(true)
  const [email, setEmail] = useState('')
  const [code, setCode] = useState('')
  const [newPassword, setNewPassword] = useState('')

  <em>// 模擬15秒後可以再次寄出</em>
  useEffect(() => {
    if (canResend) return
    setTimeout(() => {
      setCanResend(true)
    }, 15000)
  }, [canResend])

  const forgotPassword = async () => {
    try {
      await Auth.forgotPassword(email)
      setIsSent(true)
      setCanResend(false)
    } catch (err) {
      console.log(err)
      setIsSent(false)
      setCanResend(true)
    }
  }

  const forgotPasswordSubmit = async () => {
    try {
      const res = await Auth.forgotPasswordSubmit(
        email,
        code,
        newPassword
      )
      if (res === 'SUCCESS') {
        navigation.navigate('Login')
      }
    } catch (err) {
      console.log(err)
    }
  }

  return (
      <Column v="center" style={styles.container}>
        <Column style={styles.form}>
          <TextInput
            value={email}
            textContentType="emailAddress"
            keyboardType="email-address"
            placeholder={t('Login.Email')}
            onChangeText={setEmail}
          />
          <Row h="center" style={styles.sendCode}>
            <Button
              title={t(isSent ? 'Login.ResendCode' : 'Login.SendResetCode')}
              disabled={!canResend}
              onPress={forgotPassword}
            />
          </Row>
          <TextInput
            value={code}
            editable={isSent}
            style={{ opacity: !isSent ? 0.5 : 1 }}
            placeholder={t('Login.ConfirmCode')}
            onChangeText={setCode}
          />
          <TextInput
            value={newPassword}
            editable={isSent}
            style={{ opacity: !isSent ? 0.5 : 1 }}
            placeholder={t('Login.NewPassword')}
            onChangeText={setNewPassword}
          />
        </Column>
        <Button
          title={t('Login.ResetPassword')}
          style={styles.button}
          disabled={!isSent}
          onPress={forgotPasswordSubmit}
        />
      </Column>
  )
}

參考資料

Exit mobile version