Skip to content
Merged

Qa #2559

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
87a4980
Add tokenized assets to the Earn page
mul53 Sep 8, 2026
bec106f
Merge pull request #2486 from Solid-Money/claude/earn-page-stocks-743d14
mul53 Sep 8, 2026
6b4c23c
Match the Earn redesign: vault tiles, Invest search, back-to-Earn
mul53 Sep 9, 2026
e832833
Group the Invest rows into one panel
mul53 Sep 9, 2026
8ce890c
Give the vault tile chips a liquid-glass surface
mul53 Sep 9, 2026
dff3ea0
Prefetch the per-vault detail queries from Earn
mul53 Sep 9, 2026
af32a29
Refract the backdrop behind the glass surfaces on web
mul53 Sep 9, 2026
97a2262
Darken the iOS glass material
mul53 Sep 9, 2026
eb9103a
Merge pull request #2493 from Solid-Money/claude/earn-page-stocks-743d14
mul53 Sep 9, 2026
3f448ab
Set the Invest chip label colour by style, not class
mul53 Sep 9, 2026
20d76bb
Always show every vault tile, whatever the rate
mul53 Sep 9, 2026
79fa0c1
Simplify the vault tile chips
mul53 Sep 9, 2026
ede5489
Show held positions above the Invest catalog
mul53 Sep 9, 2026
2086150
Round the browse button into a pill
mul53 Sep 9, 2026
2f271b7
Merge pull request #2496 from Solid-Money/claude/earn-page-stocks-743d14
mul53 Sep 9, 2026
c868bd9
Expand the Invest catalog in place instead of navigating away
mul53 Sep 10, 2026
ff076e2
add ui fixes to earn page
mul53 Sep 10, 2026
467fd07
Merge pull request #2501 from Solid-Money/claude/earn-page-stocks-743d14
mul53 Sep 10, 2026
91847bb
Open the sell flow from an Earn position
mul53 Sep 10, 2026
4d9683d
Merge pull request #2502 from Solid-Money/claude/earn-page-stocks-743d14
mul53 Sep 10, 2026
86a5fd5
Show Invest only to holders, and label the positions list
mul53 Sep 10, 2026
53f1624
Merge pull request #2503 from Solid-Money/claude/earn-invest-requires…
mul53 Sep 10, 2026
7ac3676
Show the Invest catalog to everyone, not just existing holders
mul53 Sep 14, 2026
460cc38
Merge pull request #2518 from Solid-Money/claude/earn-invest-show-cat…
mul53 Sep 14, 2026
1da5faf
Open savings deposits on the amount form, not the token list
mul53 Sep 16, 2026
1303210
Name the vaults by yield, and give the deposit form a back arrow
mul53 Sep 16, 2026
be5e291
Send the empty deposit state to "Fund your wallet"
mul53 Sep 16, 2026
d7daef5
Merge pull request #2525 from Solid-Money/claude/deposit-vault-screen…
mul53 Sep 16, 2026
64b29b7
feat(wallet): split the deposit flow into a crypto and a cash branch
mul53 Sep 16, 2026
223d33b
fix(wallet): rebuild the deposit address card to the design
mul53 Sep 16, 2026
de57280
fix(wallet): stack the chooser's icons above their titles
mul53 Sep 16, 2026
fba9829
fix(wallet): float the chain/currency picker, and always show the cas…
mul53 Sep 16, 2026
34431c9
Merge pull request #2526 from Solid-Money/claude/wallet-deposit-scree…
mul53 Sep 16, 2026
3571fb1
Merge branch 'master' into qa
claude Sep 17, 2026
1d0c1cf
feat(rewards): buy a tier by locking FUSE or paying the annual fee
claude Sep 16, 2026
6392b17
fix(rewards): three defects found reviewing the upgrade screens
claude Sep 16, 2026
870c0e6
Merge pull request #2523 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 20, 2026
d587e2f
refactor(rewards): a tier with no price sends null, not 0
claude Sep 20, 2026
a478e20
Merge pull request #2533 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 20, 2026
00860d5
fix(rewards): eight defects on the v3 upgrade and rewards screens
claude Sep 20, 2026
768e5ce
fix(rewards): open the reconciliation window after a v3 upgrade
claude Sep 20, 2026
bacb287
Merge pull request #2537 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 20, 2026
a92006e
fix(rewards): support URL, the switch label, and default deposits to …
claude Sep 20, 2026
bb313e9
fix(rewards): stop the upgrade screen contradicting its own balance
claude Sep 20, 2026
b7b942d
Merge pull request #2538 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 20, 2026
c447158
fix(rewards): make the route switch label legible, and name soFUSE
claude Sep 20, 2026
5c998ed
fix(rewards): keep the tier celebration off the upgrade flow
claude Sep 20, 2026
f50fa7a
fix(rewards): one direction for the v3 gradients, and green on the cl…
claude Sep 20, 2026
547c1d6
Merge pull request #2539 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 20, 2026
93111fa
feat(rewards): draw the upgrade banner's tier mark as a still
claude Sep 21, 2026
a724558
feat(rewards): make the tier upgrade a modal, opened from one switcher
claude Sep 21, 2026
a8e6c58
feat(rewards): render the tier purchase activity types
claude Sep 21, 2026
f02bdf2
feat(rewards): pay a tier's lock with FUSE or WFUSE, in one press
claude Sep 21, 2026
611856f
feat(wallet): choose a chain before showing the deposit address
mul53 Sep 20, 2026
d13ed09
feat(wallet): quote the pipeline's deposit minimum, not our estimate
mul53 Sep 21, 2026
0ceb22c
fix(wallet): draw the open picker over the QR, not under it
mul53 Sep 21, 2026
cf90169
fix(wallet): animate the picker without layout animations
mul53 Sep 21, 2026
b0bc16d
revert(wallet): drop the dropdown animation
mul53 Sep 21, 2026
139b7b1
feat(wallet): animate the picker opening and closing
mul53 Sep 21, 2026
f6a1e46
Merge pull request #2541 from Solid-Money/claude/wallet-deposit-scree…
mul53 Sep 21, 2026
72b5be3
fix(rewards): do not let the review step sign an upgrade it cannot pa…
claude Sep 21, 2026
155689e
Merge remote-tracking branch 'origin/qa' into claude/festive-fermat-1…
claude Sep 21, 2026
0f4cf5d
Merge pull request #2540 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 21, 2026
685e4cf
Let the user pick which token pays for a tier lock, priced in FUSE
claude Sep 21, 2026
0daab4b
Merge pull request #2546 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 21, 2026
570bc95
feat(onramper): embed the hosted buy widget on web and native
mul53 Sep 21, 2026
d22e081
refactor(onramper): drop the native SDK, leaving the hosted widget
mul53 Sep 21, 2026
93b12df
Hide the upgrade footer on a tier the user already has
claude Sep 21, 2026
055c411
Merge pull request #2547 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 21, 2026
b03a568
Hide the upgrade CTA on either signal that the user holds the tier
claude Sep 21, 2026
256c70c
Merge pull request #2548 from Solid-Money/claude/festive-fermat-1xn11s
MusabShakeel576 Sep 21, 2026
5510e38
feat(onramper): offer the buy widget from the card funding flow
mul53 Sep 21, 2026
37e3538
fix(deps): restore the lockfile entries npm ci needs
mul53 Sep 22, 2026
f79504f
Merge pull request #2552 from Solid-Money/claude/onramper-widget
mul53 Sep 22, 2026
779e5c5
feat(wallet): give the chain and currency pickers their own steps
mul53 Sep 22, 2026
63b6ffd
style(wallet): set the deposit minimum line to 16px medium
mul53 Sep 22, 2026
52dbe03
Merge pull request #2555 from Solid-Money/claude/wallet-deposit-scree…
mul53 Sep 22, 2026
ed874fa
Hide Onramper buy crypto and Earn stocks, fix cash pills, restore Dep…
mul53 Sep 23, 2026
4946170
fix(deposit): stop Select chain's back bouncing to the address
mul53 Sep 23, 2026
16d0fb2
Merge pull request #2558 from Solid-Money/claude/qa-crypto-stock-hidi…
mul53 Sep 23, 2026
25be78c
Put savings deposits back on the amount form
mul53 Sep 23, 2026
a1e3912
feat(deposit): pick the token first, then show the address on a defau…
mul53 Sep 23, 2026
111b8d2
Revert "Hide the upgrade CTA on either signal that the user holds the…
mul53 Sep 23, 2026
b7b8c06
Revert "Hide the upgrade footer on a tier the user already has"
mul53 Sep 23, 2026
0e57adc
Revert "Let the user pick which token pays for a tier lock, priced in…
mul53 Sep 23, 2026
5a41357
Revert "fix(rewards): do not let the review step sign an upgrade it c…
mul53 Sep 23, 2026
f82967e
Revert "feat(rewards): pay a tier's lock with FUSE or WFUSE, in one p…
mul53 Sep 23, 2026
789ebc7
Revert "feat(rewards): render the tier purchase activity types"
mul53 Sep 23, 2026
7a6e6d1
Revert "feat(rewards): make the tier upgrade a modal, opened from one…
mul53 Sep 23, 2026
4baad11
Revert "feat(rewards): draw the upgrade banner's tier mark as a still"
mul53 Sep 23, 2026
8020fc0
Revert "fix(rewards): one direction for the v3 gradients, and green o…
mul53 Sep 23, 2026
750e4dc
Revert "fix(rewards): keep the tier celebration off the upgrade flow"
mul53 Sep 23, 2026
3350233
Revert "fix(rewards): make the route switch label legible, and name s…
mul53 Sep 23, 2026
fcb0e40
Revert "fix(rewards): stop the upgrade screen contradicting its own b…
mul53 Sep 23, 2026
af37f27
Revert the rewards half of "fix(rewards): support URL, the switch lab…
mul53 Sep 23, 2026
18fe8f9
Revert "fix(rewards): open the reconciliation window after a v3 upgrade"
mul53 Sep 23, 2026
b2d6f54
Revert "fix(rewards): eight defects on the v3 upgrade and rewards scr…
mul53 Sep 23, 2026
2f4026c
Revert "refactor(rewards): a tier with no price sends null, not 0"
mul53 Sep 23, 2026
66af337
Revert "fix(rewards): three defects found reviewing the upgrade screens"
mul53 Sep 23, 2026
03c17c2
Revert "feat(rewards): buy a tier by locking FUSE or paying the annua…
mul53 Sep 23, 2026
c55b6f2
Merge pull request #2561 from Solid-Money/claude/qa-crypto-stock-hidi…
mul53 Sep 23, 2026
d8c5714
feat(deposit): warn about the 0.03% fee on card and savings deposit a…
mul53 Sep 23, 2026
50da64a
Merge pull request #2563 from Solid-Money/claude/fee-collection-proce…
mul53 Sep 23, 2026
6ee35e0
fix(deposit): reorder the pickers, and put the network back in a drop…
mul53 Sep 23, 2026
ef6bbb3
feat(deposit): keep Rain cardholders on the older deposit screens
mul53 Sep 23, 2026
f2dc67a
fix(deposit): send Wirex to the wallet flow and Rain to "Fund your card"
mul53 Sep 23, 2026
7d7721f
fix(savings): swap which cardholder gets which vault deposit flow
mul53 Sep 23, 2026
2d46cb3
feat(deposit): mint the deposit address for stablecoins
mul53 Sep 23, 2026
bd3d23d
Merge pull request #2569 from Solid-Money/claude/wallet-deposit-scree…
mul53 Sep 24, 2026
c148f3c
fix(deposit): quote the fee on the wallet flow, and tie each flow to …
mul53 Sep 24, 2026
86948bd
Merge pull request #2570 from Solid-Money/claude/deposit-fee-wallet-flow
mul53 Sep 24, 2026
17ce169
fix(deposit): mint the address for Wirex cardholders only
mul53 Sep 24, 2026
b34b382
Merge pull request #2572 from Solid-Money/claude/wallet-deposit-scree…
mul53 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: 9 additions & 11 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'expo-build-properties',
{
ios: {
// 16.0 is the floor declared by OnramperReactNative.podspec
// (@onramper/onramper-react-native). CocoaPods fails resolution if the
// app target is lower, so this cannot go back below 16.0 while that
// dependency is installed.
// Was raised to 16.0 for OnramperReactNative's podspec floor. That
// package is gone, but the target is left where it is: lowering it is
// a product decision about which iOS versions we support, not a
// consequence of dropping a dependency.
deploymentTarget: '16.0',
useFrameworks: 'static',
// Static frameworks with precompiled RN core have been flaky in EAS iOS builds.
Expand Down Expand Up @@ -306,13 +306,11 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
// does not fail. See plugins/withMlkitVisionDependencies.js.
'./plugins/withMlkitVisionDependencies.js',
// Sets SWIFT_ENABLE_EXPLICIT_MODULES=NO so the Swift pods (NitroModules,
// OnramperReactNative) don't fail the app target's "Emit Swift module" phase
// on Xcode 16+. Referenced by file path, not as '@onramper/onramper-react-native':
// the package ships app.plugin.js but omits it from its package.json "exports"
// map, so resolving it as a package subpath fails with
// ERR_PACKAGE_PATH_NOT_EXPORTED. Switch to the bare package name once
// onramper fixes that upstream.
'./node_modules/@onramper/onramper-react-native/app.plugin.js',
// RCTSwiftUI) don't fail the app target's "Emit Swift module" phase on
// Xcode 16+. Vendored from @onramper/onramper-react-native's own plugin
// when that package was removed — the fix was never Onramper-specific, and
// NitroModules is still here via react-native-mmkv.
'./plugins/withSwiftExplicitModulesDisabled.js',
],
experiments: {
typedRoutes: true,
Expand Down
4 changes: 2 additions & 2 deletions app/(protected)/(tabs)/savings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
return <SavingsScreenNew />;
}

function LegacySavings() {

Check warning on line 51 in app/(protected)/(tabs)/savings.tsx

View workflow job for this annotation

GitHub Actions / lint

'LegacySavings' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 51 in app/(protected)/(tabs)/savings.tsx

View workflow job for this annotation

GitHub Actions / lint

'LegacySavings' is defined but never used. Allowed unused vars must match /^_/u
useRenderMonitor({ componentName: MONITORED_COMPONENTS.SAVINGS_SCREEN });

const { vault: vaultParam } = useLocalSearchParams<{ vault?: string }>();
Expand Down Expand Up @@ -171,11 +171,11 @@
/>
<DepositTrigger
buttonText="Deposit"
modal={DEPOSIT_MODAL.OPEN_SAVINGS_FUND}
modal={DEPOSIT_MODAL.OPEN_FORM}
preserveSelectedVault
source="savings_header"
onBeforeOpen={() => {
useDepositStore.getState().setDepositFromSolid(false);
useDepositStore.getState().setDepositFromSolid(true);
}}
/>
</View>
Expand Down
115 changes: 104 additions & 11 deletions app/(protected)/(tabs)/stocks/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import React, { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { ScrollView, View } from 'react-native';
import { Redirect } from 'expo-router';
import { Redirect, router, useLocalSearchParams } from 'expo-router';

import PageLayout from '@/components/PageLayout';
import BuyStockModal from '@/components/Stocks/BuyStockModal';
import SellStockModal from '@/components/Stocks/SellStockModal';
import { Holding, STOCKS } from '@/components/Stocks/stocksData';
import StocksDiscoverSection from '@/components/Stocks/StocksDiscoverSection';
import { XStockToken } from '@/hooks/useXStocksTokens';
import StocksEmptyHoldings from '@/components/Stocks/StocksEmptyHoldings';
import StocksHoldingsList from '@/components/Stocks/StocksHoldingsList';
import StocksPendingStrip from '@/components/Stocks/StocksPendingStrip';
import StocksPortfolioCard from '@/components/Stocks/StocksPortfolioCard';
import { Text } from '@/components/ui/text';
import { path } from '@/constants/path';
import { XSTOCKS_TOKENS } from '@/constants/xstocksTokens';
import { useDimension } from '@/hooks/useDimension';
import { useXStockHoldings } from '@/hooks/useXStockHoldings';
import { useXStockPrices } from '@/hooks/useXStockPrices';
import { XStockToken } from '@/hooks/useXStocksTokens';
import { isProduction } from '@/lib/config';

// Stocks is an in-development feature: not accessible in production builds.
Expand All @@ -33,7 +34,7 @@ function StocksPageContent() {
const { isScreenMedium } = useDimension();
const scrollRef = useRef<ScrollView>(null);

const { holdings } = useXStockHoldings();
const { holdings, isLoading: isHoldingsLoading } = useXStockHoldings();
const hasHoldings = holdings.length > 0;
const holdingTickers = holdings.map(h => h.ticker);
const holdingPrices = useXStockPrices(holdingTickers);
Expand All @@ -45,33 +46,106 @@ function StocksPageContent() {
const [buyModalOpen, setBuyModalOpen] = useState(false);
const [sellModalOpen, setSellModalOpen] = useState(false);
const [selectedHolding, setSelectedHolding] = useState<Holding | null>(null);
// Which stock the buy flow should open on. Null means "let the user pick".
const [buyToken, setBuyToken] = useState<XStockToken | null>(null);
// True when the buy flow was opened by a deep link rather than by browsing
// this screen — leaving it should return the user where they came from
// instead of stranding them on a Stocks tab they never chose to open.
const [cameFromDeepLink, setCameFromDeepLink] = useState(false);

// Open a trade straight from a `/stocks?ticker=TSLAx&action=sell` deep link
// (the Earn page uses it), then clear the params so closing the modal doesn't
// leave a link that reopens it on the next visit.
const { ticker: tickerParam, action: actionParam } = useLocalSearchParams<{
ticker?: string;
action?: string;
}>();

useEffect(() => {
if (!tickerParam) return;

const clearParams = () => router.setParams({ ticker: undefined, action: undefined });

if (actionParam === 'sell') {
const holding = holdings.find(h => h.ticker === tickerParam);

// Holdings are read from chain and arrive after this screen mounts, so
// the params are held until they land rather than dropped on the floor.
if (!holding) {
if (!isHoldingsLoading) clearParams();
return;
}

setSelectedHolding(holding);
setSellModalOpen(true);
setCameFromDeepLink(true);
clearParams();
return;
}

const token = XSTOCKS_TOKENS.find(t => t.symbol === tickerParam);
if (token) {
setBuyToken(token);
setBuyModalOpen(true);
setCameFromDeepLink(true);
}
clearParams();
}, [tickerParam, actionParam, holdings, isHoldingsLoading]);

function handleBuyPress() {
setBuyToken(null);
setCameFromDeepLink(false);
setBuyModalOpen(true);
}

function handleSellPress() {
if (holdings.length > 0) {
setSelectedHolding(holdings[0]);
setCameFromDeepLink(false);
setSellModalOpen(true);
}
}

function handleStockPress(_token: XStockToken) {
function handleStockPress(token: XStockToken) {
setBuyToken(token);
setCameFromDeepLink(false);
setBuyModalOpen(true);
}

// Leaving a deep-linked buy flow returns to the screen that opened it (the
// Earn catalog); leaving one started here just closes the modal.
function handleBuyClose() {
setBuyModalOpen(false);
if (cameFromDeepLink && router.canGoBack()) {
setCameFromDeepLink(false);
router.back();
}
}

function handleHoldingPress(holding: Holding) {
setSelectedHolding(holding);
setCameFromDeepLink(false);
setSellModalOpen(true);
}

// Same as the buy flow: a deep-linked sale returns to the screen that opened
// it rather than stranding the user on a Stocks tab they never chose.
function handleSellClose() {
setSellModalOpen(false);
if (cameFromDeepLink && router.canGoBack()) {
setCameFromDeepLink(false);
router.back();
}
}

function scrollToDiscover() {
scrollRef.current?.scrollToEnd({ animated: true });
}

const selectedStockPrice = selectedHolding
? (holdingPrices[selectedHolding.ticker] ?? STOCKS.find(s => s.ticker === selectedHolding.ticker)?.price ?? 194.23)
? (holdingPrices[selectedHolding.ticker] ??
STOCKS.find(s => s.ticker === selectedHolding.ticker)?.price ??
194.23)
: 194.23;

if (isScreenMedium) {
Expand All @@ -86,11 +160,12 @@ function StocksPageContent() {
onStockPress={handleStockPress}
onHoldingPress={handleHoldingPress}
buyModalOpen={buyModalOpen}
buyToken={buyToken}
sellModalOpen={sellModalOpen}
selectedHolding={selectedHolding}
selectedStockPrice={selectedStockPrice}
onBuyClose={() => setBuyModalOpen(false)}
onSellClose={() => setSellModalOpen(false)}
onBuyClose={handleBuyClose}
onSellClose={handleSellClose}
/>
);
}
Expand Down Expand Up @@ -140,13 +215,22 @@ function StocksPageContent() {
</ScrollView>

{/* Modals (rendered outside scroll) */}
<BuyStockModal isOpen={buyModalOpen} onClose={() => setBuyModalOpen(false)} trigger={null} />
{/* Keyed on the stock so picking a different one re-seeds the flow's
initial step instead of reusing the previous selection. */}
<BuyStockModal
key={buyToken?.symbol ?? 'picker'}
isOpen={buyModalOpen}
initialToken={buyToken}
onClose={handleBuyClose}
onExit={handleBuyClose}
trigger={null}
/>

<SellStockModal
holding={selectedHolding}
stockPrice={selectedStockPrice}
isOpen={sellModalOpen}
onClose={() => setSellModalOpen(false)}
onClose={handleSellClose}
trigger={null}
/>
</PageLayout>
Expand All @@ -163,6 +247,7 @@ type DesktopLayoutProps = {
onStockPress: (token: XStockToken) => void;
onHoldingPress: (holding: Holding) => void;
buyModalOpen: boolean;
buyToken: XStockToken | null;
sellModalOpen: boolean;
selectedHolding: Holding | null;
selectedStockPrice: number;
Expand All @@ -180,6 +265,7 @@ function DesktopLayout({
onStockPress,
onHoldingPress,
buyModalOpen,
buyToken,
sellModalOpen,
selectedHolding,
selectedStockPrice,
Expand Down Expand Up @@ -229,7 +315,14 @@ function DesktopLayout({
</View>
</View>

<BuyStockModal isOpen={buyModalOpen} onClose={onBuyClose} trigger={null} />
<BuyStockModal
key={buyToken?.symbol ?? 'picker'}
isOpen={buyModalOpen}
initialToken={buyToken}
onClose={onBuyClose}
onExit={onBuyClose}
trigger={null}
/>

<SellStockModal
holding={selectedHolding}
Expand Down
Binary file modified assets/images/earn-eth-icon.png
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/earn-eth-tile-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/earn-fuse-icon.png
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/earn-fuse-tile-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/earn-usd-icon.png
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/earn-usd-tile-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
110 changes: 110 additions & 0 deletions components/BuyCrypto/OnramperWidget/OnramperWidget.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useCallback, useState } from 'react';
import { Pressable, View } from 'react-native';
import { WebView, type WebViewNavigation } from 'react-native-webview';
import { openBrowserAsync } from 'expo-web-browser';

import { Text } from '@/components/ui/text';
import useOnramperWidget from '@/hooks/useOnramperWidget';

import {
ONRAMPER_WIDGET_HEIGHT,
OnramperWidgetError,
OnramperWidgetLoading,
type OnramperWidgetProps,
} from './OnramperWidgetStates';

/**
* Our own scheme, which the signed URL names as the success/failure redirect.
* Matches `scheme` in app.config.ts and ONRAMPER_APP_SCHEME on the backend.
*/
const APP_SCHEME = 'solid://';

/**
* Native: Onramper's hosted widget in a WebView.
*
* `about:blank` and `about:srcdoc` belong in `originWhitelist` because
* providers render themselves in nested frames — without them the flow simply
* dead-ends part way in, with no error and no callback. `allowsInlineMediaPlayback`
* is what lets camera-based KYC draw inline on iOS rather than not at all, and
* `mediaCapturePermissionGrantType` answers the WebView's own camera prompt,
* which is asked separately from the OS permission.
*/
export const OnramperWidget = ({ onOutcome }: OnramperWidgetProps) => {
const { data: session, isPending, isError, refetch } = useOnramperWidget();
const [isOpeningBrowser, setIsOpeningBrowser] = useState(false);

/**
* Intercept our own scheme rather than letting the WebView try to navigate
* to it — it cannot, and the flow would stall on a failed load.
*/
const handleShouldStartLoad = useCallback(
(request: WebViewNavigation) => {
if (!request.url.startsWith(APP_SCHEME)) return true;

onOutcome?.(request.url.includes('/failure') ? 'failure' : 'success');

return false;
},
[onOutcome],
);

/**
* The escape hatch, and not an optional one.
*
* Google Pay and ACH lean on browser payment APIs a WebView often lacks, and
* they fail silently — a button that never appears, a flow that goes nowhere,
* no error and no event. Onramper has no visibility once the user is with a
* provider, so nothing tells us it happened. The same signed URL in a Custom
* Tab or SFSafariViewController has the APIs, and still carries the deep-link
* redirects, so the user lands back here.
*/
const openInBrowser = useCallback(async () => {
if (!session) return;

setIsOpeningBrowser(true);
try {
await openBrowserAsync(session.url);
} catch (error) {
console.error('Failed to open the Onramper widget in a browser:', error);
} finally {
setIsOpeningBrowser(false);
}
}, [session]);

if (isPending) return <OnramperWidgetLoading />;
if (isError || !session) {
return <OnramperWidgetError onRetry={() => void refetch()} />;
}

return (
<View className="w-full gap-y-3">
<View
className="w-full overflow-hidden rounded-2xl"
style={{ height: ONRAMPER_WIDGET_HEIGHT }}
>
<WebView
source={{ uri: session.url }}
originWhitelist={['https://*', 'http://*', 'about:blank', 'about:srcdoc']}
allowsInlineMediaPlayback
mediaPlaybackRequiresUserAction={false}
mediaCapturePermissionGrantType="grant"
javaScriptEnabled
domStorageEnabled
onShouldStartLoadWithRequest={handleShouldStartLoad}
style={{ backgroundColor: 'transparent' }}
/>
</View>

<Pressable
className="h-11 items-center justify-center rounded-full"
onPress={() => void openInBrowser()}
disabled={isOpeningBrowser}
accessibilityRole="button"
>
<Text className="text-sm font-medium text-white/70">Having trouble? Open in browser</Text>
</Pressable>
</View>
);
};

export default OnramperWidget;
3 changes: 3 additions & 0 deletions components/BuyCrypto/OnramperWidget/OnramperWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Platform default (bundler/TS): the iframe implementation. Metro resolves
// OnramperWidget.native.tsx on iOS and Android automatically.
export { default, OnramperWidget } from './OnramperWidget.web';
Loading
Loading