Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8dfbf27
feat: normalizeTokenAddress, dualChainConnect, useQuery migration
akbarsaputrait Jul 13, 2026
2aa5c5b
chore: version bumps, build script updates, bundle analysis
akbarsaputrait Jul 13, 2026
66fbd34
fix(dualChainConnect): skip injector page on mobile dual-chain tap
akbarsaputrait Jul 13, 2026
d91171c
feat(mobile): unified wallet chooser, eager hydration, deposit addres…
akbarsaputrait Jul 13, 2026
0cc1768
chore: version bumps, bundle analysis, lockfile
akbarsaputrait Jul 13, 2026
8aa6c0e
fix(mobile): dedup injected wallets, img dimensions, deposit address …
akbarsaputrait Jul 13, 2026
aa0b54e
chore: version bumps, bundle analysis, lockfile
akbarsaputrait Jul 13, 2026
0bee72f
fix(ios): open deeplink via <a> tag click, skip WAITING_WALLET interm…
akbarsaputrait Jul 13, 2026
d9b6566
fix(sdk): guard isDNTEnabled against SSR (window undefined)
akbarsaputrait Jul 14, 2026
dd9838b
feat(sdk): emit payment_no_tokens_available on empty SelectToken
akbarsaputrait Jul 14, 2026
dfbd4f7
chore: bump sdk version
akbarsaputrait Jul 14, 2026
5e15689
docs: SSR telemetry fix + payment_no_tokens_available event
akbarsaputrait Jul 14, 2026
7b954f4
docs: revert CHANGELOG 0.1.36-beta.5 entry (non-beta releases only)
akbarsaputrait Jul 14, 2026
d4725da
docs(e2e): document NEXT_PUBLIC_E2E gate for headless Stellar signer
akbarsaputrait Jul 14, 2026
732a5ac
fix(sdk): allow payment options fetch for every appId, incl. default
akbarsaputrait Jul 14, 2026
1cfdcca
chore(release): v0.1.36
akbarsaputrait Jul 14, 2026
fbf7c91
chore: bumpp sdk version to 0.1.36
akbarsaputrait Jul 14, 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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.1.36] - 2026-07-14

### &nbsp;&nbsp;&nbsp;🚀 Features

