Skip to content

Commit

Permalink
mobile: show Landline transfers in history (#1300)
Browse files Browse the repository at this point in the history
* backend for landline transfer

* create landline history ui

* show deposit immediately after creation

* sync strategy for landline logs

* show status, estimated arrival time and help message

* fix lint issue

* update memoization based on status

* code cleanup and a test

* update help copy and sort logic

* code cleanup

* lint fix and comment

* landline change from date string to timestamp

* fix test

* change landlineaccount createdAt to string for backcompat
  • Loading branch information
andrewliu08 authored Sep 5, 2024
1 parent db3a422 commit c918393
Show file tree
Hide file tree
Showing 33 changed files with 1,501 additions and 150 deletions.
27 changes: 26 additions & 1 deletion apps/daimo-mobile/src/action/useLandlineDeposit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { OffchainAction, now, zDollarStr } from "@daimo/common";
import {
LandlineTransfer,
OffchainAction,
landlineTransferToTransferClog,
now,
zDollarStr,
} from "@daimo/common";
import { daimoChainFromId } from "@daimo/contract";
import * as Haptics from "expo-haptics";
import { useCallback } from "react";
Expand All @@ -7,6 +13,7 @@ import { stringToBytes } from "viem";
import { signAsync } from "./sign";
import { ActHandle, useActStatus } from "../action/actStatus";
import { i18n } from "../i18n";
import { getAccountManager } from "../logic/accountManager";
import { getRpcFunc } from "../logic/trpc";
import { Account } from "../storage/account";

Expand Down Expand Up @@ -57,6 +64,10 @@ export function useLandlineDeposit({
if (response.status === "success") {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setAS("success", i18.depositStatus.success());
getAccountManager().transform((a) => {
// response.transfer guaranteed to be defined on success
return depositAccountTransform(a, response.transfer!);
});
} else {
console.error("[LANDLINE] Landline deposit error:", response.error);
setAS("error", i18.depositStatus.failed());
Expand All @@ -69,3 +80,17 @@ export function useLandlineDeposit({

return { ...as, exec };
}

function depositAccountTransform(
account: Account,
landlineTransfer: LandlineTransfer
): Account {
const transferClog = landlineTransferToTransferClog(
landlineTransfer,
daimoChainFromId(account.homeChainId)
);
return {
...account,
recentTransfers: [...account.recentTransfers, transferClog],
};
}
41 changes: 38 additions & 3 deletions apps/daimo-mobile/src/i18n/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ export const en = {
cancelledLink: () => `Cancelled link`,
sent: () => `Sent`,
received: () => `Received`,
deposited: () => `Deposited`,
withdrew: () => `Withdrew`,
},
whyNoFees: {
help: {
title: () => `About this transfer`,
description: {
whyNoFees: {
firstPara: (chainName: string) =>
`This transaction settled on ${chainName}, an Ethereum rollup.`,
firstPara2Chain: (chainA: string, chainB: string) =>
Expand All @@ -69,12 +71,44 @@ export const en = {
thirdPara: () =>
`Transactions cost a few cents. Daimo sponsored this transfer, making it free.`,
},
landlineDepositProcessing: {
firstPara: () =>
"This transaction transfers funds from your connected bank account to your Daimo account.",
secondPara: () =>
"Once the funds are received by our partner, we will make an on-chain transfer to deposit the funds to your Daimo account.",
thirdPara: () =>
"Bank transfers normally cost a few dollars. Daimo sponsored this transfer, making it free.",
},
landlineDepositCompleted: {
firstPara: () =>
"This transaction transferred funds from your connected bank account to your Daimo account.",
secondPara: () =>
"Bank transfers normally cost a few dollars. Daimo sponsored this transfer, making it free.",
},
landlineWithdrawalProcessing: {
firstPara: () =>
"This transaction transfers funds from your Daimo account to your connected bank account.",
secondPara: () =>
"The funds are transferred on-chain to our partner's address. Upon receiving the funds, we initiate a bank transfer to your bank account.",
thirdPara: () =>
"Bank transfers normally cost a few dollars. Daimo sponsored this transfer, making it free.",
},
landlineWithdrawalCompleted: {
firstPara: () =>
"This transaction transferred funds from your Daimo account to your connected bank account.",
secondPara: () =>
"Bank transfers normally cost a few dollars. Daimo sponsored this transfer, making it free.",
},
},
feeText: {
free: () => `FREE`,
pending: () => `PENDING`,
fee: (amount: string) => `${amount} FEE`,
},
fundArrivalTime: {
deposit: () => `Your funds will arrive to your Daimo account`,
withdrawal: () => `Your funds will arrive to your bank account`,
},
},

// ------------ KEYROTATION ------------
Expand Down Expand Up @@ -472,7 +506,8 @@ export const en = {
landline: {
cta: () => `Connect with Landline`,
title: () => `Deposit or withdraw directly from a US bank account`,
optionRowTitle: (timeAgo: string) => `Connected ${timeAgo} ago`,
optionRowTitle: (timeAgo: string) =>
`Connected ${timeAgo} ${timeAgo === "now" ? "" : "ago"}`,
startTransfer: () => `Start transfer`,
},
binance: {
Expand Down
38 changes: 36 additions & 2 deletions apps/daimo-mobile/src/i18n/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ export const es: LanguageDefinition = {
cancelledLink: () => `Link cancelado`,
sent: () => `Enviado`,
received: () => `Recibida`,
deposited: () => `Depositado`,
withdrew: () => `Retirado`,
},
whyNoFees: {
help: {
title: () => `Sobre esta transferencia`,
description: {
whyNoFees: {
firstPara: (chainName: string) =>
`Esta transacción fue resuelta en ${chainName}, un rollup de Ethereum.`,
firstPara2Chain: (chainA: string, chainB: string) =>
Expand All @@ -71,12 +73,44 @@ export const es: LanguageDefinition = {
thirdPara: () =>
`Las transacciones cuestan unos centimos. Daimo patrocinó esta transferencia, haciéndola gratuita.`,
},
landlineDepositProcessing: {
firstPara: () =>
"Esta transacción transfiere fondos desde tu cuenta bancaria vinculada a tu cuenta Daimo.",
secondPara: () =>
"Una vez que nuestro socio reciba los fondos, realizaremos una transferencia en cadena para depositar los fondos en tu cuenta Daimo.",
thirdPara: () =>
"Las transferencias bancarias normalmente cuestan unos dólares. Daimo patrocinó esta transferencia, haciéndola gratuita.",
},
landlineDepositCompleted: {
firstPara: () =>
"Esta transacción transfirió fondos desde tu cuenta bancaria vinculada a tu cuenta Daimo.",
secondPara: () =>
"Las transferencias bancarias normalmente cuestan unos dólares. Daimo patrocinó esta transferencia, haciéndola gratuita.",
},
landlineWithdrawalProcessing: {
firstPara: () =>
"Esta transacción transfiere fondos desde tu cuenta Daimo a tu cuenta bancaria vinculada.",
secondPara: () =>
"Los fondos se transfieren en cadena a la dirección de nuestro socio. Una vez recibidos los fondos, iniciamos una transferencia bancaria a tu cuenta bancaria.",
thirdPara: () =>
"Las transferencias bancarias normalmente cuestan unos dólares. Daimo patrocinó esta transferencia, haciéndola gratuita.",
},
landlineWithdrawalCompleted: {
firstPara: () =>
"Esta transacción transfirió fondos desde tu cuenta Daimo a tu cuenta bancaria vinculada.",
secondPara: () =>
"Las transferencias bancarias normalmente cuestan unos dólares. Daimo patrocinó esta transferencia, haciéndola gratuita.",
},
},
feeText: {
free: () => `GRATIS`,
pending: () => `PENDIENTE`,
fee: (amount: string) => `${amount} TASA`,
},
fundArrivalTime: {
deposit: () => `Sus fondos llegarán a su cuenta Daimo`,
withdrawal: () => `Sus fondos llegarán a su cuenta bancaria`,
},
},

// ------------ KEYROTATION ------------
Expand Down
8 changes: 6 additions & 2 deletions apps/daimo-mobile/src/logic/accountManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import { useEffect, useState } from "react";
import { MMKV } from "react-native-mmkv";
import { Address, Hex } from "viem";

import { cacheEAccounts } from "./eAccountCache";
import { cacheLandlineAccounts } from "./landlineAccountCache";
import { getRpcFunc } from "./trpc";
import { ActHandle } from "../action/actStatus";
import { cacheEAccounts } from "../logic/addr";
import {
EnclaveKeyInfo,
deleteEnclaveKey,
Expand Down Expand Up @@ -163,7 +164,10 @@ class AccountManager {

// Cache accounts so that addresses show up with correct display names.
// Would be cleaner use a listener, but must run first.
if (account) cacheEAccounts(account.namedAccounts);
if (account) {
cacheEAccounts(account.namedAccounts);
cacheLandlineAccounts(account.landlineAccounts);
}

this.currentAccount = account;
this.mmkv.set("account", serializeAccount(account));
Expand Down
56 changes: 52 additions & 4 deletions apps/daimo-mobile/src/logic/daimoContacts.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { LandlineAccount } from "@daimo/api/src/landline/connector";
import {
EAccount,
EAccountSearchResult,
EmailAddress,
LandlineAccount,
PhoneNumber,
TransferClog,
TransferSwapClog,
canSendTo,
getAccountName,
getDisplayFromTo,
getTransferClogType,
zEmailAddress,
zPhoneNumber,
} from "@daimo/common";
import { daimoChainFromId } from "@daimo/contract";
import { Locale } from "expo-localization";
import { Address } from "viem";

import { getCachedEAccount } from "./addr";
import { getCachedEAccount } from "./eAccountCache";
import { getCachedLandlineAccount } from "./landlineAccountCache";
import { useSystemContactsSearch } from "./systemContacts";
import { getRpcHook } from "./trpc";
import IconDepositWallet from "../../assets/icon-deposit-wallet.png";
Expand Down Expand Up @@ -91,8 +98,8 @@ export function addLastTransferTimes(
return { type: "eAcc", ...otherEAcc, lastSendTime, lastRecvTime };
}

export function getContactName(r: DaimoContact) {
if (r.type === "eAcc") return getAccountName(r);
export function getContactName(r: DaimoContact, locale?: Locale) {
if (r.type === "eAcc") return getAccountName(r, locale);
else if (r.type === "email") return r.name ? r.name : r.email;
else if (r.type === "phoneNumber") return r.name ? r.name : r.phoneNumber;
else if (r.type === "landlineBankAccount")
Expand All @@ -117,6 +124,16 @@ export function getContactProfilePicture(
}
}

export function canSendToContact(otherContact: DaimoContact): boolean {
if (otherContact.type === "landlineBankAccount") {
return true;
} else if (otherContact.type === "eAcc") {
return canSendTo(otherContact as EAccount);
} else {
return false;
}
}

export function useContactSearch(
account: Account,
prefix: string,
Expand Down Expand Up @@ -220,6 +237,15 @@ export function useContactSearch(
};
}

export function eAccToContact(eAcc: EAccount): EAccountContact {
return { type: "eAcc", ...eAcc };
}

function eAccAddrToContact(addr: Address): EAccountContact {
const eAcc = getCachedEAccount(addr);
return eAccToContact(eAcc);
}

export function landlineAccountToContact(
landlineAccount: LandlineAccount
): LandlineBankAccountContact {
Expand All @@ -232,3 +258,25 @@ export function landlineAccountToContact(
bankLogo: landlineAccount.bankLogo,
};
}

function landlineAccountUuidToContact(
landlineAccountUuid: string
): LandlineBankAccountContact | null {
const account = getCachedLandlineAccount(landlineAccountUuid);
if (!account) return null;
return landlineAccountToContact(account);
}

export function getTransferClogContact(
transferClog: TransferClog,
accountAddress: Address
): LandlineBankAccountContact | EAccountContact {
if (getTransferClogType(transferClog) === "landline") {
const { accountID } = (transferClog as TransferSwapClog).offchainTransfer!;
const llContact = landlineAccountUuidToContact(accountID);
if (llContact) return llContact;
}

const [from, to] = getDisplayFromTo(transferClog);
return eAccAddrToContact(from === accountAddress ? to : from);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { EAccount } from "@daimo/common";
import { Address } from "viem";

const nameCache = new Map<Address, EAccount>();
const eAccountCache = new Map<Address, EAccount>();

export function cacheEAccounts(accounts: EAccount[]) {
for (const account of accounts) {
nameCache.set(account.addr, account);
eAccountCache.set(account.addr, account);
}
}

export function getCachedEAccount(addr: Address): EAccount {
return nameCache.get(addr) || { addr };
return eAccountCache.get(addr) || { addr };
}
16 changes: 16 additions & 0 deletions apps/daimo-mobile/src/logic/landlineAccountCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { LandlineAccount } from "@daimo/common";

// Maps Landline account uuid to account
const landlineAccountCache = new Map<string, LandlineAccount>();

export function cacheLandlineAccounts(accounts: LandlineAccount[]) {
for (const account of accounts) {
landlineAccountCache.set(account.landlineAccountUuid, account);
}
}

export function getCachedLandlineAccount(
landlineAccountUuid: string
): LandlineAccount | null {
return landlineAccountCache.get(landlineAccountUuid) || null;
}
2 changes: 1 addition & 1 deletion apps/daimo-mobile/src/storage/account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { LandlineAccount } from "@daimo/api/src/landline/connector";
import {
ChainGasConstants,
CurrencyExchangeRate,
Expand All @@ -8,6 +7,7 @@ import {
EAccount,
KeyData,
KeyRotationClog,
LandlineAccount,
LinkedAccount,
ProposedSwap,
RecommendedExchange,
Expand Down
20 changes: 13 additions & 7 deletions apps/daimo-mobile/src/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { daimoChainFromId } from "@daimo/contract";
import * as SplashScreen from "expo-splash-screen";

import { getNetworkState, updateNetworkState } from "./networkState";
import { addLandlineTransfers } from "./syncLandline";
import { i18NLocale } from "../i18n";
import { getAccountManager } from "../logic/accountManager";
import { SEND_DEADLINE_SECS } from "../logic/opSender";
Expand Down Expand Up @@ -335,15 +336,20 @@ function addNamedAccounts(old: EAccount[], found: EAccount[]): EAccount[] {

/** Add transfers based on new Transfer event logs */
function addTransfers(
old: TransferClog[],
logs: TransferClog[]
oldLogs: TransferClog[],
newLogs: TransferClog[]
): TransferClog[] {
// Sort new logs
const { logs, remaining } = addLandlineTransfers(oldLogs, newLogs);

logs.push(...remaining);

// Sort logs. Timestamp is determined by block number for on-chain txs.
// If timestamp is the same, sort by log index to ensure determinism.
logs.sort((a, b) => {
if (a.blockNumber !== b.blockNumber) return a.blockNumber! - b.blockNumber!;
return a.logIndex! - b.logIndex!;
const diff = a.timestamp - b.timestamp;
if (diff !== 0) return diff;
return (a.logIndex || 0) - (b.logIndex || 0);
});

// old finalized logs + new logs
return [...old, ...logs];
return logs;
}
Loading

0 comments on commit c918393

Please sign in to comment.