-
Notifications
You must be signed in to change notification settings - Fork 487
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(mobile): add integration with gateway backend for notifications feature #4878
base: dev
Are you sure you want to change the base?
Changes from all commits
50af7ca
67d6a07
d75da8e
07e4b56
178d4d0
34dc5e7
ef8b296
df34acc
497cce7
81d76fa
3e83d6f
6166897
dcece0e
9259893
bdb6c84
6a7264e
8158986
fd1ca3f
f5d09ab
7bf5d31
8109e9b
8364bb4
18690de
3fd07b7
c8ae5be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
import { registerRootComponent } from 'expo'; | ||
import './shim' | ||
import { registerRootComponent } from 'expo' | ||
|
||
import App from './App'; | ||
import App from './App' | ||
|
||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App); | ||
// It also ensures that whether you load the app in Expo Go or in a native build, | ||
// the environment is set up appropriately | ||
registerRootComponent(App); | ||
registerRootComponent(App) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { install } from 'react-native-quick-crypto' | ||
install() | ||
|
||
import { ethers } from 'ethers' | ||
|
||
import crypto from 'react-native-quick-crypto' | ||
|
||
ethers.randomBytes.register((length) => { | ||
return new Uint8Array(crypto.randomBytes(length)) | ||
}) | ||
|
||
ethers.computeHmac.register((algo, key, data) => { | ||
return crypto.createHmac(algo, key).update(data).digest() | ||
}) | ||
|
||
ethers.pbkdf2.register((passwd, salt, iter, keylen, algo) => { | ||
return crypto.pbkdf2Sync(passwd, salt, iter, keylen, algo) | ||
}) | ||
|
||
ethers.sha256.register((data) => { | ||
return crypto.createHash('sha256').update(data).digest() | ||
}) | ||
|
||
ethers.sha512.register((data) => { | ||
return crypto.createHash('sha512').update(data).digest() | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,53 @@ | ||
import React, { useCallback } from 'react' | ||
import React, { useCallback, useEffect, useRef } from 'react' | ||
import { AppState } from 'react-native' | ||
import { useAppDispatch } from '@/src/store/hooks' | ||
import NotificationsService from '@/src/services/notifications/NotificationService' | ||
import { toggleAppNotifications } from '@/src/store/notificationsSlice' | ||
import { useDelegateKey } from '@/src/hooks/useDelegateKey' | ||
import useNotifications from '@/src/hooks/useNotifications' | ||
import { useAuthGetNonceV1Query } from '@safe-global/store/gateway/AUTO_GENERATED/auth' | ||
|
||
import { useAppSelector, useAppDispatch } from '@/src/store/hooks' | ||
import { selectAppNotificationStatus, toggleAppNotifications } from '@/src/store/notificationsSlice' | ||
import { NotificationView } from '@/src/features/Notifications/components/NotificationView' | ||
|
||
export const NotificationsContainer = () => { | ||
const dispatch = useAppDispatch() | ||
const isAppNotificationEnabled = useAppSelector(selectAppNotificationStatus) | ||
const { enableNotifications, isAppNotificationEnabled } = useNotifications() | ||
const { data } = useAuthGetNonceV1Query() | ||
const { createDelegate, deleteDelegate, error } = useDelegateKey() | ||
const appState = useRef(AppState.currentState) | ||
|
||
const handleToggleAppNotifications = useCallback(() => { | ||
dispatch(toggleAppNotifications(!isAppNotificationEnabled)) | ||
const handleToggleAppNotifications = useCallback(async () => { | ||
const deviceNotificationStatus = await NotificationsService.isDeviceNotificationEnabled() | ||
|
||
if (!deviceNotificationStatus && !isAppNotificationEnabled) { | ||
await NotificationsService.requestPushNotificationsPermission() | ||
} else if (deviceNotificationStatus && !isAppNotificationEnabled) { | ||
enableNotifications() | ||
await createDelegate(data) | ||
} else { | ||
await deleteDelegate() | ||
if (!error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens if for whatever reason this fails? Is the user still going to receive push notifications? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed the order of the calls to avoid disable redux without the confirmation of BE. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Jonathansoufer I have my doubts that this if(!error) check is goin to work reliably. the useDelegateKey hook returns an error. here you await the deleteDelegate and thje deleteDelegate function might set the error, but if does the notificationContainer needs to rerender. I'm not sure how the flow of the code is going to be. Isn't delegate always be null, since you are in the callback and at the moment the callback has started it is null π€· Wouldn't it be easier of the deleteDelegate and createDelegate return the error or throw? |
||
dispatch(toggleAppNotifications(!isAppNotificationEnabled)) | ||
} | ||
} | ||
}, [isAppNotificationEnabled]) | ||
|
||
useEffect(() => { | ||
const subscription = AppState.addEventListener('change', async (nextAppState) => { | ||
if (appState.current.match(/inactive|background/) && nextAppState === 'active') { | ||
const deviceNotificationStatus = await NotificationsService.isDeviceNotificationEnabled() | ||
if (deviceNotificationStatus && !isAppNotificationEnabled) { | ||
enableNotifications() | ||
await createDelegate(data) | ||
} | ||
} | ||
|
||
appState.current = nextAppState | ||
}) | ||
|
||
return () => { | ||
subscription.remove() | ||
} | ||
}, [isAppNotificationEnabled]) | ||
|
||
return <NotificationView onChange={handleToggleAppNotifications} value={isAppNotificationEnabled} /> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { useState, useCallback } from 'react' | ||
import { Wallet } from 'ethers' | ||
|
||
import { AuthNonce } from '@safe-global/store/gateway/AUTO_GENERATED/auth' | ||
import { AddressInfo } from '@safe-global/store/gateway/AUTO_GENERATED/safes' | ||
|
||
import Logger from '@/src/utils/logger' | ||
import { Address } from '@/src/types/address' | ||
import { selectActiveSafe } from '@/src/store/activeSafeSlice' | ||
import { useAppDispatch, useAppSelector } from '@/src/store/hooks' | ||
import { useSign } from './useSign' | ||
import { useGTW } from './useGTW' | ||
|
||
import { selectFCMToken } from '../store/notificationsSlice' | ||
import { addDelegatedAddress } from '../store/delegatedSlice' | ||
import { useSiwe } from './useSiwe' | ||
import { getSigner } from '../utils/notifications' | ||
import { DELEGATED_ACCOUNT_TYPE, ERROR_MSG } from '../store/constants' | ||
|
||
export function useDelegateKey(safeOwner?: AddressInfo) { | ||
// Local states | ||
const [loading, setLoading] = useState<boolean>(false) | ||
const [error, setError] = useState<unknown>(null) | ||
const [delegatedAccountType, setDelegatedAccountType] = useState<DELEGATED_ACCOUNT_TYPE>() | ||
|
||
// Custom hooks | ||
const { getPrivateKey, storePrivateKey } = useSign() | ||
const { createDelegatedKeyOnBackEnd, deleteDelegatedKeyOnBackEnd } = useGTW() | ||
const { createSiweMessage } = useSiwe() | ||
// Redux | ||
const dispatch = useAppDispatch() | ||
const activeSafe = useAppSelector(selectActiveSafe) | ||
|
||
// const appSigners = useAppSelector(selectSigners) | ||
const fcmToken = useAppSelector(selectFCMToken) | ||
|
||
// Step 0 - Get the nonce to be included in the message to be sent to the backend | ||
const createDelegate = useCallback(async (data: AuthNonce | undefined) => { | ||
setLoading(true) | ||
setError(null) | ||
|
||
const nonce = data?.nonce | ||
if (!activeSafe || !fcmToken || !nonce) { | ||
throw Logger.info(ERROR_MSG) | ||
} | ||
|
||
try { | ||
// Step 1 - Try to get the safe owner's private key from keychain | ||
const safeOwnerPK = safeOwner && (await getPrivateKey(safeOwner.value)) | ||
|
||
const delegatedAccType = safeOwnerPK ? DELEGATED_ACCOUNT_TYPE.OWNER : DELEGATED_ACCOUNT_TYPE.REGULAR | ||
setDelegatedAccountType(delegatedAccType) | ||
|
||
// Step 2 - Create a new random (delegated) private key | ||
const randomDelegatedAccount = Wallet.createRandom() | ||
|
||
if (!randomDelegatedAccount) { | ||
throw Logger.error(ERROR_MSG, error) | ||
} | ||
|
||
// Step 2.1 - Store the delegated account in the redux store | ||
dispatch( | ||
addDelegatedAddress({ delegatedAddress: randomDelegatedAccount.address as Address, safes: [activeSafe] }), | ||
) | ||
|
||
// Step 2.2 - Store it in the keychain | ||
storePrivateKey(randomDelegatedAccount.address, randomDelegatedAccount.privateKey) | ||
|
||
// Step 2.3 - Define the signer account | ||
const signerAccount = getSigner(safeOwnerPK, randomDelegatedAccount) | ||
|
||
// Step 3 - Create a message following the SIWE standard | ||
const siweMessage = createSiweMessage({ | ||
address: signerAccount.address, | ||
chainId: Number(activeSafe.chainId), | ||
nonce, | ||
statement: 'SafeWallet wants you to sign in with your Ethereum account', | ||
}) | ||
|
||
// Step 4 - Triggers the backend to create the delegate | ||
await createDelegatedKeyOnBackEnd({ | ||
safeAddress: activeSafe.address, | ||
signer: signerAccount, | ||
message: siweMessage, | ||
chainId: activeSafe.chainId, | ||
fcmToken, | ||
delegatedAccount: randomDelegatedAccount, | ||
delegatedAccountType: delegatedAccType, | ||
}) | ||
} catch (err) { | ||
Logger.error('useDelegateKey: Something went wrong', err) | ||
setError(err) | ||
return | ||
} finally { | ||
setLoading(false) | ||
} | ||
}, []) | ||
|
||
const deleteDelegate = useCallback(async () => { | ||
setLoading(true) | ||
setError(null) | ||
try { | ||
await deleteDelegatedKeyOnBackEnd(activeSafe) | ||
} catch (err) { | ||
Logger.error('useDelegateKey: Something went wrong', err) | ||
setError(err) | ||
return | ||
} finally { | ||
setLoading(false) | ||
} | ||
}, []) | ||
|
||
return { | ||
loading, | ||
error, | ||
createDelegate, | ||
deleteDelegate, | ||
delegatedAccountType, | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we already imported the shim in the index.js. Do we need it here as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed. This was part of the tests we did while resolving the ethers/crypto issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you forget to push? github is still showing it.