Skip to content

Commit 58f95fc

Browse files
feat: detect preferredTokens and filtered for payment options
1 parent 836fabf commit 58f95fc

File tree

12 files changed

+811
-623
lines changed

12 files changed

+811
-623
lines changed

examples/nextjs-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"@headlessui/react": "^2.2.0",
1515
"@rainbow-me/rainbowkit": "^2.2.8",
1616
"@rozoai/intent-common": "0.1.7-beta.2",
17-
"@rozoai/intent-pay": "0.1.7-beta.6",
17+
"@rozoai/intent-pay": "0.1.7-beta.7",
1818
"@tanstack/react-query": "^5.51.11",
1919
"@types/react-syntax-highlighter": "^15.5.13",
2020
"@wagmi/core": "^2.22.0",

examples/nextjs-app/src/app/basic/README.md

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,19 @@ This demo showcases a minimal implementation of `RozoPayButton` that enables cro
2929
The main payment component that handles the entire payment flow:
3030

3131
```typescript
32+
import { TokenSymbol } from "@rozoai/intent-common";
33+
import { RozoPayButton } from "@rozoai/intent-pay";
34+
3235
<RozoPayButton
3336
appId="your-app-id"
3437
toChain={chainId}
3538
toAddress={recipientAddress}
3639
toToken={tokenAddress}
3740
toUnits={amount}
41+
preferredSymbol={[TokenSymbol.USDC, TokenSymbol.USDT]}
3842
onPaymentStarted={(event) => console.log(event)}
3943
onPaymentCompleted={(event) => console.log(event)}
40-
/>
44+
/>;
4145
```
4246

4347
### Event Callbacks
@@ -48,6 +52,48 @@ Track payment lifecycle with built-in callbacks:
4852
- `onPaymentCompleted`: Triggered when payment is confirmed on-chain
4953
- `onPayoutCompleted`: Triggered when funds arrive at destination
5054

55+
### Preferred Token Symbols
56+
57+
The `preferredSymbol` prop allows you to specify which token symbols should appear first in the token selection list. This is useful for prioritizing specific stablecoins across all supported chains.
58+
59+
**Key Features:**
60+
61+
- **Supported Symbols**: Only `USDC`, `USDT`, and `EURC` are allowed
62+
- **Default Behavior**: If not provided, defaults to `[USDC, USDT]`
63+
- **Cross-Chain**: Automatically finds matching tokens across all supported chains (Base, Polygon, Ethereum, Solana, Stellar)
64+
- **Precedence**: If `preferredTokens` is explicitly provided, it takes precedence over `preferredSymbol`
65+
66+
**Example Usage:**
67+
68+
```typescript
69+
import { TokenSymbol } from "@rozoai/intent-common";
70+
71+
// Prioritize USDC and USDT (default)
72+
<RozoPayButton
73+
preferredSymbol={[TokenSymbol.USDC, TokenSymbol.USDT]}
74+
// ... other props
75+
/>
76+
77+
// Prioritize EURC only
78+
<RozoPayButton
79+
preferredSymbol={[TokenSymbol.EURC]}
80+
// ... other props
81+
/>
82+
83+
// Multiple preferred symbols
84+
<RozoPayButton
85+
preferredSymbol={[TokenSymbol.USDC, TokenSymbol.USDT, TokenSymbol.EURC]}
86+
// ... other props
87+
/>
88+
```
89+
90+
**How It Works:**
91+
92+
- The `preferredSymbol` array is internally converted to a `preferredTokens` array
93+
- The SDK searches for all tokens matching the specified symbols across supported chains
94+
- These tokens are then prioritized in the token selection UI
95+
- Invalid symbols are filtered out with a console warning
96+
5197
## Developer Notes
5298

5399
- Uses `react-syntax-highlighter` for clean code display
@@ -57,29 +103,33 @@ Track payment lifecycle with built-in callbacks:
57103

58104
## Props Reference
59105

60-
| Prop | Type | Description |
61-
|------|------|-------------|
62-
| `appId` | `string` | Your RozoAI Intent Pay application ID |
63-
| `toChain` | `number` | Destination blockchain network ID |
64-
| `toAddress` | `Address` | Recipient wallet address (checksummed) |
65-
| `toToken` | `Address` | Token contract address to receive |
66-
| `toUnits` | `string` | Amount in token's smallest unit |
67-
| `onPaymentStarted` | `(event) => void` | Payment initiation callback |
68-
| `onPaymentCompleted` | `(event) => void` | Payment completion callback |
106+
| Prop | Type | Description |
107+
| -------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
108+
| `appId` | `string` | Your RozoAI Intent Pay application ID |
109+
| `toChain` | `number` | Destination blockchain network ID |
110+
| `toAddress` | `Address` | Recipient wallet address (checksummed) |
111+
| `toToken` | `Address` | Token contract address to receive |
112+
| `toUnits` | `string` | Amount in token's smallest unit |
113+
| `preferredSymbol` | `TokenSymbol[]` | Preferred token symbols (USDC, USDT, EURC). These tokens will appear first in the token selection list. Defaults to `[USDC, USDT]`. |
114+
| `onPaymentStarted` | `(event) => void` | Payment initiation callback |
115+
| `onPaymentCompleted` | `(event) => void` | Payment completion callback |
69116

