Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
006ba94
fix(home,card): keep Swap/Send for card-funded users, unblock card wi…
claude Sep 17, 2026
22313e0
Merge pull request #2527 from Solid-Money/claude/monday-task-review-m…
MusabShakeel576 Sep 17, 2026
231b515
fix(withdraw): prevent user cancellations from being reported as Sent…
sentry[bot] Sep 21, 2026
ba84196
Merge pull request #2543 from Solid-Money/seer/fix/sentry-withdraw-ca…
mul53 Sep 21, 2026
1567010
fix: retryable issuer decline code
MayankMittal1 Sep 21, 2026
ec9d9e5
Merge pull request #2545 from Solid-Money/mayank/fix/issuer-declined-…
MayankMittal1 Sep 21, 2026
ead48c5
fix(onboarding): stop sending a failed passkey login to "Create account"
claude Sep 22, 2026
55831ad
fix(onboarding): show the sanitized login error, not the raw API message
claude Sep 22, 2026
83e6178
Merge pull request #2549 from Solid-Money/claude/monday-ota-bug-revie…
MusabShakeel576 Sep 22, 2026
6e56a0f
feat: EURC spending on base
MayankMittal1 Sep 22, 2026
a6a90a1
Merge pull request #2553 from Solid-Money/mayank/feat/eurc-spending
MayankMittal1 Sep 22, 2026
eb1c61a
fix: eurc spend
MayankMittal1 Sep 22, 2026
087db36
Merge pull request #2554 from Solid-Money/mayank/feat/eurc-spending
MayankMittal1 Sep 22, 2026
aba1a85
fix: eurc spend enable cache
MayankMittal1 Sep 22, 2026
eac9f97
Merge pull request #2556 from Solid-Money/mayank/fix/eurc-spend-cache
MayankMittal1 Sep 22, 2026
7ff3b7e
feat: repay card credit from wallet or collateral
MayankMittal1 Sep 23, 2026
65abe9f
Merge pull request #2565 from Solid-Money/mayank/feat/credit-repay
MayankMittal1 Sep 23, 2026
f8e3d47
fix: tighten card sheets on desktop web, show repay token amount
MayankMittal1 Sep 23, 2026
c103771
Merge pull request #2566 from Solid-Money/mayank/fix/credit-repay-web…
MayankMittal1 Sep 23, 2026
20bd172
feat: home spend-mode strip, re-enable card spending, drawer fixes
MayankMittal1 Sep 24, 2026
4322890
Merge pull request #2568 from Solid-Money/mayank/feat/spend-mode-home…
MayankMittal1 Sep 24, 2026
1a73f5b
fix(referral): show each friend's progress against the bar they cleared
claude Sep 24, 2026
d19649a
Merge pull request #2571 from Solid-Money/claude/monday-tasks-review-…
MusabShakeel576 Sep 24, 2026
bb007e5
Merge pull request #2559 from Solid-Money/qa
mul53 Sep 24, 2026
38bc5f8
fix: quote credit headroom, not the daily limit, as available to borrow
MayankMittal1 Sep 24, 2026
427a25a
feat: set up spend-mode cohort cards on v2 when no module is enabled
MayankMittal1 Sep 24, 2026
7ca4996
Merge pull request #2573 from Solid-Money/mayank/fix/credit-available…
MayankMittal1 Sep 24, 2026
fc8afb5
Merge pull request #2574 from Solid-Money/mayank/feat/cohort-enable-v2
MayankMittal1 Sep 24, 2026
4a86174
feat: open spend modes to every cardholder and register new Safes on v2
MayankMittal1 Sep 24, 2026
7fc96f2
feat: hide Smart in the spend-mode picker and add the credit yield no…
MayankMittal1 Sep 24, 2026
425e6e3
Merge pull request #2577 from Solid-Money/mayank/feat/spend-mode-for-all
MayankMittal1 Sep 24, 2026
a82dc54
Merge master into the rewards v3 release
claude Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ EXPO_PUBLIC_CASH_MODULE_V2_ADDRESS=
# SolidSpendLens on Fuse — the cohort-aware read serving both module generations.
EXPO_PUBLIC_SPEND_LENS_V2_ADDRESS=

