diff --git a/.changeset/brave-otters-hide.md b/.changeset/brave-otters-hide.md new file mode 100644 index 0000000000..3bb6c885fa --- /dev/null +++ b/.changeset/brave-otters-hide.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +💄 hide defi section diff --git a/.changeset/brave-otters-wave.md b/.changeset/brave-otters-wave.md new file mode 100644 index 0000000000..bc5b479d5f --- /dev/null +++ b/.changeset/brave-otters-wave.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ restructure add funds root menu diff --git a/.changeset/brisk-terns-guide.md b/.changeset/brisk-terns-guide.md new file mode 100644 index 0000000000..ce3d5bea2c --- /dev/null +++ b/.changeset/brisk-terns-guide.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add swap and bridge swap guide sheets diff --git a/.changeset/calm-badgers-greet.md b/.changeset/calm-badgers-greet.md new file mode 100644 index 0000000000..fdde20c744 --- /dev/null +++ b/.changeset/calm-badgers-greet.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ resolve ens name for owner address diff --git a/.changeset/calm-herons-store.md b/.changeset/calm-herons-store.md new file mode 100644 index 0000000000..0b983ef8a1 --- /dev/null +++ b/.changeset/calm-herons-store.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +⚡️ cache deployed bytecode indefinitely diff --git a/.changeset/clever-crews-pay.md b/.changeset/clever-crews-pay.md new file mode 100644 index 0000000000..bf402955ad --- /dev/null +++ b/.changeset/clever-crews-pay.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +💄 implement app theme for base diff --git a/.changeset/curvy-jobs-sort.md b/.changeset/curvy-jobs-sort.md new file mode 100644 index 0000000000..fd4797bb23 --- /dev/null +++ b/.changeset/curvy-jobs-sort.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add card statement diff --git a/.changeset/deep-owls-emit.md b/.changeset/deep-owls-emit.md new file mode 100644 index 0000000000..0947e46cb2 --- /dev/null +++ b/.changeset/deep-owls-emit.md @@ -0,0 +1,5 @@ +--- +"@exactly/common": patch +--- + +🔨 codegen allowlists for all chains diff --git a/.changeset/eager-owls-list.md b/.changeset/eager-owls-list.md new file mode 100644 index 0000000000..facdf7f085 --- /dev/null +++ b/.changeset/eager-owls-list.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add cryptocurrencies asset list diff --git a/.changeset/glad-finches-map.md b/.changeset/glad-finches-map.md new file mode 100644 index 0000000000..d443d3cd3b --- /dev/null +++ b/.changeset/glad-finches-map.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ display allowlisted assets per network diff --git a/.changeset/keen-otters-match.md b/.changeset/keen-otters-match.md new file mode 100644 index 0000000000..1af9afaec4 --- /dev/null +++ b/.changeset/keen-otters-match.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add supported asset match sheet diff --git a/.changeset/neat-cranes-copy.md b/.changeset/neat-cranes-copy.md new file mode 100644 index 0000000000..d29a5967eb --- /dev/null +++ b/.changeset/neat-cranes-copy.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +💄 redesign copy address sheet diff --git a/.changeset/plush-swans-send.md b/.changeset/plush-swans-send.md new file mode 100644 index 0000000000..8532de4d93 --- /dev/null +++ b/.changeset/plush-swans-send.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add request sent screen diff --git a/.changeset/proud-lions-glow.md b/.changeset/proud-lions-glow.md new file mode 100644 index 0000000000..4e9fe236c8 --- /dev/null +++ b/.changeset/proud-lions-glow.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ redesign crypto receive screen diff --git a/.changeset/quiet-cranes-teach.md b/.changeset/quiet-cranes-teach.md new file mode 100644 index 0000000000..7a935bad0a --- /dev/null +++ b/.changeset/quiet-cranes-teach.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add bridge education sheet diff --git a/.changeset/round-moles-count.md b/.changeset/round-moles-count.md new file mode 100644 index 0000000000..a4e059f959 --- /dev/null +++ b/.changeset/round-moles-count.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +💄 redesign bridge quote screen diff --git a/.changeset/sly-foxes-sift.md b/.changeset/sly-foxes-sift.md new file mode 100644 index 0000000000..755c7dc91a --- /dev/null +++ b/.changeset/sly-foxes-sift.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add network filter to asset select sheet diff --git a/.changeset/soft-doves-teach.md b/.changeset/soft-doves-teach.md new file mode 100644 index 0000000000..f4120f1d30 --- /dev/null +++ b/.changeset/soft-doves-teach.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add network section info sheets diff --git a/.changeset/true-bottles-cough.md b/.changeset/true-bottles-cough.md new file mode 100644 index 0000000000..614b80c3cf --- /dev/null +++ b/.changeset/true-bottles-cough.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +🐛 fix swap token selection diff --git a/.changeset/warm-swifts-flip.md b/.changeset/warm-swifts-flip.md new file mode 100644 index 0000000000..5ed8e42447 --- /dev/null +++ b/.changeset/warm-swifts-flip.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +💄 toggle qr inline on receive screen diff --git a/.changeset/wet-needles-call.md b/.changeset/wet-needles-call.md new file mode 100644 index 0000000000..0b5ebd578c --- /dev/null +++ b/.changeset/wet-needles-call.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add card limit kyc flow diff --git a/.changeset/wise-crows-allow.md b/.changeset/wise-crows-allow.md new file mode 100644 index 0000000000..cf615b064c --- /dev/null +++ b/.changeset/wise-crows-allow.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ list allowlisted assets on add funds diff --git a/.changeset/witty-geese-roam.md b/.changeset/witty-geese-roam.md new file mode 100644 index 0000000000..cefbfac03c --- /dev/null +++ b/.changeset/witty-geese-roam.md @@ -0,0 +1,5 @@ +--- +"@exactly/mobile": patch +--- + +✨ add network selection to receive flow diff --git a/app.config.ts b/app.config.ts index 1dbe60c115..d1a94e4300 100644 --- a/app.config.ts +++ b/app.config.ts @@ -83,6 +83,7 @@ export default { { supportedLocales: ["en", "es", "es-AR", "es-CR", "es-GT", "es-HN", "es-NI", "es-PY", "es-SV", "es-UY", "pt"] }, ], "expo-router", + "expo-sharing", [ "@intercom/intercom-react-native", { diff --git a/common/wagmi.config.ts b/common/wagmi.config.ts index fee9b40b14..ac4a699425 100644 --- a/common/wagmi.config.ts +++ b/common/wagmi.config.ts @@ -1,7 +1,7 @@ import { defineConfig, type Plugin } from "@wagmi/cli"; import { foundry, react } from "@wagmi/cli/plugins"; import { execSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { readdirSync, readFileSync } from "node:fs"; import { env } from "node:process"; import { getAddress, type Abi } from "viem"; import { anvil, base, baseSepolia, optimism, optimismSepolia } from "viem/chains"; @@ -35,9 +35,26 @@ const [proposalManager] = loadBroadcast("ProposalManager").transactions; const [refunder] = loadBroadcast("Refunder").transactions; const [exaPreviewer] = loadBroadcast("ExaPreviewer").transactions; const swapper = (deploy.accounts.swapper as Record)[chainId] ?? deploy.accounts.swapper.default; -const allowlist = Object.keys( - (deploy.proposalManager.allowlist as Record>)[chainId] ?? {}, -).map((address) => getAddress(address)); +const allowlists = Object.fromEntries( + Object.entries(deploy.proposalManager.allowlist as Record>).map(([id, tokens]) => { + const network = { [String(optimism.id)]: "optimism", [String(base.id)]: "base" }[id]; + const underlying = network + ? readdirSync(`node_modules/@exactly/protocol/deployments/${network}`) + .flatMap((file) => /^Market([A-Za-z.]+)\.json$/.exec(file)?.[1] ?? []) + .filter((asset) => asset !== "ETHRouter") + .map((asset) => + getAddress( + ( + JSON.parse( + readFileSync(`node_modules/@exactly/protocol/deployments/${network}/${asset}.json`, "utf8"), + ) as Deployment + ).address, + ), + ) + : []; + return [id, [...underlying, ...Object.keys(tokens).map((address) => getAddress(address))]]; + }), +); if (!exaPlugin || !issuerChecker || !proposalManager || !exaPreviewer || !refunder || !swapper) { throw new Error("missing contracts"); } @@ -113,7 +130,12 @@ export default defineConfig([ }, }, ), - { name: "Allowlist", run: () => ({ content: `export const allowlist = ${JSON.stringify(allowlist)} as const` }) }, + { + name: "Allowlist", + run: () => ({ + content: `export const allowlists: Record = ${JSON.stringify(allowlists)}`, + }), + }, foundry({ forge: { build: false }, project: "../contracts", @@ -168,6 +190,7 @@ function addresses( } function chain(): Plugin { + const isBase = chainId === base.id || chainId === baseSepolia.id; if (chainId === anvil.id) { return { name: "Chain", @@ -177,6 +200,7 @@ const chain = anvil as Chain chain.rpcUrls.alchemy = chain.rpcUrls.default chain.contracts = { multicall3: { address: "${optimism.contracts.multicall3.address}" } } chain.blockExplorers = { default: { name: "Otterscan", url: "http://localhost:5100" } } +export const isBase: boolean = ${isBase} export default chain as Chain & { contracts: { multicall3: { address: \`0x\${string}\` } } rpcUrls: { alchemy: { http: readonly [string] } } @@ -197,6 +221,7 @@ export default chain as Chain & { run: () => ({ content: `import { ${importName} } from "@account-kit/infra" import { type Chain } from "viem/chains" +export const isBase: boolean = ${isBase} export default ${importName} as Chain & { contracts: { multicall3: { address: \`0x\${string}\` } } rpcUrls: { alchemy: { http: readonly [string] } } diff --git a/metro.config.cjs b/metro.config.cjs index 40b6eba0a3..691fdc683c 100644 --- a/metro.config.cjs +++ b/metro.config.cjs @@ -1,48 +1,65 @@ -const { getSentryExpoConfig } = require("@sentry/react-native/metro"); +const { getExpoTransformer, getReactNativeTransformer } = require("react-native-svg-transformer"); +const reactNativeSvgTransformer = require("react-native-svg-transformer/expo"); + const path = require("node:path"); -const config = getSentryExpoConfig(__dirname, { annotateReactComponents: true }); +const rawTransformer = getExpoTransformer() ?? getReactNativeTransformer(); +const themeable = /\b(fill|stroke)="#([0-9a-f]{6})"/g; -/** @type {import('metro-config').InputConfigT} */ -module.exports = { - ...config, - resolver: { - ...config.resolver, - extraNodeModules: { crypto: require.resolve("react-native-quick-crypto") }, - assetExts: config.resolver?.assetExts?.filter((extension) => extension !== "svg"), - sourceExts: [...(config.resolver?.sourceExts ?? []), "svg"], - blockList: [ - ...((config.resolver?.blockList && - (Array.isArray(config.resolver.blockList) ? config.resolver.blockList : [config.resolver.blockList])) ?? - []), - new RegExp(path.join(__dirname, String.raw`\.\w+/`)), - new RegExp(path.join(__dirname, "android/")), - new RegExp(path.join(__dirname, "contracts/")), - new RegExp(path.join(__dirname, "build/")), - new RegExp(path.join(__dirname, "dist/")), - new RegExp(path.join(__dirname, "ios/")), - new RegExp(path.join(__dirname, "public/")), - new RegExp(path.join(__dirname, "server/")), - ], - resolveRequest: (context, moduleName, platform) => { - if (moduleName === "tslib") return context.resolveRequest(context, "tslib/tslib.es6.js", platform); - if ( - /date-fns\/locale\.(?:js|cjs|mjs)$/.test(context.originModulePath) && - moduleName.startsWith("./locale/") && - !/^(?:en|es|pt)(?:-|$)/.test(moduleName.slice("./locale/".length).replace(/\.js$/, "")) - ) { - return { type: "empty" }; - } - try { - return context.resolveRequest(context, moduleName, platform); - } catch (error) { - if (moduleName.endsWith(".js")) return context.resolveRequest(context, moduleName.slice(0, -3), platform); - throw error; - } +module.exports = function config() { + const { getSentryExpoConfig } = require("@sentry/react-native/metro"); + const base = getSentryExpoConfig(__dirname, { annotateReactComponents: true }); + /** @type {import('metro-config').InputConfigT} */ + const merged = { + ...base, + resolver: { + ...base.resolver, + extraNodeModules: { crypto: require.resolve("react-native-quick-crypto") }, + assetExts: base.resolver?.assetExts?.filter((extension) => extension !== "svg"), + sourceExts: [...(base.resolver?.sourceExts ?? []), "svg"], + blockList: [ + ...((base.resolver?.blockList && + (Array.isArray(base.resolver.blockList) ? base.resolver.blockList : [base.resolver.blockList])) ?? + []), + new RegExp(path.join(__dirname, String.raw`\.\w+/`)), + new RegExp(path.join(__dirname, "android/")), + new RegExp(path.join(__dirname, "contracts/")), + new RegExp(path.join(__dirname, "build/")), + new RegExp(path.join(__dirname, "dist/")), + new RegExp(path.join(__dirname, "ios/")), + new RegExp(path.join(__dirname, "public/")), + new RegExp(path.join(__dirname, "server/")), + ], + resolveRequest: (context, moduleName, platform) => { + if (moduleName === "tslib") return context.resolveRequest(context, "tslib/tslib.es6.js", platform); + if ( + /date-fns\/locale\.(?:js|cjs|mjs)$/.test(context.originModulePath) && + moduleName.startsWith("./locale/") && + !/^(?:en|es|pt)(?:-|$)/.test(moduleName.slice("./locale/".length).replace(/\.js$/, "")) + ) { + return { type: "empty" }; + } + try { + return context.resolveRequest(context, moduleName, platform); + } catch (error) { + if (moduleName.endsWith(".js")) return context.resolveRequest(context, moduleName.slice(0, -3), platform); + throw error; + } + }, }, - }, - transformer: { - ...config.transformer, - babelTransformerPath: require.resolve("react-native-svg-transformer/expo"), - }, + transformer: { ...base.transformer, babelTransformerPath: require.resolve("./metro.config.cjs") }, + }; + return merged; +}; + +/** @param {import("@sentry/react-native/dist/js/tools/vendor/metro/metroBabelTransformer").BabelTransformerArgs} args */ +module.exports.transform = function transform(args) { + if (args.filename.endsWith(".svg")) { + if (args.src.includes(' data-themed=""')) { + const themed = args.src.replaceAll(' data-themed=""', "").replaceAll(themeable, '$1="var(--s$2,#$2)"'); + return rawTransformer.transform({ ...args, src: `module.exports = ${JSON.stringify(themed)};` }); + } + return reactNativeSvgTransformer.transform(args); + } + return require("@sentry/react-native/dist/js/tools/sentryBabelTransformer").transform(args); }; diff --git a/package.json b/package.json index 2375e4b193..b755f1786d 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "expo-camera": "~55.0.9", "expo-clipboard": "~55.0.8", "expo-constants": "~55.0.9", + "expo-file-system": "~55.0.23", "expo-font": "~55.0.4", "expo-haptics": "~55.0.9", "expo-image": "~55.0.5", @@ -83,6 +84,7 @@ "expo-local-authentication": "~55.0.9", "expo-localization": "~55.0.9", "expo-router": "~55.0.5", + "expo-sharing": "~55.0.21", "expo-status-bar": "~55.0.4", "expo-system-ui": "~55.0.9", "expo-updates": "~55.0.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed06467030..29b8942432 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,6 +270,9 @@ importers: expo-constants: specifier: ~55.0.9 version: 55.0.9(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(typescript@5.9.3) + expo-file-system: + specifier: ~55.0.23 + version: 55.0.24(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10)) expo-font: specifier: ~55.0.4 version: 55.0.4(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) @@ -297,6 +300,9 @@ importers: expo-router: specifier: ~55.0.5 version: 55.0.8(e648b7c2d7f97def12d5a5dbfb5d4641) + expo-sharing: + specifier: ~55.0.21 + version: 55.0.22(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) expo-status-bar: specifier: ~55.0.4 version: 55.0.4(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) @@ -1115,6 +1121,9 @@ packages: resolution: {integrity: sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + '@babel/code-frame@7.10.4': + resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1218,6 +1227,10 @@ packages: resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} + '@babel/highlight@7.25.9': + resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.2': resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} @@ -3120,12 +3133,18 @@ packages: '@expo/code-signing-certificates@0.0.6': resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==} + '@expo/config-plugins@55.0.11': + resolution: {integrity: sha512-85ZSmIK8rMfvYbG/2IHtwnykVdb6LI0ZavduM3ZwUMThyyVegYjVsO5Zek2ec8xzPhCmYX0ar5onnFEcwUDUlw==} + '@expo/config-plugins@55.0.7': resolution: {integrity: sha512-XZUoDWrsHEkH3yasnDSJABM/UxP5a1ixzRwU/M+BToyn/f0nTrSJJe/Ay/FpxkI4JSNz2n0e06I23b2bleXKVA==} '@expo/config-types@55.0.5': resolution: {integrity: sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==} + '@expo/config-types@55.0.6': + resolution: {integrity: sha512-S+GJKYoIjnWlert/9vXuTohaTsMbyOLSVxdIgPgoq3P4N1p4CWrfyZLnz6qRug8wYSO5fcYcS9mFleyEP8wRLg==} + '@expo/config@55.0.11': resolution: {integrity: sha512-14AkSmR1gOIUhCsPJ0cAo5ZduMNsPQsmFV9jBNZn1xC5Zb3D8x5eqvUie5QzWaUwdcyrq79uYJ2bTCiC6+nD0Q==} @@ -3164,6 +3183,9 @@ packages: '@expo/json-file@10.0.12': resolution: {integrity: sha512-inbDycp1rMAelAofg7h/mMzIe+Owx6F7pur3XdQ3EPTy00tme+4P6FWgHKUcjN8dBSrnbRNpSyh5/shzHyVCyQ==} + '@expo/json-file@10.0.16': + resolution: {integrity: sha512-fcVkWEj+hLuP2yt5W0aw6LmDRqSPWDLUSxOMcmFeV+algmIF59sQVKCwB9btjQLd4V6x9N0pISkQEkBubUHrCw==} + '@expo/local-build-cache-provider@55.0.7': resolution: {integrity: sha512-Qg9uNZn1buv4zJUA4ZQaz+ZnKDCipRgjoEg2Gcp8Qfy+2Gq5yZKX4YN1TThCJ01LJk/pvJsCRxXlXZSwdZppgg==} @@ -3206,6 +3228,9 @@ packages: '@expo/plist@0.5.2': resolution: {integrity: sha512-o4xdVdBpe4aTl3sPMZ2u3fJH4iG1I768EIRk1xRZP+GaFI93MaR3JvoFibYqxeTmLQ1p1kNEVqylfUjezxx45g==} + '@expo/plist@0.5.4': + resolution: {integrity: sha512-Jqppj0FULNq6Zp5JtQrFICl8TtpMjwwUbxEcEC2T3z7m+TOrTQEHZXz3D3Ay7vhbmvD+VMgfWJ4ARclJXeN8Eg==} + '@expo/prebuild-config@55.0.11': resolution: {integrity: sha512-PqjbTTHXS0dnZMH4X5/0rnLxKfQqyN1s/5lmxITn+U6WDUNibatUepfjwV+5C2jU4hv5z2haqX6e9hQ0zUtDMA==} peerDependencies: @@ -8827,8 +8852,8 @@ packages: expo-eas-client@55.0.3: resolution: {integrity: sha512-KkkjjPc4VKpLVEbOMAvWp87m1YiFEgM6fDNNk5LRynfJ4V8a287P5jlnZuRvHCFWWczTczXT9iS39r8G4VZGDQ==} - expo-file-system@55.0.12: - resolution: {integrity: sha512-MFN/3L3gm174nxP2HqKQsSsPbjAj92wuidKFGSbl3Lt6oJTS09EbTwszX5BhYeeVSprcsw8pnlxYSmhkSqGEFw==} + expo-file-system@55.0.24: + resolution: {integrity: sha512-7/HJdvaaf3kP5T3atd+6Q0/QexrGio4Fs2GVtp7G8d/xQCSu01yeGReu7tGQo9VAM67Snq+ujGlL8q2wbMXLkQ==} peerDependencies: expo: '*' react-native: '*' @@ -8955,6 +8980,13 @@ packages: resolution: {integrity: sha512-xI72FTm469FfuuBL2R5aNtthgH+GR7ygOpsx/KcPS0K8AZaZd7VjtEExbzn9/qyyYkWW3T+3dAmCDKOMX8gdmQ==} engines: {node: '>=20.16.0'} + expo-sharing@55.0.22: + resolution: {integrity: sha512-52s4FfjNMLJfKl4wxakRjbrfgya3w7XZ15tCc/7IVH2HX/q467mePBS1VqYWbuuKDjvDAL02Aj2LcFaUNwkoWQ==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + expo-status-bar@55.0.4: resolution: {integrity: sha512-BPDjUXKqv1F9j2YNGLRZfkBEZXIEEpqj+t81y4c+4fdSN3Pos7goIHXgcl2ozbKQLgKRZQyNZQtbUgh5UjHYUQ==} peerDependencies: @@ -14242,6 +14274,10 @@ snapshots: is-docker: 4.0.0 package-manager-detector: 1.6.0 + '@babel/code-frame@7.10.4': + dependencies: + '@babel/highlight': 7.25.9 + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -14396,6 +14432,13 @@ snapshots: '@babel/template': 7.28.6 '@babel/types': 7.29.0 + '@babel/highlight@7.25.9': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 @@ -16534,6 +16577,24 @@ snapshots: dependencies: node-forge: 1.4.0 + '@expo/config-plugins@55.0.11': + dependencies: + '@expo/config-types': 55.0.6 + '@expo/json-file': 10.0.16 + '@expo/plist': 0.5.4 + '@expo/sdk-runtime-versions': 1.0.0 + chalk: 4.1.2 + debug: 4.4.3(supports-color@7.2.0) + getenv: 2.0.0 + glob: 13.0.6 + resolve-from: 5.0.0 + semver: 7.8.5 + slugify: 1.6.8 + xcode: 3.0.1 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + '@expo/config-plugins@55.0.7': dependencies: '@expo/config-types': 55.0.5 @@ -16554,6 +16615,8 @@ snapshots: '@expo/config-types@55.0.5': {} + '@expo/config-types@55.0.6': {} + '@expo/config@55.0.11(typescript@5.9.3)': dependencies: '@expo/config-plugins': 55.0.7 @@ -16630,6 +16693,11 @@ snapshots: '@babel/code-frame': 7.29.0 json5: 2.2.3 + '@expo/json-file@10.0.16': + dependencies: + '@babel/code-frame': 7.10.4 + json5: 2.2.3 + '@expo/local-build-cache-provider@55.0.7(typescript@5.9.3)': dependencies: '@expo/config': 55.0.11(typescript@5.9.3) @@ -16729,6 +16797,12 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 + '@expo/plist@0.5.4': + dependencies: + '@xmldom/xmldom': 0.8.13 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + '@expo/prebuild-config@55.0.11(expo@55.0.9)(typescript@5.9.3)': dependencies: '@expo/config': 55.0.11(typescript@5.9.3) @@ -23963,7 +24037,7 @@ snapshots: expo-eas-client@55.0.3: {} - expo-file-system@55.0.12(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10)): + expo-file-system@55.0.24(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10)): dependencies: expo: 55.0.9(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.7)(bufferutil@4.1.0)(expo-router@55.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10) @@ -24109,6 +24183,17 @@ snapshots: expo-server@55.0.6: {} + expo-sharing@55.0.22(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0): + dependencies: + '@expo/config-plugins': 55.0.11 + '@expo/config-types': 55.0.6 + '@expo/plist': 0.5.4 + expo: 55.0.9(@babel/core@7.29.0)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.7)(bufferutil@4.1.0)(expo-router@55.0.8)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + react: 19.2.0 + react-native: 0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - supports-color + expo-status-bar@55.0.4(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0): dependencies: react: 19.2.0 @@ -24186,7 +24271,7 @@ snapshots: babel-preset-expo: 55.0.13(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@55.0.9)(react-refresh@0.14.2) expo-asset: 55.0.10(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0)(typescript@5.9.3) expo-constants: 55.0.9(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(typescript@5.9.3) - expo-file-system: 55.0.12(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10)) + expo-file-system: 55.0.24(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10)) expo-font: 55.0.4(expo@55.0.9)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.0)(utf-8-validate@5.0.10))(react@19.2.0) expo-keep-awake: 55.0.4(expo@55.0.9)(react@19.2.0) expo-modules-autolinking: 55.0.12(typescript@5.9.3) diff --git a/src/app/(main)/(home)/_layout.tsx b/src/app/(main)/(home)/_layout.tsx index e749dc178f..8ba94afe9e 100644 --- a/src/app/(main)/(home)/_layout.tsx +++ b/src/app/(main)/(home)/_layout.tsx @@ -6,7 +6,7 @@ import { selectionAsync } from "expo-haptics"; import Head from "expo-router/head"; import { TabList, Tabs, TabSlot, TabTrigger, useTabTrigger } from "expo-router/ui"; -import { Boxes, CalendarCheck, CreditCard, History, Home } from "@tamagui/lucide-icons"; +import { CalendarCheck, CreditCard, History, Home } from "@tamagui/lucide-icons"; import { YStack } from "tamagui"; import SafeView from "../../../components/shared/SafeView"; @@ -20,7 +20,6 @@ const tabs = [ { name: "index", title: "Home", href: "/", Icon: Home }, { name: "card", title: "Card", href: "/card", Icon: CreditCard }, { name: "pay-mode", title: "Payments", href: "/pay-mode", Icon: CalendarCheck }, - { name: "defi", title: "DeFi", href: "/defi", Icon: Boxes }, { name: "activity", title: "Activity", href: "/activity", Icon: History }, ] as const; diff --git a/src/app/(main)/(home)/defi.tsx b/src/app/(main)/(home)/defi.tsx index ef35d2a6a7..39ff2dd7dc 100644 --- a/src/app/(main)/(home)/defi.tsx +++ b/src/app/(main)/(home)/defi.tsx @@ -1 +1,7 @@ -export { default } from "../../../components/defi/DeFi"; +import React from "react"; + +import { Redirect } from "expo-router"; + +export default function DeFi() { + return ; +} diff --git a/src/app/(main)/add-funds/_layout.tsx b/src/app/(main)/add-funds/_layout.tsx index 7a9d15ba86..5d879484e4 100644 --- a/src/app/(main)/add-funds/_layout.tsx +++ b/src/app/(main)/add-funds/_layout.tsx @@ -10,9 +10,11 @@ export default function AddFundsLayout() { + + diff --git a/src/app/(main)/add-funds/assets.tsx b/src/app/(main)/add-funds/assets.tsx new file mode 100644 index 0000000000..2baf829beb --- /dev/null +++ b/src/app/(main)/add-funds/assets.tsx @@ -0,0 +1 @@ +export { default } from "../../../components/add-funds/Assets"; diff --git a/src/app/(main)/add-funds/network.tsx b/src/app/(main)/add-funds/network.tsx new file mode 100644 index 0000000000..60ac5fc5ad --- /dev/null +++ b/src/app/(main)/add-funds/network.tsx @@ -0,0 +1 @@ +export { default } from "../../../components/add-funds/Network"; diff --git a/src/app/(main)/payment-history/_layout.tsx b/src/app/(main)/payment-history/_layout.tsx new file mode 100644 index 0000000000..ba23dfba66 --- /dev/null +++ b/src/app/(main)/payment-history/_layout.tsx @@ -0,0 +1,10 @@ +import React from "react"; + +import { Stack } from "expo-router"; + +import useBackgroundColor from "../../../utils/useBackgroundColor"; + +export default function PaymentHistoryLayout() { + useBackgroundColor(); + return ; +} diff --git a/src/app/(main)/payment-history/index.tsx b/src/app/(main)/payment-history/index.tsx new file mode 100644 index 0000000000..8b8920b000 --- /dev/null +++ b/src/app/(main)/payment-history/index.tsx @@ -0,0 +1 @@ +export { default } from "../../../components/pay/History"; diff --git a/src/app/(main)/statement/_layout.tsx b/src/app/(main)/statement/_layout.tsx new file mode 100644 index 0000000000..9a61950a03 --- /dev/null +++ b/src/app/(main)/statement/_layout.tsx @@ -0,0 +1,10 @@ +import React from "react"; + +import { Stack } from "expo-router"; + +import useBackgroundColor from "../../../utils/useBackgroundColor"; + +export default function StatementLayout() { + useBackgroundColor(); + return ; +} diff --git a/src/app/(main)/statement/index.tsx b/src/app/(main)/statement/index.tsx new file mode 100644 index 0000000000..fe19c69f4d --- /dev/null +++ b/src/app/(main)/statement/index.tsx @@ -0,0 +1 @@ +export { default } from "../../../components/statement/Statement"; diff --git a/src/app/+html.tsx b/src/app/+html.tsx index e76c93c96d..5ecd8ce70c 100644 --- a/src/app/+html.tsx +++ b/src/app/+html.tsx @@ -3,6 +3,7 @@ import React, { type ReactNode } from "react"; import { ScrollViewStyleReset } from "expo-router/html"; import domain from "@exactly/common/domain"; +import { isBase } from "@exactly/common/generated/chain"; import appMetadata from "../../package.json"; @@ -28,26 +29,30 @@ export default function HTML({ children }: { children: ReactNode }) { - + `); +} + +function recolor(hex: string, brandHue: number) { + const [h, s, l] = toHsl(hex); + if (s < 0.15 || l < 0.06 || l > 0.95 || h < tealMin || h > tealMax) return `#${hex}`; + return hsl(brandHue, s, l); +} + +function hueOf(color: unknown) { + const hex = typeof color === "string" ? color.replace("#", "") : ""; + return /^[\da-f]{6}$/i.test(hex) ? toHsl(hex)[0] : fallbackHue; +} + +function toHsl(hex: string): [number, number, number] { + const r = Number.parseInt(hex.slice(0, 2), 16) / 255; + const g = Number.parseInt(hex.slice(2, 4), 16) / 255; + const b = Number.parseInt(hex.slice(4, 6), 16) / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const l = (max + min) / 2; + const d = max - min; + const s = d === 0 ? 0 : l > 0.5 ? d / (2 - max - min) : d / (max + min); + const h = d === 0 ? 0 : 60 * (max === r ? ((g - b) / d + 6) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4); + return [h, s, l]; +} + +function hsl(h: number, s: number, l: number) { + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + const [r, g, b] = + h < 60 + ? [c, x, 0] + : h < 120 + ? [x, c, 0] + : h < 180 + ? [0, c, x] + : h < 240 + ? [0, x, c] + : h < 300 + ? [x, 0, c] + : [c, 0, x]; + const channel = (v: number) => + Math.round((v + m) * 255) + .toString(16) + .padStart(2, "0"); + return `#${channel(r)}${channel(g)}${channel(b)}`; +} diff --git a/src/components/statement/Statement.tsx b/src/components/statement/Statement.tsx new file mode 100644 index 0000000000..e8087bfd23 --- /dev/null +++ b/src/components/statement/Statement.tsx @@ -0,0 +1,335 @@ +import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +import { useLocalSearchParams, useRouter } from "expo-router"; + +import { ArrowLeft, CircleCheck, CircleHelp, Coins, CreditCard, Download, FileText } from "@tamagui/lucide-icons"; +import { useToastController } from "@tamagui/toast"; +import { ScrollView, Separator, Spinner, XStack, YStack } from "tamagui"; + +import { MATURITY_INTERVAL } from "@exactly/lib"; + +import { presentArticle } from "../../utils/intercom"; +import reportError from "../../utils/reportError"; +import { downloadStatement, group } from "../../utils/statement"; +import useStatement from "../../utils/useStatement"; +import IconButton from "../shared/IconButton"; +import InfoAlert from "../shared/InfoAlert"; +import SafeView from "../shared/SafeView"; +import Skeleton from "../shared/Skeleton"; +import Text from "../shared/Text"; +import View from "../shared/View"; + +export default function Statement() { + const { + t, + i18n: { language }, + } = useTranslation(); + const router = useRouter(); + const toast = useToastController(); + const insets = useSafeAreaInsets(); + const parameter = useLocalSearchParams().maturity; + const raw = Array.isArray(parameter) ? parameter[0] : parameter; + const maturity = raw && /^\d+$/.test(raw) ? Number(raw) : undefined; + const { data, isLoading, isError, isFetching, refetch } = useStatement(maturity); + const [downloading, setDownloading] = useState(false); + + const money = (value: number) => value.toLocaleString(language, { style: "currency", currency: "USD" }); + const percent = (value: number) => + value.toLocaleString(language, { style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2 }); + const day = (value: number | string) => + new Date(typeof value === "number" ? value * 1000 : value).toLocaleDateString(language, { + year: "numeric", + month: "long", + day: "numeric", + }); + + const period = + maturity === undefined + ? undefined + : t("{{start}} to {{end}}", { start: day(maturity - MATURITY_INTERVAL), end: day(maturity) }); + + const { cards, payments, paid, due } = useMemo(() => group(data ?? []), [data]); + const empty = cards.length === 0 && payments.length === 0; + const ready = !isLoading && !empty; + const lastCard = cards.length - 1; + + function download() { + if (maturity === undefined || downloading) return; + setDownloading(true); + downloadStatement(maturity, `account-statement-${maturity}.pdf`) + .catch((error: unknown) => { + reportError(error); + toast.show(t("Couldn't download your statement. Please try again."), { + burntOptions: { haptic: "error", preset: "error" }, + }); + }) + .finally(() => { + setDownloading(false); + }); + } + + return ( + + + { + router.back(); + }} + /> + { + presentArticle("10245778").catch(reportError); + }} + /> + + + + + + + + {t("Statement")} + + + {!empty && ( + + + {t("Download")} + + {downloading ? ( + + ) : ( + + )} + + )} + + {period && ( + + {period} + + )} + + + {isLoading ? ( + + + + + + + ) : isError && !data ? ( + + { + refetch().catch(reportError); + }} + /> + + ) : empty ? ( + + + {t("No statement for this period")} + + + ) : ( + + {cards.map((card, index) => ( + + } first={index === 0}> + {t("Card **** {{lastFour}} installments", { lastFour: card.lastFour })} + + {card.dates.map((dates) => ( + + + + {day(dates.label)} + + + {dates.rows.map((row) => ( + + + + + {row.merchant} + + {row.total > 1 && ( + + {t("Installment {{current}} of {{total}}", { + current: row.current, + total: row.total, + })} + + )} + + + {money(row.amount)} + + + + ))} + + ))} + } + last={payments.length === 0 && index === lastCard} + > + + + + {t("Total installments")} + + + {t("Card **** {{lastFour}}", { lastFour: card.lastFour })} + + + + {money(card.total)} + + + + + ))} + + {payments.length > 0 && ( + <> + }> + {t("Payments")} + + {payments.map((payment) => { + const off = + payment.positionAmount > 0 ? (payment.positionAmount - payment.amount) / payment.positionAmount : 0; + return ( + + + + + {day(payment.timestamp)} + + {off >= 0.0001 ? ( + + {t("{{percent}} OFF", { percent: percent(off) })} + + ) : off <= -0.0001 ? ( + + {t("{{percent}} late fee", { percent: percent(-off) })} + + ) : null} + + + {money(payment.amount)} + + + + ); + })} + } last> + + + {t("Total payments")} + + + {money(paid)} + + + + + )} + + )} + + + + {ready && ( + + + + {t("Total due")} + + + {money(due)} + + + )} + + + ); +} + +function TimelineRow({ + node, + bullet, + first, + last, + children, +}: { + bullet?: boolean; + children: React.ReactNode; + first?: boolean; + last?: boolean; + node?: React.ReactNode; +}) { + return ( + + + + {node ? ( + + {node} + + ) : bullet ? ( + + + + ) : null} + + + {children} + + + ); +} + +function SectionTitle({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + + ); +} diff --git a/src/components/swaps/SelectorModal.tsx b/src/components/swaps/SelectorModal.tsx index 59d3daf55c..627eeb3dfa 100644 --- a/src/components/swaps/SelectorModal.tsx +++ b/src/components/swaps/SelectorModal.tsx @@ -7,6 +7,7 @@ import { XStack, YStack } from "tamagui"; import { formatUnits } from "viem"; +import useMarkets from "../../utils/useMarkets"; import usePortfolio, { type PortfolioAsset } from "../../utils/usePortfolio"; import AssetLogo from "../shared/AssetLogo"; import Input from "../shared/Input"; @@ -102,6 +103,7 @@ export default function TokenSelectModal({ }) { const [searchQuery, setSearchQuery] = useState(""); const { assets } = usePortfolio(); + const { markets } = useMarkets(); const { t, i18n: { language }, @@ -111,26 +113,31 @@ export default function TokenSelectModal({ const map = new Map(); for (const asset of assets) { const address = (asset.type === "protocol" ? asset.asset : asset.address).toLowerCase(); + if (map.get(address)?.type === "protocol") continue; map.set(address, asset); } return map; }, [assets]); + const marketAssets = useMemo(() => new Set((markets ?? []).map(({ asset }) => asset.toLowerCase())), [markets]); + const filteredTokens = useMemo(() => { const query = searchQuery.toLowerCase().trim(); const matchesQuery = (...fields: (string | undefined)[]) => fields.some((field) => field?.toLowerCase().includes(query)); return tokens.filter((token) => { if (withBalanceOnly) { - const asset = assetByAddress.get(token.address.toLowerCase()); + const address = token.address.toLowerCase(); + const asset = assetByAddress.get(address); if (!asset) return false; - return asset.type === "protocol" - ? asset.floatingDepositAssets > 0n && matchesQuery(asset.symbol, asset.assetName, asset.asset) - : (asset.amount ?? 0n) > 0n && matchesQuery(asset.symbol, asset.name, asset.address); + if (asset.type === "protocol") + return asset.floatingDepositAssets > 0n && matchesQuery(asset.symbol, asset.assetName, asset.asset); + if (marketAssets.has(address)) return false; + return (asset.amount ?? 0n) > 0n && matchesQuery(asset.symbol, asset.name, asset.address); } return matchesQuery(token.symbol, token.name, token.address); }); - }, [searchQuery, tokens, withBalanceOnly, assetByAddress]); + }, [searchQuery, tokens, withBalanceOnly, assetByAddress, marketAssets]); return ( diff --git a/src/components/swaps/Swaps.tsx b/src/components/swaps/Swaps.tsx index b5af3c92b5..98a7f457b5 100644 --- a/src/components/swaps/Swaps.tsx +++ b/src/components/swaps/Swaps.tsx @@ -4,13 +4,24 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { router } from "expo-router"; -import { ArrowLeft, Check, CircleHelp, Repeat, TriangleAlert } from "@tamagui/lucide-icons"; -import { Checkbox, ScrollView, Separator, XStack, YStack } from "tamagui"; +import { + ArrowLeft, + ArrowRight, + Check, + CircleHelp, + IdCard, + RefreshCw, + Repeat, + TriangleAlert, +} from "@tamagui/lucide-icons"; +import { useToastController } from "@tamagui/toast"; +import { Checkbox, ScrollView, Separator, Spinner, XStack, YStack } from "tamagui"; import { useMutation, useQuery } from "@tanstack/react-query"; import { waitForCallsStatus } from "@wagmi/core/actions"; import { parse } from "valibot"; import { encodeFunctionData, formatUnits, parseUnits, zeroAddress } from "viem"; +import { base } from "viem/chains"; import { useSendCalls, useSimulateContract } from "wagmi"; import alchemyAPIKey from "@exactly/common/alchemyAPIKey"; @@ -27,13 +38,14 @@ import TokenSelectModal from "./SelectorModal"; import Success from "./Success"; import SwapDetails from "./SwapDetails"; import TokenInput from "./TokenInput"; -import { presentArticle } from "../../utils/intercom"; -import { getAllowTokens, getRoute, getRouteFrom } from "../../utils/lifi"; +import { present, presentArticle } from "../../utils/intercom"; +import { balancesOptions, getAllowTokens, getRoute, getRouteFrom } from "../../utils/lifi"; import openBrowser from "../../utils/openBrowser"; -import queryClient from "../../utils/queryClient"; +import queryClient, { APIError } from "../../utils/queryClient"; import reportError from "../../utils/reportError"; import useAccount from "../../utils/useAccount"; import useAsset from "../../utils/useAsset"; +import useBeginKYC from "../../utils/useBeginKYC"; import useMarkets from "../../utils/useMarkets"; import usePortfolio from "../../utils/usePortfolio"; import useSimulateProposal from "../../utils/useSimulateProposal"; @@ -44,6 +56,7 @@ import Button from "../shared/StyledButton"; import Text from "../shared/Text"; import View from "../shared/View"; +import type { KYCStatus } from "../../utils/server"; import type { Token } from "@lifi/sdk"; export type Swap = { @@ -74,15 +87,30 @@ export default function Swaps() { const insets = useSafeAreaInsets(); const { t } = useTranslation(); const { address: account } = useAccount(); - const { externalAssets, protocolAssets } = usePortfolio(); + const { externalAssets, protocolAssets, isBalancesPending } = usePortfolio(); + const { + error: balancesError, + isFetching: isBalancesFetching, + refetch: refetchBalances, + } = useQuery(balancesOptions(account)); const [acknowledged, setAcknowledged] = useState(false); const [activeInput, setActiveInput] = useState<"from" | "to">("from"); const { markets, queryKey: marketsQueryKey } = useMarkets(); const protocolMarkets = useMemo(() => markets?.map((m) => ({ asset: m.asset, symbol: m.symbol })) ?? [], [markets]); - const { data: tokens, isLoading: isTokensLoading } = useQuery({ - queryKey: ["allowTokens", protocolMarkets], - queryFn: () => getAllowTokens(protocolMarkets), - }); + const toast = useToastController(); + const beginKYC = useBeginKYC(); + const { + data: kycStatus, + error: kycError, + isFetched: isKYCFetched, + isFetching: isKYCFetching, + refetch: refetchKYC, + } = useQuery({ queryKey: ["kyc", "status"], enabled: chain.id === base.id }); + const { + data: tokens, + isLoading: isTokensLoading, + error: tokensError, + } = useQuery({ queryKey: ["allowTokens", protocolMarkets], queryFn: () => getAllowTokens(protocolMarkets) }); const { data: { fromToken, @@ -128,19 +156,48 @@ export default function Swaps() { const { market: selectedTokenMarket, available: selectedTokenAvailable } = useAsset(getSwapAddress(fromToken)); + const payableTokens = useMemo(() => (tokens ?? []).filter((token) => getBalance(token) > 0n), [tokens, getBalance]); + useEffect(() => { if (!fromToken && !toToken && tokens && markets) { - const usdc = tokens.find(({ symbol }) => symbol === "USDC"); - const exa = tokens.find(({ symbol }) => symbol === "EXA"); - if (usdc && exa) { + const payable = payableTokens.find(({ symbol }) => symbol === "USDC") ?? payableTokens[0]; + const target = ["EXA", "WETH", "USDC"] + .map((symbol) => tokens.find((token) => token.symbol === symbol)) + .find((token) => token !== undefined && token.address !== payable?.address); + if (payable && target) { updateSwap((old) => ({ ...old, - fromToken: { token: usdc, external: isExternal(usdc.address) }, - toToken: { token: exa, external: isExternal(exa.address) }, + fromToken: { token: payable, external: isExternal(payable.address) }, + toToken: { token: target, external: isExternal(target.address) }, })); } } - }, [fromToken, isExternal, markets, toToken, tokens]); + }, [fromToken, isExternal, markets, payableTokens, toToken, tokens]); + + const kycText = kycError instanceof APIError ? kycError.text : undefined; + const processing = chain.id === base.id && kycText === "processing"; + const failed = chain.id === base.id && kycText === "bad kyc"; + + const unverified = + chain.id === base.id && + isKYCFetched && + (kycText === "not started" || + kycText === "no kyc" || + (!kycError && + !(kycStatus && "code" in kycStatus && (kycStatus.code === "ok" || kycStatus.code === "legacy kyc")))); + + const unavailable = chain.id === base.id && isKYCFetched && !!kycError && !processing && !failed && !unverified; + + const balancesUnavailable = !!balancesError && !isTokensLoading && !fromToken && payableTokens.length === 0; + + const empty = + !isTokensLoading && + !tokensError && + !isBalancesPending && + !balancesError && + !!markets && + !fromToken && + payableTokens.length === 0; const handleTokenSelect = (selected: Token) => { if (!fromToken || !toToken) return; @@ -432,141 +489,273 @@ export default function Swaps() { }} /> - - - - - {(["from", "to"] as const).map((type) => { - const tokenData = type === "from" ? fromToken : toToken; - const amount = type === "from" ? fromAmount : toAmount; - const isActive = activeInput === type; - return ( - { - updateSwap((old) => ({ ...old, tokenSelectionType: type, tokenModalOpen: true })); - setAcknowledged(false); - }} - onFocus={() => { - setAcknowledged(false); - }} - onChange={(value: bigint) => { - setActiveInput(type); - handleAmountChange(value, type); - setAcknowledged(false); + {chain.id === base.id && !isKYCFetched ? ( + + + + ) : processing ? ( + + + + {t("Verification in review")} + + + {t("Your identity verification is under review. We'll let you know once it's been processed.")} + + + ) : failed ? ( + + + + {t("We couldn’t verify your identity")} + + + {t("This may be due to missing or incorrect information. Please contact support to resolve it.")} + + + + ) : unavailable ? ( + + + + {t("An error occurred. Please try again later.")} + + + + ) : unverified ? ( + + + + {t("Verify your identity")} + + + {t("Complete identity verification to start swapping.")} + + + + ) : balancesUnavailable ? ( + + + + {t("An error occurred. Please try again later.")} + + + + ) : empty ? ( + + + + {t("Nothing to swap yet")} + + + {t("Deposit assets to start swapping.")} + + + ) : ( + <> + + + + + {(["from", "to"] as const).map((type) => { + const tokenData = type === "from" ? fromToken : toToken; + const amount = type === "from" ? fromAmount : toAmount; + const isActive = activeInput === type; + return ( + { + updateSwap((old) => ({ ...old, tokenSelectionType: type, tokenModalOpen: true })); + setAcknowledged(false); + }} + onFocus={() => { + setAcknowledged(false); + }} + onChange={(value: bigint) => { + setActiveInput(type); + handleAmountChange(value, type); + setAcknowledged(false); + }} + onUseMax={(value: bigint) => { + setActiveInput(type); + handleAmountChange(value, type); + setAcknowledged(false); + }} + /> + ); + })} + + {fromToken && toToken && route && ( + + )} + + + + + + {(caution || danger) && showWarning && ( + + + { + setAcknowledged(!acknowledged); }} - onUseMax={(value: bigint) => { - setActiveInput(type); - handleAmountChange(value, type); - setAcknowledged(false); + > + {danger ? ( + + ) : ( + + + + + + )} + + {danger + ? t( + "Swapping this much of your collateral could instantly trigger liquidation. Try a smaller amount to stay protected.", + ) + : t("I acknowledge the risks of swapping this much of my collateral assets.")} + + + + + )} + + + { + openBrowser(`https://li.fi/`).catch(reportError); + }} + /> + ), }} /> - ); - })} - - {fromToken && toToken && route && ( - - )} - - - - - - {(caution || danger) && showWarning && ( - - - { - setAcknowledged(!acknowledged); - }} - > - {danger ? ( - - ) : ( - - - - - - )} - - {danger - ? t( - "Swapping this much of your collateral could instantly trigger liquidation. Try a smaller amount to stay protected.", - ) - : t("I acknowledge the risks of swapping this much of my collateral assets.")} - - )} - - - { - openBrowser(`https://li.fi/`).catch(reportError); - }} - /> - ), - }} - /> - - - - - - updateSwap((old) => ({ ...old, tokenModalOpen: false }))} - isLoading={isTokensLoading} - title={tokenSelectionType === "from" ? t("Select token to pay") : t("Select token to receive")} - /> + + + updateSwap((old) => ({ ...old, tokenModalOpen: false }))} + isLoading={isTokensLoading} + title={tokenSelectionType === "from" ? t("Select token to pay") : t("Select token to receive")} + /> + + )} ); { diff --git a/src/i18n/es-AR.json b/src/i18n/es-AR.json index 9ea10217a8..5c03b6f860 100644 --- a/src/i18n/es-AR.json +++ b/src/i18n/es-AR.json @@ -25,6 +25,7 @@ "Choose installments": "Elegí cuotas", "Choose your preferred authentication method": "Elegí tu método de autenticación preferido", "Complete a quick identity check to access more networks.": "Completá una verificación de identidad rápida para acceder a más redes.", + "Complete identity verification to start swapping.": "Completá la verificación de identidad para empezar a intercambiar.", "Connect to Exactly Protocol to access USDC funding": "Conectate a Exactly Protocol para acceder a financiamiento en USDC", "Connect to LI.FI to swap tokens": "Conectate a LI.FI para intercambiar tokens", "Connect your wallet to Exactly Protocol": "Conectá tu billetera a Exactly Protocol", @@ -37,6 +38,7 @@ "Couldn't update the contact. Please try again.": "No se pudo actualizar el contacto. Intentá de nuevo.", "Deposit {{symbol}} directly to an external wallet": "Depositá {{symbol}} directamente en una billetera externa", "Deposit {{symbol}} into your Exa App wallet": "Depositá {{symbol}} en tu billetera de Exa App", + "Deposit assets to start swapping.": "Depositá activos para empezar a intercambiar.", "Double-check your address before sending funds to avoid losing them.": "Verificá tu dirección antes de enviar fondos para evitar perderlos.", "Enter a lower amount to swap": "Introducí una cantidad menor para intercambiar", "Enter a purchase amount": "Ingresá un monto de compra", @@ -94,7 +96,6 @@ "Maximize earnings, effortlessly": "Maximizá tus ganancias sin esfuerzo", "Move from Visa Platinum to Visa Signature and unlock premium benefits and perks.": "Pasá de Visa Platinum a Visa Signature y desbloqueá beneficios y ventajas premium.", "Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss.": "Solo enviá activos en {{chain}}. Enviar fondos desde otras redes puede causar pérdida permanente.", - "Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss. Learn more about adding funds.": "Solo enviá activos en {{chain}}. Enviar fondos desde otras redes puede causar pérdida permanente. Aprendé más sobre cómo agregar fondos.", "Only send {{crypto}} on {{network}}. Sending other assets or using other networks may cause permanent loss.": "Solo enviá {{crypto}} en {{network}}. Enviar otros activos o usar otras redes puede causar una pérdida permanente.", "Open your {{provider}} virtual account": "Abrí tu cuenta virtual de {{provider}}", "PAY NOW AND SAVE {{percent}}": "PAGÁ AHORA Y AHORRÁ {{percent}}", @@ -170,8 +171,10 @@ "You have an overdue payment. Pay now to avoid additional interest.": "Tenés un pago vencido. Pagá ahora para evitar intereses adicionales.", "You must repay each installment manually before its due date.": "Debés pagar cada cuota manualmente antes de su fecha de vencimiento.", "You send": "Enviás", + "You've reached 90% of your weekly card spending limit.": "Alcanzaste el 90% de tu límite de gasto semanal.", "Your assets can't back your card yet. Swap them to a supported asset to start spending.": "Tus activos aún no pueden respaldar tu tarjeta. Intercambialos por un activo compatible para empezar a gastar.", "Your card is awaiting activation. Follow the steps to enable it.": "Tu tarjeta está a la espera de activación. Seguí los pasos para habilitarla.", + "Your limit increase request is under review. We'll let you know once it's been processed.": "Tu solicitud de aumento de límite está en revisión. Te vamos a avisar cuando se haya procesado.", "Your password manager does not support passkey backups. Please try a different one": "Tu gestor de contraseñas no admite copias de seguridad de llaves de acceso. Por favor, probá con otro.", "Your spending limit is the maximum amount you can spend on your Exa Card.": "Tu límite de gasto es el monto máximo que podés gastar con tu Exa Card.", "Your transactions will show up here once you get started. Add funds to begin!": "Tus transacciones aparecerán aquí una vez que comiences. ¡Agregá fondos para comenzar!", diff --git a/src/i18n/es.json b/src/i18n/es.json index 76e21d55db..c677bf6a4c 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -1,5 +1,10 @@ { + "{{amount}} discount": "{{amount}} de descuento", "{{amount}} left": "{{amount}} restante", + "{{asset}} on {{chain}} isn't a supported collateral asset. To earn yield and increase your Exa Card credit limit, you'll need to swap it to a supported asset. Learn more.": "{{asset}} en {{chain}} no es un activo de colateral soportado. Para generar rendimiento y aumentar el límite de crédito de tu Exa Card, deberás hacer swap a un activo soportado. Aprende más.", + "{{asset}} on {{network}} isn't a supported collateral asset. To earn yield and increase your Exa Card credit limit, you'll need to bridge it to {{chain}} and swap it to a supported asset. Learn more.": "{{asset}} en {{network}} no es un activo de colateral soportado. Para generar rendimiento y aumentar el límite de crédito de tu Exa Card, deberás hacer bridge a {{chain}} y hacer swap a un activo soportado. Aprende más.", + "{{assets}} and more": "{{assets}} y más", + "{{chain}} is Exa App's native network. Supported assets received here generate yield and increase your Exa Card credit limit immediately. Other assets need to be swapped to a supported asset first.": "{{chain}} es la red nativa de Exa App. Los activos soportados recibidos aquí generan rendimiento y aumentan el límite de crédito de tu Exa Card de inmediato. Los demás activos primero necesitan un swap a un activo soportado.", "{{count}} installments of_one": "{{count}} cuota de", "{{count}} installments of_other": "{{count}} cuotas de", "{{count}} installments_one": "{{count}} cuota", @@ -12,11 +17,16 @@ "{{currency}} via {{methods}}": "{{currency}} vía {{methods}}", "{{discount}} off": "{{discount}} off", "{{network}} deposit address": "Dirección de depósito de {{network}}", + "{{percent}} late fee": "{{percent}} de interés", "{{percent}} OFF": "{{percent}} OFF", "{{rate}} APR": "{{rate}} TNA", + "{{source}} on {{network}} matches {{destination}} on {{chain}}. You can swap and bridge between these assets or just select another supported asset.": "{{source}} en {{network}} coincide con {{destination}} en {{chain}}. Puedes hacer swap y bridge entre estos activos o simplemente seleccionar otro activo soportado.", "{{rate}} FIXED APR": "{{rate}} TNA FIJA", + "{{symbol}} on {{network}} isn't a supported collateral asset. To earn yield and increase your Exa Card credit limit, you'll need to bridge it to {{asset}} on {{chain}}. Learn more.": "{{symbol}} en {{network}} no es un activo de colateral soportado. Para generar rendimiento y aumentar el límite de crédito de tu Exa Card, deberás hacer bridge a {{asset}} en {{chain}}. Aprende más.", "{{symbol}}, {{balance}} available": "{{symbol}}, {{balance}} disponible", "{{time}} past due": "{{time}} de atraso", + "{{start}} to {{end}}": "{{start}} a {{end}}", + "**** {{lastFour}} installments": "Cuotas **** {{lastFour}}", "~{{minutes}} min": "~{{minutes}} min", "*0% interest promo through {{month}}": "*Promo 0% de interés hasta {{month}}", "0% APR on installments": "0% TNA en cuotas", @@ -50,6 +60,7 @@ "Activate your new Exa Card": "Activa tu nueva Exa Card", "Activating your new Exa Card": "Activando tu nueva Exa Card", "Activity": "Actividad", + "Add funds request sent": "Solicitud para agregar fondos enviada", "Add funds to account": "Agregar fondos a la cuenta", "Add funds to your account": "Agregar fondos a tu cuenta", "Add funds to your account to start spending with the Exa Card.": "Agrega fondos a tu cuenta para empezar a gastar con la Exa Card.", @@ -65,6 +76,7 @@ "Airalo": "Airalo", "All Activity": "Actividad", "All deposits must be from bank accounts under your name.": "Todos los depósitos deben ser desde cuentas bancarias a tu nombre.", + "All networks": "Todas las redes", "All supported assets count toward your spending limit.": "Todos los activos compatibles cuentan para tu límite de gasto.", "Almost there! Activate your Exa Card to start spending your onchain assets instantly.": "¡Ya casi! Activa tu Exa Card para empezar a gastar tus activos on-chain al instante.", "Almost there!": "¡Ya casi!", @@ -85,6 +97,7 @@ "Arrival time ≈ {{minutes}} min.": "Tiempo de llegada ≈ {{minutes}} min.", "Asset": "Activo", "Assets are added to your balance as collateral to increase your credit limit.": "Los activos se agregan a tu balance como garantía para aumentar tu límite de crédito.", + "Assets from these networks need to be bridged to {{chain}}. Some may also require a swap to a supported asset to generate yield and increase your Exa Card credit limit. You can do both from your Portfolio.": "Los activos de estas redes necesitan un bridge a {{chain}}. Algunos también pueden requerir un swap a un activo soportado para generar rendimiento y aumentar el límite de crédito de tu Exa Card. Puedes hacer ambos desde tu Cartera.", "Assets you can hold, but they can't be used as backing. You can swap or bridge them to supported collateral assets on {{chain}} network to increase your credit limit.": "Activos que puedes mantener, pero que no pueden usarse como respaldo. Puedes intercambiarlos o transferirlos a activos de garantía compatibles en la red {{chain}} para aumentar tu límite de crédito.", "Assets you can use as backing to increase your credit limit and access features like Pay Later or funding.": "Activos que puedes usar como respaldo para aumentar tu límite de crédito y acceder a funciones como Pagar Después o financiamiento.", "Authentication cancelled": "Autenticación cancelada", @@ -119,9 +132,15 @@ "Biometrics must be enabled to use passkeys. Please enable biometrics in your device settings": "Se debe habilitar la biometría para usar llaves de acceso. Habilita la biometría en la configuración de tu dispositivo", "Borrow_home": "Préstamo", "BR Code": "Código BR", + "Breakdown": "Desglose", "Bridge {{symbol}}": "Hacer bridge de {{symbol}}", + "Bridge and swap it to a supported asset on {{chain}}.": "Haz bridge y swap a un activo soportado en {{chain}}.", + "Bridge and swap needed after receiving": "Bridge y swap necesarios después de recibir", + "Bridge and swap via": "Bridge y swap vía", "Bridge failed. Please try again.": "El bridge falló. Inténtalo de nuevo.", "Bridge failed": "El bridge falló", + "Bridge it to {{asset}} on {{chain}}.": "Haz bridge a {{asset}} en {{chain}}.", + "Bridge needed after receiving": "Bridge necesario después de recibir", "Bridge needs a few more details before creating your account.": "Bridge necesita algunos datos más antes de crear tu cuenta.", "Bridge needs more information": "Bridge necesita más información", "Bridge provides a United States virtual account, converts your {{currency}} to USDC, and sends the funds to Exa App.": "Bridge proporciona una cuenta virtual de Estados Unidos, convierte tus {{currency}} a USDC y envía los fondos a Exa App.", @@ -139,12 +158,13 @@ "Can't delete this contact while a withdrawal is in progress.": "No puedes eliminar este contacto mientras hay un retiro en curso.", "Cancel": "Cancelar", "Cannot proceed": "No se puede continuar", + "Card **** {{lastFour}}": "Tarjeta **** {{lastFour}}", + "Card **** {{lastFour}} installments": "Cuotas de la tarjeta **** {{lastFour}}", "Card activated!": "¡Tarjeta activada!", "Card details": "Detalles de la tarjeta", "Card number copied!": "¡Número de tarjeta copiado!", "Card upgraded successfully": "Tarjeta actualizada exitosamente", "Card": "Tarjeta", - "Chain {{id}}": "Cadena {{id}}", "Change the pay mode before each purchase and pay how you want.": "Cambia el modo de pago antes de cada compra y paga como quieras.", "Check out our X or Discord for updates—or report the issue so we can take a closer look.": "Revisa nuestro X o Discord para actualizaciones o reporta el problema para que podamos investigarlo más a fondo.", "Checking": "Corriente", @@ -162,6 +182,7 @@ "Collateral assets": "Activos de garantía", "Collateral": "Garantía", "Complete a quick identity check to access more networks.": "Completa una verificación de identidad rápida para acceder a más redes.", + "Complete identity verification to start swapping.": "Completa la verificación de identidad para empezar a intercambiar.", "Complete verification": "Completar verificación", "Confirm and borrow {{symbol}}": "Confirmar y pedir prestado {{symbol}}", "Confirm and receive {{symbol}}": "Confirmar y recibir {{symbol}}", @@ -193,8 +214,10 @@ "Copy": "Copiar", "Couldn't create the account. Please try again.": "No se pudo crear la cuenta. Inténtalo de nuevo.", "Couldn't delete the contact. Please try again.": "No se pudo eliminar el contacto. Inténtalo de nuevo.", + "Couldn't download your statement. Please try again.": "No se pudo descargar tu resumen. Inténtalo de nuevo.", "Couldn't load the exchange rate. Please try again.": "No se pudo cargar el tipo de cambio. Inténtalo de nuevo.", "Couldn't load your contacts. Please try again.": "No se pudieron cargar tus contactos. Inténtalo de nuevo.", + "Couldn't load your statement. Please try again.": "No se pudo cargar tu resumen. Inténtalo de nuevo.", "Couldn't update the contact. Please try again.": "No se pudo actualizar el contacto. Inténtalo de nuevo.", "Country": "País", "Create account": "Crear cuenta", @@ -203,8 +226,6 @@ "Credit limit info": "Información del límite de crédito", "Credit limit: {{asset}}": "Límite de crédito: {{asset}}", "Credit limit": "Límite de crédito", - "Crypto on-ramps are no longer available as of July 1st.": "Los on-ramps de cripto dejaron de estar disponibles desde el 1 de Julio.", - "Crypto on-ramps will no longer be available from July 1st.": "Los on-ramps de cripto dejarán de estar disponibles a partir del 1 de Julio.", "Cryptocurrencies": "Criptomonedas", "Current debt": "Deuda actual", "CVV": "CVV", @@ -225,10 +246,8 @@ "Deposit {{symbol}} into your Exa App wallet": "Deposita {{symbol}} en tu billetera de Exa App", "Deposit address": "Dirección de depósito", "Deposit alias": "Alias de depósito", + "Deposit assets to start swapping.": "Deposita activos para empezar a intercambiar.", "Deposit with PIX": "Depositar con PIX", - "Destination asset": "Activo de destino", - "Destination network": "Red de destino", - "Destination": "Destino", "Details": "Detalles", "Dismiss": "Descartar", "Disconnect from {{name}}": "Desconectarte de {{name}}", @@ -236,7 +255,10 @@ "Discounts on travel insurance": "Descuentos en seguros de viaje", "Document number": "Número de documento", "Dollars": "Dólares", + "Don't show again": "No mostrar de nuevo", "Double-check your address before sending funds to avoid losing them.": "Verifica tu dirección antes de enviar fondos para evitar perderlos.", + "Download": "Descargar", + "Download statement": "Descargar resumen", "due {{date}}": "vence {{date}}", "Due {{date}}": "Vence {{date}}", "Due in {{time}}": "Vence en {{time}}", @@ -280,8 +302,6 @@ "Error upgrading card": "Error al actualizar la tarjeta", "Error verifying identity": "Error verificando identidad", "Error": "Error", - "Estimated arrival": "Llegada estimada", - "Estimated time": "Tiempo estimado", "Euros": "Euros", "Every 28 days": "Cada 28 días", "Every passkey is strong. They’re never guessable, reused, or weak.": "Cada llave de acceso es fuerte. Son imposibles de adivinar, nunca se repiten ni son débiles.", @@ -293,7 +313,6 @@ "Exactly Protocol": "Exactly Protocol", "Example: On a $100 balance, a 0.45% daily fee adds $0.45 per day.": "Ejemplo: con un saldo de $100, un interés diario del 0,45% suma $0,45 por día.", "Exchange rate": "Tasa de cambio", - "Exchange": "Intercambio", "Exclusive discounts on travel insurance": "Descuentos exclusivos en seguros de viaje", "Expires": "Expira", "Explore decentralized services": "Explorar servicios descentralizados", @@ -305,9 +324,10 @@ "Failed to update pay mode": "Error al actualizar el modo de pago", "Failed": "Falló", "Fee": "Comisión", - "Fees": "Comisiones", "Fees and transfer times": "Comisiones y tiempos de transferencia", "Fetching best route...": "Buscando la mejor ruta...", + "Filter by network": "Filtrar por red", + "Find {{asset}} on {{network}} and select it.": "Busca {{asset}} en {{network}} y selecciónalo.", "Finished": "Finalizado", "First due date: {{date}} - then every 28 days.": "Primer vencimiento: {{date}} - luego cada 28 días.", "First installment due": "Primera cuota a pagar", @@ -320,12 +340,9 @@ "FREE": "GRATIS", "Freeze card": "Congelar tarjeta", "Freeze your card?": "¿Congelar tu tarjeta?", - "From a bank account": "Desde una cuenta bancaria", - "From another wallet": "Desde otra billetera", "From any account in your name": "Desde cualquier cuenta a tu nombre", "From any account": "Desde cualquier cuenta", "From any Argentine bank account in your name": "Desde cualquier cuenta bancaria argentina a tu nombre", - "From connected wallet": "Desde la billetera conectada", "frozen card": "tarjeta bloqueada", "Full repayment selected.": "Pago total seleccionado.", "Funding failed": "El financiamiento falló", @@ -377,6 +394,7 @@ "In-store QR payments, with crypto": "Pagos con QR en tienda, con cripto", "Increase spending limit": "Aumentar límite de gasto", "Individual": "Persona física", + "Installment {{current}} of {{total}}": "Cuota {{current}} de {{total}}", "Installment due": "Cuota a pagar", "INSTALLMENT PLANS": "PAGAR EN CUOTAS", "Installments calculator": "Calculadora de cuotas", @@ -419,6 +437,7 @@ "Learn more": "Aprende más", "Leave blank to keep it, or enter the complete new address.": "Déjalo en blanco para mantenerlo, o ingresa la nueva dirección completa.", "Level up your Exa Card": "Mejora tu Exa Card", + "LI.FI fee": "Comisión de LI.FI", "Limited-time offer": "Oferta por tiempo limitado", "Limit {{amount}} per repay. Please split larger amounts into smaller payments.": "Límite de {{amount}} por pago. Divide montos mayores en pagos más pequeños.", "Link copied!": "¡Enlace copiado!", @@ -446,6 +465,7 @@ "Minimum received": "Mínimo recibido", "Mode": "Modo", "Monthly, while the account is in use": "Mensual, mientras la cuenta esté en uso", + "More assets": "Más activos", "More info": "Más información", "Move from Visa Platinum to Visa Signature and unlock premium benefits and perks.": "Pasa de Visa Platinum a Visa Signature y desbloquea beneficios y ventajas premium.", "Multiple networks and wallets": "Múltiples redes y billeteras", @@ -457,6 +477,7 @@ "Must be at least 4 characters": "Debe tener al menos 4 caracteres", "My Exa Card": "Mi Exa Card", "N/A": "N/D", + "Native network": "Red nativa", "Network fee": "Comisión de red", "Network reminder": "Recordatorio de red", "Network": "Red", @@ -469,21 +490,27 @@ "No available assets.": "No hay activos disponibles.", "No external assets detected": "No se detectaron activos externos", "No payments pending": "Sin pagos pendientes", + "No payments yet": "Aún no hay pagos", "No recent contacts.": "No hay contactos recientes.", "No saved contacts.": "No hay contactos guardados.", + "No statement for this period": "No hay resumen para este período", "No tokens available": "No hay tokens disponibles", "No tokens found": "No se encontraron tokens", "Non-collateral assets": "Activos sin garantía", "Non-supported network": "Red no compatible", "Nothing to see here for now. Once you add funds or make a payment, all your account activity will appear in this section.": "Nada que ver por ahora. Una vez que agregues fondos o realices un pago, toda la actividad de tu cuenta aparecerá en esta sección.", + "Nothing to swap yet": "Aún no hay nada para intercambiar", "Now": "Ahora", "Numbers only": "Solo números", - "On {{chain}}": "En {{chain}}", "On chain": "On-chain", "Onchain credit is powered by Exactly Protocol and is subject to separate Terms and conditions. The Exa App does not issue or guarantee any funding.": "El crédito on-chain es ofrecido por Exactly Protocol y está sujeto a Términos y Condiciones separados. Exa App no emite ni garantiza ningún financiamiento.", + "Once received, go to your Portfolio.": "Una vez recibido, ve a tu Cartera.", + "Once received, you'll need to bridge and swap {{asset}} to a supported asset on {{chain}}.": "Una vez recibido, deberás hacer bridge y swap de {{asset}} a un activo soportado en {{chain}}.", + "Once received, you'll need to bridge to {{asset}} on {{chain}}.": "Una vez recibido, deberás hacer bridge a {{asset}} en {{chain}}.", + "Once received, you'll need to swap {{asset}} to a supported asset.": "Una vez recibido, deberás hacer swap de {{asset}} a un activo soportado.", + "Only {{assets}} on {{chain}} serve as collateral, earn yield while held, and increase your Exa Card credit limit.": "Solo {{assets}} en {{chain}} sirven como colateral, generan rendimiento mientras los mantienes y aumentan el límite de crédito de tu Exa Card.", "Only send {{crypto}} on {{network}}. Sending other assets or using other networks may cause permanent loss.": "Solo envía {{crypto}} en {{network}}. Enviar otros activos o usar otras redes puede causar una pérdida permanente.", "Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss.": "Solo envía activos en {{chain}}. Enviar fondos desde otras redes puede causar pérdida permanente.", - "Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss. Learn more about adding funds.": "Solo envía activos en {{chain}}. Enviar fondos desde otras redes puede causar pérdida permanente. Aprende más sobre cómo agregar fondos.", "Only your USDC balance counts toward your spending limit.": "Solo tu saldo en USDC cuenta para tu límite de gasto.", "Open Exa Discord": "Abrir Exa en Discord", "Open Exa on X": "Abrir Exa en X", @@ -492,9 +519,12 @@ "optional": "opcional", "Optional second line": "Segunda línea opcional", "or": "o", + "Other assets": "Otros activos", + "Other networks": "Otras redes", "Overdue payment {{date}}, {{amount}}": "Pago vencido {{date}}, {{amount}}", "Overdue payments": "Pagos vencidos", "Paid": "Pagado", + "Paid on {{date}}": "Pagado el {{date}}", "Passkeys advantages": "Ventajas de las llaves de acceso", "Passkeys are intrinsically linked with the app or website they were created for, so people can never be tricked into using their passkey to sign in to a fraudulent app or website.": "Las llaves de acceso están intrínsecamente vinculadas con la aplicación o sitio web para el que fueron creadas, por lo que las personas nunca pueden ser engañadas para usarlas en una aplicación o sitio web fraudulento.", "Passkeys replace passwords with cryptographic keys. Your private key stays on your device, while the public key is shared with the service. This ensures secure and seamless authentication.": "Las llaves de acceso reemplazan las contraseñas con claves criptográficas. Tu clave privada permanece en tu dispositivo, mientras que la clave pública se comparte con el servicio. Esto garantiza una autenticación segura y fluida.", @@ -518,6 +548,7 @@ "Payment details": "Detalles del pago", "Payment due {{date}}, {{amount}}": "Pago con vencimiento {{date}}, {{amount}}", "Payment failed": "Pago fallido", + "Payment history": "Historial de pagos", "Payment info": "Información del pago", "payment schedule": "calendario de pagos", "Payment schedule": "Calendario de pagos", @@ -529,6 +560,7 @@ "Pending requests": "Solicitudes pendientes", "Performance is variable, not guaranteed, and powered by Exactly Protocol. Yields depend on protocol performance and network activity. Past performance does not guarantee future results.": "El rendimiento es variable, no está garantizado y es impulsado por Exactly Protocol. Los rendimientos dependen del desempeño del protocolo y de la actividad de la red. El rendimiento pasado no garantiza resultados futuros.", "Pesos": "Pesos", + "Pesos, dollars, or euros": "Pesos, dólares o euros", "PIX key": "Clave PIX", "PIX Key": "Clave PIX", "Please check your internet connection and try again in a moment. If the problem persists, reinstalling the app may help.": "Revisa tu conexión a internet e inténtalo de nuevo en unos momentos. Si el problema persiste, reinstalar la aplicación puede ayudar.", @@ -539,12 +571,10 @@ "Pounds": "Libras", "Press “Continue” to proceed or “Back” to cancel.": "Presiona “Continuar” para continuar o “Atrás” para cancelar.", "Processing swap request": "Procesando solicitud de intercambio", + "Processing add funds request": "Procesando solicitud para agregar fondos", "Processing balance {{amount}}": "Saldo en procesamiento {{amount}}", "Processing balance → {{amount}}": "Saldo en procesamiento → {{amount}}", - "Processing bridge": "Procesando bridge", "Processing rollover": "Procesando refinanciamiento", - "Processing swap": "Procesando intercambio", - "Processing transfer": "Procesando transferencia", "Processing transfer...": "Procesando transferencia...", "Processing...": "Procesando...", "Processing": "Procesando", @@ -552,11 +582,13 @@ "Purchase details": "Detalles de la compra", "Reach out to our support team and we’ll get you back on track.": "Ponte en contacto con nuestro equipo de soporte y te ayudaremos a resolverlo.", "Reals": "Reales", + "Receive on": "Recibir en", "Receive USDC on {{chain}}": "Recibe USDC en {{chain}}", "Receive USDC": "Recibir USDC", "Received": "Recibido", "Receiving address": "Dirección de recepción", "Recent": "Recientes", + "Recommended": "Recomendada", "beneficiary": "beneficiario", "Beneficiary's account details": "Datos de la cuenta del beneficiario", "Recovery from this network is not yet available. Your funds are safe and will be recoverable once we add support.": "La recuperación desde esta red aún no está disponible. Tus fondos están seguros y podrán recuperarse cuando agreguemos soporte.", @@ -595,12 +627,13 @@ "Select a country": "Selecciona un país", "Select a country first": "Selecciona un país primero", "Select amount": "Selecciona el monto", + "Select another asset": "Seleccionar otro activo", "Select asset to receive": "Selecciona el activo a recibir", "Select asset to send": "Selecciona el activo a enviar", "Select asset": "Seleccionar activo", "Select country": "Seleccionar país", "Select first due date": "Selecciona la primera fecha de vencimiento", - "Select source asset": "Selecciona el activo de origen", + "Select network": "Selecciona la red", "Select state": "Selecciona un estado", "Select the asset to fund": "Selecciona el activo para financiar", "Select token to pay": "Seleccionar token a pagar", @@ -633,20 +666,20 @@ "Share {{chain}} address": "Compartir dirección de {{chain}}", "Share": "Compartir", "Show PIN": "Mostrar PIN", + "Show QR": "Mostrar QR", "Show QR Code": "Mostrar código QR", "Show sensitive": "Mostrar sensibles", + "Show wallet address": "Mostrar dirección de billetera", "Sign in": "Iniciar sesión", "Sign up with browser wallet": "Regístrate con tu billetera del navegador", "Sign up with Passkey": "Regístrate con llave de acceso", "Simulate a purchase of": "Simula una compra de", "Simulating transfer...": "Simulando transferencia...", "Simulation failed": "Simulación fallida", - "Slippage": "Deslizamiento", "Some of your assets are at risk of being liquidated.": "Algunos de tus activos están en riesgo de ser liquidados.", "Something went wrong. Please try again.": "Algo salió mal. Por favor, intenta de nuevo.", "Something’s not working as expected": "Algo no está funcionando como se esperaba", "Sort code": "Código de sucursal", - "Source network": "Red de origen", "Spending limit info": "Información del límite de gasto", "Spending limit": "Límite de gasto", "Spending limits": "Límites de gasto", @@ -654,7 +687,7 @@ "Start verification": "Iniciar verificación", "Starting...": "Iniciando...", "State": "Estado", - "Statements": "Resúmenes", + "Statement": "Resumen", "Stay connected around the world.": "Mantente conectado en todo el mundo.", "Stay connected everywhere": "Mantente conectado en todas partes", "Stay connected": "Mantener conexión", @@ -664,14 +697,18 @@ "Submitting bridge transaction...": "Enviando transacción de bridge...", "Submitting transfer transaction...": "Enviando transacción de transferencia...", "Subtotal": "Subtotal", + "Summary": "Resumen", "Support": "Soporte", + "Supported asset match": "Coincidencia de activo soportado", "Supported assets": "Activos soportados", - "Supported Assets": "Activos Soportados", "Swap {{from}} for {{to}}": "Intercambiar {{from}} por {{to}}", "Swap {{symbol}}": "Intercambiar {{symbol}}", + "Swap and bridge {{source}} to {{destination}}": "Hacer swap y bridge de {{source}} a {{destination}}", "Swap assets": "Intercambiar activos", "Swap failed. Please try again.": "El intercambio falló. Inténtalo de nuevo.", "Swap failed": "El intercambio falló", + "Swap it to a supported asset on {{chain}}.": "Haz swap a un activo soportado en {{chain}}.", + "Swap needed after receiving": "Swap necesario después de recibir", "Swap fee": "Comisión de intercambio", "Swap functionality is provided via LI.FI and executed on decentralized networks. Availability and pricing depend on network conditions and third-party protocols.": "La funcionalidad de intercambio es proporcionada por LI.FI y ejecutada en redes descentralizadas. La disponibilidad y los precios dependen de las condiciones de la red y de protocolos de terceros.", "Swap request sent": "Solicitud de intercambio enviada", @@ -725,12 +762,16 @@ "Too long": "Demasiado largo", "Top up an external wallet supported by LI.FI to unlock bridging into {{chain}}.": "Recarga una billetera externa compatible con LI.FI para habilitar el bridge hacia {{chain}}.", "Total after rollover": "Total después del refinanciamiento", + "Total due": "Total a pagar", + "Total installments": "Total de cuotas", "Total outstanding info": "Información del total pendiente", "Total outstanding": "Total pendiente", "Total": "Total", + "Total payments": "Total de pagos", "Track your spending and see how much you’ve spent with your Exa Card so far.": "Sigue tus gastos y descubre cuánto has gastado con tu Exa Card hasta ahora.", "transaction declined": "transacción rechazada", "Transaction details": "Detalles de la transacción", + "Transaction fee": "Comisión de transacción", "Transaction hash": "Hash de la transacción", "Transfer {{currency}} from bank accounts under your name to receive USDC in your Exa App.": "Transfiere {{currency}} desde cuentas bancarias a tu nombre para recibir USDC en tu Exa App.", "Transfer {{symbol}}": "Transferir {{symbol}}", @@ -768,6 +809,7 @@ "USDC funding": "Financiamiento con USDC", "Use a 1-3 letter state code": "Usa un código de estado de 1 a 3 letras", "Verification failed": "La verificación falló", + "Verification in review": "Verificación en revisión", "Verify your identity": "Verifica tu identidad", "Verifying your identity grants you access to our onchain Exa Card, enabling you to easily spend your crypto.": "Verificar tu identidad te da acceso a nuestra Exa Card on-chain, permitiéndote gastar fácilmente tu cripto.", "Verifying...": "Verificando...", @@ -779,11 +821,13 @@ "View pending request": "Ver solicitud pendiente", "View pending requests": "Ver solicitudes pendientes", "View PIN number": "Ver PIN", + "View requests": "Ver solicitudes", "View Statement": "Ver estado de cuenta", "View statement": "Ver resumen", "Visa Signature benefits": "Beneficios Visa Signature", "Visa Signature Exa Card benefits": "Beneficios de la Exa Card Visa Signature", "Visa": "Visa", + "Wallet address": "Dirección de billetera", "Wallet is busy. Please complete the pending request.": "La billetera está ocupada. Completa la solicitud pendiente.", "We couldn’t complete your verification": "No pudimos completar tu verificación", "We couldn’t verify your identity": "No pudimos verificar tu identidad", @@ -799,16 +843,19 @@ "When you make a purchase using an installment plan, you must pay each installment manually before the due date. Otherwise, a daily penalty of {{rate}} is added while the payment is late.": "Cuando haces una compra usando un plan de cuotas, debes pagar cada cuota manualmente antes de la fecha de vencimiento. De lo contrario, se agrega una penalidad diaria de {{rate}} mientras el pago esté atrasado.", "WHEN": "CUÁNDO", "with": "con", + "With connected wallet": "Con billetera conectada", "Withdrawal": "Retiro", "Yield": "Rendimiento", "You are accessing a decentralized protocol using your crypto as collateral. The Exa App does not issue funding or provide credit. No credit checks or intermediaries are involved.": "Estás accediendo a un protocolo descentralizado usando tu cripto como garantía. La Exa App no emite financiamiento ni proporciona crédito. No se realizan verificaciones de crédito ni hay intermediarios involucrados.", "You are almost set to start using the Exa Card.": "Estás casi listo para empezar a usar la Exa Card.", "You can contact us to share feedback and help us prioritize which networks to support next.": "Puedes contactarnos para compartir comentarios y ayudarnos a priorizar qué redes soportar a continuación.", + "You can hold these assets, but they don't earn yield or increase your Exa Card credit limit. Go to your Portfolio to swap or bridge them to a supported asset on {{chain}}.": "Puedes mantener estos activos, pero no generan rendimiento ni aumentan el límite de crédito de tu Exa Card. Ve a tu Cartera para hacer swap o bridge a un activo soportado en {{chain}}.", "You can reconnect at any time.": "Puedes volver a conectarte en cualquier momento.", "You can repay early and save on interest. The final amount updates automatically before you confirm.": "Puedes pagar antes y ahorrar en intereses. El monto final se actualiza automáticamente antes de confirmar.", "You don't have any contacts yet.": "Todavía no tienes contactos.", "You don't have enough USDC for this amount.": "No tienes suficiente USDC para este monto.", "You have an overdue payment. Pay now to avoid additional interest.": "Tienes un pago vencido. Págalo ahora para evitar intereses adicionales.", + "You must confirm the transactions on your external wallet.": "Debes confirmar las transacciones en tu billetera externa.", "You must repay each installment manually before its due date.": "Debes pagar cada cuota manualmente antes de su fecha de vencimiento.", "You pay": "Pagarás", "You receive": "Recibirás", @@ -821,7 +868,7 @@ "You’re all caught up! Start using your card in Pay Later mode to see payments listed here.": "¡Estás al día! Empieza a usar tu tarjeta en modo Pagar Después para ver los pagos aquí.", "You’re all set!": "¡Todo listo!", "You’re trying to borrow more than your collateral allows. Please enter a lower amount.": "Estás intentando pedir prestado más de lo que tu garantía permite. Por favor, introduce un monto menor.", - "Your {{chain}} address": "Tu dirección en {{chain}}", + "You've reached 90% of your weekly card spending limit.": "Has alcanzado el 90% de tu límite de gasto semanal.", "Your address needs to be verified": "Tu dirección necesita ser verificada", "Your assets can't back your card yet. Swap them to a supported asset to start spending.": "Tus activos aún no pueden respaldar tu tarjeta. Intercámbialos por un activo compatible para empezar a gastar.", "Your card is awaiting activation. Follow the steps to enable it.": "Tu tarjeta está a la espera de activación. Sigue los pasos para habilitarla.", @@ -833,7 +880,9 @@ "Your funds serve as collateral to increase your spending limits.": "Tus fondos sirven como garantía para aumentar tus límites de gasto.", "Your funds serve as collateral, increasing your spending limits. The more funds you add, the more you can spend with the Exa Card.": "Tus fondos sirven como garantía, aumentando tus límites de gasto. Cuantos más fondos agregues, más podrás gastar con la Exa Card.", "Your ID needs to be updated": "Tu documento necesita ser actualizado", + "Your identity verification is under review. We'll let you know once it's been processed.": "Tu verificación de identidad está en revisión. Te avisaremos cuando se haya procesado.", "Your KYC isn't approved for this currency": "Tu KYC no está aprobado para esta moneda", + "Your limit increase request is under review. We'll let you know once it's been processed.": "Tu solicitud de aumento de límite está en revisión. Te avisaremos cuando se haya procesado.", "Your password manager does not support passkey backups. Please try a different one": "Tu gestor de contraseñas no admite copias de seguridad de llaves de acceso. Por favor, prueba con otro.", "Your portfolio": "Tu cartera", "Your Portfolio": "Tu cartera", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index c994331fab..775f63b54d 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -1,5 +1,10 @@ { + "{{amount}} discount": "{{amount}} de desconto", "{{amount}} left": "{{amount}} restante", + "{{asset}} on {{chain}} isn't a supported collateral asset. To earn yield and increase your Exa Card credit limit, you'll need to swap it to a supported asset. Learn more.": "{{asset}} em {{chain}} não é um ativo de colateral suportado. Para gerar rendimento e aumentar o limite de crédito do seu Exa Card, você precisará fazer swap para um ativo suportado. Saiba mais.", + "{{asset}} on {{network}} isn't a supported collateral asset. To earn yield and increase your Exa Card credit limit, you'll need to bridge it to {{chain}} and swap it to a supported asset. Learn more.": "{{asset}} em {{network}} não é um ativo de colateral suportado. Para gerar rendimento e aumentar o limite de crédito do seu Exa Card, você precisará fazer bridge para {{chain}} e fazer swap para um ativo suportado. Saiba mais.", + "{{assets}} and more": "{{assets}} e mais", + "{{chain}} is Exa App's native network. Supported assets received here generate yield and increase your Exa Card credit limit immediately. Other assets need to be swapped to a supported asset first.": "{{chain}} é a rede nativa do Exa App. Os ativos suportados recebidos aqui geram rendimento e aumentam o limite de crédito do seu Exa Card imediatamente. Os demais ativos precisam primeiro de um swap para um ativo suportado.", "{{count}} installments of_one": "{{count}} parcela de", "{{count}} installments of_other": "{{count}} parcelas de", "{{count}} installments_one": "{{count}} parcela", @@ -12,11 +17,16 @@ "{{currency}} via {{methods}}": "{{currency}} via {{methods}}", "{{discount}} off": "{{discount}} off", "{{network}} deposit address": "Endereço de depósito {{network}}", + "{{percent}} late fee": "{{percent}} de juros", "{{percent}} OFF": "{{percent}} OFF", "{{rate}} APR": "{{rate}} APR", + "{{source}} on {{network}} matches {{destination}} on {{chain}}. You can swap and bridge between these assets or just select another supported asset.": "{{source}} em {{network}} corresponde a {{destination}} em {{chain}}. Você pode fazer swap e bridge entre esses ativos ou simplesmente selecionar outro ativo suportado.", "{{rate}} FIXED APR": "{{rate}} APR FIXA", + "{{symbol}} on {{network}} isn't a supported collateral asset. To earn yield and increase your Exa Card credit limit, you'll need to bridge it to {{asset}} on {{chain}}. Learn more.": "{{symbol}} em {{network}} não é um ativo de colateral suportado. Para gerar rendimento e aumentar o limite de crédito do seu Exa Card, você precisará fazer bridge para {{asset}} em {{chain}}. Saiba mais.", "{{symbol}}, {{balance}} available": "{{symbol}}, {{balance}} disponível", "{{time}} past due": "{{time}} de atraso", + "{{start}} to {{end}}": "{{start}} a {{end}}", + "**** {{lastFour}} installments": "Parcelas **** {{lastFour}}", "~{{minutes}} min": "~{{minutes}} min", "*0% interest promo through {{month}}": "*Promo 0% de juros até {{month}}", "0% APR on installments": "0% APR em parcelas", @@ -50,6 +60,7 @@ "Activate your new Exa Card": "Ative seu novo Exa Card", "Activating your new Exa Card": "Ativando seu novo Exa Card", "Activity": "Atividade", + "Add funds request sent": "Solicitação para adicionar fundos enviada", "Add funds to account": "Adicionar fundos à conta", "Add funds to your account": "Adicionar fundos à sua conta", "Add funds to your account to start spending with the Exa Card.": "Adicione fundos à sua conta para começar a gastar com o Exa Card.", @@ -65,6 +76,7 @@ "Airalo": "Airalo", "All Activity": "Atividade", "All deposits must be from bank accounts under your name.": "Todos os depósitos devem ser de contas bancárias em seu nome.", + "All networks": "Todas as redes", "All supported assets count toward your spending limit.": "Todos os ativos compatíveis contam para seu limite de gastos.", "Almost there! Activate your Exa Card to start spending your onchain assets instantly.": "Quase lá! Ative seu Exa Card para começar a gastar seus ativos on-chain instantaneamente.", "Almost there!": "Quase lá!", @@ -85,6 +97,7 @@ "Arrival time ≈ {{minutes}} min.": "Tempo de chegada ≈ {{minutes}} min.", "Asset": "Ativo", "Assets are added to your balance as collateral to increase your credit limit.": "Os ativos são adicionados ao seu saldo como garantia para aumentar seu limite de crédito.", + "Assets from these networks need to be bridged to {{chain}}. Some may also require a swap to a supported asset to generate yield and increase your Exa Card credit limit. You can do both from your Portfolio.": "Os ativos dessas redes precisam de um bridge para {{chain}}. Alguns também podem exigir um swap para um ativo suportado para gerar rendimento e aumentar o limite de crédito do seu Exa Card. Você pode fazer ambos no seu Portfólio.", "Assets you can hold, but they can't be used as backing. You can swap or bridge them to supported collateral assets on {{chain}} network to increase your credit limit.": "Ativos que você pode manter, mas que não podem ser usados como garantia. Você pode trocar ou transferi-los para ativos de garantia compatíveis na rede {{chain}} para aumentar seu limite de crédito.", "Assets you can use as backing to increase your credit limit and access features like Pay Later or funding.": "Ativos que você pode usar como garantia para aumentar seu limite de crédito e acessar recursos como Pagar Depois ou financiamento.", "Authentication cancelled": "Autenticação cancelada", @@ -119,9 +132,15 @@ "Biometrics must be enabled to use passkeys. Please enable biometrics in your device settings": "A biometria deve estar ativada para usar chaves de acesso. Ative a biometria nas configurações do seu dispositivo", "Borrow_home": "Empréstimo", "BR Code": "Código BR", + "Breakdown": "Detalhamento", "Bridge {{symbol}}": "Fazer bridge de {{symbol}}", + "Bridge and swap it to a supported asset on {{chain}}.": "Faça bridge e swap para um ativo suportado em {{chain}}.", + "Bridge and swap needed after receiving": "Bridge e swap necessários após o recebimento", + "Bridge and swap via": "Bridge e swap via", "Bridge failed. Please try again.": "O bridge falhou. Tente novamente.", "Bridge failed": "O bridge falhou", + "Bridge it to {{asset}} on {{chain}}.": "Faça bridge para {{asset}} em {{chain}}.", + "Bridge needed after receiving": "Bridge necessário após o recebimento", "Bridge needs a few more details before creating your account.": "Bridge precisa de mais alguns dados antes de criar sua conta.", "Bridge needs more information": "Bridge precisa de mais informações", "Bridge provides a United States virtual account, converts your {{currency}} to USDC, and sends the funds to Exa App.": "A Bridge fornece uma conta virtual dos Estados Unidos, converte seus {{currency}} em USDC e envia os fundos para o Exa App.", @@ -139,12 +158,13 @@ "Can't delete this contact while a withdrawal is in progress.": "Não é possível excluir este contato enquanto há um saque em andamento.", "Cancel": "Cancelar", "Cannot proceed": "Não é possível continuar", + "Card **** {{lastFour}}": "Cartão **** {{lastFour}}", + "Card **** {{lastFour}} installments": "Parcelas do cartão **** {{lastFour}}", "Card activated!": "Cartão ativado!", "Card details": "Detalhes do cartão", "Card number copied!": "Número do cartão copiado!", "Card upgraded successfully": "Cartão atualizado com sucesso", "Card": "Cartão", - "Chain {{id}}": "Rede {{id}}", "Change the pay mode before each purchase and pay how you want.": "Mude o modo de pagamento antes de cada compra e pague como quiser.", "Check out our X or Discord for updates—or report the issue so we can take a closer look.": "Confira nosso X ou Discord para atualizações — ou reporte o problema para que possamos investigar mais a fundo.", "Checking": "Corrente", @@ -162,6 +182,7 @@ "Collateral assets": "Ativos de garantia", "Collateral": "Garantia", "Complete a quick identity check to access more networks.": "Complete uma verificação de identidade rápida para acessar mais redes.", + "Complete identity verification to start swapping.": "Conclua a verificação de identidade para começar a trocar.", "Complete verification": "Completar verificação", "Confirm and borrow {{symbol}}": "Confirmar empréstimo de {{symbol}}", "Confirm and receive {{symbol}}": "Confirmar e receber {{symbol}}", @@ -193,8 +214,10 @@ "Copy": "Copiar", "Couldn't create the account. Please try again.": "Não foi possível criar a conta. Tente novamente.", "Couldn't delete the contact. Please try again.": "Não foi possível excluir o contato. Tente novamente.", + "Couldn't download your statement. Please try again.": "Não foi possível baixar seu extrato. Tente novamente.", "Couldn't load the exchange rate. Please try again.": "Não foi possível carregar a taxa de câmbio. Tente novamente.", "Couldn't load your contacts. Please try again.": "Não foi possível carregar seus contatos. Tente novamente.", + "Couldn't load your statement. Please try again.": "Não foi possível carregar seu extrato. Tente novamente.", "Couldn't update the contact. Please try again.": "Não foi possível atualizar o contato. Tente novamente.", "Country": "País", "Create account": "Criar conta", @@ -203,8 +226,6 @@ "Credit limit info": "Informações do limite de crédito", "Credit limit: {{asset}}": "Limite de crédito: {{asset}}", "Credit limit": "Limite de crédito", - "Crypto on-ramps are no longer available as of July 1st.": "Os on-ramps de cripto não estão mais disponíveis desde 1º de Julho.", - "Crypto on-ramps will no longer be available from July 1st.": "Os on-ramps de cripto não estarão mais disponíveis a partir de 1º de Julho.", "Cryptocurrencies": "Criptomoedas", "Current debt": "Dívida atual", "CVV": "CVV", @@ -225,10 +246,8 @@ "Deposit {{symbol}} into your Exa App wallet": "Deposite {{symbol}} na sua carteira do Exa App", "Deposit address": "Endereço de depósito", "Deposit alias": "Chave de depósito", + "Deposit assets to start swapping.": "Deposite ativos para começar a trocar.", "Deposit with PIX": "Depositar com PIX", - "Destination asset": "Ativo de destino", - "Destination network": "Rede de destino", - "Destination": "Destino", "Details": "Detalhes", "Dismiss": "Descartar", "Disconnect from {{name}}": "Desconectar de {{name}}", @@ -236,7 +255,10 @@ "Discounts on travel insurance": "Descontos em seguros de viagem", "Document number": "Número do documento", "Dollars": "Dólares", + "Don't show again": "Não mostrar novamente", "Double-check your address before sending funds to avoid losing them.": "Verifique seu endereço antes de enviar fundos para evitar perdê-los.", + "Download": "Baixar", + "Download statement": "Baixar extrato", "due {{date}}": "vence {{date}}", "Due {{date}}": "Vence {{date}}", "Due in {{time}}": "Vence em {{time}}", @@ -280,8 +302,6 @@ "Error upgrading card": "Erro ao atualizar o cartão", "Error verifying identity": "Erro ao verificar identidade", "Error": "Erro", - "Estimated arrival": "Chegada estimada", - "Estimated time": "Tempo estimado", "Euros": "Euros", "Every 28 days": "A cada 28 dias", "Every passkey is strong. They’re never guessable, reused, or weak.": "Cada chave de acesso é forte. São impossíveis de adivinhar, nunca se repetem nem são fracas.", @@ -293,7 +313,6 @@ "Exactly Protocol": "Exactly Protocol", "Example: On a $100 balance, a 0.45% daily fee adds $0.45 per day.": "Exemplo: com um saldo de $100, juros diários de 0,45% somam $0,45 por dia.", "Exchange rate": "Taxa de câmbio", - "Exchange": "Câmbio", "Exclusive discounts on travel insurance": "Descontos exclusivos em seguros de viagem", "Expires": "Expira", "Explore decentralized services": "Explorar serviços descentralizados", @@ -305,9 +324,10 @@ "Failed to update pay mode": "Erro ao atualizar o modo de pagamento", "Failed": "Falhou", "Fee": "Taxa", - "Fees": "Taxas", "Fees and transfer times": "Taxas e tempos de transferência", "Fetching best route...": "Buscando a melhor rota...", + "Filter by network": "Filtrar por rede", + "Find {{asset}} on {{network}} and select it.": "Encontre {{asset}} em {{network}} e selecione-o.", "Finished": "Finalizado", "First due date: {{date}} - then every 28 days.": "Primeiro vencimento: {{date}} - depois a cada 28 dias.", "First installment due": "Primeira parcela a pagar", @@ -320,12 +340,9 @@ "FREE": "GRÁTIS", "Freeze card": "Congelar cartão", "Freeze your card?": "Congelar seu cartão?", - "From a bank account": "De uma conta bancária", - "From another wallet": "De outra carteira", "From any account in your name": "De qualquer conta em seu nome", "From any account": "De qualquer conta", "From any Argentine bank account in your name": "De qualquer conta bancária argentina em seu nome", - "From connected wallet": "Da carteira conectada", "frozen card": "cartão bloqueado", "Full repayment selected.": "Pagamento total selecionado.", "Funding failed": "O financiamento falhou", @@ -377,6 +394,7 @@ "In-store QR payments, with crypto": "Pagamentos com QR na loja, com cripto", "Increase spending limit": "Aumentar limite de gastos", "Individual": "Pessoa física", + "Installment {{current}} of {{total}}": "Parcela {{current}} de {{total}}", "Installment due": "Parcela a pagar", "INSTALLMENT PLANS": "PAGAR EM PARCELAS", "Installments calculator": "Calculadora de parcelas", @@ -419,6 +437,7 @@ "Learn more": "Saiba mais", "Leave blank to keep it, or enter the complete new address.": "Deixe em branco para manter, ou insira o novo endereço completo.", "Level up your Exa Card": "Melhore seu Exa Card", + "LI.FI fee": "Taxa de LI.FI", "Limited-time offer": "Oferta por tempo limitado", "Limit {{amount}} per repay. Please split larger amounts into smaller payments.": "Limite de {{amount}} por pagamento. Divida valores maiores em pagamentos menores.", "Link copied!": "Link copiado!", @@ -446,6 +465,7 @@ "Minimum received": "Mínimo recebido", "Mode": "Modo", "Monthly, while the account is in use": "Mensal, enquanto a conta estiver em uso", + "More assets": "Mais ativos", "More info": "Mais informações", "Move from Visa Platinum to Visa Signature and unlock premium benefits and perks.": "Passe do Visa Platinum para o Visa Signature e desbloqueie benefícios e vantagens premium.", "Multiple networks and wallets": "Múltiplas redes e carteiras", @@ -457,6 +477,7 @@ "Must be at least 4 characters": "Deve ter pelo menos 4 caracteres", "My Exa Card": "Meu Exa Card", "N/A": "N/D", + "Native network": "Rede nativa", "Network fee": "Taxa de rede", "Network reminder": "Lembrete de rede", "Network": "Rede", @@ -469,21 +490,27 @@ "No available assets.": "Nenhum ativo disponível.", "No external assets detected": "Nenhum ativo externo detectado", "No payments pending": "Nenhum pagamento pendente", + "No payments yet": "Ainda não há pagamentos", "No recent contacts.": "Nenhum contato recente.", "No saved contacts.": "Nenhum contato salvo.", + "No statement for this period": "Não há extrato para este período", "No tokens available": "Nenhum token disponível", "No tokens found": "Nenhum token encontrado", "Non-collateral assets": "Ativos sem garantia", "Non-supported network": "Rede não compatível", "Nothing to see here for now. Once you add funds or make a payment, all your account activity will appear in this section.": "Nada para ver por enquanto. Assim que você adicionar fundos ou fizer um pagamento, toda a atividade da sua conta aparecerá nesta seção.", + "Nothing to swap yet": "Ainda não há nada para trocar", "Now": "Agora", "Numbers only": "Apenas números", - "On {{chain}}": "Na {{chain}}", "On chain": "On-chain", "Onchain credit is powered by Exactly Protocol and is subject to separate Terms and conditions. The Exa App does not issue or guarantee any funding.": "O crédito on-chain é oferecido pelo Exactly Protocol e está sujeito a Termos e Condições separados. O Exa App não emite nem garante nenhum financiamento.", + "Once received, go to your Portfolio.": "Após o recebimento, vá para o seu Portfólio.", + "Once received, you'll need to bridge and swap {{asset}} to a supported asset on {{chain}}.": "Após o recebimento, você precisará fazer bridge e swap de {{asset}} para um ativo suportado em {{chain}}.", + "Once received, you'll need to bridge to {{asset}} on {{chain}}.": "Após o recebimento, você precisará fazer bridge para {{asset}} em {{chain}}.", + "Once received, you'll need to swap {{asset}} to a supported asset.": "Após o recebimento, você precisará fazer swap de {{asset}} para um ativo suportado.", + "Only {{assets}} on {{chain}} serve as collateral, earn yield while held, and increase your Exa Card credit limit.": "Apenas {{assets}} em {{chain}} servem como colateral, geram rendimento enquanto você os mantém e aumentam o limite de crédito do seu Exa Card.", "Only send {{crypto}} on {{network}}. Sending other assets or using other networks may cause permanent loss.": "Envie apenas {{crypto}} na rede {{network}}. Enviar outros ativos ou usar outras redes pode causar perda permanente.", "Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss.": "Envie ativos apenas na {{chain}}. Enviar fundos de outras redes pode causar perda permanente.", - "Only send assets on {{chain}}. Sending funds from other networks may cause permanent loss. Learn more about adding funds.": "Envie ativos apenas na {{chain}}. Enviar fundos de outras redes pode causar perda permanente. Saiba mais sobre como adicionar fundos.", "Only your USDC balance counts toward your spending limit.": "Apenas seu saldo em USDC conta para seu limite de gastos.", "Open Exa Discord": "Abrir Exa no Discord", "Open Exa on X": "Abrir Exa no X", @@ -492,9 +519,12 @@ "optional": "opcional", "Optional second line": "Segunda linha opcional", "or": "ou", + "Other assets": "Outros ativos", + "Other networks": "Outras redes", "Overdue payment {{date}}, {{amount}}": "Pagamento atrasado {{date}}, {{amount}}", "Overdue payments": "Pagamentos atrasados", "Paid": "Pago", + "Paid on {{date}}": "Pago em {{date}}", "Passkeys advantages": "Vantagens das chaves de acesso", "Passkeys are intrinsically linked with the app or website they were created for, so people can never be tricked into using their passkey to sign in to a fraudulent app or website.": "As chaves de acesso estão intrinsecamente vinculadas ao aplicativo ou site para o qual foram criadas, então as pessoas nunca podem ser enganadas para usá-las em um aplicativo ou site fraudulento.", "Passkeys replace passwords with cryptographic keys. Your private key stays on your device, while the public key is shared with the service. This ensures secure and seamless authentication.": "As chaves de acesso substituem senhas por chaves criptográficas. Sua chave privada permanece no seu dispositivo, enquanto a chave pública é compartilhada com o serviço. Isso garante uma autenticação segura e fluida.", @@ -518,6 +548,7 @@ "Payment details": "Detalhes do pagamento", "Payment due {{date}}, {{amount}}": "Pagamento com vencimento {{date}}, {{amount}}", "Payment failed": "Pagamento recusado", + "Payment history": "Histórico de pagamentos", "Payment info": "Informações do pagamento", "payment schedule": "agenda de pagamentos", "Payment schedule": "Agenda de pagamentos", @@ -529,6 +560,7 @@ "Pending requests": "Solicitações pendentes", "Performance is variable, not guaranteed, and powered by Exactly Protocol. Yields depend on protocol performance and network activity. Past performance does not guarantee future results.": "O rendimento é variável, não é garantido e é impulsionado pelo Exactly Protocol. Os rendimentos dependem do desempenho do protocolo e da atividade da rede. O desempenho passado não garante resultados futuros.", "Pesos": "Pesos", + "Pesos, dollars, or euros": "Pesos, dólares ou euros", "PIX key": "Chave PIX", "PIX Key": "Chave PIX", "Please check your internet connection and try again in a moment. If the problem persists, reinstalling the app may help.": "Verifique sua conexão com a internet e tente novamente em instantes. Se o problema persistir, reinstalar o aplicativo pode ajudar.", @@ -539,12 +571,10 @@ "Pounds": "Libras", "Press “Continue” to proceed or “Back” to cancel.": "Pressione “Continuar” para prosseguir ou “Voltar” para cancelar.", "Processing swap request": "Processando solicitação de troca", + "Processing add funds request": "Processando solicitação para adicionar fundos", "Processing balance {{amount}}": "Saldo em processamento {{amount}}", "Processing balance → {{amount}}": "Saldo em processamento → {{amount}}", - "Processing bridge": "Processando bridge", "Processing rollover": "Processando refinanciamento", - "Processing swap": "Processando troca", - "Processing transfer": "Processando transferência", "Processing transfer...": "Processando transferência...", "Processing...": "Processando...", "Processing": "Processando", @@ -552,11 +582,13 @@ "Purchase details": "Detalhes da compra", "Reach out to our support team and we’ll get you back on track.": "Entre em contato com nossa equipe de suporte e vamos te ajudar a resolver isso.", "Reals": "Reais", + "Receive on": "Receber em", "Receive USDC on {{chain}}": "Receber USDC em {{chain}}", "Receive USDC": "Receber USDC", "Received": "Recebido", "Receiving address": "Endereço de recebimento", "Recent": "Recentes", + "Recommended": "Recomendada", "beneficiary": "beneficiário", "Beneficiary's account details": "Dados da conta do beneficiário", "Recovery from this network is not yet available. Your funds are safe and will be recoverable once we add support.": "A recuperação desta rede ainda não está disponível. Seus fundos estão seguros e poderão ser recuperados assim que adicionarmos suporte.", @@ -595,12 +627,13 @@ "Select a country": "Selecione um país", "Select a country first": "Selecione um país primeiro", "Select amount": "Selecione o valor", + "Select another asset": "Selecionar outro ativo", "Select asset to receive": "Selecione o ativo a receber", "Select asset to send": "Selecione o ativo a enviar", "Select asset": "Selecionar ativo", "Select country": "Selecionar país", "Select first due date": "Selecione a primeira data de vencimento", - "Select source asset": "Selecione o ativo de origem", + "Select network": "Selecione a rede", "Select state": "Selecione um estado", "Select the asset to fund": "Selecione o ativo para financiar", "Select token to pay": "Selecionar token a pagar", @@ -633,20 +666,20 @@ "Share {{chain}} address": "Compartilhar endereço de {{chain}}", "Share": "Compartilhar", "Show PIN": "Mostrar PIN", + "Show QR": "Mostrar QR", "Show QR Code": "Mostrar código QR", "Show sensitive": "Mostrar sensíveis", + "Show wallet address": "Mostrar endereço da carteira", "Sign in": "Entrar", "Sign up with browser wallet": "Cadastre-se com a carteira do navegador", "Sign up with Passkey": "Cadastre-se com chave de acesso", "Simulate a purchase of": "Simule uma compra de", "Simulating transfer...": "Simulando transferência...", "Simulation failed": "Simulação falhou", - "Slippage": "Deslizamento", "Some of your assets are at risk of being liquidated.": "Alguns dos seus ativos estão em risco de serem liquidados.", "Something went wrong. Please try again.": "Algo deu errado. Por favor, tente novamente.", "Something’s not working as expected": "Algo não está funcionando como esperado", "Sort code": "Código de agência", - "Source network": "Rede de origem", "Spending limit info": "Informações do limite de gastos", "Spending limit": "Limite de gastos", "Spending limits": "Limites de gastos", @@ -654,7 +687,7 @@ "Start verification": "Iniciar verificação", "Starting...": "Iniciando...", "State": "Estado", - "Statements": "Extratos", + "Statement": "Extrato", "Stay connected around the world.": "Fique conectado em todo o mundo.", "Stay connected everywhere": "Fique conectado em qualquer lugar", "Stay connected": "Manter conexão", @@ -664,14 +697,18 @@ "Submitting bridge transaction...": "Enviando transação de bridge...", "Submitting transfer transaction...": "Enviando transação de transferência...", "Subtotal": "Subtotal", + "Summary": "Resumo", "Support": "Suporte", + "Supported asset match": "Correspondência de ativo suportado", "Supported assets": "Ativos suportados", - "Supported Assets": "Ativos Suportados", "Swap {{from}} for {{to}}": "Trocar {{from}} por {{to}}", "Swap {{symbol}}": "Trocar {{symbol}}", + "Swap and bridge {{source}} to {{destination}}": "Fazer swap e bridge de {{source}} para {{destination}}", "Swap assets": "Trocar ativos", "Swap failed. Please try again.": "A troca falhou. Tente novamente.", "Swap failed": "A troca falhou", + "Swap it to a supported asset on {{chain}}.": "Faça swap para um ativo suportado em {{chain}}.", + "Swap needed after receiving": "Swap necessário após o recebimento", "Swap fee": "Taxa de troca", "Swap functionality is provided via LI.FI and executed on decentralized networks. Availability and pricing depend on network conditions and third-party protocols.": "A funcionalidade de troca é fornecida por LI.FI e executada em redes descentralizadas. A disponibilidade e os preços dependem das condições da rede e de protocolos de terceiros.", "Swap request sent": "Solicitação de troca enviada", @@ -725,12 +762,16 @@ "Too long": "Muito longo", "Top up an external wallet supported by LI.FI to unlock bridging into {{chain}}.": "Recarregue uma carteira externa compatível com LI.FI para habilitar o bridge para {{chain}}.", "Total after rollover": "Total após refinanciamento", + "Total due": "Total a pagar", + "Total installments": "Total de parcelas", "Total outstanding info": "Informações do total pendente", "Total outstanding": "Total pendente", "Total": "Total", + "Total payments": "Total de pagamentos", "Track your spending and see how much you’ve spent with your Exa Card so far.": "Acompanhe seus gastos e veja quanto você gastou com seu Exa Card até agora.", "transaction declined": "transação rejeitada", "Transaction details": "Detalhes da transação", + "Transaction fee": "Taxa de transação", "Transaction hash": "Hash da transação", "Transfer {{currency}} from bank accounts under your name to receive USDC in your Exa App.": "Transfira {{currency}} de contas bancárias em seu nome para receber USDC no seu Exa App.", "Transfer {{symbol}}": "Transferir {{symbol}}", @@ -768,6 +809,7 @@ "USDC funding": "Financiamento com USDC", "Use a 1-3 letter state code": "Use um código de estado de 1 a 3 letras", "Verification failed": "A verificação falhou", + "Verification in review": "Verificação em análise", "Verify your identity": "Verifique sua identidade", "Verifying your identity grants you access to our onchain Exa Card, enabling you to easily spend your crypto.": "Verificar sua identidade dá acesso ao nosso Exa Card on-chain, permitindo que você gaste facilmente sua cripto.", "Verifying...": "Verificando...", @@ -779,11 +821,13 @@ "View pending request": "Ver solicitação pendente", "View pending requests": "Ver solicitações pendentes", "View PIN number": "Ver PIN", + "View requests": "Ver solicitações", "View statement": "Ver extrato", "View Statement": "Ver extrato", "Visa Signature benefits": "Benefícios Visa Signature", "Visa Signature Exa Card benefits": "Benefícios do Exa Card Visa Signature", "Visa": "Visa", + "Wallet address": "Endereço da carteira", "Wallet is busy. Please complete the pending request.": "A carteira está ocupada. Conclua a solicitação pendente.", "We couldn’t complete your verification": "Não foi possível concluir sua verificação", "We couldn’t verify your identity": "Não foi possível verificar sua identidade", @@ -799,16 +843,19 @@ "When you make a purchase using an installment plan, you must pay each installment manually before the due date. Otherwise, a daily penalty of {{rate}} is added while the payment is late.": "Quando você faz uma compra usando um plano de parcelamento, você deve pagar cada parcela manualmente antes da data de vencimento. Caso contrário, uma penalidade diária de {{rate}} é adicionada enquanto o pagamento estiver atrasado.", "WHEN": "QUANDO", "with": "com", + "With connected wallet": "Com carteira conectada", "Withdrawal": "Saque", "Yield": "Rendimento", "You are accessing a decentralized protocol using your crypto as collateral. The Exa App does not issue funding or provide credit. No credit checks or intermediaries are involved.": "Você está acessando um protocolo descentralizado usando sua cripto como garantia. O Exa App não emite financiamento nem fornece crédito. Não há verificações de crédito nem intermediários envolvidos.", "You are almost set to start using the Exa Card.": "Você está quase pronto para começar a usar o Exa Card.", "You can contact us to share feedback and help us prioritize which networks to support next.": "Você pode entrar em contato conosco para compartilhar feedback e nos ajudar a priorizar quais redes suportar em seguida.", + "You can hold these assets, but they don't earn yield or increase your Exa Card credit limit. Go to your Portfolio to swap or bridge them to a supported asset on {{chain}}.": "Você pode manter esses ativos, mas eles não geram rendimento nem aumentam o limite de crédito do seu Exa Card. Vá para o seu Portfólio para fazer swap ou bridge para um ativo suportado em {{chain}}.", "You can reconnect at any time.": "Você pode se reconectar a qualquer momento.", "You can repay early and save on interest. The final amount updates automatically before you confirm.": "Você pode pagar antecipado e economizar em juros. O valor final é atualizado automaticamente antes de você confirmar.", "You don't have any contacts yet.": "Você ainda não tem contatos.", "You don't have enough USDC for this amount.": "Você não tem USDC suficiente para este valor.", "You have an overdue payment. Pay now to avoid additional interest.": "Você tem um pagamento vencido. Pague agora para evitar juros adicionais.", + "You must confirm the transactions on your external wallet.": "Você deve confirmar as transações na sua carteira externa.", "You must repay each installment manually before its due date.": "Você deve pagar cada parcela manualmente antes da data de vencimento.", "You pay": "Você paga", "You receive": "Você recebe", @@ -821,7 +868,7 @@ "You’re all caught up! Start using your card in Pay Later mode to see payments listed here.": "Você está em dia! Comece a usar seu cartão no modo Pagar Depois para ver os pagamentos aqui.", "You’re all set!": "Tudo pronto!", "You’re trying to borrow more than your collateral allows. Please enter a lower amount.": "Você está tentando emprestar mais do que sua garantia permite. Por favor, insira um valor menor.", - "Your {{chain}} address": "Seu endereço na {{chain}}", + "You've reached 90% of your weekly card spending limit.": "Você atingiu 90% do seu limite de gastos semanal.", "Your address needs to be verified": "Seu endereço precisa ser verificado", "Your assets can't back your card yet. Swap them to a supported asset to start spending.": "Seus ativos ainda não podem servir de garantia para o seu cartão. Troque-os por um ativo compatível para começar a gastar.", "Your card is awaiting activation. Follow the steps to enable it.": "Seu cartão está aguardando ativação. Siga os passos para ativá-lo.", @@ -833,7 +880,9 @@ "Your funds serve as collateral to increase your spending limits.": "Seus fundos servem como garantia para aumentar seus limites de gastos.", "Your funds serve as collateral, increasing your spending limits. The more funds you add, the more you can spend with the Exa Card.": "Seus fundos servem como garantia, aumentando seus limites de gastos. Quanto mais fundos você adicionar, mais poderá gastar com o Exa Card.", "Your ID needs to be updated": "Seu documento precisa ser atualizado", + "Your identity verification is under review. We'll let you know once it's been processed.": "Sua verificação de identidade está em análise. Avisaremos quando for processada.", "Your KYC isn't approved for this currency": "Seu KYC não está aprovado para esta moeda", + "Your limit increase request is under review. We'll let you know once it's been processed.": "Sua solicitação de aumento de limite está em análise. Avisaremos quando for processada.", "Your password manager does not support passkey backups. Please try a different one": "Seu gerenciador de senhas não suporta backup de chaves de acesso. Por favor, tente outro", "Your portfolio": "Seu portfólio", "Your Portfolio": "Seu portfólio", diff --git a/src/utils/factoryOptions.ts b/src/utils/factoryOptions.ts new file mode 100644 index 0000000000..ce66bf3ac8 --- /dev/null +++ b/src/utils/factoryOptions.ts @@ -0,0 +1,19 @@ +import { queryOptions, skipToken } from "@tanstack/react-query"; +import { getBytecode } from "@wagmi/core/actions"; + +import exaConfig from "./wagmi/exa"; + +import type { Address } from "viem"; + +export default function factoryOptions(factory: Address | undefined, chainId: number | undefined) { + return queryOptions({ + queryKey: ["factory", factory, chainId], + queryFn: + factory !== undefined && chainId !== undefined + ? async () => !!(await getBytecode(exaConfig, { address: factory, chainId })) + : skipToken, + staleTime: (query) => (query.state.data ? Infinity : 24 * 60 * 60 * 1000), + gcTime: Infinity, + retry: false, + }); +} diff --git a/src/utils/lifi.ts b/src/utils/lifi.ts index 92e7cec368..e47004827c 100644 --- a/src/utils/lifi.ts +++ b/src/utils/lifi.ts @@ -18,7 +18,7 @@ import { encodeFunctionData, formatUnits, getAddress, zeroAddress, type Address import { anvil } from "viem/chains"; import alchemyAPIKey from "@exactly/common/alchemyAPIKey"; -import chain, { allowlist, exaAddress, mockSwapperAbi, swapperAddress } from "@exactly/common/generated/chain"; +import chain, { allowlists, exaAddress, mockSwapperAbi, swapperAddress } from "@exactly/common/generated/chain"; import { Address as AddressSchema, Hex } from "@exactly/common/validation"; import alchemyChains from "./alchemyChains"; @@ -82,14 +82,8 @@ export function balancesOptions(account: Address | undefined) { if (!account) return {} as Record; ensureConfig(); const [amounts, lifiTokens, exa] = await Promise.all([ - getWalletBalances(account).catch((error: unknown) => { - reportError(error); - return {} as Record; - }), - queryClient.fetchQuery(lifiTokensOptions).catch((error: unknown) => { - reportError(error); - return [] as Token[]; - }), + getWalletBalances(account), + queryClient.fetchQuery(lifiTokensOptions), exaAddress ? getToken(chain.id, exaAddress).catch((error: unknown) => { reportError(error); @@ -226,8 +220,12 @@ export async function getAllowTokens(markets: readonly { asset: string; symbol: ensureConfig(); if (chain.testnet || chain.id === anvil.id) return []; const { tokens } = await getTokens({ chains: [chain.id] }); - const protocolAssets = markets.filter((m) => m.symbol.slice(3) !== "USDC.e").map((m) => m.asset); - const allowed = new Set([...allowlist, ...protocolAssets].map((address) => address.toLowerCase())); + const excluded = new Set(markets.filter((m) => m.symbol.slice(3) === "USDC.e").map((m) => m.asset.toLowerCase())); + const allowed = new Set( + [...(allowlists[String(chain.id)] ?? []), ...markets.map((m) => m.asset)] + .map((address) => address.toLowerCase()) + .filter((address) => !excluded.has(address)), + ); const allowTokens = tokens[chain.id]?.filter((token) => allowed.has(token.address.toLowerCase())) ?? []; if (!exaAddress) return allowTokens; try { @@ -252,6 +250,8 @@ export type RouteFrom = { value: bigint; }; +export const bridgeSlippage = 0.02; + export async function getRouteFrom({ fromChainId, toChainId, @@ -305,7 +305,7 @@ export async function getRouteFrom({ config.set({ integrator: "exa_app", userId: fromAddress }); const { estimate, transactionRequest, tool } = await getQuote({ fee: 0.0025, - slippage: 0.02, + slippage: bridgeSlippage, integrator: "exa_app", fromChain: fromChainId ?? chain.id, toChain: toChainId ?? chain.id, @@ -485,7 +485,9 @@ async function getWalletBalances(account: Address) { }), ); for (const [key, { ids, error }] of failures) { - reportError(new Error(`balances failed for chains ${ids.join(", ")}: ${key}`, { cause: error })); + const failure = new Error(`balances failed for chains ${ids.join(", ")}: ${key}`, { cause: error }); + if (ids.includes(chain.id)) throw failure; + reportError(failure); } return balances; } @@ -555,6 +557,9 @@ export const tokenCorrelation = { WETH: "ETH", "WETH.e": "ETH", + USDT0: "USDT", + "USD₮0": "USDT", + // #region liquid staked ETH cbETH: "wstETH", ETHx: "wstETH", diff --git a/src/utils/persona.ts b/src/utils/persona.ts index 5e7aad06e1..52f8f4fb28 100644 --- a/src/utils/persona.ts +++ b/src/utils/persona.ts @@ -14,20 +14,20 @@ import type { UseMutationOptions } from "@tanstack/react-query"; export const environment = (__DEV__ || process.env.EXPO_PUBLIC_ENV === "e2e" ? "sandbox" : "production") as Environment; type KYCResult = { status: "cancel" } | { status: "complete" }; -type RampKYCResult = KYCResult | { status: "error" }; +type InquiryResult = KYCResult | { status: "error" }; let current: | undefined - | { controller: AbortController; promise: Promise; type: "basic" } | { controller: AbortController; - promise: Promise; + promise: Promise; + scope: "bridge" | "cardLimit" | "manteca"; tokens?: { inquiryId: string; sessionToken: string }; - type: "bridge" | "manteca"; - }; + } + | { controller: AbortController; promise: Promise; scope: "basic" }; export function startKYC() { - if (current && !current.controller.signal.aborted && current.type === "basic") return current.promise; + if (current && !current.controller.signal.aborted && current.scope === "basic") return current.promise; current?.controller.abort(new Error("persona inquiry aborted")); const controller = new AbortController(); @@ -115,7 +115,7 @@ export function startKYC() { if (current?.controller === controller) current = undefined; }); - current = { type: "basic", controller, promise }; + current = { scope: "basic", controller, promise }; return promise; } @@ -124,20 +124,27 @@ export function cancelKYC() { } export function startMantecaKYC(tokens?: { inquiryId: string; sessionToken: string }) { - return startRampKYC("manteca", tokens); + return startScopedInquiry("manteca", tokens); } export function startAddressKYC(tokens?: { inquiryId: string; sessionToken: string }) { - return startRampKYC("bridge", tokens); + return startScopedInquiry("bridge", tokens); +} + +export function startCardLimitKYC() { + return startScopedInquiry("cardLimit"); } -function startRampKYC(type: "bridge" | "manteca", tokens?: { inquiryId: string; sessionToken: string }) { - if (current && !current.controller.signal.aborted && current.type === type && current.tokens === tokens) +function startScopedInquiry( + scope: "bridge" | "cardLimit" | "manteca", + tokens?: { inquiryId: string; sessionToken: string }, +) { + if (current && !current.controller.signal.aborted && current.scope === scope && current.tokens === tokens) return current.promise; current?.controller.abort(new Error("persona inquiry aborted")); const controller = new AbortController(); - const invalidationKey = ["kyc", type]; + const queryKey = ["kyc", scope]; const promise = (async () => { const { signal } = controller; @@ -151,11 +158,11 @@ function startRampKYC(type: "bridge" | "manteca", tokens?: { inquiryId: string; if (Platform.OS === "web") { const [{ Client }, { inquiryId, sessionToken }] = await Promise.all([ import("persona"), - tokens ?? getKYCTokens(type, await getRedirectURI()), + tokens ?? getKYCTokens(scope, await getRedirectURI()), ]); if (signal.aborted) throw signal.reason; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const onAbort = () => { client.destroy(); reject(new Error("persona inquiry aborted", { cause: signal.reason })); @@ -169,14 +176,14 @@ function startRampKYC(type: "bridge" | "manteca", tokens?: { inquiryId: string; signal.removeEventListener("abort", onAbort); globalThis.removeEventListener("pagehide", onPageHide); client.destroy(); - queryClient.invalidateQueries({ queryKey: invalidationKey }).catch(reportError); + queryClient.invalidateQueries({ queryKey }).catch(reportError); resolve({ status: "complete" }); }, onCancel: () => { signal.removeEventListener("abort", onAbort); globalThis.removeEventListener("pagehide", onPageHide); client.destroy(); - queryClient.invalidateQueries({ queryKey: invalidationKey }).catch(reportError); + queryClient.invalidateQueries({ queryKey }).catch(reportError); resolve({ status: "cancel" }); }, onError: (error) => { @@ -191,23 +198,23 @@ function startRampKYC(type: "bridge" | "manteca", tokens?: { inquiryId: string; }); } - const { inquiryId, sessionToken } = tokens ?? (await getKYCTokens(type, await getRedirectURI())); + const { inquiryId, sessionToken } = tokens ?? (await getKYCTokens(scope, await getRedirectURI())); if (signal.aborted) throw signal.reason; const { Inquiry } = await import("react-native-persona"); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const onAbort = () => reject(new Error("persona inquiry aborted", { cause: signal.reason })); signal.addEventListener("abort", onAbort, { once: true }); Inquiry.fromInquiry(inquiryId) .sessionToken(sessionToken) .onCanceled(() => { signal.removeEventListener("abort", onAbort); - queryClient.invalidateQueries({ queryKey: invalidationKey }).catch(reportError); + queryClient.invalidateQueries({ queryKey }).catch(reportError); resolve({ status: "cancel" }); }) .onComplete(() => { signal.removeEventListener("abort", onAbort); - queryClient.invalidateQueries({ queryKey: invalidationKey }).catch(reportError); + queryClient.invalidateQueries({ queryKey }).catch(reportError); resolve({ status: "complete" }); }) .onError((error) => { @@ -222,7 +229,7 @@ function startRampKYC(type: "bridge" | "manteca", tokens?: { inquiryId: string; if (current?.controller === controller) current = undefined; }); - current = { type, controller, promise, tokens }; + current = { scope, controller, promise, tokens }; return promise; } diff --git a/src/utils/queryClient.ts b/src/utils/queryClient.ts index 6018f709e5..277ee83235 100644 --- a/src/utils/queryClient.ts +++ b/src/utils/queryClient.ts @@ -104,6 +104,7 @@ queryClient.setQueryDefaults(["credential"], { throw new Error("don't refetch"); }, }); +queryClient.setQueryDefaults(["getBytecode"], { staleTime: (query) => (query.state.data ? Infinity : 0) }); queryClient.setQueryDefaults(["readContract"], { queryFn: () => { throw new Error("don't refetch"); @@ -227,6 +228,27 @@ queryClient.setQueryDefaults(["settings", "defi-intro-shown"], { gcTime: Infinity, queryFn: () => queryClient.getQueryData(["settings", "defi-intro-shown"]), }); +queryClient.setQueryDefaults(["settings", "bridge-needed-shown"], { + initialData: false, + retry: false, + staleTime: Infinity, + gcTime: Infinity, + queryFn: () => queryClient.getQueryData(["settings", "bridge-needed-shown"]), +}); +queryClient.setQueryDefaults(["settings", "bridge-swap-needed-shown"], { + initialData: false, + retry: false, + staleTime: Infinity, + gcTime: Infinity, + queryFn: () => queryClient.getQueryData(["settings", "bridge-swap-needed-shown"]), +}); +queryClient.setQueryDefaults(["settings", "swap-needed-shown"], { + initialData: false, + retry: false, + staleTime: Infinity, + gcTime: Infinity, + queryFn: () => queryClient.getQueryData(["settings", "swap-needed-shown"]), +}); queryClient.setQueryDefaults(["defi", "usdc-funding-connected"], { initialData: false, retry: false, diff --git a/src/utils/server.ts b/src/utils/server.ts index e43deb6b6d..70bffc6323 100644 --- a/src/utils/server.ts +++ b/src/utils/server.ts @@ -208,6 +208,15 @@ queryClient.setQueryDefaults(["kyc", "status"], { meta: { warnError: (error) => triage(error) === "warn" }, queryFn: () => getKYCStatus("basic", true), }); +queryClient.setQueryDefaults(["kyc", "cardLimit"], { + staleTime: 5 * 60_000, + gcTime: isServer ? Infinity : 60 * 60_000, + queryFn: () => + getKYCStatus("cardLimit").catch((error: unknown) => { + if (error instanceof APIError && error.code === 400) return { code: error.text }; + throw error; + }), +}); export type KYCStatus = Awaited>; export async function getCredential() { @@ -299,6 +308,18 @@ queryClient.setQueryDefaults(["activity", "details"], { throw new Error("don't refetch"); }, }); +queryClient.setQueryDefaults(["activity", "statement"], { + staleTime: 60_000, + gcTime: isServer ? Infinity : 60 * 60_000, +}); + +export async function getStatement(maturity: number) { + return getActivity({ maturity: String(maturity) }, "application/pdf"); +} + +export async function getStatementActivity(maturity: number) { + return getActivity({ maturity: String(maturity), include: ["card", "repay"] }); +} let authenticating: Promise | undefined; export async function auth() { diff --git a/src/utils/statement.ts b/src/utils/statement.ts new file mode 100644 index 0000000000..121634b78d --- /dev/null +++ b/src/utils/statement.ts @@ -0,0 +1,108 @@ +import { Platform } from "react-native"; + +import { File, Paths } from "expo-file-system"; +import { isAvailableAsync, shareAsync } from "expo-sharing"; + +import { getStatement } from "./server"; + +import type { getStatementActivity } from "./server"; + +export function group(items: Awaited>) { + const cards = new Map; lastFour: string }>(); + const payments: Payment[] = []; + for (const item of items) { + if (item.type === "repay") { + payments.push({ + id: item.id, + amount: item.amount, + positionAmount: item.positionAmount, + timestamp: item.timestamp, + }); + continue; + } + if (item.type !== "panda" && item.type !== "card") continue; + const lines = installments(item); + if (lines.length === 0) continue; + const card = cards.get(item.cardId) ?? { + lastFour: item.lastFour, + dates: new Map(), + }; + const key = item.timestamp.slice(0, 10); + const dates = card.dates.get(key) ?? { label: item.timestamp, rows: [] }; + dates.rows.push(...lines.map((line) => ({ merchant: item.merchant.name, ...line }))); + card.dates.set(key, dates); + cards.set(item.cardId, card); + } + const grouped = [...cards.values()] + .map(({ lastFour, dates }) => { + const days = [...dates.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => ({ key, ...value })); + const total = days.reduce((sum, { rows }) => sum + rows.reduce((amount, row) => amount + row.amount, 0), 0); + return { lastFour, dates: days, total }; + }) + .sort((a, b) => a.lastFour.localeCompare(b.lastFour)); + const purchases = grouped.reduce((sum, { total }) => sum + total, 0); + const paid = payments.reduce((sum, { amount }) => sum + amount, 0); + const settled = payments.reduce((sum, { positionAmount }) => sum + positionAmount, 0); + return { + cards: grouped, + payments: payments.sort((a, b) => a.timestamp.localeCompare(b.timestamp)), + paid, + discount: settled - paid, + due: purchases - settled, + }; +} + +export async function downloadStatement(maturity: number, filename: string) { + const bytes = await getStatement(maturity); + if (Platform.OS !== "web") return share(bytes, filename); + const url = pdf(bytes); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = filename; + document.body.append(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); +} + +async function share(bytes: Uint8Array, filename: string) { + const file = new File(Paths.cache, filename); + if (file.exists) file.delete(); + file.write(bytes); + if (!(await isAvailableAsync())) throw new Error("sharing unavailable"); + await shareAsync(file.uri, { mimeType: "application/pdf", UTI: "com.adobe.pdf", dialogTitle: filename }); +} + +function pdf(bytes: Uint8Array) { + return URL.createObjectURL(new Blob([new Uint8Array(bytes)], { type: "application/pdf" })); +} + +function installments(item: Purchase) { + if (item.type === "panda") { + return item.operations.flatMap((operation) => + "borrow" in operation + ? "installments" in operation.borrow + ? operation.borrow.installments.map((installment) => ({ + current: installment.current, + total: operation.mode, + amount: installment.amount, + })) + : [{ current: 1, total: 1, amount: operation.borrow.amount }] + : [], + ); + } + if (!("borrow" in item)) return []; + return "installments" in item.borrow + ? item.borrow.installments.map((installment) => ({ + current: installment.current, + total: item.mode, + amount: installment.amount, + })) + : [{ current: 1, total: 1, amount: item.borrow.amount }]; +} + +type Purchase = Extract>[number], { type: "card" | "panda" }>; +type Row = { amount: number; current: number; merchant: string; total: number }; +type Payment = { amount: number; id: string; positionAmount: number; timestamp: string }; diff --git a/src/utils/useCardLimit.ts b/src/utils/useCardLimit.ts new file mode 100644 index 0000000000..612cc07045 --- /dev/null +++ b/src/utils/useCardLimit.ts @@ -0,0 +1,36 @@ +import { useTranslation } from "react-i18next"; + +import { useToastController } from "@tamagui/toast"; + +import { useQuery } from "@tanstack/react-query"; + +import { newMessage } from "./intercom"; +import { startCardLimitKYC } from "./persona"; +import reportError from "./reportError"; + +import type { KYCStatus } from "./server"; + +export default function useCardLimit(enabled: boolean) { + const { t } = useTranslation(); + const toast = useToastController(); + const { data: status, isPending } = useQuery({ queryKey: ["kyc", "cardLimit"], enabled }); + function increase() { + if (status?.code !== "not started") { + newMessage(t("I want to increase my spending limit")).catch(reportError); + return; + } + startCardLimitKYC() + .catch((error: unknown) => { + reportError(error); + return { status: "error" } as const; + }) + .then((result) => { + if (result.status === "error") + toast.show(t("Something went wrong. Please try again."), { + burntOptions: { haptic: "error", preset: "error" }, + }); + }) + .catch(reportError); + } + return { increase, pending: isPending, processing: status?.code === "processing" }; +} diff --git a/src/utils/useStatement.ts b/src/utils/useStatement.ts new file mode 100644 index 0000000000..9ce9d5806e --- /dev/null +++ b/src/utils/useStatement.ts @@ -0,0 +1,10 @@ +import { skipToken, useQuery } from "@tanstack/react-query"; + +import { getStatementActivity } from "./server"; + +export default function useStatement(maturity: number | undefined) { + return useQuery({ + queryKey: ["activity", "statement", maturity], + queryFn: maturity === undefined ? skipToken : () => getStatementActivity(maturity), + }); +} diff --git a/src/utils/useStatements.ts b/src/utils/useStatements.ts new file mode 100644 index 0000000000..25bf585d08 --- /dev/null +++ b/src/utils/useStatements.ts @@ -0,0 +1,33 @@ +import { useMemo } from "react"; + +import { useQuery } from "@tanstack/react-query"; + +import { MATURITY_INTERVAL } from "@exactly/lib"; + +import useMarkets from "./useMarkets"; + +import type { ActivityItem } from "./queryClient"; + +export default function useStatements() { + const { data: activity } = useQuery({ queryKey: ["activity"] }); + const { timestamp } = useMarkets(); + return useMemo(() => { + if (!activity) return []; + const now = Number(timestamp); + const maturities = new Set(); + for (const item of activity) { + const borrows = + item.type === "panda" + ? item.operations.flatMap((operation) => ("borrow" in operation ? [operation.borrow] : [])) + : item.type === "card" && "borrow" in item + ? [item.borrow] + : []; + for (const borrow of borrows) { + if ("installments" in borrow) + for (const installment of borrow.installments) maturities.add(installment.maturity); + else maturities.add(borrow.maturity); + } + } + return [...maturities].filter((m) => m - MATURITY_INTERVAL < now).sort((a, b) => b - a); + }, [activity, timestamp]); +} diff --git a/src/utils/weeklySpend.ts b/src/utils/weeklySpend.ts new file mode 100644 index 0000000000..7a0e46a33a --- /dev/null +++ b/src/utils/weeklySpend.ts @@ -0,0 +1,10 @@ +import type { Activity } from "./server"; + +export default function weeklySpend(activity: Activity | undefined) { + if (!activity) return 0; + return activity.reduce((total, item) => { + if (item.type !== "panda" || item.status === "declined") return total; + const elapsed = Date.now() - new Date(item.timestamp).getTime(); + return elapsed <= 7 * 24 * 60 * 60 * 1000 ? total + item.usdAmount : total; + }, 0); +} diff --git a/tamagui.config.ts b/tamagui.config.ts index 59c232201f..070180b512 100644 --- a/tamagui.config.ts +++ b/tamagui.config.ts @@ -4,9 +4,22 @@ import { createAnimations } from "@tamagui/animations-moti"; import { config } from "@tamagui/config/v3"; import { createFont, createTamagui, createTokens } from "tamagui"; +import { isBase } from "@exactly/common/generated/chain"; + const tokens = createTokens({ color: { - cardBackground: "#1A181A", + baseBlue: "#0000FF", + baseBlueStrong: "#0010BF", + baseCerulean: "#3C8AFF", + baseGray0: "#FFFFFF", + baseGray10: "#EEF0F3", + baseGray100: "#0A0B0D", + baseGray15: "#DEE1E7", + baseGray30: "#B1B7C3", + baseGray50: "#717886", + baseGray80: "#32353D", + cardBackground: "#1A211E", + cardPreviewBackground: "#1A181A", creditDark1: "#18111B", creditDark5: "#48295C", creditDark9: "#8E4EC6", @@ -216,6 +229,57 @@ const tokens = createTokens({ zIndex: config.tokens.zIndex, }); +const baseLight = { + backgroundSoft: tokens.color.baseGray0, + backgroundMild: tokens.color.baseGray10, + backgroundStrong: tokens.color.baseGray15, + backgroundBrand: tokens.color.baseBlue, + backgroundBrandSoft: tokens.color.baseGray0, + backgroundBrandMild: tokens.color.baseGray10, + backgroundBanner: tokens.color.baseBlue, + uiNeutralPrimary: tokens.color.baseGray100, + uiNeutralSecondary: tokens.color.baseGray80, + uiNeutralTertiary: tokens.color.baseGray15, + uiNeutralPlaceholder: tokens.color.baseGray30, + uiNeutralInversePrimary: tokens.color.baseGray0, + uiNeutralInverseSecondary: tokens.color.baseGray15, + uiBrandPrimary: tokens.color.baseBlueStrong, + uiBrandSecondary: tokens.color.baseBlue, + uiBrandTertiary: tokens.color.baseCerulean, + interactiveBaseBrandDefault: tokens.color.baseBlue, + interactiveBaseBrandHover: tokens.color.baseBlueStrong, + interactiveBaseBrandPressed: tokens.color.baseBlue, + interactiveBaseBrandSoftDefault: tokens.color.baseGray10, + interactiveBaseBrandSoftHover: tokens.color.baseGray15, + interactiveBaseBrandSoftPressed: tokens.color.baseGray10, + interactiveOnBaseBrandDefault: tokens.color.baseGray0, + interactiveOnBaseBrandSoft: tokens.color.baseGray100, + interactiveTextBrandDefault: tokens.color.baseBlue, + interactiveTextBrandHover: tokens.color.baseBlueStrong, + interactiveTextBrandPressed: tokens.color.baseBlue, + interactiveDisabled: tokens.color.baseGray30, + interactiveOnDisabled: tokens.color.baseGray50, + borderNeutralSoft: tokens.color.baseGray15, + borderNeutralMild: tokens.color.baseGray30, + borderNeutralStrong: tokens.color.baseGray50, + borderNeutralSeparator: tokens.color.baseGray15, + borderNeutralDisabled: tokens.color.baseGray30, + borderBrandSoft: tokens.color.baseCerulean, + borderBrandMild: tokens.color.baseBlue, + borderBrandStrong: tokens.color.baseBlueStrong, + iconPrimary: tokens.color.baseGray100, + iconSecondary: tokens.color.baseGray80, + iconInversePrimary: tokens.color.baseGray0, + iconInverseSecondary: tokens.color.baseGray10, + iconBrandDefault: tokens.color.baseBlue, + iconBrandHover: tokens.color.baseBlueStrong, + iconBrandPressed: tokens.color.baseBlue, + iconBrandSoftDefault: tokens.color.baseGray10, + iconBrandSoftHover: tokens.color.baseGray15, + iconBrandSoftPressed: tokens.color.baseGray30, + iconDisabled: tokens.color.baseGray50, +}; + const sizes = config.fonts.body.size; const body = createFont({ family: "SplineSans-Regular", @@ -259,6 +323,7 @@ const tamagui = createTamagui({ themes: { light: { cardBackground: tokens.color.cardBackground, + cardPreviewBackground: tokens.color.cardPreviewBackground, cardDebitBackground: tokens.color.debitLight1, cardDebitInteractive: tokens.color.debitLight9, cardDebitText: tokens.color.debitLight12, @@ -274,6 +339,7 @@ const tamagui = createTamagui({ backgroundBrand: tokens.color.primaryLight9, backgroundBrandSoft: tokens.color.primaryLight2, backgroundBrandMild: tokens.color.primaryLight3, + backgroundBanner: tokens.color.grayscaleLight12, uiNeutralPrimary: tokens.color.grayscaleLight12, uiNeutralSecondary: tokens.color.grayscaleLight11, uiNeutralTertiary: tokens.color.grayscaleLight3, @@ -401,9 +467,11 @@ const tamagui = createTamagui({ borderColorFocus: "transparent", borderColorPress: "transparent", outlineColor: "", + ...(isBase ? baseLight : {}), }, dark: { cardBackground: tokens.color.cardBackground, + cardPreviewBackground: tokens.color.cardPreviewBackground, cardDebitBackground: tokens.color.debitDark1, cardDebitInteractive: tokens.color.debitDark9, cardDebitText: tokens.color.debitLight12, @@ -419,6 +487,7 @@ const tamagui = createTamagui({ backgroundBrand: tokens.color.primaryDark9, backgroundBrandSoft: tokens.color.primaryDark2, backgroundBrandMild: tokens.color.primaryDark3, + backgroundBanner: tokens.color.grayscaleLight12, uiNeutralPrimary: tokens.color.grayscaleDark12, uiNeutralSecondary: tokens.color.grayscaleDark11, uiNeutralTertiary: tokens.color.grayscaleDark3,