Skip to content
Open
10 changes: 10 additions & 0 deletions packages/@divvi/mobile/locales/base/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,16 @@
"title": "Unsupported events",
"description": "{{dappName}} specified some events that are not supported by {{appName}}. Some features may not work as expected."
}
},
"smartAccountConversion": {
"title": "Convert to Smart Account",
"convertButton": "Continue With Converting",
"continueWithoutConverting": "Continue Without Converting",
"descriptionRequired": "{{dappName}} requires smart wallet capabilities for this transaction. Your account will be converted to a smart account when you send this transaction, enabling advanced features like atomic transactions.",
"descriptionOptional": "{{dappName}} can use smart wallet capabilities for this transaction. Your account will be converted to a smart account when you send this transaction, enabling advanced features like atomic transactions.",
"notificationTitle": "Smart Account Benefits",
"notificationDescriptionRequired": "This transaction requires smart wallet capabilities. If you deny conversion, the transaction will be cancelled.",
"notificationDescriptionOptional": "Smart accounts enable advanced features like atomic transactions and paymaster services. You can still proceed without conversion."
}
},
"sessionInfo": "This application may ask to perform the following actions:",
Expand Down
4 changes: 4 additions & 0 deletions packages/@divvi/mobile/src/navigator/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Currency } from 'src/utils/currencies'
import { type SerializablePreparedTransactionsPossible } from 'src/viem/preparedTransactionSerialization'
import { ActionRequestProps } from 'src/walletConnect/screens/ActionRequest'
import { SessionRequestProps } from 'src/walletConnect/screens/SessionRequest'
import { SmartAccountConversionRequestProps } from 'src/walletConnect/screens/SmartAccountConversionRequest'
import { WalletConnectRequestType } from 'src/walletConnect/types'

// Typed nested navigator params
Expand Down Expand Up @@ -322,6 +323,9 @@ export type StackParamList = {
| ({
type: WalletConnectRequestType.Session
} & SessionRequestProps)
| ({
type: WalletConnectRequestType.SmartAccountConversion
} & SmartAccountConversionRequestProps)
| { type: WalletConnectRequestType.TimeOut }
[Screens.WalletConnectSessions]: undefined
[Screens.WalletSecurityPrimer]: undefined
Expand Down
20 changes: 17 additions & 3 deletions packages/@divvi/mobile/src/walletConnect/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
denyRequest,
sessionProposal as sessionProposalAction,
} from 'src/walletConnect/actions'
import { getAtomicCapabilityByWalletConnectChainId } from 'src/walletConnect/capabilities'
import { SupportedActions, SupportedEvents, rpcError } from 'src/walletConnect/constants'
import {
_acceptSession,
Expand All @@ -40,6 +41,7 @@ import {
walletConnectSaga,
} from 'src/walletConnect/saga'
import { WalletConnectRequestType } from 'src/walletConnect/types'
import { getWalletAddress } from 'src/web3/saga'
import { demoModeEnabledSelector, walletAddressSelector } from 'src/web3/selectors'
import { getSupportedNetworkIds } from 'src/web3/utils'
import { createMockStore } from 'test/utils'
Expand Down Expand Up @@ -722,10 +724,13 @@ describe('showActionRequest', () => {
123,
],
[call.fn(prepareTransactions), mockPreparedTransactions],
[call.fn(getAtomicCapabilityByWalletConnectChainId), 'unsupported'],
[call.fn(getWalletAddress), mockAccount],
[select(demoModeEnabledSelector), false],
])
.run()