# ---- Base: the EURC spend instance -------------------------------------------
# A SECOND, INDEPENDENT deployment of the spend module, not a setting on the Fuse
# one. Each instance has its own limits, its own replay markers and its own debt
# book, and none of that is visible across chains — so never fill these in from
# their Fuse counterparts above.
#
# Empty is correct until the Base deployment lands. With either address unset the
# app does not render the enable card, which is the right behaviour: a half-filled
# set is the dangerous state, not an empty one.
#
# Note the instance is DOLLAR-denominated despite being the euro deployment. EURC
# is priced within it as a floating asset at around 1.08; the euro is the asset,
# not the accounting unit.
EXPO_PUBLIC_BASE_CASH_MODULE_ADDRESS=
# SolidSpendLens on Base. Read before building the enable batch, so a partially
# completed enablement resumes instead of reverting on the half already done.
EXPO_PUBLIC_BASE_SPEND_LENS_ADDRESS=
# EURC on Base. Spendable, never collateral at launch.
EXPO_PUBLIC_BASE_EURC_ADDRESS=

# Quote the launch cashback rates baked into the app (Core 3%, Prime 4%, Ultra 5%)
# instead of the rate the rewards API reports. On unless set to "false" — flip it
# once the admin-configured rates are live.
Expand Down
2 changes: 2 additions & 0 deletions app/(protected)/(tabs)/card/activate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default function ActivateMobile() {
isUnderReview,
activationBlockedReason,
activationFailure,
retryActivation,
steps,
activeStepId,
isStepButtonEnabled,
Expand Down Expand Up @@ -56,6 +57,7 @@ export default function ActivateMobile() {
isBlocked={isCardBlocked}
blockedReason={activationBlockedReason}
failure={activationFailure}
onRetry={retryActivation}
/>
<CardActivationStepsList
steps={steps}
Expand Down
6 changes: 4 additions & 2 deletions app/welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import LoginKeyIcon from '@/assets/images/login_key_icon';
import { DesktopHero } from '@/components/Onboarding';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { PASSKEY_NOT_REGISTERED_CODE } from '@/constants/errors';
import { loginErrorMessage, PASSKEY_NOT_REGISTERED_CODE } from '@/constants/errors';
import { path } from '@/constants/path';
import { useDimension } from '@/hooks/useDimension';
import useUser from '@/hooks/useUser';
Expand Down Expand Up @@ -100,7 +100,9 @@ export default function Welcome() {
Toast.show({
type: 'error',
text1: 'Authentication failed',
text2: error?.message || 'Please try again',
// Same login call, same raw 404 — this screen showed "User not found"
// for it too, next to the account row the user just tapped.
text2: loginErrorMessage(error),
props: {
badgeText: '',
},
Expand Down
3 changes: 3 additions & 0 deletions assets/images/spend-mode-credit-notice-badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/spend-mode-credit-notice-bg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 38 additions & 6 deletions components/Card/ActivateCard/CardStatusBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Pressable, View } from 'react-native';

import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { isActivationFailureRetryable } from '@/lib/utils/cardActivationRetry';
import { openSupportDrawer } from '@/store/useSupportDrawerStore';

import type { CardActivationFailure } from '@/lib/types';
Expand All @@ -11,14 +13,22 @@ interface CardStatusBannerProps {
blockedReason: string;
/** The classified failure, when the server could name one. */
failure?: CardActivationFailure;
/**
* Starts the activation flow again — the same destination as the "Activate
* card" step. Rendered as the banner's primary action whenever the failure is
* retryable, because on a retryable failure this IS the next step and the
* steps list below is collapsed behind a disabled-looking row.
*/
onRetry?: () => void;
}

/**
* A blocker the user is expected to fix themselves rather than escalate.
* A blocker the user is expected to wait out rather than escalate.
*
* Everything else — an unsupported country, an issuer decline, a name the
* issuer cannot read — is ours to resolve, so the support action is the point
* of the card rather than a footnote.
* Only these two suppress the support action outright. Everything else offers
* support, and a retryable failure offers a retry alongside it — an issuer
* decline is ours to chase, but it is also the one the user can simply try
* again once the issuer is back.
*/
const SELF_SERVICE_CODES = new Set(['ACTIVATION_PENDING', 'TEMPORARY_FAILURE']);

Expand All @@ -40,7 +50,7 @@ const FALLBACK_DETAIL_BY_CODE: Record<string, string> = {
MISSING_VERIFIED_NAME: 'This has to be corrected on our side — retrying will not fix it.',
INVALID_PROFILE_DATA: 'This has to be corrected on our side — retrying will not fix it.',
ISSUER_DECLINED:
'This is a problem at our card issuer, not with your account or your verification. We are already tracking it.',
'This was a problem at our card issuer, not with your account or your verification. Please try again — if it keeps failing, contact support.',
ACTIVATION_PENDING: 'This page updates itself — there is no need to keep retrying.',
TEMPORARY_FAILURE: 'Please try again in a few minutes.',
};
Expand All @@ -50,13 +60,21 @@ export function CardStatusBanner({
isBlocked,
blockedReason,
failure,
onRetry,
}: CardStatusBannerProps) {
// A recorded failure outranks the pending state: a card that is "on its way"
// and an issuance that just failed are the same screen, and the failure is
// the newer fact.
if (failure) {
const selfService = SELF_SERVICE_CODES.has(failure.code);
const detail = failure.detail || FALLBACK_DETAIL_BY_CODE[failure.code];
// ACTIVATION_PENDING is the one retryable code with nothing to press: the
// card account is mid-provisioning and the page polls itself, so a button
// would only invite the retry loop the copy is talking the user out of.
const canRetry =
Boolean(onRetry) &&
failure.code !== 'ACTIVATION_PENDING' &&
isActivationFailureRetryable(failure);

return (
<View
Expand All @@ -69,12 +87,26 @@ export function CardStatusBanner({

{!!detail && <Text className="mt-2 text-sm leading-5 text-white/70">{detail}</Text>}

{canRetry && (
<Button
variant="brand"
accessibilityRole="button"
accessibilityLabel="Try activating your card again"
onPress={onRetry}
className="mt-4 self-start px-5"
>
<Text className="text-base font-semibold text-black">Try again</Text>
</Button>
)}

{/* Support stays available next to a retry, not instead of it: an issuer
decline is still ours to chase if trying again does not clear it. */}
{!selfService && (
<Pressable
accessibilityRole="button"
accessibilityLabel="Contact support about card activation"
onPress={() => openSupportDrawer()}
className="mt-4 self-start rounded-lg bg-white/10 px-4 py-2"
className={`self-start rounded-lg bg-white/10 px-4 py-2 ${canRetry ? 'mt-3' : 'mt-4'}`}
>
<Text className="text-sm font-semibold text-white">Contact support</Text>
</Pressable>
Expand Down
16 changes: 15 additions & 1 deletion components/Card/CardWithdrawForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import { withdrawFromCard, withdrawFromCardToSavings } from '@/lib/api';
import { EXPO_PUBLIC_CARD_FUNDING_CHAIN_ID } from '@/lib/config';
import { CardProvider } from '@/lib/types';
import { cn, formatNumber, getCardDepositTokenSymbol } from '@/lib/utils';
import { assetLabel, toAmountInputValue } from '@/lib/utils/cardHelpers';
import {
assetLabel,
isDifferentCollateralAsset,
toAmountInputValue,
} from '@/lib/utils/cardHelpers';
import { CardDepositSource } from '@/store/useCardDepositStore';
import { useCardWithdrawStore } from '@/store/useCardWithdrawStore';

Expand Down Expand Up @@ -457,6 +461,16 @@ export default function CardWithdrawForm() {
assets={collateral?.tokens}
selectedTokenAddress={collateral?.tokenAddress}
onSelectAsset={asset => {
// Re-picking the asset already selected must leave the amount
// alone: the row the user taps is the one the trigger names, the
// cap has not moved, and wiping it there turned "press Max, then
// confirm the destination" into an empty field and a withdrawal
// that could not be submitted.
if (
!isDifferentCollateralAsset(asset, selectedTokenAddress ?? fundingTokenAddress)
) {
return;
}
setSelectedTokenAddress(asset.tokenAddress);
// The cap belongs to the old asset; clear it rather than
// validate the typed amount against a balance it never had.
Expand Down
Loading
Loading