=> {
+ if (!isAlchemyChain(chainId)) {
+ return fetchBlockscoutTokenTransfers({
+ chainId,
+ address,
+ token,
+ filter,
+ explorerUrl: blockscoutExplorerUrl,
+ });
+ }
+ try {
+ return await fetchAlchemyTokenTransfers({ chainId, address, token, filter });
+ } catch (err) {
+ console.warn(
+ `[data-source] alchemy transfers failed for chain ${chainId}, falling back to blockscout`,
+ err,
+ );
+ return fetchBlockscoutTokenTransfers({
+ chainId,
+ address,
+ token,
+ filter,
+ explorerUrl: blockscoutExplorerUrl,
+ });
+ }
+};
diff --git a/lib/getTokenIcon.tsx b/lib/getTokenIcon.tsx
index 40374db4f..43fade8a3 100644
--- a/lib/getTokenIcon.tsx
+++ b/lib/getTokenIcon.tsx
@@ -17,6 +17,10 @@ const getTokenIcon = ({ logoUrl, tokenSymbol, size = 24 }: GetTokenIconProps): T
// Fallback to default token icons based on symbol
switch (tokenSymbol?.toUpperCase()) {
case 'USDC':
+ // Bridged USDC variants (e.g. USDC.e on Fuse/Arbitrum, used by the
+ // borrow-and-deposit-to-card flow) share the USDC icon. Without this the
+ // detail page fell back to the "U" placeholder.
+ case 'USDC.E':
return {
type: 'image',
source: getAsset('images/usdc-4x.png'),
diff --git a/lib/observe.ts b/lib/observe.ts
new file mode 100644
index 000000000..1a194d35f
--- /dev/null
+++ b/lib/observe.ts
@@ -0,0 +1,50 @@
+import { EXPO_PUBLIC_ENVIRONMENT } from '@/lib/config';
+
+import type { ComponentType } from 'react';
+
+type ObserveModule = typeof import('expo-observe');
+
+// expo-observe (and its expo-app-metrics dependency) resolve their native
+// modules at import time and throw when the binary doesn't include them, e.g.
+// an OTA update reaching a build created before expo-observe was added
+// (runtimeVersion policy is appVersion). Metrics are best-effort, so fall back
+// to no-ops instead of crashing the app at startup.
+let observe: ObserveModule | undefined;
+try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ observe = require('expo-observe');
+} catch {
+ observe = undefined;
+}
+
+/**
+ * Configures EAS Observe metric dispatching. Call once at app startup, before
+ * the first metrics are collected.
+ *
+ * Debug builds collect but never dispatch metrics by default; pass
+ * `dispatchInDebug: true` here to test the pipeline locally.
+ */
+export function configureObserve() {
+ observe?.default.configure({
+ environment: EXPO_PUBLIC_ENVIRONMENT || 'development',
+ });
+}
+
+/**
+ * Records the time-to-interactive startup metric. Call once the splash screen
+ * is hidden and the first real UI is visible.
+ */
+export function markAppInteractive() {
+ observe?.AppMetrics.markInteractive();
+}
+
+/**
+ * Wraps the root layout with `AppMetricsRoot`, which records the
+ * time-to-first-render startup metric (the SDK 55 equivalent of SDK 56's
+ * `ObserveRoot`).
+ */
+export function withObserve>(
+ Component: ComponentType
,
+): ComponentType
{
+ return observe ? observe.AppMetricsRoot.wrap(Component) : Component;
+}
diff --git a/lib/thirdweb.ts b/lib/thirdweb.ts
index 757fab5d6..91839f6a5 100644
--- a/lib/thirdweb.ts
+++ b/lib/thirdweb.ts
@@ -1,5 +1,5 @@
import { createThirdwebClient, defineChain } from 'thirdweb';
-import { arbitrum, base, mainnet, polygon } from 'thirdweb/chains';
+import { arbitrum, base, bsc, mainnet, polygon } from 'thirdweb/chains';
import { darkTheme } from 'thirdweb/react';
import { createWallet } from 'thirdweb/wallets';
@@ -50,7 +50,7 @@ const fuse = defineChain({
},
});
-const chains = [mainnet, fuse, polygon, base, arbitrum];
+const chains = [mainnet, fuse, polygon, base, arbitrum, bsc];
export const getChain = (chainId: number) => {
return chains.find(chain => chain.id === chainId);
diff --git a/lib/types.ts b/lib/types.ts
index 71ca785c7..cd6593b93 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -435,6 +435,22 @@ export interface CardDetailsResponseDto extends CardResponse {
provider?: CardProvider;
}
+/**
+ * A single warning entry surfaced for a user's KYC. Mirrors Didit's per-block warning shape:
+ * `risk` is the tag (DOCUMENT_EXPIRED, DATE_OF_BIRTH_NOT_DETECTED, ...) — same key space as
+ * DIDIT_WARNING_DESCRIPTIONS overrides; `short_description` / `long_description` are Didit's
+ * pre-formatted user-facing copy. Backend also synthesises one of these (with
+ * `risk: 'CARD_ACTIVATION_FAILED'`) when Rain rejects the forwarded application.
+ */
+export interface KycWarning {
+ risk: string;
+ log_type?: string;
+ short_description?: string;
+ long_description?: string;
+ feature?: string;
+ node_id?: string;
+}
+
export interface CardStatusResponse {
status?: CardStatus;
activationBlocked?: boolean;
@@ -444,8 +460,8 @@ export interface CardStatusResponse {
provider?: CardProvider;
/** Internal KYC status (covers Didit rejection before Rain is reached) */
kycStatus?: KycStatus;
- /** Warning tags or reasons from Didit verification (e.g. DOCUMENT_EXPIRED). */
- kycWarnings?: string[];
+ /** Warning entries from Didit verification (e.g. DOCUMENT_EXPIRED) and Rain forward failures. */
+ kycWarnings?: KycWarning[];
/** Rain KYC: application status from Rain */
rainApplicationStatus?: RainApplicationStatus;
/** Rain: link for needsVerification redirect */
@@ -657,6 +673,7 @@ export enum TransactionType {
CANCEL_WITHDRAW = 'cancel_withdraw',
BRIDGE_DEPOSIT = 'bridge_deposit',
BORROW_AND_DEPOSIT_TO_CARD = 'borrow_and_deposit_to_card',
+ CARD_DEPOSIT = 'card_deposit',
BRIDGE_TRANSFER = 'bridge_transfer',
BANK_TRANSFER = 'bank_transfer',
CARD_TRANSACTION = 'card_transaction',
@@ -672,6 +689,9 @@ export enum TransactionType {
FAST_WITHDRAW = 'fast_withdraw',
REPAY_AND_WITHDRAW_COLLATERAL = 'repay_and_withdraw_collateral',
WITHDRAW_COLLATERAL = 'withdraw_collateral',
+ AGENT_X402_PAYMENT = 'agent_x402_payment',
+ AGENT_WALLET_DEPOSIT = 'agent_wallet_deposit',
+ RESCUE_TOKEN = 'rescue_token',
}
export enum TransactionDirection {
@@ -791,6 +811,7 @@ export type BridgeDeposit = {
deadline: number;
};
trackingId?: string;
+ category?: DepositCategory;
};
export type BridgeTransactionRequest = {
@@ -814,8 +835,14 @@ export type Deposit = {
};
trackingId?: string;
vault?: VaultType;
+ category?: DepositCategory;
};
+export enum DepositCategory {
+ SAVINGS = 'SAVINGS',
+ CARD = 'CARD',
+}
+
export enum DepositTransactionStatus {
PENDING = 'pending',
FAILED = 'failed',
@@ -923,12 +950,15 @@ export interface Cashback {
fuseUsdPrice?: string;
fiatAmount?: string;
fiatCurrency?: string;
+ payoutAt?: string;
createdAt: string;
}
export interface CashbackInfo {
amount: string;
isPending: boolean;
+ isEscrowed: boolean;
+ payoutAt?: string;
}
export interface SourceDepositInstructions {
@@ -1244,6 +1274,7 @@ export interface CardTransaction {
merchant_city?: string;
merchant_country?: string;
local_transaction_details?: LocalTransactionDetails;
+ declined_reason?: string;
}
export interface CardTransactionsResponse {
@@ -1317,7 +1348,8 @@ export interface ActivityEvents {
export interface UpdateActivityEvent {
status?: TransactionStatus;
- txHash?: string;
+ hash?: string;
+ url?: string;
userOpHash?: string;
metadata?: Record;
}
@@ -1519,6 +1551,54 @@ export interface AddressBookResponse {
skipped2faAt?: Date;
}
+export type AgentSummary = {
+ agentEoaAddress?: string;
+};
+
+export type AgentApiKeySummary = {
+ id: string;
+ prefix: string;
+ name?: string;
+ createdAt: string;
+ lastUsedAt?: string;
+ revokedAt?: string;
+};
+
+export type GenerateAgentApiKeyResponse = AgentApiKeySummary & { key: string };
+
+/**
+ * Envelope returned by the Turnkey SDK's `stampX` methods. `body` is the
+ * exact stringified bytes the SDK signed — we MUST forward it verbatim;
+ * re-serializing on the server changes key order and breaks the stamp.
+ */
+export type SignedTurnkeyRequest = {
+ url: string;
+ body: string;
+ stamp: { stampHeaderName: string; stampHeaderValue: string };
+};
+
+export type ProvisioningActivity = {
+ url: string;
+ body: Record;
+};
+
+export type ProvisioningInitResponse = {
+ provisioningId: string;
+ subOrganizationId: string;
+ /**
+ * Set when the agent's wallet path was already derived in Turnkey from a
+ * prior failed provisioning attempt. The `activity` in this case is the
+ * createUsers body — the client should skip the wallet-account stamp.
+ */
+ agentEoaAddress?: string;
+ activity: ProvisioningActivity;
+};
+
+export type ProvisioningStepInput = {
+ provisioningId: string;
+ signed: SignedTurnkeyRequest;
+};
+
export interface WhatsNewStep {
imageUrl: string;
title: string;
diff --git a/lib/utils/__tests__/card-deposit-activity.test.ts b/lib/utils/__tests__/card-deposit-activity.test.ts
new file mode 100644
index 000000000..098beb9b8
--- /dev/null
+++ b/lib/utils/__tests__/card-deposit-activity.test.ts
@@ -0,0 +1,185 @@
+///
+import { getTransactionCategory, isSourceReceiptFinalizable } from '@/constants/transaction';
+import {
+ ActivityEvent,
+ TransactionCategory,
+ TransactionStatus,
+ TransactionType,
+} from '@/lib/types';
+import {
+ deduplicateTransactions,
+ resolveCardDepositTransferTx,
+} from '@/lib/utils/deduplicateTransactions';
+
+function makeActivity(overrides: Partial = {}): ActivityEvent {
+ return {
+ clientTxId: 'tx-1',
+ type: TransactionType.CARD_DEPOSIT,
+ status: TransactionStatus.SUCCESS,
+ amount: '0.01',
+ symbol: 'USDC',
+ timestamp: '1781426763',
+ title: 'Deposit to Card',
+ ...overrides,
+ } as ActivityEvent;
+}
+
+describe('deduplicateTransactions — connect-wallet card deposit', () => {
+ // Real shape from a Wallet-source Rain card deposit: the frontend creates the
+ // base trackingId activity (with the on-chain hash) and the backend Temporal
+ // workflow creates `${trackingId}_card`. They must render as ONE row.
+ const frontend = makeActivity({
+ clientTxId: 'mqdjhtjp-g038q1a0',
+ title: 'Deposit USDC to Card',
+ userOpHash: '0x92d8602bde4171b6686544d4d2fa61ba0b5db07cb4458da85a94ae6347a1b527',
+ hash: '0x7843ea7492494d0adfb3b913c6bfb2f87daf5a6f6714a12df6c79387d60664cb',
+ toAddress: '0x9e852a0d1bd9738d52b90a5e907138575822d69e',
+ metadata: { source: 'transaction-hook' },
+ });
+ const backendCard = makeActivity({
+ clientTxId: 'mqdjhtjp-g038q1a0_card',
+ title: 'Deposit to Card',
+ userOpHash: '0x4acf1672858e422d8a760b2ffde946f3ad90d4bfe0965a77d8e2150bf9f665f3',
+ toAddress: '0xcf06a945cecc2651b78d055b6246ae1622c9e966',
+ metadata: {},
+ });
+
+ it('collapses trackingId and trackingId_card into a single row', () => {
+ const result = deduplicateTransactions([backendCard, frontend]);
+ expect(result).toHaveLength(1);
+ });
+
+ it('keeps the row carrying the on-chain hash (the explorer link)', () => {
+ const result = deduplicateTransactions([backendCard, frontend]);
+ expect(result[0].clientTxId).toBe('mqdjhtjp-g038q1a0');
+ expect(result[0].hash).toBe(frontend.hash);
+ });
+
+ it('removes the Blockscout-synced Send that mirrors a Wallet→card deposit', () => {
+ // Real shape: the frontend card_deposit (approve userOp hash) and the
+ // Blockscout-synced "Send USDC" (the on-chain transfer hash) share the same
+ // card funding toAddress + timestamp but have different hashes.
+ const cardDeposit = makeActivity({
+ clientTxId: 'mqdtqm1z-0xxndjzj',
+ title: 'Deposit USDC to Card',
+ hash: '0x1e6e5edd8850a6d072aa7cb592843b2638c1604b1a48b0cfdffc9ea56456cfe7',
+ userOpHash: '0x5f5b9152b8ef76d3b55adc363efbe5cf7fcda5643eb02e74339be32531473553',
+ toAddress: '0x9e852a0d1bd9738d52b90a5e907138575822d69e',
+ metadata: { source: 'transaction-hook' },
+ });
+ const blockscoutSend = makeActivity({
+ clientTxId: 'blockscout_8453_0xeb41_outgoing',
+ type: TransactionType.SEND,
+ title: 'Send USDC',
+ shortTitle: 'Send',
+ hash: '0xeb41c0c152e3183d217a60ccae5ab5a4818366bdca58149e71e9b8172688733d',
+ toAddress: '0x9e852a0d1bd9738d52b90a5e907138575822d69e',
+ metadata: { source: 'blockscout' },
+ });
+ const result = deduplicateTransactions([blockscoutSend, cardDeposit]);
+ expect(result).toHaveLength(1);
+ expect(result[0].type).toBe(TransactionType.CARD_DEPOSIT);
+ });
+
+ it('keeps an unrelated Send to a different address', () => {
+ const cardDeposit = makeActivity({
+ clientTxId: 'dep-x',
+ toAddress: '0x9e852a0d1bd9738d52b90a5e907138575822d69e',
+ hash: '0xaaaa000000000000000000000000000000000000000000000000000000000001',
+ });
+ const unrelatedSend = makeActivity({
+ clientTxId: 'send-x',
+ type: TransactionType.SEND,
+ title: 'Send USDC',
+ toAddress: '0x1111111111111111111111111111111111111111',
+ hash: '0xbbbb000000000000000000000000000000000000000000000000000000000002',
+ });
+ const result = deduplicateTransactions([cardDeposit, unrelatedSend]);
+ expect(result).toHaveLength(2);
+ });
+
+ it('still keeps a savings deposit and its _savings step separate', () => {
+ const base = makeActivity({
+ clientTxId: 'dep-1',
+ type: TransactionType.DEPOSIT,
+ title: 'Deposit USDC',
+ hash: '0x1111111111111111111111111111111111111111111111111111111111111111',
+ });
+ const savings = makeActivity({
+ clientTxId: 'dep-1_savings',
+ type: TransactionType.DEPOSIT,
+ title: 'Deposit soUSD to Savings',
+ });
+ const result = deduplicateTransactions([base, savings]);
+ expect(result).toHaveLength(2);
+ });
+});
+
+describe('resolveCardDepositTransferTx', () => {
+ const cardDeposit = makeActivity({
+ clientTxId: 'mqdtqm1z-0xxndjzj',
+ title: 'Deposit USDC to Card',
+ hash: '0x1e6e5edd8850a6d072aa7cb592843b2638c1604b1a48b0cfdffc9ea56456cfe7', // approve userOp
+ toAddress: '0x9e852a0d1bd9738d52b90a5e907138575822d69e',
+ });
+ const transferSend = makeActivity({
+ clientTxId: 'blockscout_8453_0xeb41_outgoing',
+ type: TransactionType.SEND,
+ title: 'Send USDC',
+ hash: '0xeb41c0c152e3183d217a60ccae5ab5a4818366bdca58149e71e9b8172688733d', // real transfer
+ toAddress: '0x9e852a0d1bd9738d52b90a5e907138575822d69e',
+ url: 'https://base.blockscout.com/tx/0xeb41c0c152e3183d217a60ccae5ab5a4818366bdca58149e71e9b8172688733d',
+ });
+
+ it('returns the sibling Send transfer tx (hash + url) for a card deposit', () => {
+ const result = resolveCardDepositTransferTx(cardDeposit, [cardDeposit, transferSend]);
+ expect(result).toEqual({ hash: transferSend.hash, url: transferSend.url });
+ });
+
+ it('returns undefined when there is no sibling transfer', () => {
+ expect(resolveCardDepositTransferTx(cardDeposit, [cardDeposit])).toBeUndefined();
+ });
+
+ it('returns undefined for non card-deposit types', () => {
+ const send = makeActivity({ clientTxId: 's', type: TransactionType.SEND });
+ expect(resolveCardDepositTransferTx(send, [send, transferSend])).toBeUndefined();
+ });
+});
+
+describe('getTransactionCategory', () => {
+ it('labels a card-bound bridge_deposit as Card deposit', () => {
+ expect(getTransactionCategory(TransactionType.BRIDGE_DEPOSIT, 'Deposit soUSD to Card')).toBe(
+ TransactionCategory.CARD_DEPOSIT,
+ );
+ });
+
+ it('leaves a real bridge_deposit as External wallet transfer', () => {
+ expect(getTransactionCategory(TransactionType.BRIDGE_DEPOSIT, 'Bridge to Arbitrum')).toBe(
+ TransactionCategory.EXTERNAL_WALLET_TRANSFER,
+ );
+ });
+
+ it('falls back to the static category for other types', () => {
+ expect(getTransactionCategory(TransactionType.CARD_TRANSACTION, 'Card Deposit')).toBe(
+ TransactionCategory.CARD_DEPOSIT,
+ );
+ expect(getTransactionCategory(TransactionType.SEND, 'Sent USDC')).toBe(
+ TransactionCategory.WALLET_TRANSFER,
+ );
+ });
+});
+
+describe('isSourceReceiptFinalizable', () => {
+ // Cross-chain card deposits must NOT be marked complete on their source-chain
+ // receipt — they bridge for minutes and finalize via the Rain webhook.
+ it('is false for cross-chain card deposit types', () => {
+ expect(isSourceReceiptFinalizable(TransactionType.BRIDGE_DEPOSIT)).toBe(false);
+ expect(isSourceReceiptFinalizable(TransactionType.BORROW_AND_DEPOSIT_TO_CARD)).toBe(false);
+ expect(isSourceReceiptFinalizable(TransactionType.CARD_DEPOSIT)).toBe(false);
+ });
+
+ it('is true for same-chain types resolved by a source-chain receipt', () => {
+ expect(isSourceReceiptFinalizable(TransactionType.SEND)).toBe(true);
+ expect(isSourceReceiptFinalizable(TransactionType.CARD_TRANSACTION)).toBe(true);
+ });
+});
diff --git a/lib/utils/borrowAndBridge.ts b/lib/utils/borrowAndBridge.ts
new file mode 100644
index 000000000..8543fb29b
--- /dev/null
+++ b/lib/utils/borrowAndBridge.ts
@@ -0,0 +1,256 @@
+import * as Sentry from '@sentry/react-native';
+import { Address } from 'abitype';
+import { Chain, erc20Abi, pad, TransactionReceipt } from 'viem';
+import { readContract } from 'viem/actions';
+import { fuse, mainnet } from 'viem/chains';
+import { encodeFunctionData, parseUnits } from 'viem/utils';
+
+import { USDC_STARGATE } from '@/constants/addresses';
+import { useActivityActions } from '@/hooks/useActivityActions';
+import { AaveV3Pool_ABI } from '@/lib/abis/AaveV3Pool';
+import BridgePayamster_ABI from '@/lib/abis/BridgePayamster';
+import { CardDepositManager_ABI } from '@/lib/abis/CardDepositManager';
+import { ADDRESSES } from '@/lib/config';
+import { executeTransactions, USER_CANCELLED_TRANSACTION } from '@/lib/execute';
+import { StargateQuoteParams, TransactionType } from '@/lib/types';
+import { getStargateChainId, getStargateQuote } from '@/lib/utils/stargate';
+import { publicClient } from '@/lib/wagmi';
+
+import type { SmartAccountClient } from 'permissionless';
+
+// EIP-3009 / Aave LTV — keep one source of truth shared by every flow that
+// borrows USDC against soUSD on Fuse and bridges via Stargate to a chosen
+// destination address.
+const SO_USD_LTV = 70n;
+
+// AccountantWithRateProviders.getRate() — shared between card + agent flows.
+const ACCOUNTANT_ABI = [
+ {
+ inputs: [],
+ name: 'getRate',
+ outputs: [{ internalType: 'uint256', name: 'rate', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+] as const;
+
+export interface BorrowAndBridgeUser {
+ safeAddress: string;
+ suborgId: string;
+ signWith: string;
+ userId: string;
+}
+
+export interface BorrowAndBridgeParams {
+ /** Connected user (must have safeAddress + AA signer context). */
+ user: BorrowAndBridgeUser;
+ /** Receiver of bridged USDC on the destination chain. */
+ destinationAddress: Address;
+ /** EVM chain id of the destination (e.g. base.id, arbitrum.id). */
+ destinationChainId: number;
+ /** Stargate's chain key for the destination ('base', 'arbitrum', ...). */
+ destinationChainKey: string;
+ /** USDC contract on the destination chain. */
+ destinationToken: Address;
+ /** Borrow amount as a human-readable USDC string (e.g. '10.5'). */
+ amountToBorrow: string;
+ /** AA signer factory used by the card flow (`useUser().safeAA`). */
+ safeAA: (chain: Chain, suborgId: string, signWith: string) => Promise;
+ /** Activity tracking — wires receipt + status into the in-app feed. */
+ trackTransaction: ReturnType['trackTransaction'];
+ /** Activity payload metadata. */
+ activityType: TransactionType;
+ activityTitle: string;
+ /** Optional Sentry/analytics breadcrumb tag (purely cosmetic). */
+ flowTag?: string;
+}
+
+/**
+ * Core "borrow USDC.e against soUSD on Fuse → Stargate-bridge to a
+ * destination" flow used by both the card-funding and agent-wallet
+ * deposit paths. The CardDepositManager is destination-agnostic (the
+ * receiver allowlist is gated by `isWhitelistEnabled` which is off in
+ * prod), so the same on-chain plumbing handles both.
+ */
+export async function executeBorrowAndBridge(
+ params: BorrowAndBridgeParams,
+): Promise {
+ const {
+ user,
+ destinationAddress,
+ destinationChainId,
+ destinationChainKey,
+ destinationToken,
+ amountToBorrow,
+ safeAA,
+ trackTransaction,
+ activityType,
+ activityTitle,
+ flowTag = 'borrow_and_bridge',
+ } = params;
+
+ const rate = await readContract(publicClient(mainnet.id), {
+ address: ADDRESSES.ethereum.accountant,
+ abi: ACCOUNTANT_ABI,
+ functionName: 'getRate',
+ });
+
+ const borrowAmountWei = parseUnits(amountToBorrow, 6);
+ const supplyAmountWei = (borrowAmountWei * 100n * 1000000n) / (SO_USD_LTV * rate);
+
+ const supplyApproveCalldata = encodeFunctionData({
+ abi: erc20Abi,
+ functionName: 'approve',
+ args: [ADDRESSES.fuse.aaveV3Pool, supplyAmountWei],
+ });
+
+ const supplyCalldata = encodeFunctionData({
+ abi: AaveV3Pool_ABI,
+ functionName: 'supply',
+ args: [ADDRESSES.fuse.vault, supplyAmountWei, user.safeAddress as Address, 0],
+ });
+
+ const borrowCalldata = encodeFunctionData({
+ abi: AaveV3Pool_ABI,
+ functionName: 'borrow',
+ args: [USDC_STARGATE, borrowAmountWei, 2, 0, user.safeAddress as Address],
+ });
+
+ Sentry.addBreadcrumb({
+ message: `Starting ${flowTag} transaction`,
+ category: 'bridge',
+ data: {
+ amount: amountToBorrow,
+ amountWei: borrowAmountWei.toString(),
+ userAddress: user.safeAddress,
+ destinationAddress,
+ destinationChainId,
+ chainId: fuse.id,
+ },
+ });
+
+ // 5% slippage envelope on the destination amount.
+ const dstAmountMin = (borrowAmountWei * 95n) / 100n;
+
+ const quoteParams: StargateQuoteParams = {
+ srcToken: USDC_STARGATE,
+ srcChainKey: 'fuse',
+ dstToken: destinationToken,
+ dstChainKey: destinationChainKey,
+ srcAddress: ADDRESSES.fuse.bridgePaymasterAddress,
+ dstAddress: destinationAddress,
+ srcAmount: borrowAmountWei.toString(),
+ dstAmountMin: dstAmountMin.toString(),
+ };
+ const quote = await getStargateQuote(quoteParams);
+ const taxiQuote = quote.quotes.find(q => q.route.includes('taxi'));
+ if (!taxiQuote) throw new Error('Taxi route not available from Stargate');
+ if (taxiQuote.error) throw new Error(`Stargate quote error: ${taxiQuote.error}`);
+
+ const bridgeStep = taxiQuote.steps.find(step => step.type === 'bridge');
+ if (!bridgeStep) throw new Error('No bridge step found in Stargate quote');
+
+ const { transaction } = bridgeStep;
+ const nativeFeeAmount = BigInt(transaction.value);
+
+ const sendParam = {
+ dstEid: getStargateChainId(destinationChainId) as number,
+ to: pad(destinationAddress, { size: 32 }),
+ amountLD: borrowAmountWei,
+ minAmountLD: dstAmountMin,
+ extraOptions: '0x' as `0x${string}`,
+ composeMsg: '0x' as `0x${string}`,
+ oftCmd: '0x' as `0x${string}`,
+ };
+
+ const calldata = encodeFunctionData({
+ abi: CardDepositManager_ABI,
+ functionName: 'depositUsingStargate',
+ args: [
+ transaction.to as Address,
+ user.safeAddress as Address,
+ sendParam,
+ nativeFeeAmount,
+ ADDRESSES.fuse.bridgePaymasterAddress,
+ ],
+ });
+
+ const transactions = [
+ {
+ to: ADDRESSES.fuse.vault,
+ data: supplyApproveCalldata,
+ value: 0n,
+ },
+ {
+ to: ADDRESSES.fuse.aaveV3Pool,
+ data: supplyCalldata,
+ value: 0n,
+ },
+ {
+ to: ADDRESSES.fuse.aaveV3Pool,
+ data: borrowCalldata,
+ value: 0n,
+ },
+ // Approve USDC.e from Safe to CardDepositManager (manager is destination-agnostic).
+ {
+ to: USDC_STARGATE,
+ data: encodeFunctionData({
+ abi: erc20Abi,
+ functionName: 'approve',
+ args: [ADDRESSES.fuse.cardDepositManager, borrowAmountWei],
+ }),
+ value: 0n,
+ },
+ // Forward the LZ native fee from BridgePaymaster (which is sponsored
+ // for the depositUsingStargate selector) and let the manager call
+ // Stargate's send().
+ {
+ to: ADDRESSES.fuse.bridgePaymasterAddress,
+ data: encodeFunctionData({
+ abi: BridgePayamster_ABI,
+ functionName: 'callWithValue',
+ args: [
+ ADDRESSES.fuse.cardDepositManager,
+ '0x37fe667d', // depositUsingStargate selector
+ calldata,
+ nativeFeeAmount,
+ ],
+ }),
+ value: 0n,
+ },
+ ];
+
+ const smartAccountClient = await safeAA(fuse, user.suborgId, user.signWith);
+
+ const result = await trackTransaction(
+ {
+ type: activityType,
+ title: activityTitle,
+ shortTitle: activityTitle,
+ amount: amountToBorrow,
+ symbol: 'USDC.e',
+ chainId: fuse.id,
+ fromAddress: user.safeAddress,
+ toAddress: destinationAddress,
+ metadata: {
+ description: `${activityTitle} ${amountToBorrow} USDC from Fuse to ${destinationAddress} on chain ${destinationChainId}`,
+ fee: transaction.value,
+ sourceSymbol: 'USDC.e',
+ tokenAddress: USDC_STARGATE,
+ },
+ },
+ onUserOpHash =>
+ executeTransactions(
+ smartAccountClient,
+ transactions,
+ `${activityTitle} failed`,
+ fuse,
+ onUserOpHash,
+ ),
+ );
+
+ const transactionResult =
+ result && typeof result === 'object' && 'transaction' in result ? result.transaction : result;
+
+ return transactionResult as TransactionReceipt | typeof USER_CANCELLED_TRANSACTION;
+}
diff --git a/lib/utils/cardHelpers.ts b/lib/utils/cardHelpers.ts
index ae889c20b..c4f55e7f9 100644
--- a/lib/utils/cardHelpers.ts
+++ b/lib/utils/cardHelpers.ts
@@ -117,12 +117,15 @@ export const getCashbackAmount = (
}
const isPending = PENDING_CASHBACK_STATUSES.includes(cashback.status);
+ const isEscrowed = cashback.status === CashbackStatus.Escrowed;
// For pending cashbacks without fuseAmount yet, show pending indicator without amount
if (!cashback.fuseAmount) {
return {
amount: 'Pending',
isPending: true,
+ isEscrowed,
+ payoutAt: cashback.payoutAt,
};
}
@@ -137,5 +140,7 @@ export const getCashbackAmount = (
return {
amount: `+$${amount.toFixed(2)}`,
isPending,
+ isEscrowed,
+ payoutAt: cashback.payoutAt,
};
};
diff --git a/lib/utils/deduplicateTransactions.ts b/lib/utils/deduplicateTransactions.ts
index 0532f8158..904a2e9f6 100644
--- a/lib/utils/deduplicateTransactions.ts
+++ b/lib/utils/deduplicateTransactions.ts
@@ -32,6 +32,23 @@ function isDuplicate(a: ActivityEvent, b: ActivityEvent): boolean {
}
}
+ // A connect-wallet card deposit creates TWO card_deposit activities for the
+ // same user action: the frontend's optimistic one (trackingId) and the
+ // backend Temporal workflow's card-funding one (`${trackingId}_card`).
+ // Unlike the savings flow above (two distinct user-visible steps), these are
+ // the same step — collapse them so the deposit shows once. The keep-decision
+ // below prefers the row with an on-chain hash (the frontend doc), which
+ // carries the explorer link.
+ if (a.clientTxId && b.clientTxId && a.clientTxId !== b.clientTxId) {
+ const aIsCard = a.clientTxId.endsWith('_card');
+ const bIsCard = b.clientTxId.endsWith('_card');
+ if (aIsCard !== bIsCard) {
+ const cardId = aIsCard ? a.clientTxId : b.clientTxId;
+ const otherId = aIsCard ? b.clientTxId : a.clientTxId;
+ if (cardId === `${otherId}_card`) return true;
+ }
+ }
+
// Normalize hash values for comparison (lowercase, trim)
const normalizeHash = (hash: string | undefined) => hash?.toLowerCase().trim();
const aHash = normalizeHash(a.hash);
@@ -59,6 +76,47 @@ function isDuplicate(a: ActivityEvent, b: ActivityEvent): boolean {
return false;
}
+/**
+ * Activity types that represent a "deposit to card" from the user's
+ * perspective. Used to (a) prioritise the card-deposit row over the raw SEND
+ * when they collide, and (b) suppress the Blockscout-synced SEND that mirrors
+ * the on-chain transfer of a card deposit.
+ */
+export const CARD_DEPOSIT_ACTIVITY_TYPES: readonly TransactionType[] = [
+ TransactionType.BRIDGE_DEPOSIT,
+ TransactionType.CARD_TRANSACTION,
+ TransactionType.CARD_DEPOSIT,
+ TransactionType.BORROW_AND_DEPOSIT_TO_CARD,
+];
+
+/**
+ * For a card-deposit activity, find the sibling on-chain USDC transfer that the
+ * Blockscout/Alchemy sync indexed as a separate "Send" (the real money
+ * movement). The card-deposit row's own hash is, for connect-wallet deposits,
+ * the approve userOp — not the transfer — so the UI should link to this Send's
+ * tx instead. Matches the same toAddress + chain + 5-minute window used by the
+ * Send-dedup pass. Returns the transfer's hash (and url, if synced).
+ */
+export function resolveCardDepositTransferTx(
+ activity: ActivityEvent,
+ allActivities: ActivityEvent[],
+): { hash: string; url?: string } | undefined {
+ if (!activity?.toAddress || !CARD_DEPOSIT_ACTIVITY_TYPES.includes(activity.type)) {
+ return undefined;
+ }
+ const toAddress = activity.toAddress.toLowerCase();
+ const ts = parseInt(activity.timestamp || '0');
+ const send = allActivities.find(
+ a =>
+ a.type === TransactionType.SEND &&
+ !!a.hash &&
+ a.toAddress?.toLowerCase() === toAddress &&
+ a.chainId === activity.chainId &&
+ Math.abs(parseInt(a.timestamp || '0') - ts) < 300,
+ );
+ return send?.hash ? { hash: send.hash, url: send.url } : undefined;
+}
+
/**
* Check if a transaction is a card deposit
*/
@@ -66,12 +124,12 @@ function isCardDeposit(transaction: ActivityEvent): boolean {
// Guard against null/corrupted transactions
if (!transaction || !transaction.type) return false;
+ if (CARD_DEPOSIT_ACTIVITY_TYPES.includes(transaction.type)) return true;
+
return (
- transaction.type === TransactionType.BRIDGE_DEPOSIT ||
- transaction.type === TransactionType.CARD_TRANSACTION ||
- (transaction.type === TransactionType.SEND &&
- transaction.toAddress &&
- transaction.metadata?.description?.toLowerCase().includes('card'))
+ transaction.type === TransactionType.SEND &&
+ !!transaction.toAddress &&
+ !!transaction.metadata?.description?.toLowerCase().includes('card')
);
}
@@ -162,6 +220,8 @@ export function deduplicateTransactions(transactions: ActivityEvent[]): Activity
if (currentIsCardDeposit || existingIsCardDeposit || sameCardAddress) {
const typePriority = {
[TransactionType.BRIDGE_DEPOSIT]: 3,
+ [TransactionType.BORROW_AND_DEPOSIT_TO_CARD]: 3,
+ [TransactionType.CARD_DEPOSIT]: 3,
[TransactionType.CARD_TRANSACTION]: 2,
[TransactionType.SEND]: 1,
};
@@ -238,18 +298,20 @@ export function deduplicateTransactions(transactions: ActivityEvent[]): Activity
let deduplicatedArray = Array.from(deduplicated.values());
- // Second pass: Remove SEND transactions that have a corresponding BRIDGE_DEPOSIT or CARD_TRANSACTION
- // with the same toAddress (card funding address) and similar timestamp
+ // Second pass: Remove SEND transactions that mirror a card deposit. The
+ // Blockscout/Alchemy sync indexes the on-chain USDC transfer of a card
+ // deposit as a separate "Send USDC" row (a different tx hash than the
+ // frontend activity — e.g. the transfer vs the approve userOp — so hash
+ // dedup misses it). Drop the SEND when a card-deposit activity shares the
+ // same toAddress (card funding address) within 5 minutes.
deduplicatedArray = deduplicatedArray.filter(transaction => {
// Keep all non-SEND transactions
if (transaction.type !== TransactionType.SEND) return true;
- // For SEND transactions, check if there's a BRIDGE_DEPOSIT or CARD_TRANSACTION with same toAddress
const hasCardDepositTransaction = deduplicatedArray.some(
tx =>
tx !== transaction &&
- (tx.type === TransactionType.BRIDGE_DEPOSIT ||
- tx.type === TransactionType.CARD_TRANSACTION) &&
+ CARD_DEPOSIT_ACTIVITY_TYPES.includes(tx.type) &&
tx.toAddress?.toLowerCase() === transaction.toAddress?.toLowerCase() &&
Math.abs(parseInt(tx.timestamp || '0') - parseInt(transaction.timestamp || '0')) < 300, // Within 5 minutes
);
diff --git a/lib/utils/utils.ts b/lib/utils/utils.ts
index 69388df19..8ba4b9529 100644
--- a/lib/utils/utils.ts
+++ b/lib/utils/utils.ts
@@ -333,12 +333,15 @@ export const parseStampHeaderValueCredentialId = (stampHeaderValue: string) => {
export const getArbitrumFundingAddress = (cardDetails: CardResponse) => {
const ARBITRUM_CHAIN = 'arbitrum';
- if (cardDetails?.funding_instructions?.chain === ARBITRUM_CHAIN) {
- return cardDetails?.funding_instructions?.address;
+ if (
+ cardDetails?.funding_instructions?.chain === ARBITRUM_CHAIN &&
+ cardDetails?.funding_instructions?.address
+ ) {
+ return cardDetails.funding_instructions.address;
}
return cardDetails?.additional_funding_instructions?.find(
- instruction => instruction.chain === ARBITRUM_CHAIN,
+ instruction => instruction.chain === ARBITRUM_CHAIN && instruction.address,
)?.address;
};
@@ -370,9 +373,12 @@ export function getCardFundingAddress(
provider: CardProvider | null | undefined,
contracts: RainContractResponseDto[] | null | undefined,
): string | undefined {
- if (provider === CardProvider.RAIN && contracts?.length) {
- const rainContract = contracts.find(c => c.chainId === EXPO_PUBLIC_CARD_FUNDING_CHAIN_ID);
- if (rainContract?.depositAddress) return rainContract.depositAddress;
+ if (provider === CardProvider.RAIN) {
+ if (!contracts?.length) return undefined;
+ const rainContract = contracts.find(
+ c => Number(c.chainId) === EXPO_PUBLIC_CARD_FUNDING_CHAIN_ID,
+ );
+ return rainContract?.depositAddress || undefined;
}
return cardDetails ? getArbitrumFundingAddress(cardDetails) : undefined;
}
diff --git a/lib/wagmi.ts b/lib/wagmi.ts
index acd98a732..ba909cfb0 100644
--- a/lib/wagmi.ts
+++ b/lib/wagmi.ts
@@ -6,6 +6,7 @@ import {
arbitrum,
base,
baseSepolia,
+ bsc,
fuse,
mainnet,
polygon,
@@ -15,7 +16,7 @@ import { EXPO_PUBLIC_ALCHEMY_API_KEY } from './config';
polyfill();
-const chains: [Chain, ...Chain[]] = [fuse, mainnet, polygon, base, baseSepolia, arbitrum];
+const chains: [Chain, ...Chain[]] = [fuse, mainnet, polygon, base, baseSepolia, arbitrum, bsc];
export const getChain = (chainId: number): Chain | undefined => {
return chains.find((chain: Chain) => chain.id === chainId);
@@ -28,6 +29,7 @@ export const rpcUrls: Record = {
[base.id]: `https://base-mainnet.g.alchemy.com/v2/${EXPO_PUBLIC_ALCHEMY_API_KEY}`,
[baseSepolia.id]: `https://base-sepolia.g.alchemy.com/v2/${EXPO_PUBLIC_ALCHEMY_API_KEY}`,
[arbitrum.id]: `https://arb-mainnet.g.alchemy.com/v2/${EXPO_PUBLIC_ALCHEMY_API_KEY}`,
+ [bsc.id]: `https://bnb-mainnet.g.alchemy.com/v2/${EXPO_PUBLIC_ALCHEMY_API_KEY}`,
};
const transports: Record> = {
@@ -37,6 +39,7 @@ const transports: Record> = {
[base.id]: http(rpcUrls[base.id]),
[baseSepolia.id]: http(rpcUrls[baseSepolia.id]),
[arbitrum.id]: http(rpcUrls[arbitrum.id]),
+ [bsc.id]: http(rpcUrls[bsc.id]),
};
export const publicClient = (chainId: number) =>
diff --git a/package-lock.json b/package-lock.json
index 9d369c6ed..a4e0025df 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -80,10 +80,12 @@
"expo-font": "~55.0.4",
"expo-haptics": "~55.0.13",
"expo-image": "~55.0.8",
+ "expo-insights": "~55.0.15",
"expo-intent-launcher": "~55.0.11",
"expo-linear-gradient": "~55.0.12",
"expo-linking": "~55.0.11",
"expo-notifications": "~55.0.17",
+ "expo-observe": "~0.2.2",
"expo-router": "~55.0.11",
"expo-splash-screen": "~55.0.16",
"expo-symbols": "~55.0.7",
@@ -22393,6 +22395,20 @@
}
}
},
+ "node_modules/expo-app-metrics": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/expo-app-metrics/-/expo-app-metrics-0.2.2.tgz",
+ "integrity": "sha512-S+4/3SV92wjBizj2QJmmVG6PlcwvaDedFSJSInp1XLIjgVkngpc/oAFS5UTMiH2X+lW6H/RSoPRk7Hd+fuoOmg==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-updates-interface": "~55.1.6"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-application": {
"version": "55.0.13",
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-55.0.13.tgz",
@@ -22668,6 +22684,18 @@
}
}
},
+ "node_modules/expo-insights": {
+ "version": "55.0.17",
+ "resolved": "https://registry.npmjs.org/expo-insights/-/expo-insights-55.0.17.tgz",
+ "integrity": "sha512-X1uELdl4lP7+qs5ewtAPaFWrWa7Lp0Ltkq93skDc8fBVgl3aUqLrAdnz4UMyy5waM2JnVr0WtUqOIQW9+6e+jg==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-eas-client": "~55.0.5"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-intent-launcher": {
"version": "55.0.11",
"resolved": "https://registry.npmjs.org/expo-intent-launcher/-/expo-intent-launcher-55.0.11.tgz",
@@ -22777,6 +22805,20 @@
"react-native": "*"
}
},
+ "node_modules/expo-observe": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/expo-observe/-/expo-observe-0.2.2.tgz",
+ "integrity": "sha512-LF0Fmjrl+p3j/VgIhsKVE/6gffQM28nawNBA7/rGkEKqcYDKA3JBBPo0zAEE4OMJzOHExr7FYS/z6LzzaUYfJA==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-app-metrics": "~0.2.2",
+ "expo-eas-client": "~55.0.5"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-router": {
"version": "55.0.11",
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-55.0.11.tgz",
@@ -22968,9 +23010,9 @@
}
},
"node_modules/expo-updates-interface": {
- "version": "55.1.5",
- "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-55.1.5.tgz",
- "integrity": "sha512-YOk9vhplWi0djoeqxMlEQgcDFeOGhnj4dWU0v1QvF5RqpqwLGdx780E0k3zL85xw6LXljVN78d6g8z51qIZu5g==",
+ "version": "55.1.6",
+ "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-55.1.6.tgz",
+ "integrity": "sha512-evxNpagCkjT3lE6bGV570TFzRtKuIuLY8I37RYHoriXCJ+ZKCN1hbmklK29uAixya+BxGpeTI2K4FqYeJLvfrw==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
diff --git a/package.json b/package.json
index aebe5d499..384df2745 100644
--- a/package.json
+++ b/package.json
@@ -101,10 +101,12 @@
"expo-font": "~55.0.4",
"expo-haptics": "~55.0.13",
"expo-image": "~55.0.8",
+ "expo-insights": "~55.0.15",
"expo-intent-launcher": "~55.0.11",
"expo-linear-gradient": "~55.0.12",
"expo-linking": "~55.0.11",
"expo-notifications": "~55.0.17",
+ "expo-observe": "~0.2.2",
"expo-router": "~55.0.11",
"expo-splash-screen": "~55.0.16",
"expo-symbols": "~55.0.7",
@@ -226,6 +228,5 @@
}
}
},
- "private": true,
- "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
+ "private": true
}
diff --git a/patches/@didit-protocol+sdk-react-native+3.2.8.patch b/patches/@didit-protocol+sdk-react-native+3.2.8.patch
new file mode 100644
index 000000000..709aa91f4
--- /dev/null
+++ b/patches/@didit-protocol+sdk-react-native+3.2.8.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/@didit-protocol/sdk-react-native/app.plugin.js b/node_modules/@didit-protocol/sdk-react-native/app.plugin.js
+index 7943542..45ae038 100644
+--- a/node_modules/@didit-protocol/sdk-react-native/app.plugin.js
++++ b/node_modules/@didit-protocol/sdk-react-native/app.plugin.js
+@@ -12,7 +12,7 @@ const MAVEN_REPO =
+ const MAVEN_LINE = ` maven { url "${MAVEN_REPO}" }`;
+
+ const PODSPEC_URL =
+- 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/main/DiditSDK.podspec';
++ 'https://raw.githubusercontent.com/didit-protocol/sdk-ios/3.2.9/DiditSDK.podspec';
+
+ const POD_LINE = ` pod 'DiditSDK', :podspec => '${PODSPEC_URL}'`;
+
diff --git a/store/useCardWelcomePopupStore.ts b/store/useCardWelcomePopupStore.ts
new file mode 100644
index 000000000..1f795e1a5
--- /dev/null
+++ b/store/useCardWelcomePopupStore.ts
@@ -0,0 +1,24 @@
+import { create } from 'zustand';
+import { createJSONStorage, persist } from 'zustand/middleware';
+
+import mmkvStorage from '@/lib/mmvkStorage';
+
+interface CardWelcomePopupState {
+ shouldShowWelcomePopup: boolean;
+ setShouldShowWelcomePopup: (value: boolean) => void;
+}
+
+const CARD_WELCOME_POPUP_STORAGE_KEY = 'card-welcome-popup-storage';
+
+export const useCardWelcomePopupStore = create()(
+ persist(
+ set => ({
+ shouldShowWelcomePopup: false,
+ setShouldShowWelcomePopup: (value: boolean) => set({ shouldShowWelcomePopup: value }),
+ }),
+ {
+ name: CARD_WELCOME_POPUP_STORAGE_KEY,
+ storage: createJSONStorage(() => mmkvStorage(CARD_WELCOME_POPUP_STORAGE_KEY)),
+ },
+ ),
+);
diff --git a/store/useUserStore.ts b/store/useUserStore.ts
index 2e5512f2c..178d3178d 100644
--- a/store/useUserStore.ts
+++ b/store/useUserStore.ts
@@ -13,6 +13,12 @@ interface UserState {
signupUser: SignupUser;
safeAddressSynced: Record;
redirectFrom: string | null;
+ /**
+ * userId awaiting passkey authentication after the welcome-page user
+ * selection. Scoped to a single session — survives the TurnkeyProvider
+ * re-mount triggered by credentialId changes but is not persisted.
+ */
+ pendingAuthUserId: string | null;
_hasHydrated: boolean;
storeUser: (user: User) => void;
updateUser: (user: User) => void;
@@ -24,13 +30,14 @@ interface UserState {
setSignupUser: (user: SignupUser) => void;
markSafeAddressSynced: (userId: string) => void;
setRedirectFrom: (path: string | null) => void;
+ setPendingAuthUserId: (userId: string | null) => void;
setHasHydrated: (state: boolean) => void;
}
// Selectors - pure functions for deriving state
// These can be used with useUserStore(selector) for optimal re-render behavior
-/** Get the currently selected user, or the only user if there's just one */
+/** Get the currently selected user */
export const selectSelectedUser = ({ users }: UserState): User | undefined =>
users.find(u => u.selected);
@@ -48,6 +55,7 @@ export const useUserStore = create()(
signupUser: { username: '' },
safeAddressSynced: {},
redirectFrom: null,
+ pendingAuthUserId: null,
_hasHydrated: false,
setHasHydrated: (state: boolean) => set({ _hasHydrated: state }),
@@ -124,6 +132,8 @@ export const useUserStore = create()(
),
setRedirectFrom: (path: string | null) => set({ redirectFrom: path }),
+
+ setPendingAuthUserId: (userId: string | null) => set({ pendingAuthUserId: userId }),
}),
{
name: USER.storageKey,
@@ -132,7 +142,7 @@ export const useUserStore = create()(
state?.setHasHydrated(true);
},
partialize: state => {
- const { redirectFrom, ...rest } = state;
+ const { redirectFrom, pendingAuthUserId, ...rest } = state;
return rest;
},
},