Skip to content
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

Open
wants to merge 22 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50af7ca
feat: add dedicated hook for delegation
Jonathansoufer Feb 4, 2025
67d6a07
chore: move constants to global
Jonathansoufer Feb 4, 2025
d75da8e
fix: typo
Jonathansoufer Feb 4, 2025
07e4b56
feat: create delegate hook
Jonathansoufer Feb 5, 2025
178d4d0
refactor: sending signature
Jonathansoufer Feb 5, 2025
34dc5e7
chore: add siwe lib and utils
Jonathansoufer Feb 7, 2025
ef8b296
fix: add cockie auth on RTK
Jonathansoufer Feb 7, 2025
df34acc
chore: add debug logger
Jonathansoufer Feb 7, 2025
497cce7
feat: add siwe hook
Jonathansoufer Feb 7, 2025
81d76fa
feat: add useDelegate hook
Jonathansoufer Feb 7, 2025
3e83d6f
Merge branch 'dev' into feat/notifications-gtw-integration
Jonathansoufer Feb 7, 2025
6166897
chore: add support to random numbers generation
Jonathansoufer Feb 13, 2025
dcece0e
chore: adds sign w ethereum hook
Jonathansoufer Feb 13, 2025
9259893
chore: adds specific function to return signer
Jonathansoufer Feb 13, 2025
bdb6c84
chore: removes console.log
Jonathansoufer Feb 13, 2025
6a7264e
chore: adds hook to hold interaction w/ gateway
Jonathansoufer Feb 13, 2025
8158986
chore: adds redux structure for delegators
Jonathansoufer Feb 13, 2025
fd1ca3f
chore: adds timeout wrapper
Jonathansoufer Feb 13, 2025
f5d09ab
chore: adds userId
Jonathansoufer Feb 13, 2025
7bf5d31
refactor: useDelegator hook
Jonathansoufer Feb 13, 2025
8109e9b
refactor: fixes key generation and granted rights
Jonathansoufer Feb 18, 2025
8364bb4
Merge branch 'dev' into feat/notifications-gtw-integration
Jonathansoufer Feb 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"react-native-device-info": "^14.0.1",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-gesture-handler": "~2.20.2",
"react-native-get-random-values": "^1.11.0",
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
"react-native-keychain": "^9.2.2",
"react-native-mmkv": "^3.1.0",
"react-native-pager-view": "^6.5.1",
Expand All @@ -103,6 +104,7 @@
"react-redux": "^9.1.2",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"siwe": "^3.0.0",
"tamagui": "^1.117.1",
"timezone-mock": "^1.3.6",
"tsconfig-paths-webpack-plugin": "^4.2.0"
Expand Down Expand Up @@ -135,6 +137,7 @@
"@types/lodash": "^4.17.13",
"@types/node": "^22.9.1",
"@types/react": "~18.3.12",
"@types/react-native-get-random-values": "^1",
compojoom marked this conversation as resolved.
Show resolved Hide resolved
"babel-loader": "^8.4.1",
"eslint": "^9.19.0",
"eslint-config-prettier": "^9.1.0",
Expand Down
176 changes: 176 additions & 0 deletions apps/mobile/src/hooks/useDelegateKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { useState, useCallback } from 'react'
import DeviceInfo from 'react-native-device-info'
import 'react-native-get-random-values'
import { HDNodeWallet, Wallet } from 'ethers'
import { useAuthGetNonceV1Query, useAuthVerifyV1Mutation } from '@safe-global/store/gateway/AUTO_GENERATED/auth'
import {
useNotificationsDeleteSubscriptionV2Mutation,
useNotificationsUpsertSubscriptionsV2Mutation,
} from '@safe-global/store/gateway/AUTO_GENERATED/notifications'