// 2 calls, one in loading state and one in the action request state
// 2 calls: loading state, then action request state
expect(navigate).toHaveBeenCalledTimes(2)
expect(navigate).toHaveBeenNthCalledWith(1, Screens.WalletConnectRequest, {
type: WalletConnectRequestType.Loading,
Expand Down Expand Up @@ -763,7 +768,7 @@ describe('showActionRequest', () => {
])
.run()

// 2 calls, one in loading state and one in the action request state
// 2 calls: loading state, then action request state
expect(navigate).toHaveBeenCalledTimes(2)
expect(navigate).toHaveBeenNthCalledWith(1, Screens.WalletConnectRequest, {
type: WalletConnectRequestType.Loading,
Expand Down Expand Up @@ -801,7 +806,7 @@ describe('showActionRequest', () => {
])
.run()

// 2 calls, one in loading state and one in the action request state
// 2 calls: loading state, then action request state
expect(navigate).toHaveBeenCalledTimes(2)
expect(navigate).toHaveBeenNthCalledWith(1, Screens.WalletConnectRequest, {
type: WalletConnectRequestType.Loading,
Expand Down Expand Up @@ -1424,6 +1429,9 @@ describe('wallet_sendCalls', () => {
123,
],
[call.fn(prepareTransactions), mockPreparedTransactions],
[call.fn(getAtomicCapabilityByWalletConnectChainId), 'unsupported'],
[call.fn(getWalletAddress), mockAccount],
[select(demoModeEnabledSelector), false],
])
.run()

Expand Down Expand Up @@ -1529,6 +1537,9 @@ describe('wallet_sendCalls', () => {
123,
],
[call.fn(prepareTransactions), mockPreparedTransactions],
[call.fn(getAtomicCapabilityByWalletConnectChainId), 'unsupported'],
[call.fn(getWalletAddress), mockAccount],
[select(demoModeEnabledSelector), false],
])
.run()

Expand Down Expand Up @@ -1618,6 +1629,9 @@ describe('wallet_sendCalls', () => {
123,
],
[call.fn(prepareTransactions), mockPreparedTransactions],
[call.fn(getAtomicCapabilityByWalletConnectChainId), 'unsupported'],
[call.fn(getWalletAddress), mockAccount],
[select(demoModeEnabledSelector), false],
])
.run()

Expand Down
62 changes: 45 additions & 17 deletions packages/@divvi/mobile/src/walletConnect/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ function* showActionRequest(request: WalletKitTypes.EventArguments['session_requ
}

const demoModeEnabled = yield* select(demoModeEnabledSelector)
const supportedChains = yield* call(getSupportedChains)
if (demoModeEnabled) {
navigate(Screens.DemoModeAuthBlock)
yield* put(denyRequest(request, getSdkError('USER_REJECTED')))
Expand Down Expand Up @@ -643,6 +644,12 @@ function* showActionRequest(request: WalletKitTypes.EventArguments['session_requ
}
}

// If the action doesn't require user consent, accept it immediately
if (isNonInteractiveMethod(method)) {
yield* put(acceptRequest({ method, request }))
return
}

if (method === SupportedActions.wallet_sendCalls) {
const walletAddress = yield* call(getWalletAddress)

Expand Down Expand Up @@ -685,27 +692,37 @@ function* showActionRequest(request: WalletKitTypes.EventArguments['session_requ
}
}

// Navigate to loading state after capability checks pass
navigate(Screens.WalletConnectRequest, {
type: WalletConnectRequestType.Loading,
origin: WalletConnectPairingOrigin.Deeplink,
})

if (atomic === 'ready') {
// TODO: suggest user to enable atomic operations
// NOTE: deny if atomicRequired is true, and user didn't enable atomic operations
// Show smart account conversion prompt
const rawTxs: unknown[] = request.params.request.params[0].calls
const {
hasInsufficientGasFunds,
feeCurrenciesSymbols,
result: preparedRequest,
} = yield* prepareNormalizedTransactions(rawTxs, request.params.chainId)

navigate(Screens.WalletConnectRequest, {
type: WalletConnectRequestType.SmartAccountConversion,
method,
request,
supportedChains,
version: 2,
atomicRequired: request.params.request.params[0].atomicRequired,
hasInsufficientGasFunds,
feeCurrenciesSymbols,
preparedRequest,
})
return
}
}

// If the action doesn't require user consent, accept it immediately
if (isNonInteractiveMethod(method)) {
yield* put(acceptRequest({ method, request }))
return
}

// since there are some network requests needed to prepare the transactions,
// add a loading state
navigate(Screens.WalletConnectRequest, {
type: WalletConnectRequestType.Loading,
origin: WalletConnectPairingOrigin.Deeplink,
})

const supportedChains = yield* call(getSupportedChains)

// Handle message signing requests
if (isMessageMethod(method)) {
navigate(Screens.WalletConnectRequest, {
type: WalletConnectRequestType.Action,
Expand All @@ -714,8 +731,19 @@ function* showActionRequest(request: WalletKitTypes.EventArguments['session_requ
supportedChains,
version: 2,
})
return
}

// Navigate to loading state for other interactive requests
navigate(Screens.WalletConnectRequest, {
type: WalletConnectRequestType.Loading,
origin: WalletConnectPairingOrigin.Deeplink,
})

// We have either:
// A transaction request
// A sendCalls request, and the user already has a smart account OR smart accounts are not enabled.

if (isTransactionMethod(method)) {
const rawTx: unknown = request.params.request.params[0]
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface BaseProps {
testId: string
children?: React.ReactNode
buttonText?: string | null
secondaryButtonText?: string | null
buttonLoading?: boolean
}

Expand Down Expand Up @@ -91,6 +92,7 @@ function RequestContent(props: Props) {
testId,
children,
buttonText,
secondaryButtonText,
buttonLoading,
} = props
const { t } = useTranslation()
Expand Down Expand Up @@ -174,15 +176,33 @@ function RequestContent(props: Props) {
{children}

{type == 'confirm' && (
<Button
type={BtnTypes.PRIMARY}
size={BtnSizes.FULL}
text={buttonText ?? t('allow')}
showLoading={showButtonLoading}
disabled={showButtonLoading}
onPress={onPress}
testID={`${testId}/Allow`}
/>
<>
<Button
type={BtnTypes.PRIMARY}
size={BtnSizes.FULL}
text={buttonText ?? t('allow')}
showLoading={showButtonLoading}
disabled={showButtonLoading}
onPress={onPress}
testID={`${testId}/Allow`}
/>
{!!secondaryButtonText && (
<Button
type={BtnTypes.SECONDARY}
size={BtnSizes.FULL}
text={secondaryButtonText}
showLoading={showButtonLoading}
disabled={showButtonLoading}
onPress={() => {
if (props.type === 'confirm') {
props.onDeny()
}
}}
testID={`${testId}/Dismiss`}
style={styles.secondaryButton}
/>
)}
</>
)}
{type == 'dismiss' && (
<Button
Expand Down Expand Up @@ -219,6 +239,9 @@ const styles = StyleSheet.create({
requestDetailValue: {
...typeScale.labelSemiBoldSmall,
},
secondaryButton: {
marginTop: Spacing.Regular16,
},
})

export default RequestContent
Loading
Loading