Skip to content

Commit 2c9bd8e

Browse files
authored
fix(homepage): route production onramp through vechain/onramp-proxy (#655)
* fix(homepage): route production onramp through vechain/onramp-proxy #654 repointed deploy-cloudfront's NEXT_PUBLIC_TRANSAK_API_URL_PROD secret at vechain/onramp-proxy (GET contract) and fixed the next-template example's widgetUrlBuilder to match, but the actual production build (examples/homepage, deployed to vechainkit.vechain.org) was left on the old vechain-kit-infra POST/JSON contract — so the deployed widgetUrlBuilder now POSTs JSON to a GET-only endpoint and every production onramp call 405s. Ports the same production/staging branch already working in next-template. * fix(transak): widen the widget container to Transak's own 80dvh guidance Reported: the "Add card details" step rendered clipped/overlapping the quote screen underneath it inside the embedded widget. Root cause (confirmed against @transak/ui-js-sdk@1.0.0's actual runtime): in containerId mode the SDK always renders the iframe at width:100%; height:100% of its container — widgetWidth/widgetHeight config options are silently ignored in that mode (they only apply to the SDK's own standalone overlay, used when no containerId is passed). So the container's own CSS is what bounds the widget, and both copies of it (TransakCheckoutModal, TransakOnrampContent) capped it at maxH="620px" — well under Transak's own documented single-embed sizing of `height: 80dvh` (docs.transak.com/integration/web/iframe), which is closer to what the deeper flow steps (KYC, add-card-details) actually need. Raises both containers to 80dvh (560-900px clamp) to match. * fix(transak): widen the modal, add a loading spinner for the widget iframe Reported on the preview deploy: opening the widget showed a blank white container for a few seconds (the iframe loading Transak's remote app), and the whole flow felt cramped -- the modal wasn't using the available screen space. - useTransakCheckout: track `widgetReady`, flipped by the SDK's TRANSAK_WIDGET_INITIALISED event (previously unused). False from the moment the widget opens until the iframe has actually rendered. - TransakCheckoutModal / TransakOnrampContent: render a centered Spinner as a sibling overlay while `!widgetReady` -- never as a child of the SDK's own container div, since the SDK appends/removes the iframe via direct DOM calls (containerId mode) that would fight React for that node's children. - TransakCheckoutModal: BaseModal size 'sm' -> 'lg' (Transak's own single-embed sample uses a 500px-wide container; 'sm' was 384px). - AccountModal: the 'transak-onramp' content type now gets its own larger size/height ('lg', up to 90dvh) instead of the fixed 485px/'sm' every other AccountModal screen uses -- that fixed cap was squeezing an embedded external payment app (quote, KYC, add-card-details) into a size tuned for simple menu/settings screens. * feat(playground): add Payments example, total-balance-in-USD demo - New "Payments" page (mirrors the "Transactions" page's structure): PayWithTransakButton for the drop-in Buy VET button, plus a useTransakCheckout demo for a fully custom trigger. Added to the sidebar nav under Build, status NEW. - "Reading Data" page: new DemoSection for useTotalBalance -- one USD figure across liquid tokens + staking positions, the same total the AccountModal shows. - Wired transak config into the playground's VechainKitProviderWrapper (same dual-contract widgetUrlBuilder as the homepage example: prod -> vechain/onramp-proxy, staging -> vechain-kit-infra), gated on the new NEXT_PUBLIC_TRANSAK_* env vars (added to .env.example, all optional -- the Payments page degrades to the hook's built-in "not configured" error when unset, same as every other example integration here).
1 parent dab7335 commit 2c9bd8e

11 files changed

Lines changed: 415 additions & 24 deletions

File tree

‎examples/homepage/src/app/providers/VechainKitProviderWrapper.tsx‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,44 @@ export function VechainKitProviderWrapper({ children }: Props) {
215215
}) => {
216216
const apiUrl =
217217
process.env.NEXT_PUBLIC_TRANSAK_API_URL ?? '';
218+
const environment = process.env
219+
.NEXT_PUBLIC_TRANSAK_ENVIRONMENT as
220+
| 'staging'
221+
| 'production'
222+
| undefined;
223+
224+
// Production goes through vechain/onramp-proxy
225+
// (the shared VeChain onramp backend, GET
226+
// contract). Staging keeps the vechain-kit-infra
227+
// proxy: onramp-proxy is production-only.
228+
if (environment === 'production') {
229+
const params = new URLSearchParams({
230+
provider: 'transak',
231+
address: walletAddress,
232+
os: 'web',
233+
theme: 'LIGHT',
234+
currency: fiatCurrency,
235+
referrerDomain:
236+
typeof window !== 'undefined'
237+
? window.location.origin
238+
: '',
239+
});
240+
if (fiatAmount) {
241+
params.set('fiatAmount', fiatAmount);
242+
}
243+
const res = await fetch(
244+
`${apiUrl}/?${params.toString()}`,
245+
);
246+
const json = await res.json();
247+
if (!res.ok || !json?.url) {
248+
throw new Error(
249+
json?.error ??
250+
'Failed to create Transak widget URL',
251+
);
252+
}
253+
return json.url as string;
254+
}
255+
218256
const res = await fetch(
219257
`${apiUrl}/api/transak/widget-url`,
220258
{

‎examples/playground/.env.example‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,12 @@ NEXT_PUBLIC_DELEGATOR_URL=
77
# Network Configuration -- main | test | solo
88
NEXT_PUBLIC_NETWORK_TYPE=
99

10-
# Translations
10+
# Transak onramp -- staging key from dashboard.transak.com (leave NEXT_PUBLIC_TRANSAK_API_URL
11+
# empty to hide the "Payments" page's Buy VET demo)
12+
NEXT_PUBLIC_TRANSAK_API_KEY=
13+
# Mini-server (or vechain/onramp-proxy in production) base URL that mints the Secure Widget URL
14+
NEXT_PUBLIC_TRANSAK_API_URL=
15+
NEXT_PUBLIC_TRANSAK_ENVIRONMENT=staging
16+
17+
# Translations
1118
OPENAI_API_KEY=

‎examples/playground/src/app/(playground)/data/page.tsx‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
useGetTokenUsdPrice,
1616
useGetVot3Balance,
1717
useIsPerson,
18+
useTotalBalance,
1819
useWallet,
1920
} from '@vechain/vechain-kit';
2021
import { useTranslation } from 'react-i18next';
@@ -93,6 +94,14 @@ const { data: roundId } = useCurrentAllocationsRoundId();
9394
const { data: isPerson } = useIsPerson(address);
9495
`;
9596

97+
const TOTAL_BALANCE_SNIPPET = `import { useTotalBalance } from '@vechain/vechain-kit';
98+
99+
function PortfolioValue({ address }) {
100+
const { totalBalanceUsd, isLoading } = useTotalBalance({ address });
101+
return <p>{isLoading ? '…' : \`$\${totalBalanceUsd.toFixed(2)}\`}</p>;
102+
}
103+
`;
104+
96105
export default function DataPage() {
97106
const { t } = useTranslation();
98107
const { account } = useWallet();
@@ -103,6 +112,10 @@ export default function DataPage() {
103112
const { data: vetPrice, isLoading: l3 } = useGetTokenUsdPrice('VET');
104113
const { data: roundId } = useCurrentAllocationsRoundId();
105114
const { data: isPerson } = useIsPerson(address);
115+
// Liquid holdings + staking positions (Stargate, Navigator, BetterSwap
116+
// LP, Juicy), summed to one USD figure -- the same total the AccountModal
117+
// itself shows.
118+
const { totalBalanceUsd, isLoading: l4 } = useTotalBalance({ address });
106119

107120
return (
108121
<VStack align="stretch" spacing={6}>
@@ -142,6 +155,26 @@ export default function DataPage() {
142155
</VStack>
143156
</DemoSection>
144157

158+
<DemoSection
159+
title={t('Total balance')}
160+
description={t(
161+
'One USD figure across every liquid token plus staking positions (Stargate, Navigator, BetterSwap LP, Juicy) -- the same total the AccountModal shows.',
162+
)}
163+
hooks={['useTotalBalance']}
164+
status="STABLE"
165+
code={TOTAL_BALANCE_SNIPPET}
166+
aiPrompt={t(
167+
'Show the connected user\'s total portfolio value in USD using useTotalBalance from @vechain/vechain-kit. Display it as a large "$X,XXX.XX" figure with a Chakra UI Skeleton while loading, and show a small "no assets yet" hint when hasAnyBalance is false.',
168+
)}
169+
aiSkills={['vechain-kit']}
170+
>
171+
<DataRow
172+
label={t('Total balance')}
173+
value={`$${totalBalanceUsd.toFixed(2)}`}
174+
loading={l4}
175+
/>
176+
</DemoSection>
177+
145178
<DemoSection
146179
title={t('Token prices')}
147180
description={t(
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use client';
2+
3+
import { Heading, HStack, Text, VStack } from '@chakra-ui/react';
4+
import { PayWithTransakButton, useTransakCheckout } from '@vechain/vechain-kit';
5+
import { useTranslation } from 'react-i18next';
6+
import { DemoSection } from '../../components/demo/DemoSection';
7+
import { ConnectGate } from '../../components/demo/ConnectGate';
8+
9+
const BUTTON_SNIPPET = `import { PayWithTransakButton } from '@vechain/vechain-kit';
10+
11+
function BuyVetButton() {
12+
return (
13+
<PayWithTransakButton
14+
fiatAmount="30"
15+
fiatCurrency="USD"
16+
onSuccess={() => console.log('VET purchased!')}
17+
onError={(err) => console.error(err)}
18+
/>
19+
);
20+
}
21+
`;
22+
23+
const HOOK_SNIPPET = `import { useTransakCheckout } from '@vechain/vechain-kit';
24+
25+
function BuyVetCustom() {
26+
const { open, status, widgetReady } = useTransakCheckout(
27+
() => console.log('done'),
28+
);
29+
30+
return (
31+
<button onClick={() => open({ fiatAmount: '30', fiatCurrency: 'USD' })}>
32+
Buy VET
33+
</button>
34+
// status/widgetReady drive your own UI -- see TransakCheckoutModal
35+
// in the vechain-kit source for a full reference implementation.
36+
);
37+
}
38+
`;
39+
40+
function CustomHookDemo() {
41+
const { t } = useTranslation();
42+
const { open, status } = useTransakCheckout();
43+
44+
return (
45+
<VStack align="stretch" spacing={3}>
46+
<Text fontSize="sm">
47+
{t(
48+
'Same top-up rail, driven directly from useTransakCheckout instead of the pre-built button -- use this when you need your own trigger UI.',
49+
)}
50+
</Text>
51+
<HStack>
52+
<Text fontSize="xs" opacity={0.6}>
53+
{t('status')}: {status}
54+
</Text>
55+
</HStack>
56+
<button
57+
onClick={() => open({ fiatAmount: '30', fiatCurrency: 'USD' })}
58+
style={{
59+
padding: '8px 16px',
60+
borderRadius: 8,
61+
border: '1px solid rgba(128,128,128,0.3)',
62+
cursor: 'pointer',
63+
}}
64+
>
65+
{t('Buy $30 VET (custom trigger)')}
66+
</button>
67+
</VStack>
68+
);
69+
}
70+
71+
export default function PaymentsPage() {
72+
const { t } = useTranslation();
73+
74+
return (
75+
<VStack align="stretch" spacing={6}>
76+
<VStack align="stretch" spacing={2}>
77+
<Heading size="lg">{t('Payments')}</Heading>
78+
<Text opacity={0.7}>
79+
{t(
80+
'Let users top up their wallet with VET bought via card -- no exchange, no bridging. Powered by Transak.',
81+
)}
82+
</Text>
83+
</VStack>
84+
85+
<ConnectGate feature={t('Payments')}>
86+
<DemoSection
87+
title={t('Buy VET')}
88+
description={t(
89+
'Drop-in button: opens the Transak widget, tracks order status, and closes itself on success.',
90+
)}
91+
hooks={['PayWithTransakButton', 'useTransakCheckout']}
92+
status="NEW"
93+
code={BUTTON_SNIPPET}
94+
aiPrompt={t(
95+
'Add a "Buy VET" button to my Next.js app using PayWithTransakButton from @vechain/vechain-kit. Default to $30 USD, and show a toast on success/error. Requires a `transak.widgetUrlBuilder` configured on VeChainKitProvider that mints a Secure Widget URL from a backend -- see the "Fiat onramp" recipe in the vechain-kit docs for a reference proxy implementation.',
96+
)}
97+
aiSkills={['vechain-kit']}
98+
>
99+
<PayWithTransakButton fiatAmount="30" fiatCurrency="USD" />
100+
</DemoSection>
101+
102+
<DemoSection
103+
title={t('Custom trigger')}
104+
description={t(
105+
'The same flow via the raw hook, for a fully custom button/card instead of the pre-built one.',
106+
)}
107+
hooks={['useTransakCheckout']}
108+
status="NEW"
109+
code={HOOK_SNIPPET}
110+
aiPrompt={t(
111+
'Build a custom "Top up wallet" card in my app using useTransakCheckout from @vechain/vechain-kit directly (not PayWithTransakButton) so I can fully control the trigger UI. Show the live status (idle/processing/success/error) and disable the button while processing.',
112+
)}
113+
aiSkills={['vechain-kit']}
114+
>
115+
<CustomHookDemo />
116+
</DemoSection>
117+
</ConnectGate>
118+
</VStack>
119+
);
120+
}

‎examples/playground/src/app/components/layout/navItems.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
LuArrowLeftRight,
77
LuPenLine,
88
LuDatabase,
9+
LuCreditCard,
910
LuLayoutGrid,
1011
LuPalette,
1112
LuBookOpen,
@@ -67,6 +68,12 @@ export const NAV_GROUPS: NavGroup[] = [
6768
icon: LuArrowLeftRight,
6869
label: 'Transactions',
6970
},
71+
{
72+
href: '/payments',
73+
icon: LuCreditCard,
74+
label: 'Payments',
75+
status: 'NEW',
76+
},
7077
{
7178
href: '/signing',
7279
icon: LuPenLine,

‎examples/playground/src/app/providers/VechainKitProviderWrapper.tsx‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,97 @@ export function VechainKitProviderWrapper({ children }: Props) {
232232
// nodeUrl: 'http://localhost:8669',
233233
}}
234234
allowCustomTokens={true}
235+
transak={
236+
process.env.NEXT_PUBLIC_TRANSAK_API_URL
237+
? {
238+
apiKey: process.env.NEXT_PUBLIC_TRANSAK_API_KEY,
239+
widgetUrlBuilder: async ({
240+
walletAddress,
241+
fiatAmount,
242+
fiatCurrency,
243+
cryptoCurrency,
244+
network,
245+
}) => {
246+
const apiUrl =
247+
process.env.NEXT_PUBLIC_TRANSAK_API_URL ?? '';
248+
const environment = process.env
249+
.NEXT_PUBLIC_TRANSAK_ENVIRONMENT as
250+
| 'staging'
251+
| 'production'
252+
| undefined;
253+
254+
// Same dual-contract widgetUrlBuilder as the
255+
// homepage example: production goes through
256+
// vechain/onramp-proxy (GET), staging keeps the
257+
// vechain-kit-infra proxy (POST) -- see the
258+
// "Payments" playground page for the demo.
259+
if (environment === 'production') {
260+
const params = new URLSearchParams({
261+
provider: 'transak',
262+
address: walletAddress,
263+
os: 'web',
264+
theme: 'LIGHT',
265+
currency: fiatCurrency,
266+
referrerDomain:
267+
typeof window !== 'undefined'
268+
? window.location.origin
269+
: '',
270+
});
271+
if (fiatAmount) {
272+
params.set('fiatAmount', fiatAmount);
273+
}
274+
const res = await fetch(
275+
`${apiUrl}/?${params.toString()}`,
276+
);
277+
const json = await res.json();
278+
if (!res.ok || !json?.url) {
279+
throw new Error(
280+
json?.error ??
281+
'Failed to create Transak widget URL',
282+
);
283+
}
284+
return json.url as string;
285+
}
286+
287+
const res = await fetch(
288+
`${apiUrl}/api/transak/widget-url`,
289+
{
290+
method: 'POST',
291+
headers: {
292+
'Content-Type': 'application/json',
293+
},
294+
body: JSON.stringify({
295+
environment,
296+
widgetParams: {
297+
apiKey: process.env
298+
.NEXT_PUBLIC_TRANSAK_API_KEY,
299+
referrerDomain:
300+
typeof window !== 'undefined'
301+
? window.location.origin
302+
: '',
303+
walletAddress,
304+
fiatAmount,
305+
fiatCurrency,
306+
cryptoCurrencyCode: cryptoCurrency,
307+
network,
308+
defaultCryptoCurrency: 'VET',
309+
disableWalletAddressForm: true,
310+
},
311+
}),
312+
},
313+
);
314+
const json = await res.json();
315+
if (!res.ok) {
316+
throw new Error(
317+
json?.error ??
318+
'Failed to create Transak widget URL',
319+
);
320+
}
321+
return json.data.widgetUrl as string;
322+
},
323+
}
324+
: undefined
325+
}
235326
// contractAddresses={{
236327
// b3trContractAddress:
237328
// '0x026771d1be764467f8bdb78bb230df10c924b00d',

0 commit comments

Comments
 (0)