diff --git a/app/business/page.tsx b/app/business/page.tsx index f06e4b6..0ccff2d 100644 --- a/app/business/page.tsx +++ b/app/business/page.tsx @@ -10,10 +10,10 @@ import { ArrowRight, Briefcase, Users, PiggyBank, CreditCard, Settings, Zap } fr import { useApiOpts } from '@/hooks/use-api'; import * as businessApi from '@/lib/api/business'; import type { BusinessStatsResponse } from '@/lib/api/business'; -import { featureFlags } from '@/lib/features'; +import { logger } from '@/lib/logger'; const businessServices = [ - { id: 'sme', title: 'SME Services', description: 'Business accounts, transfers & statements', icon: Briefcase, badge: 'Pro', href: '/business/sme' }, + { id: 'sme', title: 'SME Services', description: 'Business accounts, transfers & statements', icon: Briefcase, badge: 'Pro', href: '/sme' }, { id: 'salary', title: 'Payroll', description: 'Disburse salaries and manage batches', icon: Users, badge: 'New', href: '/salary' }, { id: 'campaigns', title: 'Crowdfunding', description: 'Raise funds for projects via Trivela', icon: Zap, badge: 'Alpha', href: '/campaigns/1' }, { id: 'enterprise', title: 'Enterprise', description: 'Bulk transfers and treasury management', icon: PiggyBank, href: '/enterprise' }, @@ -29,10 +29,6 @@ export default function BusinessPage() { const opts = useApiOpts(); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); - const visibleBusinessServices = businessServices.filter((service) => { - if (service.id === 'gateway') return featureFlags.businessGateway; - return true; - }); useEffect(() => { const fetchStats = async () => { @@ -42,7 +38,7 @@ export default function BusinessPage() { setStats(data); } catch (err) { // Fallback: show loading state instead of error UI - console.error('Business stats error:', err); + logger.error('Business stats error:', err); } finally { setLoading(false); } @@ -77,7 +73,7 @@ export default function BusinessPage() {
- {visibleBusinessServices.map((service) => { + {businessServices.map((service) => { const Icon = service.icon; return ( - +
+

Something went wrong

+

An unexpected error occurred

+ {error.digest &&

Error ID: {error.digest}

}
+
); } \ No newline at end of file diff --git a/app/mint/page.tsx b/app/mint/page.tsx index 1a0d79c..db015bb 100644 --- a/app/mint/page.tsx +++ b/app/mint/page.tsx @@ -19,20 +19,18 @@ import { import { Skeleton } from '@/components/ui/skeleton'; import { ArrowDown, ArrowUp, ArrowLeft } from 'lucide-react'; import { useApiOpts } from '@/hooks/use-api'; -import { useApiError } from '@/hooks/use-api-error'; -import { ApiErrorDisplay } from '@/components/ui/api-error-display'; import { useBalance } from '@/hooks/use-balance'; import { useAuth } from '@/contexts/auth-context'; import { getWalletSecretAnyLocal } from '@/lib/wallet-storage'; import { ensureAcbuTrustlineClient } from '@/lib/stellar/trustlines'; import { useStellarWalletsKit } from '@/lib/stellar-wallets-kit'; +import { submitBurnRedeemSingleClient } from '@/lib/stellar/burning'; import { Keypair } from '@stellar/stellar-sdk'; import * as ratesApi from '@/lib/api/rates'; import * as fiatApi from '@/lib/api/fiat'; -import type { QuoteResponse, RatesResponse } from '@/types/api'; +import type { RatesResponse } from '@/types/api'; import { formatAmount } from '@/lib/utils'; -import { submitBurnRedeemSingleClient } from '@/lib/stellar/burning'; - +import { logger } from '@/lib/logger'; const MINT_NETWORK_FEE_TEXT = "Estimated at confirmation"; const BURN_PROCESSING_FEE_TEXT = "Estimated at confirmation"; @@ -53,48 +51,6 @@ function estimateAcbuFromFiat( return n / localPerAcbu; } -function getNumericValue(value: unknown): number | null { - if (typeof value === 'number' && Number.isFinite(value)) return value; - if (typeof value === 'string' && value.trim()) { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : null; - } - return null; -} - -function getQuoteFee(quote: QuoteResponse | null): number | null { - if (!quote) return null; - - return ( - getNumericValue(quote.network_fee) ?? - getNumericValue(quote.processing_fee) ?? - getNumericValue(quote.fee_amount) ?? - getNumericValue(quote.fee) ?? - getNumericValue(quote.total_fee) - ); -} - -function getQuoteReceiveAmount(quote: QuoteResponse | null): number | null { - if (!quote) return null; - - return ( - getNumericValue(quote.receive_amount) ?? - getNumericValue(quote.payout_amount) ?? - getNumericValue(quote.local_amount) ?? - getNumericValue(quote.amount) - ); -} - -function formatQuotedFee( - quote: QuoteResponse | null, - currency: string, - fallback: string, -): string { - const fee = getQuoteFee(quote); - if (fee === null) return fallback; - return `${currency} ${formatAmount(fee)}`; -} - /** * Mint and Burn page for ACBU tokens. */ @@ -103,13 +59,13 @@ export default function MintPage() { const { userId, stellarAddress } = useAuth(); const { balance, balanceSource, loading: balanceLoading, refresh: refreshBalance } = useBalance(); const kit = useStellarWalletsKit(); - const { uiError: mintUiError, setApiError: setMintApiError, clearError: clearMintError, isSubmitDisabled: isMintDisabled } = useApiError(); - const { uiError: burnUiError, setApiError: setBurnApiError, clearError: clearBurnError, isSubmitDisabled: isBurnDisabled } = useApiError(); const [activeTab, setActiveTab] = useState<'mint' | 'burn' | 'rates'>('mint'); const [step, setStep] = useState<'input' | 'confirm' | 'success'>('input'); const [burnAmount, setBurnAmount] = useState(''); + const [burnError, setBurnError] = useState(''); const [rates, setRates] = useState(null); const [ratesLoading, setRatesLoading] = useState(false); + const [mintError, setMintError] = useState(''); const [txId, setTxId] = useState(null); const [executing, setExecuting] = useState(false); const [fiatAccounts, setFiatAccounts] = useState([]); @@ -117,9 +73,6 @@ export default function MintPage() { const [fiatAmount, setFiatAmount] = useState(''); const [mintQuoteRates, setMintQuoteRates] = useState(null); const [mintAcbuReceived, setMintAcbuReceived] = useState(null); - - const [mintQuote, setMintQuote] = useState(null); - const [burnQuote, setBurnQuote] = useState(null); const rateRows = Array.isArray((rates as { rates?: Array<{ currency?: string; rate?: number }> } | null)?.rates) ? ((rates as { rates?: Array<{ currency?: string; rate?: number }> }).rates ?? []) : []; @@ -144,48 +97,6 @@ export default function MintPage() { }; }, [opts.token]); - useEffect(() => { - if (!fiatAmount || parseFloat(fiatAmount) <= 0 || !selectedFiatCurrency) { - setMintQuote(null); - return; - } - - let cancelled = false; - ratesApi - .getQuote(fiatAmount, selectedFiatCurrency, opts) - .then((quote) => { - if (!cancelled) setMintQuote(quote); - }) - .catch(() => { - if (!cancelled) setMintQuote(null); - }); - - return () => { - cancelled = true; - }; - }, [fiatAmount, selectedFiatCurrency, opts]); - - useEffect(() => { - if (!burnAmount || parseFloat(burnAmount) <= 0 || !selectedFiatCurrency) { - setBurnQuote(null); - return; - } - - let cancelled = false; - ratesApi - .getQuote(burnAmount, selectedFiatCurrency, opts) - .then((quote) => { - if (!cancelled) setBurnQuote(quote); - }) - .catch(() => { - if (!cancelled) setBurnQuote(null); - }); - - return () => { - cancelled = true; - }; - }, [burnAmount, selectedFiatCurrency, opts]); - useEffect(() => { fiatApi .getFiatAccounts(opts) @@ -195,57 +106,28 @@ export default function MintPage() { setSelectedFiatCurrency(res.accounts[0].currency); } }) - .catch(console.error); + .catch((e) => logger.error('Failed to get fiat accounts', e)); }, [opts.token]); - useEffect(() => { - if (activeTab !== "rates") return; - setRatesLoading(true); - ratesApi - .getRates(opts) - .then(setRates) - .catch(() => setRates(null)) - .finally(() => setRatesLoading(false)); - }, [activeTab, opts.token]); + useEffect(() => { + if (activeTab !== "rates") return; + setRatesLoading(true); + ratesApi + .getRates(opts) + .then(setRates) + .catch(() => setRates(null)) + .finally(() => setRatesLoading(false)); + }, [activeTab, opts.token]); - const handleMintConfirm = () => { - setMintError(""); - setStep("confirm"); - }; - - const handleBurnConfirm = () => setStep("confirm"); - - const handleExecuteMint = async () => { - if (!fiatAmount || parseFloat(fiatAmount) <= 0 || !selectedFiatCurrency) - return; - setMintError(""); - setExecuting(true); - try { - if (!userId) { - throw new Error( - "Not signed in — refresh and try again.", - ); - } - const secret = await getWalletSecretAnyLocal(userId, stellarAddress); const handleMintConfirm = () => { - clearMintError(); + setMintError(""); setStep("confirm"); }; - // Burn tab: deep-link to the dedicated /burn page with amount and currency - // prefilled. The /burn page collects the required recipient bank account - // details and calls the real burn API — avoiding a fake success here. - const handleBurnConfirm = () => { - if (!burnAmount || parseFloat(burnAmount) <= 0 || !selectedFiatCurrency) return; - const params = new URLSearchParams({ - amount: burnAmount, - currency: selectedFiatCurrency, - }); - router.push(`/burn?${params.toString()}`); - }; + const handleBurnConfirm = () => setStep("confirm"); const handleExecuteMint = async () => { if (!fiatAmount || parseFloat(fiatAmount) <= 0 || !selectedFiatCurrency) return; - clearMintError(); + setMintError(""); setExecuting(true); try { // Default setup: make sure the recipient trusts the ACBU asset @@ -262,181 +144,68 @@ export default function MintPage() { } const secret = await getWalletSecretAnyLocal(userId, stellarAddress); - let accountId: string; - let trust: Awaited> | null = null; + let accountId: string; + let trust: + | Awaited> + | null = null; - if (secret) { - accountId = Keypair.fromSecret(secret).publicKey(); - if (stellarAddress && accountId !== stellarAddress) { - throw new Error( - `Local wallet (${accountId.slice(0, 6)}…${accountId.slice(-4)}) doesn't match the account on record (${stellarAddress.slice(0, 6)}…${stellarAddress.slice(-4)}). Re-import the correct seed from Settings, or update the wallet address, then retry.`, - ); - } - trust = await ensureAcbuTrustlineClient({ userSecret: secret }); - } else { - if (!kit) { - throw new Error( - "Your wallet secret isn't available on this device and the wallet connector isn't ready yet. Please wait a moment and retry.", - ); - } - accountId = await new Promise((resolve, reject) => { - kit - .openModal({ - onWalletSelected: async (selectedOption: { id: string }) => { - try { - kit.setWallet(selectedOption.id); - const { address } = await kit.getAddress(); - resolve(address); - } catch (err) { - reject(err); - } - }, - }) - .catch(reject); - }); - - if (stellarAddress && accountId !== stellarAddress) { - throw new Error( - `Connected wallet (${accountId.slice(0, 6)}…${accountId.slice(-4)}) doesn't match the account on record (${stellarAddress.slice(0, 6)}…${stellarAddress.slice(-4)}). Connect the correct wallet (or update your linked wallet), then retry.`, - ); - } + if (secret) { + // Guard: the locally stored seed MUST derive to the public key the + // backend treats as this user's Stellar account, otherwise we'd + // keep adding trustlines to the wrong account forever while the + // mint contract tries to deposit into the real recipient. + accountId = Keypair.fromSecret(secret).publicKey(); + if (stellarAddress && accountId !== stellarAddress) { + throw new Error( + `Local wallet (${accountId.slice(0, 6)}…${accountId.slice(-4)}) doesn't match the account on record (${stellarAddress.slice(0, 6)}…${stellarAddress.slice(-4)}). Re-import the correct seed from Settings, or update the wallet address, then retry.`, + ); + } + trust = await ensureAcbuTrustlineClient({ userSecret: secret }); + } else { + if (!kit) { + throw new Error( + "Your wallet secret isn't available on this device and the wallet connector isn't ready yet. Please wait a moment and retry.", + ); + } + accountId = await new Promise((resolve, reject) => { + kit + .openModal({ + onWalletSelected: async (selectedOption: { id: string }) => { + try { + kit.setWallet(selectedOption.id); + const { address } = await kit.getAddress(); + resolve(address); + } catch (err) { + reject(err); + } + }, + }) + .catch(reject); + }); + + if (stellarAddress && accountId !== stellarAddress) { + throw new Error( + `Connected wallet (${accountId.slice(0, 6)}…${accountId.slice(-4)}) doesn't match the account on record (${stellarAddress.slice(0, 6)}…${stellarAddress.slice(-4)}). Connect the correct wallet (or update your linked wallet), then retry.`, + ); + } - trust = await ensureAcbuTrustlineClient({ - external: { kit, address: accountId }, - }); - } + trust = await ensureAcbuTrustlineClient({ + external: { kit, address: accountId }, + }); + } - console.info("[mint] ACBU trustline ensured", { - account: accountId, - added: trust?.added, - visible: trust?.visible, - txHash: trust?.txHash, - }); - if (trust?.added && !trust.visible) { - throw new Error( - "ACBU trustline was submitted but hasn't appeared on Horizon yet. Please retry the mint in a few seconds.", - ); - } + logger.info("[mint] ACBU trustline ensured", { + account: accountId, + added: trust?.added, + visible: trust?.visible, + txHash: trust?.txHash, + }); + if (trust?.added && !trust.visible) { + throw new Error( + "ACBU trustline was submitted but hasn't appeared on Horizon yet. Please retry the mint in a few seconds.", + ); + } - const res = await fiatApi.postOnRamp( - fiatAmount, - selectedFiatCurrency, - opts, - ); - setTxId( - res.blockchain_tx_hash || - res.transaction_id || - res.transactionId || - null, - ); - const acbu = - res.acbuAmount ?? - (typeof res.acbu_amount === "number" ? res.acbu_amount : undefined); - setMintAcbuReceived( - typeof acbu === "number" && Number.isFinite(acbu) ? acbu : null, - ); - refreshBalance(); - setStep("success"); - } catch (e) { - setMintError(e instanceof Error ? e.message : "Mint failed"); - } finally { - setExecuting(false); - } - }; - - const handleExecuteBurn = async () => { - if (!burnAmount || parseFloat(burnAmount) <= 0 || !selectedFiatCurrency) - return; - setBurnError(""); - setExecuting(true); - try { - if (!userId) { - throw new Error("Not signed in — refresh and try again."); - } - if (!stellarAddress) { - throw new Error("No linked Stellar wallet address."); - } - const secret = await getWalletSecretAnyLocal(userId, stellarAddress); - let burnTxHash: string; - if (secret) { - const localPubKey = Keypair.fromSecret(secret).publicKey(); - if (stellarAddress && localPubKey !== stellarAddress) { - throw new Error( - `Local wallet (${localPubKey.slice(0, 6)}…${localPubKey.slice(-4)}) doesn't match the account on record (${stellarAddress.slice(0, 6)}…${stellarAddress.slice(-4)}). Re-import the correct seed from Settings, or update the wallet address, then retry.`, - ); - } - const submit = await submitBurnRedeemSingleClient({ - userAddress: stellarAddress, - amountAcbu: burnAmount, - currency: selectedFiatCurrency, - userSecret: secret, - }); - burnTxHash = submit.transactionHash; - } else { - if (!kit) { - throw new Error( - "Your wallet secret isn't available on this device and the wallet connector isn't ready yet. Please wait a moment and retry.", - ); - } - const address = await new Promise((resolve, reject) => { - kit - .openModal({ - onWalletSelected: async (selectedOption: { id: string }) => { - try { - kit.setWallet(selectedOption.id); - const { address } = await kit.getAddress(); - resolve(address); - } catch (err) { - reject(err); - } - }, - }) - .catch(reject); - }); - if (stellarAddress && address !== stellarAddress) { - throw new Error( - `Connected wallet (${address.slice(0, 6)}…${address.slice(-4)}) doesn't match the account on record (${stellarAddress.slice(0, 6)}…${stellarAddress.slice(-4)}). Connect the correct wallet (or update your linked wallet), then retry.`, - ); - } - const submit = await submitBurnRedeemSingleClient({ - userAddress: stellarAddress, - amountAcbu: burnAmount, - currency: selectedFiatCurrency, - external: { kit, address }, - }); - burnTxHash = submit.transactionHash; - } - const res = await fiatApi.postOffRamp( - burnAmount, - selectedFiatCurrency, - burnTxHash, - opts, - ); - setTxId(res.transaction_id || res.transactionId || null); - setStep("success"); - } catch (e) { - setBurnError(e instanceof Error ? e.message : "Burn failed"); - } finally { - setExecuting(false); - } - }; - - const handleExecute = async () => { - if (activeTab === "mint") { - await handleExecuteMint(); - } else { - await handleExecuteBurn(); - } - }; - - const resetForm = () => { - setStep("input"); - setFiatAmount(""); - setBurnAmount(""); - setBurnError(""); - setTxId(null); - setMintAcbuReceived(null); - }; const res = await fiatApi.postOnRamp( fiatAmount, selectedFiatCurrency, @@ -457,7 +226,7 @@ export default function MintPage() { refreshBalance(); setStep("success"); } catch (e) { - setMintApiError(e); + setMintError(e instanceof Error ? e.message : "Mint failed"); } finally { setExecuting(false); } @@ -465,7 +234,7 @@ export default function MintPage() { const handleExecuteBurn = async () => { if (!burnAmount || parseFloat(burnAmount) <= 0 || !selectedFiatCurrency) return; - clearBurnError(); + setBurnError(""); setExecuting(true); try { if (!userId) { @@ -533,39 +302,27 @@ export default function MintPage() { setTxId(res.transaction_id || res.transactionId || null); setStep("success"); } catch (e) { - setBurnApiError(e); + setBurnError(e instanceof Error ? e.message : "Burn failed"); } finally { setExecuting(false); } }; const handleExecute = async () => { - // Burn is handled by deep-linking to /burn — only mint uses this dialog. if (activeTab === "mint") { await handleExecuteMint(); + } else { + await handleExecuteBurn(); } }; const resetForm = () => { setStep("input"); setFiatAmount(""); setBurnAmount(""); - clearBurnError(); - clearMintError(); + setBurnError(""); setTxId(null); setMintAcbuReceived(null); }; - const mintFeeText = formatQuotedFee( - mintQuote, - mintQuote?.currency || selectedFiatCurrency || 'FIAT', - 'Unavailable until quote loads', - ); - const burnFeeText = formatQuotedFee( - burnQuote, - burnQuote?.currency || selectedFiatCurrency || 'FIAT', - 'Unavailable until quote loads', - ); - const quotedBurnReceiveAmount = getQuoteReceiveAmount(burnQuote); - return ( <>
@@ -580,265 +337,61 @@ export default function MintPage() {
- -
- -

- ACBU Balance -

-

- {balanceLoading ? '...' : `ACBU ${formatAmount(balance)}`} -

-

- {balanceSource === "stellar" - ? "ACBU balance from Stellar Horizon." - : "Link a wallet to see your on-chain ACBU balance."} -

-
-
- - - setActiveTab(v as "mint" | "burn" | "rates") - } - > - - - Mint - - - Burn - - - Rates - - - - -
-

- Mint ACBU via custodial on-ramp (demo basket fiat held on the minting - contract). -

- {mintError && ( -

- {mintError} -

- )} -
- - -

- Select which fiat currency to use for minting -

-
-
- -
- - {selectedFiatCurrency || "FIAT"} - - setFiatAmount(e.target.value)} - className="border-border text-lg font-semibold" - aria-describedby="fiat-amount-hint" - /> + +
+ +

+ ACBU Balance +

+

+ {balanceLoading ? '...' : `ACBU ${formatAmount(balance)}`} +

+

+ {balanceSource === "stellar" + ? "ACBU balance from Stellar Horizon." + : "Link a wallet to see your on-chain ACBU balance."} +

+
-

- Enter the amount of fiat currency to exchange for ACBU -

-
- {estimatedMintAcbu != null && ( - -

- Estimated ACBU (from latest rates) -

-

- ≈ {formatAmount(estimatedMintAcbu)} ACBU -

-
- )} - -
- - Network Fee - - - {MINT_NETWORK_FEE_TEXT} - -
-
- -
- - -
-

- Burn ACBU on-chain for the selected basket slice (no simulated bank - credit). -

- {burnError && ( -

- {burnError} -

- )} -
- - -

- Select which fiat currency to receive when burning -

-
-
- -
- - ACBU - - setBurnAmount(e.target.value)} - className="border-border text-lg font-semibold" - aria-describedby="burn-amount-hint" - /> -
-

- Available: ACBU{" "} - {balanceLoading ? '...' : formatAmount(balance)} -

-
- -
- - You'll receive - - - {burnAmount && selectedFiatCurrency - ? `~ ${selectedFiatCurrency} (based on current rate)` - : "—"} - -
-
- - Processing Fee - - - {BURN_PROCESSING_FEE_TEXT} - -
-
- -
-
+ + + Mint + + + Burn + + + Rates + + +

Mint ACBU via custodial on-ramp (demo basket fiat held on the minting contract).

- {mintUiError && ( - + {mintError && ( +

+ {mintError} +

)}
@@ -930,8 +483,10 @@ export default function MintPage() { Burn ACBU on-chain for the selected basket slice (no simulated bank credit).

- {burnUiError && ( - + {burnError && ( +

+ {burnError} +

)}
@@ -1000,7 +555,7 @@ export default function MintPage() { Processing Fee - {burnFeeText} + {BURN_PROCESSING_FEE_TEXT}
@@ -1014,7 +569,7 @@ export default function MintPage() { className="w-full bg-primary text-primary-foreground hover:bg-primary/90 mt-6" > - Continue to Burn & Redeem + Burn & Redeem
@@ -1024,124 +579,107 @@ export default function MintPage() { {ratesLoading ? ( ) : rateRows.length ? ( - rateRows.map((r) => ( - + rateRows.map((r: { currency?: string; rate?: number }) => ( +
-

ACBU/{r.currency}

-

{formatRate(r.rate)}

+

ACBU/{r.currency ?? 'Rate'}

+

{r.rate != null ? String(r.rate) : '—'}

)) ) : ( - -

No rates available. Use the API to load rates.

-
+

No rates available. Use the API to load rates.

)} - -

How it works

-
    -
  • • Mint converts local fiat to native ACBU
  • -
  • • Burn redeems ACBU for fiat
  • -
  • • Rates from backend
  • -
-
+

How it works

  • • Mint converts local fiat to native ACBU
  • • Burn redeems ACBU for fiat
  • • Rates from backend
- !open && setStep("input")}> - - - - {activeTab === "mint" - ? "Confirm Mint" - : "Confirm Burn"} - - - {activeTab === "mint" && - `Mint ACBU by exchanging ${selectedFiatCurrency} ${formatAmount(fiatAmount)}${ - estimatedMintAcbu != null - ? ` (≈ ${formatAmount(estimatedMintAcbu)} ACBU at current rates)` - : "" - }`} - {activeTab === "burn" && - `Burn ACBU ${formatAmount(burnAmount)} and withdraw to bank account`} - - -
-
- - Amount: - - - {activeTab === "mint" - ? `${selectedFiatCurrency} ${fiatAmount}` - : `ACBU ${formatAmount(burnAmount)}`} - -
-
-
- setStep("input")} - disabled={executing} - > - Cancel - - - {executing ? "Processing..." : "Confirm"} - -
-
-
- - !open && resetForm()}> - - - Operation Complete - - Your {activeTab} operation has been submitted - successfully. - - -
-

- Transaction ID: {txId ?? "—"} -

- {activeTab === "mint" && mintAcbuReceived != null && ( -

- ACBU credited (app ledger):{" "} - {formatAmount(mintAcbuReceived)} ACBU -

- )} - {activeTab === "mint" && ( -

- If the Stellar mint contract errors, tokens are still - recorded in the app; your balance may show - "app ledger" until on-chain mint works. -

- )} -
- - Done - -
-
- - ); -} \ No newline at end of file + + + + + {activeTab === "mint" + ? "Confirm Mint" + : "Confirm Burn"} + + + {activeTab === "mint" && + `Mint ACBU by exchanging ${selectedFiatCurrency} ${formatAmount(fiatAmount)}${ + estimatedMintAcbu != null + ? ` (≈ ${formatAmount(estimatedMintAcbu)} ACBU at current rates)` + : "" + }`} + {activeTab === "burn" && + `Burn ACBU ${formatAmount(burnAmount)} and withdraw to bank account`} + + +
+
+ + Amount: + + + {activeTab === "mint" + ? `${selectedFiatCurrency} ${fiatAmount}` + : `ACBU ${formatAmount(burnAmount)}`} + +
+
+
+ setStep("input")} + disabled={executing} + > + Cancel + + + {executing ? "Processing..." : "Confirm"} + +
+
+
+ + + + + Operation Complete + + Your {activeTab} operation has been submitted + successfully. + + +
+

+ Transaction ID: {txId ?? "—"} +

+ {activeTab === "mint" && mintAcbuReceived != null && ( +

+ ACBU credited (app ledger):{" "} + {formatAmount(mintAcbuReceived)} ACBU +

+ )} + {activeTab === "mint" && ( +

+ If the Stellar mint contract errors, tokens are still + recorded in the app; your balance may show + "app ledger" until on-chain mint works. +

+ )} +
+ + Done + +
+
+ + ); +} diff --git a/app/savings/deposit/page.tsx b/app/savings/deposit/page.tsx index ea8e939..8f55b1b 100644 --- a/app/savings/deposit/page.tsx +++ b/app/savings/deposit/page.tsx @@ -6,33 +6,11 @@ import { PageContainer } from "@/components/layout/page-container"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { ArrowLeft, AlertCircle } from "lucide-react"; +import { ArrowLeft } from "lucide-react"; import { useApiOpts } from "@/hooks/use-api"; -import { useApiError } from "@/hooks/use-api-error"; -import { ApiErrorDisplay } from "@/components/ui/api-error-display"; import * as userApi from "@/lib/api/user"; import * as savingsApi from "@/lib/api/savings"; -import { resolveRecipient } from "@/lib/api/recipient"; - -/** - * Resolve any user identifier (Stellar address, phone, alias, pay URI) - * through the backend recipient resolver to obtain the canonical pay_uri. - * Falls back to the raw value when the resolver is unavailable so that - * Stellar-format addresses still work offline. - */ -async function resolveUserUri( - raw: string, - opts: Parameters[1], -): Promise { - try { - const resolved = await resolveRecipient(raw, opts); - if (resolved.pay_uri) return resolved.pay_uri; - if (resolved.alias) return resolved.alias; - } catch { - // Resolver unavailable — fall through to raw value. - } - return raw; -} +import { logger } from "@/lib/logger"; export default function SavingsDepositPage() { const opts = useApiOpts(); @@ -40,42 +18,22 @@ export default function SavingsDepositPage() { const [amount, setAmount] = useState(""); const [termSeconds, setTermSeconds] = useState("0"); const [loading, setLoading] = useState(false); - const [resolving, setResolving] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); useEffect(() => { - let cancelled = false; - setResolving(true); - setError(""); - - userApi.getReceive(opts).then(async (data) => { + userApi.getReceive(opts).then((data) => { const uri = (data.pay_uri ?? data.alias) as string | undefined; - if (!uri || typeof uri !== 'string') { - if (!cancelled) setResolving(false); - return; - } - - // Resolve through backend recipient resolver so phone-based IDs, - // aliases, and other non-Stellar identifiers are accepted. - const resolved = await resolveUserUri(uri, opts); - if (!cancelled) setUser(resolved); + if (uri && typeof uri === 'string') setUser(uri); }).catch((e) => { - if (!cancelled) { - setError(e instanceof Error ? e.message : 'Failed to load receive address'); - } - }).finally(() => { - if (!cancelled) setResolving(false); + logger.error(e instanceof Error ? e.message : 'Failed to load receive address'); }); - - return () => { cancelled = true; }; }, [opts.token]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!user.trim() || !amount || parseFloat(amount) <= 0) return; setError(""); - setSuccess(""); setLoading(true); try { await savingsApi.savingsDeposit( @@ -88,7 +46,7 @@ export default function SavingsDepositPage() { ); setSuccess("Deposit submitted."); } catch (e) { - setApiError(e); + setError(e instanceof Error ? e.message : "Deposit failed"); } finally { setLoading(false); } @@ -109,10 +67,7 @@ export default function SavingsDepositPage() { {error && ( -
- -

{error}

-
+

{error}

)} {success && (

{success}

@@ -127,15 +82,10 @@ export default function SavingsDepositPage() { - {resolving && ( -

- Verifying account identifier… -

- )}