Site icon May's Notes

React Native 使用 Amplify 進行身份驗證(3) – 記住登入狀態、自動登入

React Native logo

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

主要的身份驗證部分在前兩天已經寫完了,這篇要分享的是我在過程中遇到的問題以及解決方式。

修改驗證信內容

這是驗證信預設的內容:

如果想要修改的話,可以到 Amazon Cognito -> User pools -> Messaging 最底下有個 Message templates 找到 Verification message template 編輯即可

重啟 APP 後不會記住登入狀態

當使用 Auth.signIn 登入後關閉 App 重啟,Auth.currentAuthenticatedUser() 會回傳 The user is not authenticated,它不會記住關閉 App 前的登入狀態。

尋找解法的時候我看到了這個 issue,發文的人和我的疑問是一樣的,下面有回覆說是因為 Amplify 所使用的 AsyncStorage 過舊,新版本已經不再支持,所以重新覆寫一下 Storage 的部分即可:

<em>// services/storage.ts</em>
import AsyncStorage from '@react-native-async-storage/async-storage'
<em>/*
 * Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
 * the License. A copy of the License is located at
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
 * and limitations under the License.
 */</em>

const MEMORY_KEY_PREFIX = '@StorageService:'
let dataMemory: any = {}

<em>/** @class */</em>
export class MemoryStorageNew {
  static syncPromise: Promise<any> | null = null
  <em>/**
   * This is used to set a specific item in storage
   * @param {string} key - the key for the item
   * @param {object} value - the value
   * @returns {string} value that was set
   */</em>
  static setItem(key: any, value: any) {
    AsyncStorage.setItem(MEMORY_KEY_PREFIX + key, value)
    dataMemory[key] = value
    return dataMemory[key]
  }

  <em>/**
   * This is used to get a specific key from storage
   * @param {string} key - the key for the item
   * This is used to clear the storage
   * @returns {string} the data item
   */</em>
  static getItem(key: string) {
    return Object.prototype.hasOwnProperty.call(dataMemory, key)
      ? dataMemory[key]
      : undefined
  }

  <em>/**
   * This is used to remove an item from storage
   * @param {string} key - the key being set
   * @returns {string} value - value that was deleted
   */</em>
  static removeItem(key: string) {
    AsyncStorage.removeItem(MEMORY_KEY_PREFIX + key)
    return delete dataMemory[key]
  }

  <em>/**
   * This is used to clear the storage
   * @returns {string} nothing
   */</em>
  static clear() {
    dataMemory = {}
    return dataMemory
  }

  <em>/**
   * Will sync the MemoryStorage data from AsyncStorage to storageWindow MemoryStorage
   * @returns {void}
   */</em>
  static sync() {
    if (!MemoryStorageNew.syncPromise) {
      MemoryStorageNew.syncPromise = new Promise((res, rej) => {
        AsyncStorage.getAllKeys((errKeys, keys) => {
          if (errKeys) rej(errKeys)
          const memoryKeys = keys!.filter(key =>
            key.startsWith(MEMORY_KEY_PREFIX)
          )
          AsyncStorage.multiGet(memoryKeys, (err, stores) => {
            if (err) rej(err)
            stores!.map((result, index, store) => {
              const key = store[index][0]
              const value = store[index][1]
              const memoryKey = key.replace(MEMORY_KEY_PREFIX, '')
              dataMemory[memoryKey] = value
            })
            res(true)
          })
        })
      })
    }
    return MemoryStorageNew.syncPromise
  }
}

<em>/** @class */</em>
export default class StorageHelper {
  private storageWindow: any
  <em>/**
   * This is used to get a storage object
   * @returns {object} the storage
   */</em>
  constructor() {
    this.storageWindow = MemoryStorageNew
  }

  <em>/**
   * This is used to return the storage
   * @returns {object} the storage
   */</em>
  getStorage() {
    return this.storageWindow
  }
}

在 Amplify.configure 設置 Auth.storage 為 MemoryStorageNew

import { MemoryStorageNew } from '@/services/storage'
import awsconfig from './aws-exports'

Amplify.configure({
  ...awsconfig,
  ssr: true,
  Auth: {
    storage: MemoryStorageNew,
  }
})

實作”記住我”功能

接續上面的繼續說,如果今天登入表單可以自行勾選是否要記住登入狀態,那就需要稍作調整,因為上面的例子是無論如何都記憶住登入狀態。

在網上查了很多資料,得到的都是 React 的解法,就是根據是否要記住登入狀態來修改 storage 為 localStorage 或者 sessionStorage,但是 RN 上無法這樣做。

所以我在調用 getUser 時傳入 isAutomatic 來判斷是不是應用剛啟用正在檢驗身份,如果是並且 rememberMe 為 true 或者調用 Auth.signIn 的話才要自動登入。

import AsyncStorage from '@react-native-async-storage/async-storage'

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

const [user, setUser] = useState<any>(null)
const [rememberMe, setRememberMe] = useState(false)

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

const getUser = async (isAutomatic: boolean) => {
    try {
      const rememberMe = await AsyncStorage.getItem('rememberMe')
      setRememberMe(rememberMe === 'true')
      <em>// 勾選"記住我" 或使用 login function 登入</em>
      const shouldKeepLoggedIn = isAutomatic && rememberMe === 'true'
      if (shouldKeepLoggedIn || !isAutomatic) {
        const currentUser = await Auth.currentAuthenticatedUser()
        setUser(currentUser)
      }
    } catch (error) {
      console.error(error)
      setUser(null)
    }
}

const login = async (data) => {
    try {
      const { email, password } = data
      await Auth.signIn(email, password)

      <em>// 保存"記住我"的勾選結果</em>
      if (rememberMe) {
        await AsyncStorage.setItem('rememberMe', 'true')
      } else {
        await AsyncStorage.setItem('rememberMe', 'false')
      }
    } catch (error: any) {
        console.log(error)
    }
}

參考資料

Exit mobile version