diff --git a/.changeset/clear-trains-move.md b/.changeset/clear-trains-move.md
new file mode 100644
index 000000000..87da4aaa6
--- /dev/null
+++ b/.changeset/clear-trains-move.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 adjust tamagui tokens
diff --git a/.changeset/curly-waves-yell.md b/.changeset/curly-waves-yell.md
new file mode 100644
index 000000000..7e877eb62
--- /dev/null
+++ b/.changeset/curly-waves-yell.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 restyle card status component
diff --git a/.changeset/easy-snails-brake.md b/.changeset/easy-snails-brake.md
new file mode 100644
index 000000000..59839c8d3
--- /dev/null
+++ b/.changeset/easy-snails-brake.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 restyle action buttons
diff --git a/.changeset/frank-schools-clap.md b/.changeset/frank-schools-clap.md
new file mode 100644
index 000000000..ebe1d047d
--- /dev/null
+++ b/.changeset/frank-schools-clap.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 restructure sheets layout
diff --git a/.changeset/giant-parrots-enter.md b/.changeset/giant-parrots-enter.md
new file mode 100644
index 000000000..b4a81d2e1
--- /dev/null
+++ b/.changeset/giant-parrots-enter.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 adjust home scroll view gaps
diff --git a/.changeset/khaki-queens-smash.md b/.changeset/khaki-queens-smash.md
new file mode 100644
index 000000000..56a9a5012
--- /dev/null
+++ b/.changeset/khaki-queens-smash.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 apply tamagui spacing tokens
diff --git a/.changeset/lazy-planes-dance.md b/.changeset/lazy-planes-dance.md
new file mode 100644
index 000000000..2df024e57
--- /dev/null
+++ b/.changeset/lazy-planes-dance.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 refine benefits carousel
diff --git a/.changeset/little-hoops-train.md b/.changeset/little-hoops-train.md
new file mode 100644
index 000000000..05bbc9f8f
--- /dev/null
+++ b/.changeset/little-hoops-train.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 replace fonts
diff --git a/.changeset/salty-fans-mate.md b/.changeset/salty-fans-mate.md
new file mode 100644
index 000000000..d583340bd
--- /dev/null
+++ b/.changeset/salty-fans-mate.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 add vertical frame to styled button
diff --git a/.changeset/soft-beans-grow.md b/.changeset/soft-beans-grow.md
new file mode 100644
index 000000000..639e6a270
--- /dev/null
+++ b/.changeset/soft-beans-grow.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💫 unify animation parameters
diff --git a/.changeset/spicy-doodles-give.md b/.changeset/spicy-doodles-give.md
new file mode 100644
index 000000000..49e038838
--- /dev/null
+++ b/.changeset/spicy-doodles-give.md
@@ -0,0 +1,5 @@
+---
+"@exactly/mobile": patch
+---
+
+💄 restyle portfolio summary
diff --git a/.maestro/flows/local.yaml b/.maestro/flows/local.yaml
index 7d058821c..72f0941a3 100644
--- a/.maestro/flows/local.yaml
+++ b/.maestro/flows/local.yaml
@@ -24,5 +24,7 @@ tags: [critical]
file: ../subflows/mint.yaml
env: { asset: ETH, to: "${output.account}", amount: "0.1" }
- runFlow: ../subflows/verifyIdentity.yaml
+- runFlow: ../subflows/readBenefits.yaml
- runFlow: ../subflows/activateCard.yaml
+- runFlow: ../subflows/readHome.yaml
- runFlow: ../subflows/storeCoverage.yaml
diff --git a/.maestro/subflows/activateCard.yaml b/.maestro/subflows/activateCard.yaml
index 16ce793ac..0f870de12 100644
--- a/.maestro/subflows/activateCard.yaml
+++ b/.maestro/subflows/activateCard.yaml
@@ -1,16 +1,13 @@
appId: ${APP_ID ?? "app.exactly"}
---
- tapOn: Card
-- runFlow:
- when: { true: "${maestro.platform != 'web'}" }
- commands:
- - tapOn: \$[\s\d,.\xa0]+, AVAILABLE BALANCE
-- runFlow:
- when: { platform: web }
- commands: [{ tapOn: Available balance }]
+- tapOn: Card details
+- waitForAnimationToEnd
- runFlow:
when: { visible: Accept and enable card }
- commands: [{ tapOn: Accept and enable card }]
+ commands:
+ - runFlow: { file: scrollTo.yaml, env: { element: Accept and enable card } }
+ - tapOn: Accept and enable card
- assertVisible: Manually add your card to Apple Pay & Google Pay to make contactless payments.
- tapOn: Close
- tapOn: Freeze card
@@ -42,4 +39,26 @@ appId: ${APP_ID ?? "app.exactly"}
text: \$[\s\d,.\xa0]+
above: Available balance
- tapOn: Home
-- assertVisible: SPENDING LIMIT
+- runFlow:
+ when: { visible: Spending limit }
+ commands:
+ - tapOn: Later in 1
+ - assertVisible: How installment repayment works
+ - tapOn: Close
+ - tapOn: Tap here to change the number of installments
+ - runFlow: { file: ../subflows/tapAria.yaml, env: { aria: Close } }
+- runFlow: { file: scrollTo.yaml, env: { element: Learn more } }
+- tapOn:
+ text: Learn more
+ rightOf: Exa Card pay mode
+- assertVisible: Change the pay mode before each purchase and pay how you want.
+- tapOn: Close
+- runFlow: { file: scrollTo.yaml, env: { element: Credit limit info } }
+- runFlow: { file: ../subflows/tapAria.yaml, env: { aria: Credit limit info } }
+- assertVisible: It's based on the value of your collateral assets and updates as their value changes.
+- tapOn: Close
+- tapOn: Now
+- runFlow: { file: scrollTo.yaml, env: { element: Spending limit info } }
+- runFlow: { file: ../subflows/tapAria.yaml, env: { aria: Spending limit info } }
+- assertVisible: It's based on the USDC available in your balance.
+- tapOn: Close
diff --git a/.maestro/subflows/borrow.yaml b/.maestro/subflows/borrow.yaml
index 869426b66..d6433ebfd 100644
--- a/.maestro/subflows/borrow.yaml
+++ b/.maestro/subflows/borrow.yaml
@@ -7,7 +7,7 @@ appId: ${APP_ID ?? "app.exactly"}
- tapOn: DeFi
- runFlow:
when: { visible: Explore decentralized services }
- commands: [{ tapOn: "Explore decentralized services" }]
+ commands: [{ tapOn: 'Explore decentralized services' }]
- tapOn: USDC funding
- runFlow:
when: { visible: Connect wallet to Exactly Protocol }
@@ -32,7 +32,7 @@ appId: ${APP_ID ?? "app.exactly"}
- runScript: ../dist/getAccount.js
- runScript:
file: ../dist/hookProposals.js
- env: { account: "${output.account}" }
+ env: { account: '${output.account}' }
- tapOn: Close
- tapOn: Home
- runFlow:
diff --git a/.maestro/subflows/copyAria.yaml b/.maestro/subflows/copyAria.yaml
new file mode 100644
index 000000000..8ab07a2fe
--- /dev/null
+++ b/.maestro/subflows/copyAria.yaml
@@ -0,0 +1,12 @@
+appId: ${APP_ID ?? "app.exactly"}
+---
+# HACK https://github.com/mobile-dev-inc/Maestro/issues/2914
+- runFlow:
+ when: { true: "${maestro.platform != 'web'}" }
+ commands:
+ - copyTextFrom: "${aria}"
+- runFlow:
+ when: { platform: web }
+ commands:
+ - copyTextFrom:
+ id: "${aria}"
diff --git a/.maestro/subflows/dismissNotifications.yaml b/.maestro/subflows/dismissNotifications.yaml
index 7b18d26ba..88864e3a5 100644
--- a/.maestro/subflows/dismissNotifications.yaml
+++ b/.maestro/subflows/dismissNotifications.yaml
@@ -7,5 +7,5 @@ appId: ${APP_ID ?? "app.exactly"}
- tapOn: Not now
- pressKey: home
- launchApp: { permissions: { all: deny, camera: allow } }
- - extendedWaitUntil: { visible: Your portfolio, timeout: 180000 }
+ - extendedWaitUntil: { visible: Portfolio, timeout: 180000 }
- assertNotVisible: Stay updated
diff --git a/.maestro/subflows/readBenefits.yaml b/.maestro/subflows/readBenefits.yaml
new file mode 100644
index 000000000..d683b4397
--- /dev/null
+++ b/.maestro/subflows/readBenefits.yaml
@@ -0,0 +1,33 @@
+appId: ${APP_ID ?? "app.exactly"}
+---
+- repeat:
+ while:
+ notVisible: Copy your ID and get 30 days of travel insurance for free on Pax Assistance.
+ commands:
+ - extendedWaitUntil:
+ visible: 30 days of free travel insurance
+ timeout: 15000
+ - tapOn: Get now
+- assertVisible: "Copy Pax ID [a-z0-9]{10}"
+- assertVisible: Get benefit
+- runFlow: { file: tapAria.yaml, env: { aria: Close } }
+- repeat:
+ while:
+ notVisible: Stay connected around the world.
+ commands:
+ - extendedWaitUntil:
+ visible: 20% OFF on eSims
+ timeout: 15000
+ - tapOn: 20% OFF on eSims
+- assertVisible: Get benefit
+- runFlow: { file: tapAria.yaml, env: { aria: Close } }
+- repeat:
+ while:
+ notVisible: Visa Signature Exa Card benefits
+ commands:
+ - extendedWaitUntil:
+ visible: Visa Signature benefits
+ timeout: 15000
+ - tapOn: Learn more
+- assertVisible: Go to Visa
+- runFlow: { file: tapAria.yaml, env: { aria: Close } }
diff --git a/.maestro/subflows/readHome.yaml b/.maestro/subflows/readHome.yaml
new file mode 100644
index 000000000..db97fd8b2
--- /dev/null
+++ b/.maestro/subflows/readHome.yaml
@@ -0,0 +1,51 @@
+appId: ${APP_ID ?? "app.exactly"}
+---
+- tapOn: Home
+- assertVisible: ${output.account.slice(0, 6)}…${output.account.slice(-4)}
+- runFlow:
+ when: { true: "${maestro.platform != 'web'}" }
+ commands: [{ assertVisible: Settings }]
+- runFlow:
+ when: { platform: web }
+ commands: [{ assertVisible: { id: Settings } }]
+- runFlow: { file: tapAria.yaml, env: { aria: Hide sensitive } }
+- runFlow:
+ when: { true: "${maestro.platform != 'web'}" }
+ commands:
+ - assertNotVisible:
+ text: \$[\s\d,.\xa0]+
+ below: Portfolio
+- runFlow:
+ when: { platform: web }
+ commands:
+ - assertNotVisible:
+ id: \$[\s\d,.\xa0]+
+- runFlow: { file: tapAria.yaml, env: { aria: Show sensitive } }
+- runFlow:
+ when: { true: "${maestro.platform != 'web'}" }
+ commands:
+ - assertVisible:
+ text: \$[\s\d,.\xa0]+
+ below: Portfolio
+- runFlow:
+ when: { platform: web }
+ commands:
+ - assertVisible:
+ id: \$[\s\d,.\xa0]+
+- runFlow: readPortfolio.yaml
+- assertTrue: ${output.portfolio > 0}
+- tapOn: Manage portfolio
+- assertVisible: Your Portfolio
+- runFlow: { file: tapAria.yaml, env: { aria: Back } }
+- assertVisible: Add funds
+- assertVisible: Send
+- assertVisible: Swap
+- assertNotVisible: Getting Started
+- runFlow: { file: scrollTo.yaml, env: { element: Upcoming payments } }
+- runFlow: { file: scrollTo.yaml, env: { element: Latest activity } }
+- assertNotVisible: No activity yet
+- tapOn: View all
+- tapOn: Home
+- tapOn: Home
+- waitForAnimationToEnd
+- assertVisible: Portfolio
diff --git a/.maestro/subflows/readPortfolio.yaml b/.maestro/subflows/readPortfolio.yaml
index f0ca91da0..ec4f197cf 100644
--- a/.maestro/subflows/readPortfolio.yaml
+++ b/.maestro/subflows/readPortfolio.yaml
@@ -1,6 +1,6 @@
appId: ${APP_ID ?? "app.exactly"}
---
-- copyTextFrom:
- below: Your portfolio
- text: ^(US)?\$[\s\d,.\xa0]+$
+- runFlow:
+ file: copyAria.yaml
+ env: { aria: "^(US)?\\$[\\s\\d,.\\xa0]+$" }
- evalScript: ${output.portfolio = Number(maestro.copiedText.replace(/\D/g, "")) / 100}
diff --git a/.maestro/subflows/repay.yaml b/.maestro/subflows/repay.yaml
index fd53a2bc6..d46403571 100644
--- a/.maestro/subflows/repay.yaml
+++ b/.maestro/subflows/repay.yaml
@@ -1,7 +1,7 @@
appId: ${APP_ID ?? "app.exactly"}
---
- waitForAnimationToEnd
-- scrollUntilVisible: { element: Upcoming payments }
+- runFlow: { file: scrollTo.yaml, env: { element: Repay } }
- copyTextFrom:
text: \d+[,.]\d{2}
below: Upcoming payments
@@ -43,7 +43,7 @@ appId: ${APP_ID ?? "app.exactly"}
file: ../subflows/tapWhileAria.yaml
env: { aria: Pending proposals, tap: Home }
- waitForAnimationToEnd
-- scrollUntilVisible: { element: Upcoming payments }
+- runFlow: { file: scrollTo.yaml, env: { element: Repay } }
- runFlow:
when: { true: "${!amount}" }
commands: [{ assertNotVisible: "${output.maturity}" }]
diff --git a/.maestro/subflows/rollover.yaml b/.maestro/subflows/rollover.yaml
index 5ed03f68b..08e8122d7 100644
--- a/.maestro/subflows/rollover.yaml
+++ b/.maestro/subflows/rollover.yaml
@@ -1,7 +1,7 @@
appId: ${APP_ID ?? "app.exactly"}
---
- waitForAnimationToEnd
-- scrollUntilVisible: { element: Upcoming payments }
+- runFlow: { file: scrollTo.yaml, env: { element: Repay, offset: 1 } }
- copyTextFrom:
text: \d+[,.]\d{2}
below: Upcoming payments
@@ -28,7 +28,7 @@ appId: ${APP_ID ?? "app.exactly"}
file: ../subflows/tapWhileAria.yaml
env: { aria: Pending proposals, tap: Home }
- waitForAnimationToEnd
-- scrollUntilVisible: { element: Upcoming payments }
+- runFlow: { file: scrollTo.yaml, env: { element: Repay } }
- copyTextFrom:
text: \d+[,.]\d{2}
below: Upcoming payments
diff --git a/.maestro/subflows/scrollTo.yaml b/.maestro/subflows/scrollTo.yaml
new file mode 100644
index 000000000..2eea5383b
--- /dev/null
+++ b/.maestro/subflows/scrollTo.yaml
@@ -0,0 +1,46 @@
+appId: ${APP_ID ?? "app.exactly"}
+---
+# HACK https://github.com/mobile-dev-inc/maestro/issues/1775
+# maestro web uses window.scroll() which doesn't reach inner scroll containers
+- runFlow:
+ when: { true: "${maestro.platform != 'web'}" }
+ commands:
+ - scrollUntilVisible: { element: "${element}" }
+- runFlow:
+ when: { platform: web }
+ commands:
+ - evalScript: ${output.scrollDeadline = Date.now() + 30000}
+ - evalScript: ${output.found = false}
+ - repeat:
+ while:
+ true: "${!output.found}"
+ commands:
+ - runFlow:
+ when: { visible: "${element}" }
+ commands: [{ evalScript: "${output.found = true}" }]
+ - runFlow:
+ when: { visible: { id: "${element}" } }
+ commands: [{ evalScript: "${output.found = true}" }]
+ - runFlow:
+ when: { true: "${!output.found}" }
+ commands:
+ - runFlow:
+ when: { true: "${Date.now() >= output.scrollDeadline}" }
+ commands: [{ evalScript: "${throw new Error('scroll timeout')}" }]
+ - swipe:
+ start: "50%,40%"
+ end: "50%,10%"
+ duration: 500
+- runFlow:
+ when: { true: "${typeof offset !== 'undefined' && Number(offset) > 0}" }
+ commands:
+ - evalScript: ${output.scrollExtra = 0}
+ - repeat:
+ while:
+ true: "${output.scrollExtra < Number(offset)}"
+ commands:
+ - swipe:
+ start: "50%,70%"
+ end: "50%,50%"
+ duration: 300
+ - evalScript: ${output.scrollExtra = output.scrollExtra + 1}
diff --git a/app.config.ts b/app.config.ts
index bdbe41dd7..68a55d386 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -79,11 +79,7 @@ export default {
[
"expo-font",
{
- fonts: [
- "src/assets/fonts/BDOGrotesk-DemiBold.otf",
- "src/assets/fonts/BDOGrotesk-Regular.otf",
- "src/assets/fonts/IBMPlexMono-Medm.otf",
- ],
+ fonts: ["src/assets/fonts/SplineSans-Regular.otf", "src/assets/fonts/SplineSans-SemiBold.otf"],
} satisfies FontProps,
],
"expo-asset",
diff --git a/cspell.json b/cspell.json
index 06bdd5fc3..e533b8c96 100644
--- a/cspell.json
+++ b/cspell.json
@@ -50,7 +50,6 @@
"decisioned",
"defi",
"delegatecall",
- "demi",
"deployless",
"dieguezguille",
"dismissable",
@@ -73,12 +72,10 @@
"gitmoji",
"gitmojis",
"graaljs",
- "grotesk",
"hdpi",
"hexlify",
"hideable",
"hono",
- "IBMPlexMono-Medm",
"IERC",
"indoc",
"infinitism",
@@ -151,6 +148,7 @@
"solmate",
"sourcify",
"spkg",
+ "splinesans",
"spotlightjs",
"staticcall",
"streamingfast",
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index 0ceb0cc2f..13245326d 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -31,9 +31,8 @@ import { WagmiProvider } from "wagmi";
import domain from "@exactly/common/domain";
-import BDOGroteskDemiBold from "../assets/fonts/BDOGrotesk-DemiBold.otf";
-import BDOGroteskRegular from "../assets/fonts/BDOGrotesk-Regular.otf";
-import IBMPlexMonoMedium from "../assets/fonts/IBMPlexMono-Medm.otf";
+import SplineSansRegular from "../assets/fonts/SplineSans-Regular.otf";
+import SplineSansSemiBold from "../assets/fonts/SplineSans-SemiBold.otf";
import AppIcon from "../assets/icon.png";
import ThemeProvider from "../components/context/ThemeProvider";
import Error from "../components/shared/Error";
@@ -160,9 +159,8 @@ export default wrap(function RootLayout() {
const navigationContainer = useNavigationContainerRef();
useServerFonts({
- "BDOGrotesk-DemiBold": BDOGroteskDemiBold as FontSource,
- "BDOGrotesk-Regular": BDOGroteskRegular as FontSource,
- "IBMPlexMono-Medm": IBMPlexMonoMedium as FontSource,
+ "SplineSans-Regular": SplineSansRegular as FontSource,
+ "SplineSans-SemiBold": SplineSansSemiBold as FontSource,
});
useServerAssets([AppIcon]);
useEffect(() => {
diff --git a/src/assets/fonts/BDOGrotesk-DemiBold.otf b/src/assets/fonts/BDOGrotesk-DemiBold.otf
deleted file mode 100644
index d968f184f..000000000
Binary files a/src/assets/fonts/BDOGrotesk-DemiBold.otf and /dev/null differ
diff --git a/src/assets/fonts/BDOGrotesk-Regular.otf b/src/assets/fonts/BDOGrotesk-Regular.otf
deleted file mode 100644
index 9bfe3ec91..000000000
Binary files a/src/assets/fonts/BDOGrotesk-Regular.otf and /dev/null differ
diff --git a/src/assets/fonts/IBMPlexMono-Medm.otf b/src/assets/fonts/IBMPlexMono-Medm.otf
deleted file mode 100644
index f99385d31..000000000
Binary files a/src/assets/fonts/IBMPlexMono-Medm.otf and /dev/null differ
diff --git a/src/assets/fonts/SplineSans-Regular.otf b/src/assets/fonts/SplineSans-Regular.otf
new file mode 100644
index 000000000..11c39dd8d
Binary files /dev/null and b/src/assets/fonts/SplineSans-Regular.otf differ
diff --git a/src/assets/fonts/SplineSans-SemiBold.otf b/src/assets/fonts/SplineSans-SemiBold.otf
new file mode 100644
index 000000000..d81616815
Binary files /dev/null and b/src/assets/fonts/SplineSans-SemiBold.otf differ
diff --git a/src/assets/images/card-bg.svg b/src/assets/images/card-bg.svg
new file mode 100644
index 000000000..170f33f67
--- /dev/null
+++ b/src/assets/images/card-bg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/images/exa.svg b/src/assets/images/exa.svg
new file mode 100644
index 000000000..97faef5cc
--- /dev/null
+++ b/src/assets/images/exa.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/activity/Activity.tsx b/src/components/activity/Activity.tsx
index ebf8d3c78..722e28791 100644
--- a/src/components/activity/Activity.tsx
+++ b/src/components/activity/Activity.tsx
@@ -77,7 +77,7 @@ export default function Activity() {
ListHeaderComponent={
<>
-
+
{t("All Activity")}
diff --git a/src/components/activity/PendingProposals.tsx b/src/components/activity/PendingProposals.tsx
index 0b8298a6b..65548554f 100644
--- a/src/components/activity/PendingProposals.tsx
+++ b/src/components/activity/PendingProposals.tsx
@@ -146,7 +146,7 @@ export default function PendingProposals() {
return (
-
+
{
if (router.canGoBack()) {
diff --git a/src/components/activity/details/ActivityDetails.tsx b/src/components/activity/details/ActivityDetails.tsx
index c5296ebac..83551a219 100644
--- a/src/components/activity/details/ActivityDetails.tsx
+++ b/src/components/activity/details/ActivityDetails.tsx
@@ -27,7 +27,7 @@ export default function ActivityDetails() {
if (!item) return null;
return (
-
+
{
if (router.canGoBack()) {
diff --git a/src/components/add-funds/AddCrypto.tsx b/src/components/add-funds/AddCrypto.tsx
index 45a465522..ad8a9edac 100644
--- a/src/components/add-funds/AddCrypto.tsx
+++ b/src/components/add-funds/AddCrypto.tsx
@@ -72,16 +72,18 @@ export default function AddCrypto() {
-
+
{t("Your {{chain}} address", { chain: chain.name })}
- {address && (
-
- {shortenHex(address, 10, 12)}
-
- )}
+ {address && {shortenHex(address, 10, 12)}}
);
diff --git a/src/components/defi/IntroSheet.tsx b/src/components/defi/IntroSheet.tsx
index c4df59dd4..967cfb8c7 100644
--- a/src/components/defi/IntroSheet.tsx
+++ b/src/components/defi/IntroSheet.tsx
@@ -3,7 +3,7 @@ import { Trans, useTranslation } from "react-i18next";
import { Pressable } from "react-native";
import { ArrowRight, Info, X } from "@tamagui/lucide-icons";
-import { ScrollView, XStack, YStack } from "tamagui";
+import { XStack, YStack } from "tamagui";
import Defi from "../../assets/images/defi.svg";
import { presentArticle } from "../../utils/intercom";
@@ -17,7 +17,7 @@ import View from "../shared/View";
export default function IntroSheet({ open, onClose }: { onClose: () => void; open: boolean }) {
const { t } = useTranslation();
return (
-
+
void; ope
-
-
-
-
-
-
-
-
-
- {t("Welcome to DeFi")}
-
-
- {t("Access decentralized services provided by third-party DeFi protocols.")}
-
-
-
-
-
-
-
-
-
- {
- presentArticle("11731646").catch(reportError);
- }}
- />
- ),
- }}
- />
-
-
-
- {t("Explore decentralized services")}
-
-
-
-
-
+
+
+
+
+
+
+
+ {t("Welcome to DeFi")}
+
+
+ {t("Access decentralized services provided by third-party DeFi protocols.")}
+
-
-
+
+
+
+
+
+
+
+ {
+ presentArticle("11731646").catch(reportError);
+ }}
+ />
+ ),
+ }}
+ />
+
+
+
+ {t("Explore decentralized services")}
+
+
+
+
+
+
);
diff --git a/src/components/getting-started/GettingStarted.tsx b/src/components/getting-started/GettingStarted.tsx
index f6e2be629..e71e94890 100644
--- a/src/components/getting-started/GettingStarted.tsx
+++ b/src/components/getting-started/GettingStarted.tsx
@@ -44,9 +44,9 @@ export default function GettingStarted() {
const { steps } = useOnboardingSteps({ hasKYC, isDeployed });
return (
-
-
-
+
+
+
-
+
+
@@ -46,7 +45,7 @@ function AssetRow({ asset }: { asset: AssetItem }) {
-
+
{rate === undefined ? (
asset.market ? (
<>
@@ -73,7 +72,7 @@ function AssetRow({ asset }: { asset: AssetItem }) {
>
)}
-
+
{`$${(Number(usdValue) / 1e18).toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}
@@ -94,7 +93,7 @@ function AssetRow({ asset }: { asset: AssetItem }) {
function AssetSection({ title, assets }: { assets: AssetItem[]; title: string }) {
if (assets.length === 0) return null;
return (
-
+
{title}
diff --git a/src/components/home/CardLimits.tsx b/src/components/home/CardLimits.tsx
deleted file mode 100644
index 6c78b5a75..000000000
--- a/src/components/home/CardLimits.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-
-import { useRouter } from "expo-router";
-
-import { ChevronRight, Info } from "@tamagui/lucide-icons";
-import { XStack, YStack } from "tamagui";
-
-import { useQuery } from "@tanstack/react-query";
-
-import { marketUSDCAddress, previewerAddress } from "@exactly/common/generated/chain";
-import { useReadPreviewerExactly } from "@exactly/common/generated/hooks";
-import { borrowLimit, WAD, withdrawLimit } from "@exactly/lib";
-
-import useAccount from "../../utils/useAccount";
-import AssetLogo from "../shared/AssetLogo";
-import Text from "../shared/Text";
-
-import type { CardDetails } from "../../utils/server";
-
-export default function CardLimits({ onPress }: { onPress: () => void }) {
- const {
- t,
- i18n: { language },
- } = useTranslation();
- const { address } = useAccount();
- const router = useRouter();
- const { data: card } = useQuery({ queryKey: ["card", "details"] });
- const { data: markets } = useReadPreviewerExactly({
- address: previewerAddress,
- args: address ? [address] : undefined,
- query: { enabled: !!address },
- });
- const isCredit = card ? card.mode > 0 : false;
- return (
-
-
-
- {isCredit ? null : }
-
- {`$${(markets
- ? Number(
- isCredit ? borrowLimit(markets, marketUSDCAddress) : withdrawLimit(markets, marketUSDCAddress, WAD),
- ) / 1e6
- : 0
- ).toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}
-
-
-
-
- {t("Spending limit")}
-
-
-
-
- {
- router.push("/pay-mode");
- }}
- >
-
- {isCredit ? t("{{count}} installments", { count: card?.mode }) : t("Pay Now")}
-
-
-
-
- );
-}
diff --git a/src/components/home/CardStatus.tsx b/src/components/home/CardStatus.tsx
index 59e67a58d..a0f53152e 100644
--- a/src/components/home/CardStatus.tsx
+++ b/src/components/home/CardStatus.tsx
@@ -1,66 +1,334 @@
-import React from "react";
-import { Platform } from "react-native";
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Platform, Pressable, StyleSheet, type View as RNView } from "react-native";
+import Animated, {
+ Easing,
+ interpolate,
+ interpolateColor,
+ useAnimatedReaction,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+} from "react-native-reanimated";
+import { scheduleOnRN } from "react-native-worklets";
-import { useRouter } from "expo-router";
+import { selectionAsync } from "expo-haptics";
-import { XStack, YStack } from "tamagui";
+import { CalendarDays, ChevronRight, CreditCard, Info, Wallet, Zap } from "@tamagui/lucide-icons";
+import { useTheme, View, XStack, YStack } from "tamagui";
-import { SIGNATURE_PRODUCT_ID } from "@exactly/common/panda";
+import { useQuery } from "@tanstack/react-query";
-import CardLimits from "./CardLimits";
-import SignatureCard from "../../assets/images/card-signature.svg";
-import Card from "../../assets/images/card.svg";
+import CardBg from "../../assets/images/card-bg.svg";
+import Exa from "../../assets/images/exa.svg";
+import reportError from "../../utils/reportError";
+import Amount from "../shared/Amount";
+import Text from "../shared/Text";
-export default function CardStatus({ onInfoPress, productId }: { onInfoPress: () => void; productId: string }) {
- const router = useRouter();
+export default function CardStatus({
+ collateral,
+ creditLimit,
+ spotlightRef,
+ mode,
+ onCreditLimitInfoPress,
+ onDetailsPress,
+ onInstallmentsPress,
+ onLearnMorePress,
+ onModeChange,
+ onSpendingLimitInfoPress,
+ spendingLimit,
+}: {
+ collateral: bigint;
+ creditLimit: bigint;
+ mode: number;
+ onCreditLimitInfoPress: () => void;
+ onDetailsPress: () => void;
+ onInstallmentsPress: () => void;
+ onLearnMorePress: () => void;
+ onModeChange: (mode: number) => void;
+ onSpendingLimitInfoPress: () => void;
+ spendingLimit: bigint;
+ spotlightRef?: React.RefObject;
+}) {
+ const { t } = useTranslation();
return (
-
-
-
+
+
+ {t("Exa Card pay mode")}
+
+
+
+ {t("Learn more")}
+
+
+
+
+
+ {
+ selectionAsync().catch(reportError);
+ onDetailsPress();
+ }}
+ >
+
+
+
+
+
+
+
+
+ {t("Details")}
+
+
+
+
+
-
+
+ );
+}
+
+function PayModeToggle({
+ spotlightRef,
+ mode,
+ onInstallmentsPress,
+ onModeChange,
+}: {
+ mode: number;
+ onInstallmentsPress: () => void;
+ onModeChange: (mode: number) => void;
+ spotlightRef?: React.RefObject;
+}) {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const { data: lastInstallments } = useQuery({ queryKey: ["settings", "installments"] });
+ const isDebit = mode === 0;
+ const [width, setWidth] = useState(0);
+ const neutral = theme.uiNeutralSecondary.val;
+ const progress = useSharedValue(isDebit ? 0 : 1);
+ const [nowColor, setNowColor] = useState(isDebit ? theme.cardDebitText.val : neutral);
+ const [laterColor, setLaterColor] = useState(isDebit ? neutral : theme.cardCreditText.val);
+ const [borderColor, setBorderColor] = useState(
+ isDebit ? theme.cardDebitInteractive.val : theme.cardCreditInteractive.val,
+ );
+ useEffect(() => {
+ progress.value = withTiming(isDebit ? 0 : 1, { duration: 512, easing: Easing.bezier(0.7, 0, 0.3, 1) });
+ }, [isDebit, progress]);
+ /* istanbul ignore next */
+ const pillStyle = useAnimatedStyle(() => ({
+ backgroundColor: interpolateColor(
+ progress.value,
+ [0, 1],
+ [theme.cardDebitInteractive.val, theme.cardCreditInteractive.val],
+ ),
+ transform: [{ translateX: interpolate(progress.value, [0, 1], [0, width / 2]) }],
+ }));
+ /* istanbul ignore next */
+ useAnimatedReaction(
+ () => progress.value,
+ (value) => {
+ scheduleOnRN(setNowColor, interpolateColor(value, [0, 1], [theme.cardDebitText.val, neutral]));
+ scheduleOnRN(setLaterColor, interpolateColor(value, [0, 1], [neutral, theme.cardCreditText.val]));
+ scheduleOnRN(
+ setBorderColor,
+ interpolateColor(value, [0, 1], [theme.cardDebitInteractive.val, theme.cardCreditInteractive.val]),
+ );
+ },
+ [
+ theme.cardDebitText.val,
+ theme.cardCreditText.val,
+ theme.cardDebitInteractive.val,
+ theme.cardCreditInteractive.val,
+ theme.cardDebitBorder.val,
+ theme.cardCreditBorder.val,
+ neutral,
+ ],
+ );
+ return (
+ {
+ setWidth(event.nativeEvent.layout.width);
+ }}
+ >
+
+ {
- router.push("/card");
+ if (isDebit) return;
+ selectionAsync().catch(reportError);
+ onModeChange(0);
}}
>
- {productId === SIGNATURE_PRODUCT_ID ? (
-
- ) : (
-
- )}
-
+
+
+
+ {t("Now")}
+
+
+
+ {
+ selectionAsync().catch(reportError);
+ if (mode > 0) onInstallmentsPress();
+ else onModeChange(lastInstallments ?? 1);
+ }}
+ >
+
+
+
+ {t("Later in {{count}}", { count: mode > 0 ? mode : (lastInstallments ?? 1) })}
+
+
+
);
}
+
+function LimitPaginator({
+ collateral,
+ creditLimit,
+ mode,
+ onCreditLimitInfoPress,
+ onSpendingLimitInfoPress,
+ spendingLimit,
+}: {
+ collateral: bigint;
+ creditLimit: bigint;
+ mode: number;
+ onCreditLimitInfoPress: () => void;
+ onSpendingLimitInfoPress: () => void;
+ spendingLimit: bigint;
+}) {
+ const {
+ t,
+ i18n: { language },
+ } = useTranslation();
+ const [width, setWidth] = useState(0);
+ return (
+ setWidth(event.nativeEvent.layout.width)}
+ >
+ 0 ? -width : 0} animation="default" animateOnly={["transform"]}>
+
+
+
+
+ {t("Spending limit")}
+
+
+
+
+
+
+
+ $
+
+
+ {(Number(spendingLimit) / 1e6).toLocaleString(language, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })}
+
+
+
+
+
+
+
+
+ {t("Credit limit")}
+
+
+
+
+
+
+ {t("Collateral {{value}}", {
+ value: `$${(Number(collateral) / 1e18).toLocaleString(language, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
+ })}
+
+
+
+
+ $
+
+
+ {(Number(creditLimit) / 1e6).toLocaleString(language, {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ })}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ details: { position: "absolute", top: 8, right: 8 },
+ exa: { position: "absolute", top: 12, left: 16 },
+ learnMore: { flexDirection: "row", alignItems: "center", gap: 4 },
+ pill: { position: "absolute", top: 2, bottom: 2, left: 2, right: 2, borderRadius: 9999 },
+ segment: { flex: 1, justifyContent: "center", alignItems: "center" },
+});
diff --git a/src/components/home/CreditLimitSheet.tsx b/src/components/home/CreditLimitSheet.tsx
new file mode 100644
index 000000000..56d5891b3
--- /dev/null
+++ b/src/components/home/CreditLimitSheet.tsx
@@ -0,0 +1,70 @@
+import React from "react";
+import { Trans, useTranslation } from "react-i18next";
+import { Pressable } from "react-native";
+
+import { ExternalLink, X } from "@tamagui/lucide-icons";
+import { XStack, YStack } from "tamagui";
+
+import { presentArticle } from "../../utils/intercom";
+import reportError from "../../utils/reportError";
+import Button from "../shared/Button";
+import ModalSheet from "../shared/ModalSheet";
+import Text from "../shared/Text";
+
+export default function CreditLimitSheet({ onClose, open }: { onClose: () => void; open: boolean }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+
+
+ {t("Credit limit")}
+
+
+
+
+
+
+
+ }}
+ />
+
+
+ {t("It's based on the value of your collateral assets and updates as their value changes.")}
+
+
+
+
+
+ {
+ presentArticle("9922633").catch(reportError);
+ }}
+ contained
+ main
+ spaced
+ fullwidth
+ iconAfter={}
+ >
+ {t("Learn more")}
+
+
+
+ {t("Close")}
+
+
+
+
+
+ );
+}
diff --git a/src/components/home/GettingStarted.tsx b/src/components/home/GettingStarted.tsx
index 61b6fae52..577f817d0 100644
--- a/src/components/home/GettingStarted.tsx
+++ b/src/components/home/GettingStarted.tsx
@@ -40,7 +40,7 @@ export default function GettingStarted({ isDeployed, hasKYC }: { hasKYC: boolean
borderRadius="$r3"
opacity={1}
transform={[{ translateY: 0 }]}
- animation="moderate"
+ animation="default"
animateOnly={["opacity", "transform"]}
enterStyle={{ opacity: 0, transform: [{ translateY: -20 }] }}
exitStyle={{ opacity: 0, transform: [{ translateY: -20 }] }}
@@ -50,7 +50,7 @@ export default function GettingStarted({ isDeployed, hasKYC }: { hasKYC: boolean
{t("Getting Started")}
-
+
{
diff --git a/src/components/home/Home.tsx b/src/components/home/Home.tsx
index 181c18031..8b59bfaca 100644
--- a/src/components/home/Home.tsx
+++ b/src/components/home/Home.tsx
@@ -1,40 +1,51 @@
-import React, { useRef, useState } from "react";
+import React, { useCallback, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
-import { RefreshControl } from "react-native";
+import { RefreshControl, type View as RNView } from "react-native";
-import { useRouter } from "expo-router";
+import { useFocusEffect, useRouter } from "expo-router";
import { AnimatePresence, ScrollView, YStack } from "tamagui";
import { TimeToFullDisplay } from "@sentry/react-native";
-import { useQuery } from "@tanstack/react-query";
+import { useMutation, useQuery } from "@tanstack/react-query";
import { useBytecode } from "wagmi";
import accountInit from "@exactly/common/accountInit";
-import { exaPluginAddress, exaPreviewerAddress, previewerAddress } from "@exactly/common/generated/chain";
+import {
+ exaPluginAddress,
+ exaPreviewerAddress,
+ marketUSDCAddress,
+ previewerAddress,
+} from "@exactly/common/generated/chain";
import {
useReadExaPreviewerPendingProposals,
useReadPreviewerExactly,
useReadUpgradeableModularAccountGetInstalledPlugins,
} from "@exactly/common/generated/hooks";
import { PLATINUM_PRODUCT_ID } from "@exactly/common/panda";
-import { healthFactor, WAD } from "@exactly/lib";
+import { borrowLimit, healthFactor, WAD, withdrawLimit } from "@exactly/lib";
import CardUpgradeSheet from "./card-upgrade/CardUpgradeSheet";
import CardStatus from "./CardStatus";
+import CreditLimitSheet from "./CreditLimitSheet";
import GettingStarted from "./GettingStarted";
import HomeActions from "./HomeActions";
import HomeDisclaimer from "./HomeDisclaimer";
+import InstallmentsSheet from "./InstallmentsSheet";
+import InstallmentsSpotlight from "./InstallmentsSpotlight";
+import PayModeSheet from "./PayModeSheet";
import PortfolioSummary from "./PortfolioSummary";
-import SpendingLimitsSheet from "./SpendingLimitsSheet";
+import SpendingLimitSheet from "./SpendingLimitSheet";
import VisaSignatureBanner from "./VisaSignatureBanner";
import VisaSignatureModal from "./VisaSignatureSheet";
import queryClient from "../../utils/queryClient";
import reportError from "../../utils/reportError";
+import { setCardMode } from "../../utils/server";
import useAccount from "../../utils/useAccount";
import usePortfolio from "../../utils/usePortfolio";
import useTabPress from "../../utils/useTabPress";
import BenefitsSection from "../benefits/BenefitsSection";
+import ManualRepaymentSheet from "../pay-mode/ManualRepaymentSheet";
import OverduePayments from "../pay-mode/OverduePayments";
import PaymentSheet from "../pay-mode/PaymentSheet";
import UpcomingPayments from "../pay-mode/UpcomingPayments";
@@ -57,8 +68,24 @@ export default function Home() {
t,
i18n: { language },
} = useTranslation();
- const [spendingLimitsInfoSheetOpen, setSpendingLimitsInfoSheetOpen] = useState(false);
+ const [creditLimitSheetOpen, setCreditLimitSheetOpen] = useState(false);
+ const [installmentsSheetOpen, setInstallmentsSheetOpen] = useState(false);
+ const [payModeSheetOpen, setPayModeSheetOpen] = useState(false);
+ const [spendingLimitSheetOpen, setSpendingLimitSheetOpen] = useState(false);
const [visaSignatureModalOpen, setVisaSignatureModalOpen] = useState(false);
+ const [manualRepaymentSheetOpen, setManualRepaymentSheetOpen] = useState(false);
+ const pendingModeRef = useRef(0);
+
+ const [focused, setFocused] = useState(false);
+ useFocusEffect(
+ useCallback(() => {
+ setFocused(true);
+ return () => {
+ setFocused(false);
+ };
+ }, []),
+ );
+ const spotlightRef = useRef(null);
const { address: account } = useAccount();
const { data: credential } = useQuery({ queryKey: ["credential"] });
@@ -73,8 +100,9 @@ export default function Home() {
query: { enabled: !!account && !!credential },
});
const {
- portfolio: { balanceUSD, depositMarkets },
+ portfolio: { balanceUSD },
averageRate,
+ assets,
totalBalanceUSD,
} = usePortfolio();
@@ -111,8 +139,51 @@ export default function Home() {
kycStatus && "code" in kycStatus && (kycStatus.code === "ok" || kycStatus.code === "legacy kyc"),
);
const { data: card } = useQuery({ queryKey: ["card", "details"], enabled: !!account && !!bytecode });
+ const { data: spotlightShown } = useQuery({ queryKey: ["settings", "installments-spotlight"] });
+ const { mutateAsync: mutateMode } = useMutation({
+ mutationKey: ["card", "mode"],
+ mutationFn: setCardMode,
+ onMutate: async (newMode) => {
+ await queryClient.cancelQueries({ queryKey: ["card", "details"] });
+ const previous = queryClient.getQueryData(["card", "details"]);
+ queryClient.setQueryData(["card", "details"], (old: CardDetails) => ({ ...old, mode: newMode }));
+ return { previous };
+ },
+ onError: (error, _, context) => {
+ if (context?.previous) queryClient.setQueryData(["card", "details"], context.previous);
+ reportError(error);
+ },
+ onSettled: async (data) => {
+ await queryClient.invalidateQueries({ queryKey: ["card", "details"] });
+ if (data && "mode" in data && data.mode > 0) queryClient.setQueryData(["settings", "installments"], data.mode);
+ },
+ });
+
+ const { data: manualRepaymentAcknowledged } = useQuery({ queryKey: ["manual-repayment-acknowledged"] });
+ function handleModeChange(mode: number) {
+ if (mode === 0 || manualRepaymentAcknowledged) {
+ mutateMode(mode).catch(reportError);
+ return;
+ }
+ pendingModeRef.current = mode;
+ setManualRepaymentSheetOpen(true);
+ }
+
+ const collateralUSD = useMemo(
+ () =>
+ markets?.reduce(
+ (total, market) =>
+ total +
+ (market.floatingDepositAssets > 0n
+ ? (market.floatingDepositAssets * market.usdPrice) / 10n ** BigInt(market.decimals)
+ : 0n),
+ 0n,
+ ) ?? 0n,
+ [markets],
+ );
const scrollRef = useRef(null);
+ const scrollOffsetRef = useRef(0);
const refresh = () => {
queryClient.invalidateQueries({ queryKey: ["activity"], exact: true }).catch(reportError);
queryClient.invalidateQueries({ queryKey: ["kyc", "status"], exact: true }).catch(reportError);
@@ -137,10 +208,14 @@ export default function Home() {
backgroundColor="transparent"
contentContainerStyle={{ backgroundColor: "$backgroundMild" }}
showsVerticalScrollIndicator={false}
+ scrollEventThrottle={16}
+ onScroll={(event) => {
+ scrollOffsetRef.current = event.nativeEvent.contentOffset.y;
+ }}
refreshControl={}
>
-
+
{markets && healthFactor(markets) < HEALTH_FACTOR_THRESHOLD && }
{(showKYCMigration || showPluginOutdated) && (
@@ -161,40 +236,61 @@ export default function Home() {
}}
/>
)}
-
+
-
-
- {card && (
- {
- setSpendingLimitsInfoSheetOpen(true);
+ {(card ?? (isKYCFetched && (!isKYCApproved || !bytecode))) && (
+
+
+ {card && (
+ {
+ setCreditLimitSheetOpen(true);
+ }}
+ onDetailsPress={() => {
+ router.push("/card");
+ }}
+ onInstallmentsPress={() => {
+ setInstallmentsSheetOpen(true);
+ }}
+ onLearnMorePress={() => {
+ setPayModeSheetOpen(true);
+ }}
+ onModeChange={handleModeChange}
+ onSpendingLimitInfoPress={() => {
+ setSpendingLimitSheetOpen(true);
+ }}
+ spendingLimit={markets ? withdrawLimit(markets, marketUSDCAddress, WAD) : 0n}
+ />
+ )}
+
+ {card?.productId === PLATINUM_PRODUCT_ID && (
+ {
+ setVisaSignatureModalOpen(true);
}}
- productId={card.productId}
/>
)}
-
- {card?.productId === PLATINUM_PRODUCT_ID && (
- {
- setVisaSignatureModalOpen(true);
- }}
- />
- )}
-
- {isKYCFetched && (!isKYCApproved || !bytecode) && (
-
- )}
-
- {isKYCFetched && isKYCApproved && }
+
+ {isKYCFetched && (!isKYCApproved || !bytecode) && (
+
+ )}
+
+
+ )}
+ {isKYCFetched && isKYCApproved && }
+
router.setParams({ maturity: String(m) })} />
router.setParams({ maturity: String(m) })} />
@@ -209,10 +305,42 @@ export default function Home() {
queryClient.resetQueries({ queryKey: ["card-upgrade"] }).catch(reportError);
}}
/>
- {
+ setInstallmentsSheetOpen(false);
+ }}
+ onModeChange={handleModeChange}
+ />
+ {
+ setCreditLimitSheetOpen(false);
+ }}
+ />
+ {
+ setManualRepaymentSheetOpen(false);
+ }}
+ onActionPress={() => {
+ queryClient.setQueryData(["manual-repayment-acknowledged"], true);
+ mutateMode(pendingModeRef.current).catch(reportError);
+ setManualRepaymentSheetOpen(false);
+ }}
+ penaltyRate={markets?.find(({ market }) => market === marketUSDCAddress)?.penaltyRate}
+ />
+ {
+ setPayModeSheetOpen(false);
+ }}
+ />
+ {
- setSpendingLimitsInfoSheetOpen(false);
+ setSpendingLimitSheetOpen(false);
}}
/>
+ {card && card.mode > 0 && !spotlightShown && focused && (
+ {
+ queryClient.setQueryData(["settings", "installments-spotlight"], true);
+ }}
+ onPress={() => {
+ setInstallmentsSheetOpen(true);
+ }}
+ />
+ )}
diff --git a/src/components/home/HomeActions.tsx b/src/components/home/HomeActions.tsx
index e8ceff87b..e5f8d9085 100644
--- a/src/components/home/HomeActions.tsx
+++ b/src/components/home/HomeActions.tsx
@@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
import { useRouter } from "expo-router";
-import { ArrowDownToLine, ArrowUpRight } from "@tamagui/lucide-icons";
-import { XStack, YStack } from "tamagui";
+import { ArrowDownToLine, ArrowUpRight, Repeat } from "@tamagui/lucide-icons";
+import { XStack } from "tamagui";
import { useQuery } from "@tanstack/react-query";
import { useBytecode, useReadContract } from "wagmi";
@@ -32,8 +32,9 @@ export default function HomeActions() {
() => [
{ key: "deposit", title: t("Add funds"), Icon: ArrowDownToLine },
{ key: "send", title: t("Send"), Icon: ArrowUpRight },
+ ...(bytecode ? [{ key: "swap" as const, title: t("Swap"), Icon: Repeat }] : []),
],
- [t],
+ [bytecode, t],
);
const { data: installedPlugins } = useReadUpgradeableModularAccountGetInstalledPlugins({
@@ -79,33 +80,44 @@ export default function HomeActions() {
}
};
return (
-
+
{actions.map(({ key, title, Icon }) => {
+ const disabled = key !== "deposit" && !bytecode;
+ const handlePress = disabled
+ ? undefined
+ : () => {
+ switch (key) {
+ case "deposit":
+ router.push("/add-funds");
+ break;
+ case "send":
+ handleSend().catch(reportError);
+ break;
+ case "swap":
+ router.push("/swaps");
+ break;
+ }
+ };
return (
-
- {
- switch (key) {
- case "deposit":
- router.push("/add-funds");
- break;
- case "send":
- handleSend().catch(reportError);
- break;
- }
- }}
- width="100%"
- >
- {title}
+
+
-
+ {title}
+
);
})}
diff --git a/src/components/home/InstallmentsSheet.tsx b/src/components/home/InstallmentsSheet.tsx
new file mode 100644
index 000000000..eb708ba94
--- /dev/null
+++ b/src/components/home/InstallmentsSheet.tsx
@@ -0,0 +1,181 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Pressable } from "react-native";
+import Carousel, { type ICarouselInstance } from "react-native-reanimated-carousel";
+
+import { selectionAsync } from "expo-haptics";
+
+import { Check, X } from "@tamagui/lucide-icons";
+import { View, XStack, YStack } from "tamagui";
+
+import MAX_INSTALLMENTS from "@exactly/common/MAX_INSTALLMENTS";
+
+import reportError from "../../utils/reportError";
+import useInstallmentRates from "../../utils/useInstallmentRates";
+import Button from "../shared/Button";
+import ModalSheet from "../shared/ModalSheet";
+import SafeView from "../shared/SafeView";
+import Skeleton from "../shared/Skeleton";
+import Text from "../shared/Text";
+
+export default function InstallmentsSheet({
+ mode,
+ onClose,
+ onModeChange,
+ open,
+}: {
+ mode: number;
+ onClose: () => void;
+ onModeChange: (mode: number) => void;
+ open: boolean;
+}) {
+ const {
+ t,
+ i18n: { language },
+ } = useTranslation();
+ const [selected, setSelected] = useState(mode > 0 ? mode : 1);
+ useEffect(() => {
+ if (open) setSelected(mode > 0 ? mode : 1); // eslint-disable-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
+ }, [mode, open]);
+ const carouselRef = useRef(null);
+ const rates = useInstallmentRates();
+ const [width, setWidth] = useState(0);
+ const handleLayout = useCallback((event: { nativeEvent: { layout: { width: number } } }) => {
+ setWidth(event.nativeEvent.layout.width);
+ }, []);
+ const perPage = Math.max(1, Math.floor((width - 2 * PADDING + GAP) / (CARD_SIZE + GAP)));
+ const pageWidth = perPage * (CARD_SIZE + GAP);
+ const pages: number[][] = [];
+ for (let index = 0; index < INSTALLMENTS.length; index += perPage) {
+ pages.push(INSTALLMENTS.slice(index, index + perPage));
+ }
+ return (
+
+
+
+
+
+
+
+ {t("Set installments")}
+
+
+
+
+
+
+ {t(
+ "Choose how many installments to use for future card purchases. You can always change this before each purchase.",
+ )}
+
+
+
+ {width === 0 ? undefined : (
+ (
+
+ {page.map((installment) => {
+ const isSelected = selected === installment;
+ return (
+ {
+ setSelected(installment);
+ selectionAsync().catch(reportError);
+ const target = Math.floor((installment - 1) / perPage);
+ if (target !== carouselRef.current?.getCurrentIndex()) {
+ requestAnimationFrame(() =>
+ carouselRef.current?.scrollTo({ index: target, animated: true }),
+ );
+ }
+ }}
+ >
+
+ {installment}
+
+ {rates ? (
+ rates.installments[installment - 1]?.payments === undefined ? (
+
+ {t("N/A")}
+
+ ) : (
+
+ {t("{{apr}} APR", {
+ apr: (Number(rates.installments[installment - 1]?.rate) / 1e18).toLocaleString(
+ language,
+ { style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2 },
+ ),
+ })}
+
+ )
+ ) : (
+
+ )}
+
+ );
+ })}
+
+ )}
+ />
+ )}
+
+
+
+ {
+ if (selected !== mode) onModeChange(selected);
+ onClose();
+ }}
+ contained
+ main
+ spaced
+ fullwidth
+ iconAfter={}
+ >
+ {t("Set Pay Later in {{count}}", { count: selected })}
+
+
+
+
+
+ );
+}
+
+const CARD_SIZE = 104;
+const GAP = 8;
+const PADDING = 24;
+const INSTALLMENTS = Array.from({ length: MAX_INSTALLMENTS }, (_, index) => index + 1);
diff --git a/src/components/home/InstallmentsSpotlight.tsx b/src/components/home/InstallmentsSpotlight.tsx
new file mode 100644
index 000000000..15236d3ae
--- /dev/null
+++ b/src/components/home/InstallmentsSpotlight.tsx
@@ -0,0 +1,150 @@
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ Modal,
+ Platform,
+ Pressable,
+ StatusBar,
+ StyleSheet,
+ useWindowDimensions,
+ type View as RNView,
+} from "react-native";
+import SVG, { Defs, Mask, Rect } from "react-native-svg";
+
+import { Theme, View, YStack, type ScrollView } from "tamagui";
+
+import Text from "../shared/Text";
+
+export default function InstallmentsSpotlight({
+ onDismiss,
+ onPress,
+ scrollOffset,
+ scrollRef,
+ targetRef,
+}: {
+ onDismiss: () => void;
+ onPress: () => void;
+ scrollOffset: React.RefObject;
+ scrollRef: React.RefObject;
+ targetRef: React.RefObject;
+}) {
+ const { t } = useTranslation();
+ const { width: screenWidth, height: windowHeight } = useWindowDimensions();
+ const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
+ const screenHeight = windowHeight + statusBarHeight;
+ const [target, setTarget] = useState<{ height: number; width: number; x: number; y: number }>();
+ useEffect(() => {
+ let scrolled = false;
+ let attempts = 0;
+ const id = setInterval(() => {
+ if (++attempts > 10) {
+ clearInterval(id);
+ return;
+ }
+ targetRef.current?.measureInWindow((x, y, width, height) => {
+ if (width > 0 && height > 0 && y >= 0 && y + height <= screenHeight) {
+ clearInterval(id);
+ setTarget({ x, y, width, height });
+ return;
+ }
+ if (!scrolled) {
+ scrolled = true;
+ if (width > 0 && height > 0) {
+ const contentY = scrollOffset.current + y;
+ scrollRef.current?.scrollTo({ y: Math.max(0, contentY - screenHeight / 3), animated: true });
+ } else {
+ scrollRef.current?.scrollTo({ y: 0, animated: true });
+ }
+ }
+ });
+ }, 500);
+ return () => clearInterval(id);
+ }, [screenHeight, scrollOffset, scrollRef, targetRef]);
+ if (!target) return null;
+ const cutout = {
+ x: target.x - 8,
+ y: target.y - 8 + statusBarHeight,
+ width: target.width + 16,
+ height: target.height + 16,
+ };
+ const cutoutRadius = cutout.height / 2;
+ const tooltipTop = cutout.y + cutout.height + 12;
+ const tooltipLeft = Math.max(16, Math.min(cutout.x + cutout.width / 2 - 100, screenWidth - 216));
+ const arrowLeft = cutout.x + cutout.width / 2 - tooltipLeft - 6;
+ return (
+
+
+
+
+ {
+ onPress();
+ onDismiss();
+ }}
+ />
+
+ {
+ onPress();
+ onDismiss();
+ }}
+ >
+
+
+ {t("Tap here to change the number of installments")}
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({ cutoutPress: { position: "absolute" } });
diff --git a/src/components/home/PayModeSheet.tsx b/src/components/home/PayModeSheet.tsx
new file mode 100644
index 000000000..d6df27bae
--- /dev/null
+++ b/src/components/home/PayModeSheet.tsx
@@ -0,0 +1,127 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Pressable } from "react-native";
+
+import { CalendarDays, ExternalLink, X, Zap } from "@tamagui/lucide-icons";
+import { XStack, YStack } from "tamagui";
+
+import MAX_INSTALLMENTS from "@exactly/common/MAX_INSTALLMENTS";
+
+import { presentArticle } from "../../utils/intercom";
+import reportError from "../../utils/reportError";
+import Button from "../shared/Button";
+import ModalSheet from "../shared/ModalSheet";
+import Text from "../shared/Text";
+
+export default function PayModeSheet({ onClose, open }: { onClose: () => void; open: boolean }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+
+
+ {t("Exa Card pay mode")}
+
+
+
+
+
+
+ {t("Change the pay mode before each purchase and pay how you want.")}
+
+
+
+
+
+
+
+ {t("Now")}
+
+
+
+ {t("Pay instantly using your available USDC.")}
+
+
+
+
+
+
+ {t("Later")}
+
+
+
+ {t(
+ "Pay without selling your crypto. Use it as collateral to unlock a credit limit and split purchases into up to {{max}} installments.",
+ { max: MAX_INSTALLMENTS },
+ )}
+
+
+
+
+
+ {
+ presentArticle("9465994").catch(reportError);
+ }}
+ contained
+ main
+ spaced
+ fullwidth
+ iconAfter={}
+ >
+ {t("Learn more")}
+
+
+
+ {t("Close")}
+
+
+
+
+
+ );
+}
diff --git a/src/components/home/Portfolio.tsx b/src/components/home/Portfolio.tsx
index 6f44a1e50..acf3c7153 100644
--- a/src/components/home/Portfolio.tsx
+++ b/src/components/home/Portfolio.tsx
@@ -40,8 +40,16 @@ export default function Portfolio() {
return (
-
+
{
if (router.canGoBack()) {
router.back();
@@ -86,7 +94,6 @@ export default function Portfolio() {
{
- router.push("/portfolio");
- }}
- >
-
-
- {t("Your portfolio")}
+ router.push("/portfolio")}>
+
+
+ {t("Portfolio")}
-
-
-
- {`$${(Number(totalBalanceUSD) / 1e18).toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}
-
- {country === "US" && processingBalance ? (
- {
- router.push("/activity");
- }}
- gap="$s2"
- alignItems="center"
- >
-
- {t("Processing balance {{amount}}", {
- amount: `$${processingBalance.toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
- })}
+
+
+ {t("Manage portfolio")}
-
+
- ) : balanceUSD > 0n ? (
-
- ) : null}
+
+
+
+
+ {country === "US" && processingBalance ? (
+ {
+ router.push("/activity");
+ }}
+ gap="$s2"
+ alignItems="center"
+ >
+
+ {t("Processing balance {{amount}}", {
+ amount: `$${processingBalance.toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 })}`,
+ })}
+
+
+
+ ) : balanceUSD > 0n ? (
+
+ {t("{{rate}} APR", {
+ rate: (Number(averageRate) / 1e18).toLocaleString(language, {
+ style: "percent",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 2,
+ }),
+ })}
+
+ ) : null}
+
+ {visible.length > 0 && (
+
+ {visible.map((asset, index) => (
+ 0 ? -12 : 0}
+ zIndex={visible.length - index}
+ >
+
+
+ ))}
+ {extra > 0 && (
+
+
+ +{extra}
+
+
+ )}
+
+ )}
+
);
}
diff --git a/src/components/home/SpendingLimitSheet.tsx b/src/components/home/SpendingLimitSheet.tsx
new file mode 100644
index 000000000..a9fcbdef1
--- /dev/null
+++ b/src/components/home/SpendingLimitSheet.tsx
@@ -0,0 +1,70 @@
+import React from "react";
+import { Trans, useTranslation } from "react-i18next";
+import { Pressable } from "react-native";
+
+import { ExternalLink, X } from "@tamagui/lucide-icons";
+import { XStack, YStack } from "tamagui";
+
+import { presentArticle } from "../../utils/intercom";
+import reportError from "../../utils/reportError";
+import Button from "../shared/Button";
+import ModalSheet from "../shared/ModalSheet";
+import Text from "../shared/Text";
+
+export default function SpendingLimitSheet({ onClose, open }: { onClose: () => void; open: boolean }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+
+
+ {t("Spending limit")}
+
+
+
+
+
+
+
+ }}
+ />
+
+
+ {t("It's based on the USDC available in your balance.")}
+
+
+
+
+
+ {
+ presentArticle("9922633").catch(reportError);
+ }}
+ contained
+ main
+ spaced
+ fullwidth
+ iconAfter={}
+ >
+ {t("Learn more")}
+
+
+
+ {t("Close")}
+
+
+
+
+
+ );
+}
diff --git a/src/components/home/SpendingLimitsSheet.tsx b/src/components/home/SpendingLimitsSheet.tsx
deleted file mode 100644
index 13348b2c6..000000000
--- a/src/components/home/SpendingLimitsSheet.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import React from "react";
-import { useTranslation } from "react-i18next";
-import { Pressable } from "react-native";
-
-import { X } from "@tamagui/lucide-icons";
-import { ScrollView, XStack, YStack } from "tamagui";
-
-import { presentArticle } from "../../utils/intercom";
-import reportError from "../../utils/reportError";
-import Button from "../shared/Button";
-import ModalSheet from "../shared/ModalSheet";
-import SafeView from "../shared/SafeView";
-import Text from "../shared/Text";
-import View from "../shared/View";
-
-export default function SpendingLimitsSheet({ open, onClose }: { onClose: () => void; open: boolean }) {
- const { t } = useTranslation();
- return (
-
-
-
-
-
-
- {t("Spending limit")}
-
-
- {t("Your spending limit is the maximum amount you can spend on your Exa Card.")}
-
-
-
-
-
-
- {t("WHEN")}
-
-
-
- {t("PAY NOW")}
-
-
-
- {t("IS ENABLED")}
-
-
- {t("Only your USDC balance counts toward your spending limit.")}
-
-
-
-
- {t("WHEN")}
-
-
-
- {t("INSTALLMENTS")}
-
-
-
- {t("IS ENABLED")}
-
-
- {t("All supported assets count toward your spending limit.")}
-
-
-
- }
- >
- {t("Close")}
-
- {
- presentArticle("9922633").catch(reportError);
- }}
- >
-
- {t("Learn more about your spending limit")}
-
-
-
-
-
-
-
- );
-}
diff --git a/src/components/loans/Amount.tsx b/src/components/loans/Amount.tsx
index 05d5f7da8..6fde66ff9 100644
--- a/src/components/loans/Amount.tsx
+++ b/src/components/loans/Amount.tsx
@@ -60,7 +60,14 @@ export default function Amount() {
}, []);
return (
-
+
{
queryClient.setQueryData(["loan"], (old) => ({ ...old, amount: undefined }));
diff --git a/src/components/loans/Asset.tsx b/src/components/loans/Asset.tsx
index ae54952ec..82cd16ef6 100644
--- a/src/components/loans/Asset.tsx
+++ b/src/components/loans/Asset.tsx
@@ -34,7 +34,14 @@ export default function Asset() {
});
return (
-
+
{
if (router.canGoBack()) {
@@ -93,7 +100,7 @@ export default function Asset() {
height={16}
backgroundColor={selected ? "$interactiveBaseBrandDefault" : "$uiNeutralSecondary"}
borderRadius="$r_0"
- padding={4}
+ padding="$s2"
alignItems="center"
justifyContent="center"
>
diff --git a/src/components/loans/Installments.tsx b/src/components/loans/Installments.tsx
index b75bf6edc..65a2a6464 100644
--- a/src/components/loans/Installments.tsx
+++ b/src/components/loans/Installments.tsx
@@ -38,7 +38,14 @@ export default function Installments() {
}, []);
return (
-
+
{
if (router.canGoBack()) {
diff --git a/src/components/loans/Maturity.tsx b/src/components/loans/Maturity.tsx
index c7d88bcb4..c7bcc80bd 100644
--- a/src/components/loans/Maturity.tsx
+++ b/src/components/loans/Maturity.tsx
@@ -46,7 +46,14 @@ export default function Maturity() {
}, []);
return (
-
+
{
queryClient.setQueryData(["loan"], (old) => ({ ...old, maturity: undefined }));
diff --git a/src/components/loans/Receiver.tsx b/src/components/loans/Receiver.tsx
index 9f0d12354..4b32f6084 100644
--- a/src/components/loans/Receiver.tsx
+++ b/src/components/loans/Receiver.tsx
@@ -70,7 +70,14 @@ export default function Receiver() {
}, []);
return (
-
+
{
queryClient.setQueryData(["loan"], (old) => ({ ...old, receiver: undefined }));
@@ -128,7 +135,7 @@ export default function Receiver() {
>
{receiverType === "internal" && }
-
+
{t("Your Exa account")}
{t("Deposit {{symbol}} into your Exa App wallet", { symbol })}
@@ -159,7 +166,7 @@ export default function Receiver() {
>
{receiverType === "external" && }
-
+
{t("External address on {{chain}}", { chain: chain.name })}
{t("Deposit {{symbol}} directly to an external wallet", { symbol })}
diff --git a/src/components/loans/Review.tsx b/src/components/loans/Review.tsx
index 8f49a47ff..40332a770 100644
--- a/src/components/loans/Review.tsx
+++ b/src/components/loans/Review.tsx
@@ -199,7 +199,7 @@ export default function Review() {
-
+
{
@@ -593,7 +593,7 @@ export default function Pay() {
{t("Subtotal")}
-
+
{isRouteFetching ? (
@@ -670,7 +670,7 @@ export default function Pay() {
{t("You will pay")}
-
+
{isRouteFetching ? (
diff --git a/src/components/pay-mode/PaySelector.tsx b/src/components/pay-mode/PaySelector.tsx
index 1c974f61f..9f47abfd3 100644
--- a/src/components/pay-mode/PaySelector.tsx
+++ b/src/components/pay-mode/PaySelector.tsx
@@ -115,7 +115,7 @@ export default function PaySelector() {
<>
-
+
{t("Pay Mode")}
@@ -191,7 +191,7 @@ export default function PaySelector() {
-
+
-
+
{Array.from({ length: MAX_INSTALLMENTS }, (_, index) => index + 1).map((installment) => (
)}
-
-
+
+
{t("Repay")}
diff --git a/src/components/pay-mode/PaymentsActions.tsx b/src/components/pay-mode/PaymentsActions.tsx
index 03d5b01eb..715e12ad3 100644
--- a/src/components/pay-mode/PaymentsActions.tsx
+++ b/src/components/pay-mode/PaymentsActions.tsx
@@ -23,10 +23,10 @@ const StyledAction = styled(View, {
export default function PaymentsActions() {
const { t } = useTranslation();
return (
-
+
-
+
{t("Auto-pay")}
@@ -37,7 +37,7 @@ export default function PaymentsActions() {
-
+
{t("Collateral")}
diff --git a/src/components/roll-debt/RollDebt.tsx b/src/components/roll-debt/RollDebt.tsx
index d8cb7290a..9ec33355a 100644
--- a/src/components/roll-debt/RollDebt.tsx
+++ b/src/components/roll-debt/RollDebt.tsx
@@ -70,7 +70,7 @@ export default function Pay() {
return (
-
+
-
-
+
+
{
@@ -310,7 +310,7 @@ export default function Amount() {
{t("To:")}
-
+
{shortenHex(receiver)}
diff --git a/src/components/send-funds/Asset.tsx b/src/components/send-funds/Asset.tsx
index 4cb12577a..94a9864cc 100644
--- a/src/components/send-funds/Asset.tsx
+++ b/src/components/send-funds/Asset.tsx
@@ -39,8 +39,8 @@ export default function AssetSelection() {
return (
-
-
+
+
{
@@ -75,7 +75,7 @@ export default function AssetSelection() {
{t("To:")}
-
+
{shortenHex(receiver)}
diff --git a/src/components/send-funds/Contact.tsx b/src/components/send-funds/Contact.tsx
index d0a6f6afe..39d4f5b42 100644
--- a/src/components/send-funds/Contact.tsx
+++ b/src/components/send-funds/Contact.tsx
@@ -48,7 +48,7 @@ export default function Contact({
) : null}
-
+
{shortenHex(address)}
diff --git a/src/components/send-funds/Receiver.tsx b/src/components/send-funds/Receiver.tsx
index ea1861138..76e0f6a69 100644
--- a/src/components/send-funds/Receiver.tsx
+++ b/src/components/send-funds/Receiver.tsx
@@ -54,8 +54,8 @@ export default function ReceiverSelection() {
return (
-
-
+
+
-
+
{shortenHex(receiver ?? "", 3, 5)}
{receiver && isFirstSend && (
diff --git a/src/components/shared/Amount.tsx b/src/components/shared/Amount.tsx
new file mode 100644
index 000000000..82384c6c6
--- /dev/null
+++ b/src/components/shared/Amount.tsx
@@ -0,0 +1,106 @@
+import React, { type ComponentPropsWithoutRef } from "react";
+import { useTranslation } from "react-i18next";
+import { Platform } from "react-native";
+
+import { XStack } from "tamagui";
+
+import { useQuery } from "@tanstack/react-query";
+
+import Text from "./Text";
+
+export default function Amount({
+ amount,
+ children,
+ label,
+ status = "neutral",
+ ...properties
+}: ComponentPropsWithoutRef & {
+ amount?: number;
+ label?: string;
+ status?: "danger" | "neutral" | "success";
+}) {
+ const {
+ i18n: { language },
+ } = useTranslation();
+ const { data: hidden } = useQuery({ queryKey: ["settings", "sensitive"] });
+ const formatted =
+ amount === undefined
+ ? undefined
+ : amount.toLocaleString(language, { style: "decimal", minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ const { color, wholeColor } = palette[status];
+
+ if (!formatted) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ const whole = formatted.slice(0, -3) || "0";
+ const decimal = formatted.slice(-3);
+ const web = Platform.OS === "web";
+ const a11y = hidden ? "***" : (label ?? `$${formatted}`);
+
+ return (
+
+
+ {`$${formatted}`}
+
+
+ $
+
+
+ {whole}
+
+
+ {decimal}
+
+
+ );
+}
+
+const palette = {
+ neutral: { color: "$uiNeutralSecondary", wholeColor: undefined },
+ danger: { color: "$uiErrorTertiary", wholeColor: "$uiErrorSecondary" },
+ success: { color: "$uiSuccessTertiary", wholeColor: "$uiSuccessSecondary" },
+};
diff --git a/src/components/shared/AssetSelector.tsx b/src/components/shared/AssetSelector.tsx
index 8b1925dee..88cb60f48 100644
--- a/src/components/shared/AssetSelector.tsx
+++ b/src/components/shared/AssetSelector.tsx
@@ -100,7 +100,7 @@ export default function AssetSelector({
paddingHorizontal="$s4"
borderRadius="$r3"
>
-
+
diff --git a/src/components/shared/CopyAddressSheet.tsx b/src/components/shared/CopyAddressSheet.tsx
index 75bbb1f7f..0ac124f65 100644
--- a/src/components/shared/CopyAddressSheet.tsx
+++ b/src/components/shared/CopyAddressSheet.tsx
@@ -43,7 +43,7 @@ export default function CopyAddressSheet({ open, onClose }: { onClose: () => voi
{t("Double-check your address before sending funds to avoid losing them.")}
-
+
{address}
-
+
{t("Some of your assets are at risk of being liquidated.")}
@@ -42,7 +42,7 @@ export default function LiquidationAlert() {
presentArticle("9975910").catch(reportError);
}}
>
-
+
{t("Learn more")}
diff --git a/src/components/shared/ModalSheet.tsx b/src/components/shared/ModalSheet.tsx
index e207017a7..80eac7825 100644
--- a/src/components/shared/ModalSheet.tsx
+++ b/src/components/shared/ModalSheet.tsx
@@ -22,7 +22,7 @@ export default function ModalSheet({
dismissOnSnapToBottom
unmountChildrenWhenHidden
forceRemoveScrollEnabled={open}
- animation="moderate"
+ animation="default"
dismissOnOverlayPress
onOpenChange={(isOpen: boolean) => {
if (!isOpen) onClose();
diff --git a/src/components/shared/ProfileHeader.tsx b/src/components/shared/ProfileHeader.tsx
index 48e47cbf9..7d090963a 100644
--- a/src/components/shared/ProfileHeader.tsx
+++ b/src/components/shared/ProfileHeader.tsx
@@ -37,7 +37,7 @@ export default function ProfileHeader() {
return (
-
+
{address && (
@@ -54,14 +54,12 @@ export default function ProfileHeader() {
}}
>
-
- {hidden ? "0x..." : shortenHex(address)}
-
+ {hidden ? "0x..." : shortenHex(address)}
)}
-
+
{hidden ? : }
diff --git a/src/components/shared/StyledButton.tsx b/src/components/shared/StyledButton.tsx
index 3cc621e1b..afcb1fd46 100644
--- a/src/components/shared/StyledButton.tsx
+++ b/src/components/shared/StyledButton.tsx
@@ -2,7 +2,7 @@ import type React from "react";
import { use, useMemo, type ComponentPropsWithoutRef } from "react";
import type { ArrowRight } from "@tamagui/lucide-icons";
-import { createStyledContext, Spinner, styled, withStaticProperties, XStack } from "tamagui";
+import { createStyledContext, Spinner, styled, withStaticProperties, XStack, YStack } from "tamagui";
import Text from "./Text";
@@ -26,6 +26,13 @@ const ButtonContext = createStyledContext<{
loading: false,
});
+const pressable = (hover: object, press: object) => ({
+ hoverStyle: hover,
+ pressStyle: press,
+ "$group-column-hover": hover,
+ "$group-column-press": press,
+});
+
const ButtonFrame = styled(XStack, {
name: "Button",
context: ButtonContext,
@@ -43,29 +50,37 @@ const ButtonFrame = styled(XStack, {
primary: {
true: {
backgroundColor: "$interactiveBaseBrandDefault",
- hoverStyle: { backgroundColor: "$interactiveBaseBrandHover" },
- pressStyle: { backgroundColor: "$interactiveBaseBrandPressed" },
+ ...pressable(
+ { backgroundColor: "$interactiveBaseBrandHover" },
+ { backgroundColor: "$interactiveBaseBrandPressed" },
+ ),
},
},
secondary: {
true: {
backgroundColor: "$interactiveBaseBrandSoftDefault",
- hoverStyle: { backgroundColor: "$interactiveBaseBrandSoftHover" },
- pressStyle: { backgroundColor: "$interactiveBaseBrandSoftPressed" },
+ ...pressable(
+ { backgroundColor: "$interactiveBaseBrandSoftHover" },
+ { backgroundColor: "$interactiveBaseBrandSoftPressed" },
+ ),
},
},
danger: {
true: {
backgroundColor: "$interactiveBaseErrorDefault",
- hoverStyle: { backgroundColor: "$interactiveBaseErrorHover" },
- pressStyle: { backgroundColor: "$interactiveBaseErrorPressed" },
+ ...pressable(
+ { backgroundColor: "$interactiveBaseErrorHover" },
+ { backgroundColor: "$interactiveBaseErrorPressed" },
+ ),
},
},
dangerSecondary: {
true: {
backgroundColor: "$interactiveBaseErrorSoftDefault",
- hoverStyle: { backgroundColor: "$interactiveBaseErrorSoftHover" },
- pressStyle: { backgroundColor: "$interactiveBaseErrorSoftPressed" },
+ ...pressable(
+ { backgroundColor: "$interactiveBaseErrorSoftHover" },
+ { backgroundColor: "$interactiveBaseErrorSoftPressed" },
+ ),
},
},
outlined: {
@@ -74,8 +89,10 @@ const ButtonFrame = styled(XStack, {
borderColor: "$interactiveBaseBrandDefault",
borderWidth: 1,
color: "$interactiveBaseBrandDefault",
- hoverStyle: { backgroundColor: "$interactiveBaseBrandSoftDefault", color: "$interactiveOnBaseBrandDefault" },
- pressStyle: { backgroundColor: "$interactiveBaseBrandSoftHover", color: "$interactiveOnBaseBrandDefault" },
+ ...pressable(
+ { backgroundColor: "$interactiveBaseBrandSoftDefault", color: "$interactiveOnBaseBrandDefault" },
+ { backgroundColor: "$interactiveBaseBrandSoftHover", color: "$interactiveOnBaseBrandDefault" },
+ ),
},
},
transparent: {
@@ -83,8 +100,10 @@ const ButtonFrame = styled(XStack, {
backgroundColor: "transparent",
borderColor: "transparent",
color: "$interactiveBaseBrandDefault",
- hoverStyle: { backgroundColor: "$interactiveBaseBrandSoftDefault", color: "$interactiveOnBaseBrandDefault" },
- pressStyle: { backgroundColor: "$interactiveBaseBrandSoftHover", color: "$interactiveOnBaseBrandDefault" },
+ ...pressable(
+ { backgroundColor: "$interactiveBaseBrandSoftDefault", color: "$interactiveOnBaseBrandDefault" },
+ { backgroundColor: "$interactiveBaseBrandSoftHover", color: "$interactiveOnBaseBrandDefault" },
+ ),
disabledStyle: {
backgroundColor: "transparent",
borderColor: "transparent",
@@ -98,8 +117,7 @@ const ButtonFrame = styled(XStack, {
backgroundColor: "$interactiveDisabled",
borderColor: "transparent",
cursor: "not-allowed",
- hoverStyle: { backgroundColor: "$interactiveDisabled" },
- pressStyle: { backgroundColor: "$interactiveDisabled" },
+ ...pressable({ backgroundColor: "$interactiveDisabled" }, { backgroundColor: "$interactiveDisabled" }),
},
},
} as const,
@@ -150,4 +168,61 @@ const ButtonIcon = (properties: { children: React.ReactElement;
};
-export default withStaticProperties(ButtonFrame, { Props: ButtonContext.Provider, Text: ButtonText, Icon: ButtonIcon });
+const ButtonColumnFrame = styled(YStack, {
+ name: "ButtonColumn",
+ context: ButtonContext,
+ alignItems: "center",
+ gap: "$s3_5",
+ cursor: "pointer",
+ group: "column",
+ pointerEvents: "box-only",
+ variants: {
+ primary: { true: {} },
+ secondary: { true: {} },
+ danger: { true: {} },
+ dangerSecondary: { true: {} },
+ outlined: { true: {} },
+ transparent: { true: {} },
+ loading: { true: {} },
+ disabled: { true: { cursor: "not-allowed" } },
+ } as const,
+});
+
+const ButtonColumn = ({
+ primary,
+ secondary,
+ danger,
+ dangerSecondary,
+ outlined,
+ transparent,
+ loading,
+ disabled,
+ ...properties
+}: ComponentPropsWithoutRef) => {
+ const context = { primary, secondary, danger, dangerSecondary, outlined, transparent, loading, disabled };
+ return (
+
+
+
+ );
+};
+
+const ButtonLabel = (properties: ComponentPropsWithoutRef) => {
+ const { disabled } = use(ButtonContext.context);
+ return (
+
+ );
+};
+
+export default withStaticProperties(ButtonFrame, {
+ Props: ButtonContext.Provider,
+ Text: ButtonText,
+ Icon: ButtonIcon,
+ Column: ButtonColumn,
+ Label: ButtonLabel,
+});
diff --git a/src/components/shared/Text.tsx b/src/components/shared/Text.tsx
index a39180e60..4a51c8d35 100644
--- a/src/components/shared/Text.tsx
+++ b/src/components/shared/Text.tsx
@@ -5,21 +5,23 @@ import { styled, Text as TamaguiText } from "tamagui";
import { useQuery } from "@tanstack/react-query";
const StyledText = styled(TamaguiText, {
+ fontVariant: ["stylistic-one", "stylistic-two", "stylistic-three"],
defaultVariants: { primary: true },
variants: {
emphasized: { true: { fontWeight: "bold" } },
primary: { true: { color: "$uiNeutralPrimary" } },
secondary: { true: { color: "$uiNeutralSecondary" } },
- title: { true: { fontSize: 28, letterSpacing: -0.2 } },
- title2: { true: { fontSize: 22, letterSpacing: -0.2 } },
- title3: { true: { fontSize: 20, letterSpacing: -0.2 } },
- headline: { true: { fontSize: 17, letterSpacing: -0.1 } },
- body: { true: { fontSize: 17, letterSpacing: -0.1 } },
- callout: { true: { fontSize: 16, letterSpacing: -0.2 } },
- subHeadline: { true: { fontSize: 15, letterSpacing: 0 } },
- footnote: { true: { fontSize: 13, letterSpacing: 0 } },
- caption: { true: { fontSize: 12, letterSpacing: 0 } },
- caption2: { true: { fontSize: 11, letterSpacing: 0 } },
+ largeTitle: { true: { fontSize: 36, lineHeight: 47, letterSpacing: -0.072 } },
+ title: { true: { fontSize: 30, lineHeight: 39, letterSpacing: -0.06 } },
+ title2: { true: { fontSize: 23, lineHeight: 30, letterSpacing: -0.046 } },
+ title3: { true: { fontSize: 21, lineHeight: 27, letterSpacing: -0.042 } },
+ headline: { true: { fontSize: 18, lineHeight: 23, letterSpacing: -0.036 } },
+ body: { true: { fontSize: 18, lineHeight: 23, letterSpacing: -0.036 } },
+ callout: { true: { fontSize: 17, lineHeight: 22, letterSpacing: -0.034 } },
+ subHeadline: { true: { fontSize: 16, lineHeight: 21, letterSpacing: -0.032 } },
+ footnote: { true: { fontSize: 14, lineHeight: 18, letterSpacing: -0.028 } },
+ caption: { true: { fontSize: 13, lineHeight: 17, letterSpacing: -0.026 } },
+ caption2: { true: { fontSize: 12, lineHeight: 16, letterSpacing: -0.024 } },
brand: { true: { color: "$interactiveBaseBrandDefault" } },
centered: { true: { textAlign: "center" } },
pill: { true: { fontWeight: "bold", paddingHorizontal: 4, paddingVertical: 2, borderRadius: "$r2" } },
diff --git a/src/components/shared/TransactionDetails.tsx b/src/components/shared/TransactionDetails.tsx
index a053700b8..00ca07b9e 100644
--- a/src/components/shared/TransactionDetails.tsx
+++ b/src/components/shared/TransactionDetails.tsx
@@ -76,7 +76,7 @@ export default function TransactionDetails({ hash }: { hash?: string }) {
openBrowser(`${explorerUrl}/tx/${hash}`).catch(reportError);
}}
>
-
+
{shortenHex(hash)}
diff --git a/src/components/swaps/Swaps.tsx b/src/components/swaps/Swaps.tsx
index 15e053695..1001ddae3 100644
--- a/src/components/swaps/Swaps.tsx
+++ b/src/components/swaps/Swaps.tsx
@@ -378,7 +378,7 @@ export default function Swaps() {
-
+
{(caution || danger) && showWarning && (
diff --git a/src/components/swaps/TokenInput.tsx b/src/components/swaps/TokenInput.tsx
index 04492ed4d..9f900a464 100644
--- a/src/components/swaps/TokenInput.tsx
+++ b/src/components/swaps/TokenInput.tsx
@@ -171,7 +171,6 @@ export default function TokenInput({
color={
isDanger ? "$uiErrorSecondary" : isActive ? "$uiNeutralPrimary" : "$uiNeutralPlaceholder"
}
- fontFamily="BDOGrotesk-Regular"
fontSize={28}
fontWeight="bold"
letterSpacing={-0.2}
diff --git a/src/i18n/es.json b/src/i18n/es.json
index 6ec984399..4e82c71c5 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -2,6 +2,8 @@
"Exa Account": "Cuenta Exa",
"Send": "Enviar",
"Swap": "Intercambiar",
+ "Portfolio": "Cartera",
+ "Manage portfolio": "Administrar cartera",
"Settings": "Configuración",
"Support": "Soporte",
"Logout": "Cerrar sesión",
@@ -72,6 +74,28 @@
"Select token to pay": "Seleccionar token a pagar",
"Select token to receive": "Seleccionar token a recibir",
"Spending limit": "LÃmite de gasto",
+ "Spending limit info": "Información del lÃmite de gasto",
+ "Credit limit": "LÃmite de crédito",
+ "Credit limit info": "Información del lÃmite de crédito",
+ "Collateral {{value}}": "GarantÃa {{value}}",
+ "Exa Card pay mode": "Exa Card modo de pago",
+ "Now": "Ahora",
+ "Later in {{count}}_one": "{{count}} cuota",
+ "Later in {{count}}_other": "{{count}} cuotas",
+ "Set installments": "Configurar cuotas",
+ "Choose how many installments to use for future card purchases. You can always change this before each purchase.": "Elige cuántas cuotas usar para futuras compras con tarjeta. Siempre puedes cambiar esto antes de cada compra.",
+ "Set Pay Later in {{count}}_one": "Pagar en {{count}} cuota",
+ "Set Pay Later in {{count}}_other": "Pagar en {{count}} cuotas",
+ "Installments calculator": "Calculadora de cuotas",
+ "The maximum amount you can spend using Pay Now.": "El monto máximo que puedes gastar usando Pagar ahora.",
+ "It's based on the USDC available in your balance.": "Se basa en el USDC disponible en tu balance.",
+ "The maximum amount you can spend using Pay Later.": "El monto máximo que puedes gastar usando Pagar después.",
+ "It's based on the value of your collateral assets and updates as their value changes.": "Se basa en el valor de tus activos en garantÃa y se actualiza a medida que su valor cambia.",
+ "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.",
+ "Later": "Después",
+ "Pay instantly using your available USDC.": "Paga instantáneamente usando tu USDC disponible.",
+ "Pay without selling your crypto. Use it as collateral to unlock a credit limit and split purchases into up to {{max}} installments.": "Paga sin vender tu cripto. Úsalo como garantÃa para desbloquear un lÃmite de crédito y dividir compras en hasta {{max}} cuotas.",
+ "Tap here to change the number of installments": "Toca aquà para cambiar la cantidad de cuotas",
"Pay Now": "Pagar ahora",
"{{count}} installments of_one": "{{count}} cuota de",
"{{count}} installments of_other": "{{count}} cuotas de",
@@ -442,7 +466,7 @@
"Hide sensitive": "Ocultar sensibles",
"Pending proposals": "Solicitudes pendientes",
"Some of your assets are at risk of being liquidated.": "Algunos de tus activos están en riesgo de ser liquidados.",
- "Learn more": "Saber más",
+ "Learn more": "Aprende más",
"An account upgrade is required to access the latest features.": "Es necesaria una actualización de cuenta para acceder a las últimas funciones.",
"Processing balance → {{amount}}": "Saldo en procesamiento → {{amount}}",
"Pending requests → {{count}}_one": "Solicitud pendiente → {{count}}",
@@ -506,6 +530,7 @@
"Get benefit": "Obtener beneficio",
"Pax ID copied!": "¡ID de Pax copiado!",
"Failed to load": "Error al cargar",
+ "Copy Pax ID {{id}}": "Copiar ID de Pax {{id}}",
"COPY ID": "COPIAR ID",
"Terms & conditions": "Términos y condiciones",
"Go to Visa": "Ir a Visa",
diff --git a/src/utils/queryClient.ts b/src/utils/queryClient.ts
index 7b05dbe41..3fd3ca08b 100644
--- a/src/utils/queryClient.ts
+++ b/src/utils/queryClient.ts
@@ -126,6 +126,13 @@ queryClient.setQueryDefaults(["settings", "installments"], {
gcTime: Infinity,
queryFn: () => queryClient.getQueryData(["settings", "installments"]),
});
+queryClient.setQueryDefaults(["settings", "installments-spotlight"], {
+ initialData: false,
+ retry: false,
+ staleTime: Infinity,
+ gcTime: Infinity,
+ queryFn: () => queryClient.getQueryData(["settings", "installments-spotlight"]),
+});
queryClient.setQueryDefaults(["simulate-purchase", "installments"], {
initialData: 1,
retry: false,
diff --git a/src/utils/useInstallmentRates.ts b/src/utils/useInstallmentRates.ts
new file mode 100644
index 000000000..6cd4ba6d2
--- /dev/null
+++ b/src/utils/useInstallmentRates.ts
@@ -0,0 +1,90 @@
+import { useMemo } from "react";
+
+import { marketUSDCAddress } from "@exactly/common/generated/chain";
+import MAX_INSTALLMENTS from "@exactly/common/MAX_INSTALLMENTS";
+import MIN_BORROW_INTERVAL from "@exactly/common/MIN_BORROW_INTERVAL";
+import {
+ fixedRate,
+ fixedUtilization,
+ globalUtilization,
+ MATURITY_INTERVAL,
+ ONE_YEAR,
+ splitInstallments,
+ WAD,
+} from "@exactly/lib";
+
+import reportError from "./reportError";
+import useAsset from "./useAsset";
+
+export default function useInstallmentRates(amount = 100_000_000n) {
+ const { market } = useAsset(marketUSDCAddress);
+ return useMemo(() => {
+ if (!market) return;
+ const now = Math.floor(Date.now() / 1000);
+ const nextMaturity = now - (now % MATURITY_INTERVAL) + MATURITY_INTERVAL;
+ const firstMaturity = nextMaturity - now < MIN_BORROW_INTERVAL ? nextMaturity + MATURITY_INTERVAL : nextMaturity;
+ if (amount <= 0n) {
+ const installments = [];
+ for (let count = 1; count <= MAX_INSTALLMENTS; count++) {
+ installments.push({ count, payments: Array.from({ length: count }).fill(0n), rate: 0n, total: 0n });
+ }
+ return { installments, firstMaturity };
+ }
+ const {
+ fixedPools,
+ floatingBackupBorrowed,
+ floatingUtilization,
+ interestRateModel: { parameters },
+ totalFloatingBorrowAssets,
+ totalFloatingDepositAssets,
+ } = market;
+ const marketUtilization = globalUtilization(
+ totalFloatingDepositAssets,
+ totalFloatingBorrowAssets,
+ floatingBackupBorrowed,
+ );
+ const borrowImpact = totalFloatingDepositAssets > 0n ? (amount * WAD - 1n) / totalFloatingDepositAssets + 1n : 0n;
+ try {
+ const installments = [];
+ for (let count = 1; count <= MAX_INSTALLMENTS; count++) {
+ const poolUtilizations = fixedPools
+ .filter(({ maturity }) => maturity >= firstMaturity && maturity < firstMaturity + count * MATURITY_INTERVAL)
+ .map(({ supplied, borrowed }) => fixedUtilization(supplied, borrowed, totalFloatingDepositAssets));
+ if (poolUtilizations.length === 0) {
+ installments.push({ count, payments: undefined, rate: 0n, total: 0n });
+ continue;
+ }
+ if (count === 1) {
+ const rate = fixedRate(
+ firstMaturity,
+ fixedPools.length,
+ (poolUtilizations[0] ?? 0n) + borrowImpact,
+ floatingUtilization,
+ marketUtilization + borrowImpact,
+ parameters,
+ now,
+ );
+ const fee = (amount * rate * BigInt(firstMaturity - now)) / (WAD * ONE_YEAR);
+ const total = amount + fee;
+ installments.push({ count, payments: [total], rate, total });
+ continue;
+ }
+ const { installments: payments, effectiveRate } = splitInstallments(
+ amount,
+ totalFloatingDepositAssets,
+ firstMaturity,
+ fixedPools.length,
+ poolUtilizations,
+ floatingUtilization,
+ marketUtilization,
+ parameters,
+ now,
+ );
+ installments.push({ count, payments, rate: effectiveRate, total: payments.reduce((a, b) => a + b, 0n) });
+ }
+ return { installments, firstMaturity };
+ } catch (error) {
+ reportError(error);
+ }
+ }, [market, amount]);
+}
diff --git a/src/utils/useInstallments.ts b/src/utils/useInstallments.ts
index 8dfc24666..ab86b0ab4 100644
--- a/src/utils/useInstallments.ts
+++ b/src/utils/useInstallments.ts
@@ -49,6 +49,7 @@ export default function useInstallments({
market.floatingBackupBorrowed,
),
market.interestRateModel.parameters,
+ timestamp,
);
}
} catch (error) {
diff --git a/tamagui.config.ts b/tamagui.config.ts
index ada431a39..5200d9a2c 100644
--- a/tamagui.config.ts
+++ b/tamagui.config.ts
@@ -1,3 +1,5 @@
+import { Easing } from "react-native-reanimated";
+
import { createAnimations } from "@tamagui/animations-moti";
import { config } from "@tamagui/config/v3";
import { createFont, createTamagui, createTokens } from "tamagui";
@@ -12,11 +14,11 @@ const tokens = createTokens({
creditLight9: "#8E4EC6",
debitDark1: "#0D141F",
debitDark5: "#154467",
- debitDark9: "#7CE2FE",
+ debitDark9: "#2775CA",
debitLight1: "#F9FEFF",
debitLight5: "#BEE7F5",
- debitLight9: "#7CE2FE",
- debitLight12: "#1D3E56",
+ debitLight9: "#2775CA",
+ debitLight12: "#EBF5FF",
feedbackErrorDark1: "#191111",
feedbackErrorDark10: "#EC5D5E",
feedbackErrorDark11: "#FF9592",
@@ -190,9 +192,7 @@ const tokens = createTokens({
s0: 0,
true: 0,
s1: 2,
- s1_5: 3,
s2: 4,
- s2_5: 6,
s3: 8,
s3_5: 12,
s4: 16,
@@ -202,11 +202,11 @@ const tokens = createTokens({
s7: 40,
s8: 48,
s9: 64,
- s10: 104,
- s11: 120,
- s12: 144,
- s13: 160,
- s14: 184,
+ s10: 80,
+ s11: 96,
+ s12: 120,
+ s13: 144,
+ s14: 208,
},
radius: { r0: 0, true: 4, r1: 2, r2: 4, r3: 8, r4: 12, r5: 16, r6: 20, r_0: 9999 },
size: config.tokens.size,
@@ -215,36 +215,33 @@ const tokens = createTokens({
zIndex: config.tokens.zIndex,
});
+const sizes = config.fonts.body.size;
const body = createFont({
- family: "BDOGrotesk-Regular",
+ family: "SplineSans-Regular",
face: {
- 400: { normal: "BDOGrotesk-Regular" },
- 600: { normal: "BDOGrotesk-DemiBold" },
- 700: { normal: "BDOGrotesk-DemiBold" },
+ 400: { normal: "SplineSans-Regular" },
+ 600: { normal: "SplineSans-SemiBold" },
+ 700: { normal: "SplineSans-SemiBold" },
},
- size: config.fonts.body.size,
- weight: { regular: 400, semibold: 600 },
+ size: sizes,
+ lineHeight: Object.fromEntries(
+ Object.entries(sizes).map(([k, v]) => [k, Math.round(Number(v) * 1.3)]),
+ ) as typeof sizes,
+ letterSpacing: Object.fromEntries(Object.entries(sizes).map(([k, v]) => [k, Number(v) * -0.002])) as typeof sizes,
+ weight: { regular: 400, semibold: 600, bold: 700 },
});
const tamagui = createTamagui({
...config,
tokens,
- fonts: {
- body,
- heading: body,
- mono: createFont({
- family: "IBMPlexMono-Medm",
- face: { 500: { normal: "IBMPlexMono-Medm" } },
- weight: { medium: 500 },
- size: config.fonts.mono.size,
- }),
- },
+ groups: { column: { pseudo: true } },
+ fonts: { body, heading: body },
defaultFont: "body",
animations: createAnimations({
bouncy: { type: "spring", damping: 9, mass: 0.9, stiffness: 150 },
lazy: { type: "spring", damping: 18, stiffness: 50 },
slow: { type: "spring", damping: 15, stiffness: 40 },
- moderate: { type: "spring", damping: 15, mass: 0.2, stiffness: 100 },
+ default: { type: "timing", duration: 512, easing: Easing.bezier(0.7, 0, 0.3, 1) },
quick: { type: "spring", damping: 25, mass: 1.2, stiffness: 250 },
tooltip: { type: "spring", damping: 10, mass: 0.9, stiffness: 100 },
}),
@@ -389,9 +386,9 @@ const tamagui = createTamagui({
backgroundColor: "",
backgroundHover: "",
backgroundPress: "",
- borderColor: "",
- borderColorFocus: "",
- borderColorPress: "",
+ borderColor: "transparent",
+ borderColorFocus: "transparent",
+ borderColorPress: "transparent",
outlineColor: "",
},
dark: {
@@ -429,7 +426,7 @@ const tamagui = createTamagui({
uiWarningSecondary: tokens.color.feedbackWarningDark9,
uiWarningTertiary: tokens.color.feedbackWarningDark7,
uiInfoPrimary: tokens.color.feedbackInformationDark11,
- uiInfoSecondary: tokens.color.feedbackInformationLight9,
+ uiInfoSecondary: tokens.color.feedbackInformationDark9,
uiInfoTertiary: tokens.color.feedbackInformationDark7,
interactiveBaseBrandDefault: tokens.color.primaryDark9,
interactiveBaseBrandHover: tokens.color.primaryDark10,
@@ -438,8 +435,8 @@ const tamagui = createTamagui({
interactiveBaseBrandSoftHover: tokens.color.primaryDark4,
interactiveBaseBrandSoftPressed: tokens.color.primaryDark5,
interactiveBaseSuccessDefault: tokens.color.feedbackSuccessDark9,
- interactiveBaseSuccessHover: tokens.color.feedbackSuccessLight10,
- interactiveBaseSuccessPressed: tokens.color.feedbackSuccessLight11,
+ interactiveBaseSuccessHover: tokens.color.feedbackSuccessDark10,
+ interactiveBaseSuccessPressed: tokens.color.feedbackSuccessDark11,
interactiveBaseSuccessSoftDefault: tokens.color.feedbackSuccessDark3,
interactiveBaseSuccessSoftHover: tokens.color.feedbackSuccessDark4,
interactiveBaseSuccessSoftPressed: tokens.color.feedbackSuccessDark5,
@@ -485,7 +482,7 @@ const tamagui = createTamagui({
interactiveTextWarningHover: tokens.color.feedbackWarningDark10,
interactiveTextWarningPressed: tokens.color.feedbackWarningDark11,
interactiveTextInfoDefault: tokens.color.feedbackInformationDark9,
- interactiveTextInfoHover: tokens.color.feedbackInformationLight10,
+ interactiveTextInfoHover: tokens.color.feedbackInformationDark10,
interactiveTextInfoPressed: tokens.color.feedbackInformationDark11,
interactiveDisabled: tokens.color.grayscaleDark4,
interactiveOnDisabled: tokens.color.grayscaleDark8,
@@ -533,9 +530,9 @@ const tamagui = createTamagui({
backgroundColor: "",
backgroundHover: "",
backgroundPress: "",
- borderColor: "",
- borderColorFocus: "",
- borderColorPress: "",
+ borderColor: "transparent",
+ borderColorFocus: "transparent",
+ borderColorPress: "transparent",
outlineColor: "",
},
},
@@ -544,5 +541,9 @@ const tamagui = createTamagui({
export type Config = typeof tamagui;
declare module "tamagui" {
interface TamaguiCustomConfig extends Config {} // eslint-disable-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-empty-interface
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface TypeOverride {
+ groupNames(): "column";
+ }
}
export default tamagui;