70117
## Cross-Chain Payments
71118

72119
For Stellar/Solana destinations, use bridge configuration:
73120

74121
```typescript
122+
import { TokenSymbol } from "@rozoai/intent-common";
123+
75124
<RozoPayButton
76125
appId="your-app-id"
77126
toChain={8453} // Base Chain
78127
toToken="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" // Base USDC
79128
toAddress="0x..." // Any EVM address
80129
toStellarAddress="GABC..." // or toSolanaAddress
81130
toUnits="1000000"
82-
/>
131+
preferredSymbol={[TokenSymbol.USDC, TokenSymbol.USDT]}
132+
/>;
83133
```
84134

85135
## Learn More

examples/nextjs-app/src/app/basic/page.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,20 @@ export default function DemoBasic() {
500500
Amount in USD (e.g., 100.00)
501501
</dd>
502502
</div>
503+
<div>
504+
<dt className="font-mono font-semibold text-blue-800">
505+
preferredSymbol
506+
</dt>
507+
<dd className="text-gray-700 ml-4">
508+
Preferred token symbols (USDC, USDT, EURC). These tokens
509+
will appear first in the token selection list. Defaults to{" "}
510+
<code className="bg-blue-100 px-1 rounded">
511+
[USDC, USDT]
512+
</code>
513+
. Automatically finds matching tokens across all supported
514+
chains.
515+
</dd>
516+
</div>
503517
<div>
504518
<dt className="font-mono font-semibold text-blue-800">
505519
onPaymentStarted / onPaymentCompleted

packages/connectkit/bundle-analysis.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/connectkit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@rozoai/intent-pay",
33
"private": false,
4-
"version": "0.1.7-beta.6",
4+
"version": "0.1.7-beta.7",
55
"author": "RozoAI",
66
"homepage": "https://github.com/RozoAI/intent-pay",
77
"license": "BSD-2-Clause",

packages/connectkit/src/components/DaimoPayModal/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ export const RozoPayModal: React.FC<{
195195
setSelectedStellarTokenOption(undefined);
196196
context.setRoute(ROUTES.SELECT_TOKEN, meta);
197197
}
198+
} else if (context.route === ROUTES.SELECT_WALLET_CHAIN) {
199+
setSelectedWallet(undefined);
200+
context.setRoute(ROUTES.CONNECTORS, meta);
198201
} else {
199202
context.setRoute(ROUTES.SELECT_METHOD, meta);
200203
}

packages/connectkit/src/hooks/useSolanaPaymentOptions.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
setupRefreshState,
88
shouldSkipRefresh,
99
} from "./refreshUtils";
10+
import { useSupportedChains } from "./useSupportedChains";
1011