import Logger from '@/src/utils/logger'
import { Address } from '@/src/types/address'
import { selectActiveSafe } from '@/src/store/activeSafeSlice'
import { useAppSelector } from '@/src/store/hooks'
import { useSiwe } from './useSiwe'
import { useSign } from './useSign'
import { isAndroid } from '../config/constants'
import { selectFCMToken } from '../store/notificationsSlice'

const ERROR_MSG = 'useDelegateKey: Something went wrong'

export function useDelegateKey() {
// Local states
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<unknown>(null)

// Queries
const [authVerifyV1] = useAuthVerifyV1Mutation()
const [notificationsUpsertSubscriptionsV2] = useNotificationsUpsertSubscriptionsV2Mutation()
const [notificationsDeleteSubscriptionsV2] = useNotificationsDeleteSubscriptionV2Mutation()
// Custom hooks
const { signMessage } = useSiwe()
const { getPrivateKey } = useSign()

// Redux states
const activeSafe = useAppSelector(selectActiveSafe)
const fcmToken = useAppSelector(selectFCMToken)

// Step 0 - Get the nonce to be included in the message to be sent to the backend
const { data } = useAuthGetNonceV1Query()

const createDelegate = useCallback(
async (ownerAddress: Address) => {
setLoading(true)
setError(null)
const nonce = data?.nonce
// Step 1 - Try to get the owner's private key
const ownerPrivateKey = await getPrivateKey()
compojoom marked this conversation as resolved.
Show resolved Hide resolved

try {
if (!ownerAddress || !activeSafe || !nonce || !fcmToken) {
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
throw Logger.info(ERROR_MSG)
}

// Step 2 - Create a new random (delegated) private key in case the owner's private key is not available
//TODO: Double check if we have a wallet stored already avoiding to create a new one
const signerAccount = ownerPrivateKey ? new Wallet(ownerPrivateKey) : Wallet.createRandom()

if (!signerAccount) {
throw Logger.error(ERROR_MSG, error)
}
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved

// Step 3 - Create a message following the SIWE standard
const siweMessage = `SafeWallet wants you to sign in with your Ethereum account:
${signerAccount.address}

Sign in with Ethereum to the app.

URI: https://safe.global
Version: 1
Chain ID: ${activeSafe.chainId}
Nonce: ${nonce}
Issued At: ${new Date().toISOString()}`
compojoom marked this conversation as resolved.
Show resolved Hide resolved

// Step 4 - Triggers the backend to create the delegate
await createOnBackEnd({
safeAddress: activeSafe.address,
signer: signerAccount,
message: siweMessage,
chainId: activeSafe.chainId,
fcmToken,
})
} catch (err) {
Logger.error('useDelegateKey: Something went wrong', err)
setError(err)
return
} finally {
setLoading(false)
}
},
[data, activeSafe, fcmToken],
)

const deleteDelegate = useCallback(async () => {
setLoading(true)
setError(null)
try {
await deleteOnBackEnd()
} catch (err) {
Logger.error('useDelegateKey: Something went wrong', err)
setError(err)
return
} finally {
setLoading(false)
}
}, [])

const createOnBackEnd = useCallback(
Jonathansoufer marked this conversation as resolved.
Show resolved Hide resolved
async ({
safeAddress,
signer,
message,
chainId,
fcmToken,
}: {
safeAddress: Address
signer: HDNodeWallet | Wallet
message: string
chainId: string
fcmToken: string
}) => {
const signature = await signMessage({ signer, message })
try {
await authVerifyV1({
siweDto: {
message,
signature,
},
})

const deviceUuid = await DeviceInfo.getUniqueId()

await notificationsUpsertSubscriptionsV2({
upsertSubscriptionsDto: {
cloudMessagingToken: fcmToken,
safes: [
{
chainId,
address: safeAddress,
notificationTypes: ['MESSAGE_CONFIRMATION_REQUEST', 'CONFIRMATION_REQUEST'],
},
],
deviceType: isAndroid ? 'ANDROID' : 'IOS',
deviceUuid,
},
})
} catch (err) {
Logger.error('CreateDelegateFailed', err)
setError(err)
return
}
},
[],
)

const deleteOnBackEnd = useCallback(async () => {
try {
await notificationsDeleteSubscriptionsV2({
deviceUuid: await DeviceInfo.getUniqueId(),
chainId: activeSafe.chainId,
safeAddress: activeSafe.address,
})
} catch (err) {
Logger.error('DeleteDelegateFailed', err)
setError(err)
return
}
}, [])

return {
loading,
error,
createDelegate,
deleteDelegate,
}
}
4 changes: 2 additions & 2 deletions apps/mobile/src/hooks/useSign/useSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export function useSign() {
await Keychain.setGenericPassword(
'signer_address',
JSON.stringify({
encryptyedPassword: encryptyedPrivateKey.encryptedText,
iv: encryptyedPrivateKey.iv,
encryptedPassword: encryptedPrivateKey.encryptedText,
iv: encryptedPrivateKey.iv,
}),
{
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE,
Expand Down
36 changes: 36 additions & 0 deletions apps/mobile/src/hooks/useSiwe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { HDNodeWallet, Wallet } from 'ethers'
import { useCallback } from 'react'
import { SiweMessage } from 'siwe'

interface SiweMessageProps {
address: string
chainId: number
nonce: string
statement: string
}

export function useSiwe() {
const createSiweMessage = useCallback(({ address, chainId, nonce, statement }: SiweMessageProps) => {
const message = new SiweMessage({
address,
chainId,
domain: 'global.safe.mobileapp',
statement,
nonce,
uri: 'https://safe.global',
version: '1',
issuedAt: new Date().toISOString(),
})
return message.prepareMessage()
}, [])

const signMessage = useCallback(async ({ signer, message }: { signer: HDNodeWallet | Wallet; message: string }) => {
const signature = await signer.signMessage(message)
return signature
}, [])

return {
createSiweMessage,
signMessage,
}
}
24 changes: 14 additions & 10 deletions apps/mobile/src/services/notifications/FCMService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class FCMService {
async saveFCMToken(): Promise<void> {
try {
const fcmToken = await messaging().getToken()

Logger.info('FCMService :: fcmToken', fcmToken)

if (fcmToken) {
store.dispatch(savePushToken(fcmToken))
}
Expand Down Expand Up @@ -52,16 +55,17 @@ class FCMService {
}

async registerAppWithFCM(): Promise<void> {
if (!messaging().registerDeviceForRemoteMessages) {
await messaging()
.registerDeviceForRemoteMessages()
.then((status: unknown) => {
Logger.info('registerDeviceForRemoteMessages status', status)
})
.catch((error) => {
Logger.error('registerAppWithFCM: Something went wrong', error)
})
}
// if (!messaging().registerDeviceForRemoteMessages) {
console.log('registerAppWithFCM :: CALLED')
await messaging()
.registerDeviceForRemoteMessages()
.then((status: unknown) => {
Logger.info('registerDeviceForRemoteMessages status', status)
})
.catch((error) => {
Logger.error('registerAppWithFCM: Something went wrong', error)
})
// }
}
}
export default new FCMService()
3 changes: 3 additions & 0 deletions apps/mobile/src/store/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,6 @@ export enum PressActionId {
}

export const LAUNCH_ACTIVITY = 'global.safe.mobileapp.ui.MainActivity'

export const asymmetricKey = 'safe'
export const keychainGenericPassword = 'safeuser'
compojoom marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 6 additions & 0 deletions packages/store/src/gateway/cgwClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const getBaseUrl = () => {
}
export const rawBaseQuery = fetchBaseQuery({
baseUrl: '/',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'Set-Cookie': 'HttpOnly;Secure;SameSite=None',
},
Comment on lines +15 to +19
Copy link
Contributor

@compojoom compojoom Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this block necessary? Especially the set-cookie header is a response header. It shouldn't do anything here?

})

export const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (
Expand Down
Loading
Loading