- NormalizeTokenAddress, dualChainConnect, useQuery migration &nbsp;-&nbsp; by @akbarsaputrait [<samp>(8dfbf)</samp>](https://github.com/RozoAI/intent-pay/commit/8dfbf278)
- **mobile**: Unified wallet chooser, eager hydration, deposit address loading state &nbsp;-&nbsp; by @akbarsaputrait [<samp>(d9117)</samp>](https://github.com/RozoAI/intent-pay/commit/d91171c3)
- **sdk**: Emit payment_no_tokens_available on empty SelectToken &nbsp;-&nbsp; by @akbarsaputrait [<samp>(dd983)</samp>](https://github.com/RozoAI/intent-pay/commit/dd9838b2)

### &nbsp;&nbsp;&nbsp;🐞 Bug Fixes

- **connectkit**:
- Remove memo requirement from Solana payment flows &nbsp;-&nbsp; by @akbarsaputrait [<samp>(c98b4)</samp>](https://github.com/RozoAI/intent-pay/commit/c98b46fe)
- Update memo requirement for Stellar payments &nbsp;-&nbsp; by @akbarsaputrait [<samp>(86934)</samp>](https://github.com/RozoAI/intent-pay/commit/869342e2)
- **dualChainConnect**:
- Skip injector page on mobile dual-chain tap &nbsp;-&nbsp; by @akbarsaputrait [<samp>(66fbd)</samp>](https://github.com/RozoAI/intent-pay/commit/66fbd342)
- **ios**:
- Open deeplink via <a> tag click, skip WAITING_WALLET intermediate page &nbsp;-&nbsp; by @akbarsaputrait [<samp>(0bee7)</samp>](https://github.com/RozoAI/intent-pay/commit/0bee72f3)
- **mobile**:
- Dedup injected wallets, img dimensions, deposit address order &nbsp;-&nbsp; by @akbarsaputrait [<samp>(8aa6c)</samp>](https://github.com/RozoAI/intent-pay/commit/8aa6c0ec)
- **sdk**:
- Guard isDNTEnabled against SSR (window undefined) &nbsp;-&nbsp; by @akbarsaputrait [<samp>(d9b65)</samp>](https://github.com/RozoAI/intent-pay/commit/d9b6566f)
- Allow payment options fetch for every appId, incl. default &nbsp;-&nbsp; by @akbarsaputrait [<samp>(732a5)</samp>](https://github.com/RozoAI/intent-pay/commit/732a5ac4)

---


## [0.1.34] - 2026-07-07

### &nbsp;&nbsp;&nbsp;🚀 Features
Expand Down
1 change: 1 addition & 0 deletions docs/ANALYTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The SDK sends anonymous payment funnel events to RozoAI's PostHog project. This
| `payment_completed` | On-chain confirmed | `destination_chain`, `app_name` |
| `payment_failed` | Terminal failure | `error_type`, `destination_chain`, `app_name` |
| `payment_cancelled` | Modal closed mid-flow | `last_state`, `reason`, `app_name` |
| `payment_no_tokens_available` | SelectToken empty state renders (wallet connected, no eligible balances) | `token_mode`, `connected_chains`, `preferred_token_symbols`, `preferred_token_count`, `app_name` |
| `error_occurred` | Error page shown | `error_type`, `error_title`, `can_retry`, `app_name` |
| `payment_validation_error` | Invalid chain/token config | `error_type`, `app_name` |

Expand Down
21 changes: 21 additions & 0 deletions docs/PROVIDER_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,27 @@ export function Providers({ children }) {

---

### ⚠️ SSR `ReferenceError: window is not defined` from telemetry (fixed in 0.1.36+)

Before 0.1.36, the SDK's `isDNTEnabled()` telemetry guard touched
`window.doNotTrack` unconditionally. Under Next.js SSR, `window` is undefined,
so rendering `<RozoPayProvider>` on the server threw and returned HTTP 500 for
the entire page.

**Workaround (older SDK versions):** pass `telemetry={false}` to disable the
built-in telemetry path.

```tsx
<RozoPayProvider telemetry={false} ...>
```

**On 0.1.36+:** the guard is SSR-safe (`typeof window === "undefined"` short-
circuits before any `window.*` access). Host apps can drop the
`telemetry={false}` workaround unless they want to opt out of telemetry for
privacy reasons — see [ANALYTICS.md](./ANALYTICS.md#opting-out).

---

### ❌ Using SDK hooks outside the provider

```tsx
Expand Down
48 changes: 39 additions & 9 deletions docs/e2e/06-wallet-stellar.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

Stellar uses a different approach: the SDK accepts an injectable `stellarKit` prop,
so instead of automating a browser extension, a secret-key signer runs inside the
page. Playwright injects the secret at runtime — it never touches the app bundle or
any env var the app reads — so this signer cannot activate in production.
page. Playwright injects the secret at runtime and the app only wires the kit
when it was built with `NEXT_PUBLIC_E2E=1` — so the headless-signer code path
dead-code-eliminates out of production bundles at build time, and even in an
E2E build the kit does nothing unless Playwright has already injected a secret.

---

Expand Down Expand Up @@ -73,8 +75,21 @@ export function createHeadlessStellarKit(secret: string): StellarWalletsKit {
## 3. Wire the kit into your provider

In the component that renders `<RozoPayProvider />`, read the runtime-injected
window variable and pass the kit as the `stellarKit` prop. Use a `useState`
initializer so the kit is only constructed once per page load.
window variable and pass the kit as the `stellarKit` prop. Two safety gates:

1. `NEXT_PUBLIC_E2E` env flag — the Playwright `webServer` config sets this to
`1` when starting the dev server. In production builds it's `undefined`,
so Next.js's bundler tree-shakes the `require("@/lib/e2e-stellar-kit")`
call and the entire `@creit.tech/stellar-wallets-kit` module graph never
enters the client chunk.
2. `window.__E2E_STELLAR_SECRET__` — the actual secret. Even in an E2E build,
the kit is only constructed when Playwright injected the secret via
`addInitScript`. The provider deletes the property from `window`
immediately after reading it, so later scripts on the same page (analytics,
third-party widgets) can't scrape it.

Use a `useState` initializer so the kit is only constructed once per page
load — the kit registers a custom element and throws on a second construction.

```tsx
import { createHeadlessStellarKit } from "@/lib/e2e-stellar-kit"
Expand All @@ -83,10 +98,21 @@ import type { StellarWalletsKit } from "@creit.tech/stellar-wallets-kit"
// Inside your provider component:
const [stellarKit] = useState<StellarWalletsKit | undefined>(() => {
if (typeof window === "undefined") return undefined
const secret = (window as Window & { __E2E_STELLAR_SECRET__?: string })
.__E2E_STELLAR_SECRET__
// Env-flag gate — lets the bundler drop the entire import graph in prod.
if (!process.env.NEXT_PUBLIC_E2E) return undefined

const w = window as Window & { __E2E_STELLAR_SECRET__?: string }
const secret = w.__E2E_STELLAR_SECRET__
// Delete immediately so the key isn't readable later in the session.
delete w.__E2E_STELLAR_SECRET__
if (!secret) return undefined

try {
// require() inside the guarded branch: only resolved at build time when
// NEXT_PUBLIC_E2E=1, so the stellarWalletsKit bundle never enters the
// production chunk graph.
const { createHeadlessStellarKit } =
require("@/lib/e2e-stellar-kit") as typeof import("@/lib/e2e-stellar-kit")
return createHeadlessStellarKit(secret)
} catch (err) {
console.error("[E2E] Failed to create headless Stellar kit:", err)
Expand All @@ -101,9 +127,9 @@ return (
)
```

Because the kit only builds when `__E2E_STELLAR_SECRET__` is on `window` — and
Playwright is the only thing that sets it — the headless signer is impossible to
activate in a real user session.
Both gates must hold to activate the signer: the build must have been started
with `NEXT_PUBLIC_E2E=1` **and** Playwright must inject the secret before the
page loads. Neither is reachable from a real user session.

---

Expand Down Expand Up @@ -145,5 +171,9 @@ test.describe("Bridge: Stellar → EVM (mainnet, real funds)", () => {
## Notes

- `useStellarSigner` uses `page.addInitScript` — it must be called before navigation.
- Playwright starts the dev server with `NEXT_PUBLIC_E2E=1` (see
`examples/nextjs-app/e2e/playwright.config.ts` → `webServer.command:
"NEXT_PUBLIC_E2E=1 pnpm dev"`). Production `pnpm build` runs without the
flag, so the headless-kit import is dead-code-eliminated and never ships.
- No browser extension needed; Stellar flows can run headless.
- Source wallet needs XLM for fees and USDC to send. Destination wallet needs a USDC trustline.
5 changes: 4 additions & 1 deletion examples/nextjs-app/.env.e2e.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ E2E_EVM_ADDRESS=""
# - ADDRESS (G...) receives when Stellar is the DESTINATION → needs a trustline.
#
# The test injects the SECRET into the app at runtime, so you do NOT need to set
# any NEXT_PUBLIC_ secret by hand. Must be funded and have a USDC trustline.
# any NEXT_PUBLIC_ secret by hand. The Playwright webServer starts the dev
# server with NEXT_PUBLIC_E2E=1, which is what wires the headless Stellar kit;
# production `pnpm build` runs without the flag, so the signer never ships.
# Must be funded and have a USDC trustline.
E2E_STELLAR_SECRET=""
E2E_STELLAR_ADDRESS=""

Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ playwright-report/

# chainwright wallet cache
.wallet-cache/

.wallet-context/
4 changes: 2 additions & 2 deletions examples/nextjs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
},
"dependencies": {
"@creit.tech/stellar-wallets-kit": "^1.9.5",
"@rozoai/intent-common": "0.1.21",
"@rozoai/intent-pay": "0.1.34",
"@rozoai/intent-common": "0.1.23",
"@rozoai/intent-pay": "0.1.36",
"@stellar/stellar-sdk": "^14.6.1",
"@tanstack/react-query": "^5.0.0",
"class-variance-authority": "^0.7.1",
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
"test:e2e": "pnpm --filter \"examples/nextjs-app\" test:e2e",
"lint": "pnpm --filter @rozoai/intent-common run lint && pnpm --filter @rozoai/intent-pay run lint && pnpm --filter examples/nextjs-app run lint",
"format": "pnpm --filter @rozoai/intent-common run format && pnpm --filter @rozoai/intent-pay run format && pnpm --filter examples/nextjs-app run format",
"build": "pnpm run build:common && pnpm run build:pay",
"build": "pnpm run build:common && pnpm run build:pay && pnpm run build:example",
"build:common": "cd packages/pay-common && pnpm run build",
"build:pay": "cd packages/connectkit && pnpm run build",
"build:example": "cd examples/nextjs-app && pnpm run build",
"dev": "pnpm run dev:common & pnpm run dev:pay & pnpm run dev:example:16",
"dev:common": "cd packages/pay-common && pnpm run dev",
"dev:pay": "cd packages/connectkit && pnpm run dev",
"dev:example": "cd examples/nextjs-app && pnpm run dev",
"dev:example:16": "cd examples/nextjs-16 && pnpm run dev",
"start:example": "cd examples/nextjs-app && pnpm run start",
"install:local": "pnpm install",
"prepare": "husky",
"release": "pnpm run build && cd packages/pay-common && pnpm publish && cd ../../packages/connectkit && pnpm run release",
Expand Down
2 changes: 1 addition & 1 deletion packages/connectkit/bundle-analysis.html

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions packages/connectkit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rozoai/intent-pay",
"version": "0.1.34",
"version": "0.1.36",
"private": false,
"description": "Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.",
"keywords": [
Expand Down Expand Up @@ -48,7 +48,7 @@
"@reown/appkit": "^1.7.0",
"@rollup/plugin-image": "^3.0.3",
"@rollup/plugin-typescript": "^12.1.2",
"@rozoai/intent-common": "0.1.21",
"@rozoai/intent-common": "0.1.23",
"@solana/spl-memo": "^0.2.5",
"@solana/spl-token": "^0.4.14",
"@solana/wallet-adapter-base": "^0.9.27",
Expand Down
62 changes: 18 additions & 44 deletions packages/connectkit/src/components/Common/ConnectorList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
ExternalPaymentOptions,
ExternalPaymentOptionsString,
} from "@rozoai/intent-common";
import { ExternalPaymentOptions, ExternalPaymentOptionsString } from "@rozoai/intent-common";
import { useWallet as useSolanaWalletAdapter } from "@solana/wallet-adapter-react";
import { useEffect, useMemo } from "react";
import { ROUTES } from "../../../constants/routes";
Expand Down Expand Up @@ -29,11 +26,7 @@ import {
SkeletonLabel,
} from "./styles";

const ConnectorList = ({
customDeeplink = null,
}: {
customDeeplink?: string | null;
}) => {
const ConnectorList = ({ customDeeplink = null }: { customDeeplink?: string | null }) => {
const context = usePayContext();
const { isMobile } = useIsMobile();

Expand All @@ -55,15 +48,12 @@ const ConnectorList = ({
// If customDeeplink is provided, we don't need to wait for hydration
const shouldWaitForHydration = useMemo(
() => !customDeeplink && isMobile && !context.paymentState.isDepositFlow,
[customDeeplink, isMobile, context.paymentState.isDepositFlow]
[customDeeplink, isMobile, context.paymentState.isDepositFlow],
);

const ready = useMemo(
() =>
customDeeplink ||
!shouldWaitForHydration ||
paymentState === "payment_unpaid",
[customDeeplink, shouldWaitForHydration, paymentState]
() => customDeeplink || !shouldWaitForHydration || paymentState === "payment_unpaid",
[customDeeplink, shouldWaitForHydration, paymentState],
);

useEffect(() => {
Expand All @@ -79,20 +69,14 @@ const ConnectorList = ({
<Alert error>No connectors found in ConnectKit config.</Alert>
)}
{!ready && walletsToDisplay.length > 0 && (
<ConnectorsContainer
$mobile={isMobile}
$totalResults={walletsToDisplay.length}
>
<ConnectorsContainer $mobile={isMobile} $totalResults={walletsToDisplay.length}>
{walletsToDisplay.map((_, idx) => (
<SkeletonConnectorItem key={idx} />
))}
</ConnectorsContainer>
)}
{ready && walletsToDisplay.length > 0 && (
<ConnectorsContainer
$mobile={isMobile}
$totalResults={walletsToDisplay.length}
>
<ConnectorsContainer $mobile={isMobile} $totalResults={walletsToDisplay.length}>
{walletsToDisplay.map((wallet) => (
<ConnectorItem
key={wallet.id}
Expand Down Expand Up @@ -124,8 +108,7 @@ const ConnectorItem = ({
const solanaWallets = useSolanaWalletAdapter();

// The "Other" 2x2 connector, goes to the MobileConnectors page.
const redirectToMoreWallets =
isMobile && wallet.id === WALLET_ID_OTHER_WALLET;
const redirectToMoreWallets = isMobile && wallet.id === WALLET_ID_OTHER_WALLET;
const redirectToMobileWallets = wallet.id === WALLET_ID_MOBILE_WALLETS;

// Safari requires opening popup on user gesture, so we connect immediately here
Expand All @@ -143,10 +126,10 @@ const ConnectorItem = ({
}

const hasEthereum = paymentOptions.includes(
ExternalPaymentOptions.Ethereum as ExternalPaymentOptionsString
ExternalPaymentOptions.Ethereum as ExternalPaymentOptionsString,
);
const hasSolana = paymentOptions.includes(
ExternalPaymentOptions.Solana as ExternalPaymentOptionsString
ExternalPaymentOptions.Solana as ExternalPaymentOptionsString,
);

return { hasEthereum, hasSolana };
Expand Down Expand Up @@ -199,9 +182,11 @@ const ConnectorItem = ({
// wallet-standard provider (e.g. Phantom): connect both chains at once
// instead of forcing a chain choice. SELECT_METHOD already shows
// separate "Pay with [eth]"/"Pay with [sol]" tiles once both are connected.
// Connect EVM directly (no intermediate injector page — we're already inside
// the wallet's in-app browser) and select Solana simultaneously.
if (isMobile && wallet.connector && wallet.solanaConnectorName) {
context.setPendingConnectorId(wallet.id);
context.setRoute(ROUTES.CONNECT, meta);
context.setDualChainConnect(true);
connect({ connector: wallet.connector });
solanaWallets.select(wallet.solanaConnectorName);
return;
}
Expand All @@ -216,18 +201,10 @@ const ConnectorItem = ({
context.setPendingConnectorId(WALLET_ID_MOBILE_WALLETS);
context.setRoute(ROUTES.CONNECT, meta);
}
} else if (
context.paymentState.isDepositFlow &&
isMobile &&
!wallet.connector
) {
} else if (context.paymentState.isDepositFlow && isMobile && !wallet.connector) {
context.paymentState.setSelectedWallet(wallet);
context.setRoute(ROUTES.SELECT_WALLET_AMOUNT, meta);
} else if (
isMobile &&
wallet.getRozoPayDeeplink != null &&
!wallet.connector
) {
} else if (isMobile && wallet.getRozoPayDeeplink != null && !wallet.connector) {
context.paymentState.openInWalletBrowser({
wallet,
customDeeplink: customDeeplink ?? undefined,
Expand All @@ -243,14 +220,11 @@ const ConnectorItem = ({

return (
<ConnectorButton type="button" onClick={onClick}>
<ConnectorIcon
data-small={wallet.iconShouldShrink}
data-shape={wallet.iconShape}
>
<ConnectorIcon data-small={wallet.iconShouldShrink} data-shape={wallet.iconShape}>
{wallet.iconConnector ?? wallet.icon}
</ConnectorIcon>
<ConnectorLabel>
{isMobile ? wallet.shortName ?? wallet.name : wallet.name}
{isMobile ? (wallet.shortName ?? wallet.name) : wallet.name}
{!context.options?.hideRecentBadge && isRecent && (
<RecentlyUsedTag>
<span>Recent</span>
Expand Down
Loading
Loading