1112
/** Wallet payment options. User picks one. */
1213
export function useSolanaPaymentOptions({
@@ -34,10 +35,25 @@ export function useSolanaPaymentOptions({
3435
// Track if we have initial data to prevent clearing options on refresh (prevents flickering)
3536
const hasInitialData = useRef<boolean>(false);
3637

38+
const { chains } = useSupportedChains();
39+
40+
// Get Solana chain IDs from supported chains
41+
const solanaChainIds = useMemo(() => {
42+
return new Set(
43+
chains.filter((c) => c.type === "solana").map((c) => c.chainId)
44+
);
45+
}, [chains]);
46+
3747
const stableAppId = useMemo(() => {
3848
return payParams?.appId;
3949
}, [payParams]);
4050

51+
const memoizedPreferredTokens = useMemo(
52+
() => payParams?.preferredTokens,
53+
// eslint-disable-next-line react-hooks/exhaustive-deps
54+
[JSON.stringify(payParams?.preferredTokens)]
55+
);
56+
4157
const filteredOptions = useMemo(() => {
4258
if (!options) return [];
4359

@@ -96,11 +112,17 @@ export function useSolanaPaymentOptions({
96112
}
97113

98114
try {
115+
// Filter preferredTokenAddress to only include Solana chain tokens
116+
const solanaPreferredTokenAddresses = (memoizedPreferredTokens ?? [])
117+
.filter((t) => solanaChainIds.has(t.chainId))
118+
.map((t) => t.token);
119+
99120
const newOptions = await trpc.getSolanaPaymentOptions.query({
100121
pubKey: address,
101122
// API expects undefined for deposit flow.
102123
usdRequired: isDepositFlow ? undefined : usdRequired,
103124
appId: stableAppId,
125+
preferredTokenAddress: solanaPreferredTokenAddresses,
104126
});
105127
setOptions(newOptions);
106128
hasInitialData.current = true;
@@ -114,7 +136,14 @@ export function useSolanaPaymentOptions({
114136
isApiCallInProgress.current = false;
115137
setIsLoading(false);
116138
}
117-
}, [address, usdRequired, isDepositFlow, trpc, stableAppId]);
139+
}, [
140+
address,
141+
usdRequired,
142+
isDepositFlow,
143+
trpc,
144+
stableAppId,
145+
memoizedPreferredTokens,
146+
]);
118147

119148
// Create refresh function using shared utility
120149
const refreshOptions = createRefreshFunction(fetchBalances, {
@@ -145,6 +174,7 @@ export function useSolanaPaymentOptions({
145174
usdRequired,
146175
isDepositFlow,
147176
stableAppId,
177+
memoizedPreferredTokens,
148178
});
149179

150180
// Skip if we've already executed with these exact parameters
@@ -162,15 +192,22 @@ export function useSolanaPaymentOptions({
162192
lastExecutedParams,
163193
isApiCallInProgress,
164194
});
165-
}, [address, usdRequired, isDepositFlow, stableAppId]);
195+
}, [
196+
address,
197+
usdRequired,
198+
isDepositFlow,
199+
stableAppId,
200+
memoizedPreferredTokens,
201+
]);
166202

167203
// Initial fetch when hook mounts with valid parameters or when key parameters change
168204
useEffect(() => {
169205
if (address != null && usdRequired != null && stableAppId != null) {
170206
refreshOptions();
171207
}
172208
// refreshOptions is stable (created from fetchBalances which only changes when dependencies change)
173-
}, [address, usdRequired, stableAppId]); // eslint-disable-line react-hooks/exhaustive-deps
209+
// eslint-disable-next-line react-hooks/exhaustive-deps
210+
}, [address, usdRequired, stableAppId, memoizedPreferredTokens]);
174211

175212
return {
176213
options: filteredOptions,

packages/connectkit/src/hooks/useStellarPaymentOptions.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ export function useStellarPaymentOptions({
2323
isDepositFlow: boolean;
2424
payParams: PayParams | undefined;
2525
}) {
26-
const { tokens } = useSupportedChains();
26+
const { chains, tokens } = useSupportedChains();
27+
28+
// Get Stellar chain IDs from supported chains
29+
const stellarChainIds = useMemo(() => {
30+
return new Set(
31+
chains.filter((c) => c.type === "stellar").map((c) => c.chainId)
32+
);
33+
}, [chains]);
2734

2835
const [options, setOptions] = useState<WalletPaymentOption[] | null>(null);
2936
const [isLoading, setIsLoading] = useState(false);
@@ -102,14 +109,17 @@ export function useStellarPaymentOptions({
102109
setIsLoading(true);
103110

104111
try {
112+
// Filter preferredTokenAddress to only include Stellar chain tokens
113+
const stellarPreferredTokenAddresses = (memoizedPreferredTokens ?? [])
114+
.filter((t) => stellarChainIds.has(t.chainId))
115+
.map((t) => t.token);
116+
105117
const newOptions = await trpc.getStellarPaymentOptions.query({
106118
stellarAddress: address,
107119
// API expects undefined for deposit flow.
108120
usdRequired: isDepositFlow ? undefined : usdRequired,
109121
appId: stableAppId,
110-
preferredTokenAddress: (memoizedPreferredTokens ?? []).map(
111-
(t) => t.token
112-
),
122+
preferredTokenAddress: stellarPreferredTokenAddresses,
113123
});
114124
setOptions(newOptions);
115125
} catch (error) {
@@ -119,7 +129,16 @@ export function useStellarPaymentOptions({
119129
isApiCallInProgress.current = false;
120130
setIsLoading(false);
121131
}
122-
}, [address, usdRequired, isDepositFlow, trpc, stableAppId]);
132+
}, [
133+
address,
134+
usdRequired,
135+
isDepositFlow,
136+
trpc,
137+
stableAppId,
138+
memoizedPreferredTokens,
139+
// stellarChainIds is derived from chains and is stable, so we don't need it in deps
140+
// eslint-disable-next-line react-hooks/exhaustive-deps
141+
]);
123142

124143
// Create refresh function using shared utility
125144
const refreshOptions = createRefreshFunction(fetchBalances, {
@@ -143,6 +162,7 @@ export function useStellarPaymentOptions({
143162
usdRequired,
144163
isDepositFlow,
145164
stableAppId,
165+
memoizedPreferredTokens,
146166
});
147167

148168
// Skip if we've already executed with these exact parameters
@@ -160,14 +180,21 @@ export function useStellarPaymentOptions({
160180
lastExecutedParams,
161181
isApiCallInProgress,
162182
});
163-
}, [address, usdRequired, isDepositFlow, stableAppId]);
183+
}, [
184+
address,
185+
usdRequired,
186+
isDepositFlow,
187+
stableAppId,
188+
memoizedPreferredTokens,
189+
]);
164190

165191
// Initial fetch when hook mounts with valid parameters or when key parameters change
166192
useEffect(() => {
167193
if (address != null && usdRequired != null && stableAppId != null) {
168194
refreshOptions();
169195
}
170-
}, [address, usdRequired, stableAppId]);
196+
// eslint-disable-next-line react-hooks/exhaustive-deps
197+
}, [address, usdRequired, stableAppId, memoizedPreferredTokens]);
171198

172199
return {
173200
options: filteredOptions,

0 commit comments

Comments
 (0)