Skip to content

fix(transak): open checkout in a new tab instead of an embedded iframe - #656

Merged
Vombato merged 3 commits into
mainfrom
fix/transak-open-new-tab
Aug 10, 2026
Merged

fix(transak): open checkout in a new tab instead of an embedded iframe#656
Vombato merged 3 commits into
mainfrom
fix/transak-open-new-tab

Conversation

@victhorbi

@victhorbi victhorbi commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Problem

In production, the Transak checkout embedded via containerId (iframe mode, @transak/ui-js-sdk) fails with:

Access Denied. Error code: T-INF-102 ; 403 Forbidden
sdkName: @transak/ui-js-sdk
sdkVersion: 1.0.0

Copying the exact same Secure Widget URL from devtools' Network tab into a new browser tab loads it successfully.

Diagnosis

This is not the same code path #655 verified — that PR's end-to-end check used top-level navigation (preview_start opening the widget URL directly), never the actual containerId iframe integration the app ships. So the iframe path was never actually exercised until this report.

Transak's own docs (docs.transak.com/integration/web/iframe) confirm two relevant facts about the Secure Widget URL:

  • It's domain-validated via a mandatory referrerDomain passed at creation time.
  • It's single-use with a 5-minute TTL.

That the identical URL fails when loaded in our iframe but succeeds when loaded top-level — and is still valid on that second, later attempt — points at a check that runs (and rejects) before the session gets consumed, and that check is context-sensitive (iframe vs. top-level), not just domain-string-sensitive. The likely mechanism: Transak's server inspects the frame context of the request (e.g. Sec-Fetch-Dest: iframe) and rejects it unless the parent origin is on a separate, explicit iframe-embedding allowlist — distinct from the referrerDomain used for session pinning, which vechainkit.vechain.org evidently satisfies for top-level loads but apparently not for iframe embedding.

I want to be upfront that this is circumstantial — Transak doesn't publish a per-error-code reference for T-INF-102, so I can't cite their side of it directly — but it's consistent with every signal gathered (the docs above, Transak's CSP headers, and the fact that vechainkit.vechain.org itself sends no X-Frame-Options/CSP that would explain a same-side block).

Fix

Rather than chase a possibly separate iframe-allowlisting request with Transak, this drops the @transak/ui-js-sdk containerId integration entirely and opens the Secure Widget URL in a new tab, via a plain anchor (target="_blank" rel="noopener noreferrer") — mirroring the pattern already shipped and working in vechain/agent-marketplace's TopUpModal.

  • useTransakCheckout: drops the SDK instance + postMessage event wiring in favor of widgetUrl + a new markCompleted() the caller invokes once the user confirms they finished in the Transak tab. status gains a 'ready' step (URL fetched, waiting on the user) in place of the old SDK-driven success/failure auto-detection.
  • TransakCheckoutModal / TransakOnrampContent: swap the embedded iframe container + spinner-over-iframe for the 'ready' step's link + manual confirm button; drop the container sizing that was tuned for the iframe (fix(homepage): route production onramp through vechain/onramp-proxy #655).
  • PayWithTransakButton: forwards widgetUrl/markCompleted instead of widgetReady.
  • Removes the now-unused @transak/ui-js-sdk dependency and its container DOM id.
  • New copy translated to all 17 locales via yarn translate.

Known gap

There is no automatic order-success/failure detection anymore, since the widget now runs in its own browsing context with no postMessage channel back to us. The user confirms manually via "I've completed my purchase" — the same tradeoff TopUpModal already made in agent-marketplace. A future iteration could look at whether Transak's redirect-URL param can restore an automatic signal without going back to an iframe.

Verification

  • tsc --noEmit, eslint, prettier --check clean on all touched files.
  • yarn build (tsdown) succeeds.
  • Manually verified the full state machine in the browser (playground /payments demo, wallet-gate + hook state forced open for the check, fully reverted after): ready → renders <a href={widgetUrl} target="_blank" rel="noopener noreferrer"> (confirmed via computed DOM attributes) → markCompleted()success screen.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Transak checkout now opens securely in a new browser tab.
    • Added processing, ready, success, and error states.
    • Added expired-link regeneration and manual purchase completion confirmation.
  • Bug Fixes

    • Improved checkout state handling when opening or closing payment flows.
    • Prevented accidental modal dismissal during active checkout.
  • Localization

    • Expanded translations across supported languages for wallet, payment, transaction, security, NFT, staking, and account-related flows.

Production reports show the embedded (containerId) Transak widget failing
with "Access Denied. Error code: T-INF-102 ; 403 Forbidden" while the exact
same Secure Widget URL loads fine as a top-level navigation (confirmed by
copying the URL from devtools into a fresh tab).

Transak's own docs (docs.transak.com/integration/web/iframe) confirm the
Secure Widget URL is domain-validated via `referrerDomain` and is single-use
with a 5-minute TTL. That the identical URL fails embedded but succeeds
top-level -- and is still valid on the second attempt -- points to a
domain/frame-context check that runs before the session is consumed:
Transak's server likely rejects the request when it detects it is being
loaded inside a third-party iframe (e.g. via `Sec-Fetch-Dest: iframe`)
unless the parent origin is on a separate, explicit iframe-embedding
allowlist -- distinct from the `referrerDomain` used for session pinning,
which `vechainkit.vechain.org` evidently satisfies for top-level loads but
not for iframe embedding. This is circumstantial (Transak doesn't publish
per-error-code docs), but consistent with every signal gathered.

Rather than chase per-domain iframe allowlisting with Transak, drop the
`@transak/ui-js-sdk` containerId/iframe integration entirely and open the
Secure Widget URL in a new tab via a plain anchor
(target="_blank" rel="noopener noreferrer"), mirroring the pattern already
shipped and working in vechain/agent-marketplace's TopUpModal.

- useTransakCheckout: replaces the SDK instance + postMessage event wiring
  with widgetUrl + a manual markCompleted() the caller invokes once the user
  confirms they finished in the Transak tab. There is no postMessage/order
  event to detect completion from a separate browsing context, so status
  gains a `'ready'` step (URL fetched, waiting on the user) in place of the
  old SDK-driven success/failure auto-detection.
- TransakCheckoutModal / TransakOnrampContent: swap the embedded iframe
  container + spinner-over-iframe for the 'ready' step's link + manual
  confirm button; drop the container sizing tuned for the iframe.
- PayWithTransakButton: forwards widgetUrl/markCompleted instead of
  widgetReady.
- Removes the now-unused `@transak/ui-js-sdk` dependency and its container
  DOM id.
- Adds the new copy to all 17 locales via `yarn translate`.

Known gap: no automatic order-success/failure detection now that the widget
runs in its own tab -- same manual-confirm tradeoff TopUpModal already
accepted in agent-marketplace. A future iteration could look at whether
Transak's redirect-URL param can restore that signal without an iframe.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@victhorbi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 572e5c2e-4df7-4090-80ed-77e2d3f6d33e

📥 Commits

Reviewing files that changed from the base of the PR and between d9bb745 and 028cc46.

📒 Files selected for processing (2)
  • examples/playground/src/app/(playground)/payments/page.tsx
  • packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts
📝 Walkthrough

Walkthrough

Transak checkout now generates a Secure Widget URL without embedding the SDK. The modal opens Transak in a new tab and supports URL expiration and manual completion. Checkout consumers support the ready state. Localization catalogs were reordered and expanded.

Changes

Transak checkout flow

Layer / File(s) Summary
Checkout hook state and API
packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts, packages/vechain-kit/package.json
The hook removes SDK state, generates five-minute widgetUrl values, adds ready, tracks expiration, and exposes URL-open and completion callbacks.
Checkout modal and onramp integration
packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx, packages/vechain-kit/src/components/PayWithTransakButton/PayWithTransakButton.tsx, packages/vechain-kit/src/components/AccountModal/.../TransakOnrampContent.tsx, examples/playground/src/app/(playground)/payments/page.tsx
The UI opens valid URLs in a new tab, supports expired-link regeneration, preserves success and error flows, and confirms completion manually. Examples describe the updated flow and ready status.
Localization catalog expansion
packages/vechain-kit/src/languages/*.json
The English catalog was reordered. Other catalogs were reordered and expanded with additional translated UI strings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PayWithTransakButton
  participant useTransakCheckout
  participant TransakCheckoutModal
  participant SecureWidgetURLBuilder
  PayWithTransakButton->>useTransakCheckout: Start checkout
  useTransakCheckout->>SecureWidgetURLBuilder: Generate Secure Widget URL
  SecureWidgetURLBuilder-->>useTransakCheckout: Return widgetUrl
  useTransakCheckout-->>TransakCheckoutModal: Return ready status and widgetUrl
  TransakCheckoutModal->>TransakCheckoutModal: Open widgetUrl in a new tab
  TransakCheckoutModal->>useTransakCheckout: Confirm completion
Loading

Possibly related PRs

  • vechain/vechain-kit#649: Introduces the Transak checkout implementation that this change replaces with external URL handling and manual completion.

Suggested labels: safe-to-deploy, safe-to-build

Suggested reviewers: davidecarpini

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change from an embedded Transak iframe to a new browser tab.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/transak-open-new-tab

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Size Change: +278 kB (+3.08%)

Total Size: 9.31 MB

Filename Size Change
packages/vechain-kit/dist/index-BmZpdzZn.d.mts 0 B -5.63 kB (removed) 🏆
packages/vechain-kit/dist/index-BmZpdzZn.d.mts.map 0 B -2.99 kB (removed) 🏆
packages/vechain-kit/dist/index-DESPlu9H.d.cts 0 B -190 kB (removed) 🏆
packages/vechain-kit/dist/index-DESPlu9H.d.cts.map 0 B -51.7 kB (removed) 🏆
packages/vechain-kit/dist/index-HV8L_0Fr.d.mts 0 B -190 kB (removed) 🏆
packages/vechain-kit/dist/index-HV8L_0Fr.d.mts.map 0 B -50.4 kB (removed) 🏆
packages/vechain-kit/dist/index.cjs 1.21 MB +49.8 kB (+4.31%)
packages/vechain-kit/dist/index.cjs.map 2.99 MB +88.4 kB (+3.04%)
packages/vechain-kit/dist/index.mjs 1.16 MB +49.5 kB (+4.45%)
packages/vechain-kit/dist/index.mjs.map 2.93 MB +88.1 kB (+3.11%)
packages/vechain-kit/dist/index--hSO7Xv4.d.mts 5.63 kB +5.63 kB (new file) 🆕
packages/vechain-kit/dist/index--hSO7Xv4.d.mts.map 2.99 kB +2.99 kB (new file) 🆕
packages/vechain-kit/dist/index-CtQxF8gW.d.mts 191 kB +191 kB (new file) 🆕
packages/vechain-kit/dist/index-CtQxF8gW.d.mts.map 50.6 kB +50.6 kB (new file) 🆕
packages/vechain-kit/dist/index-IvC1slZQ.d.cts 191 kB +191 kB (new file) 🆕
packages/vechain-kit/dist/index-IvC1slZQ.d.cts.map 51.8 kB +51.8 kB (new file) 🆕
ℹ️ View Unchanged
Filename Size
packages/vechain-kit/dist/assets 4.1 kB
packages/vechain-kit/dist/assets-BpQtJMK4.mjs 51.4 kB
packages/vechain-kit/dist/assets-BpQtJMK4.mjs.map 74.7 kB
packages/vechain-kit/dist/assets-qmvJ4lSm.cjs 59 kB
packages/vechain-kit/dist/assets-qmvJ4lSm.cjs.map 76.2 kB
packages/vechain-kit/dist/assets/index.cjs 717 B
packages/vechain-kit/dist/assets/index.d.cts 973 B
packages/vechain-kit/dist/assets/index.d.mts 973 B
packages/vechain-kit/dist/assets/index.mjs 719 B
packages/vechain-kit/dist/index-DksqLJEA.d.cts 5.63 kB
packages/vechain-kit/dist/index-DksqLJEA.d.cts.map 2.99 kB
packages/vechain-kit/dist/index.d.cts 25 kB
packages/vechain-kit/dist/index.d.mts 25 kB
packages/vechain-kit/dist/utils 4.1 kB
packages/vechain-kit/dist/utils-Bf4wG_2j.cjs 27.4 kB
packages/vechain-kit/dist/utils-Bf4wG_2j.cjs.map 69.8 kB
packages/vechain-kit/dist/utils-Y7vw7K0X.mjs 22.2 kB
packages/vechain-kit/dist/utils-Y7vw7K0X.mjs.map 69 kB
packages/vechain-kit/dist/utils/index.cjs 2.02 kB
packages/vechain-kit/dist/utils/index.d.cts 3.1 kB
packages/vechain-kit/dist/utils/index.d.mts 3.1 kB
packages/vechain-kit/dist/utils/index.mjs 2.04 kB

compressed-size-action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx (1)

84-87: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The back button abandons the checkout without cancelling it.

If the user presses back while status is 'processing', this handler sets step to 'form'. It does not cancel the pending widgetUrlBuilder call. When the promise resolves, the hook sets status to 'ready', the effect at Line 60 fires, and the component leaves the form and shows the ready panel without user action.

This component never calls the hook's close or reset. Destructure reset from useTransakCheckout and call it when the user returns to the form, both here and in the "Try again" handler at Line 247. This fix depends on the hook discarding stale results; see the related comment on packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts Lines 134-137.

🐛 Proposed fix
     const {
         open: startCheckout,
         status,
         widgetUrl,
         markCompleted,
+        reset: resetCheckout,
     } = useTransakCheckout(
+    const backToForm = () => {
+        resetCheckout();
+        setStep('form');
+    };
+
     const handleBack = () => {
         setCurrentContent('main');
     };
                     <ModalBackButton
                         onClick={
-                            step === 'form' ? handleBack : () => setStep('form')
+                            step === 'form' ? handleBack : backToForm
                         }
                     />

Apply backToForm to the "Try again" button at Line 247 as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx`
around lines 84 - 87, Destructure reset from useTransakCheckout and update the
back-to-form flow to call reset before returning to the form, including the
onClick handler currently using setStep('form') and the “Try again” handler
around the existing backToForm logic. Ensure both paths cancel the pending
checkout and then transition to the form.
🧹 Nitpick comments (4)
packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx (2)

165-183: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make the success action consistent with the other dismiss actions.

The "Done" button calls onClose directly. The error footer and the overlay path call handleClose, which also calls onReset. PayWithTransakButton hides this difference because its close schedules reset. A different consumer that passes an onClose without a reset keeps status at 'success', and a visibility rule such as isOpen={status !== 'idle'} keeps the modal open.

♻️ Proposed change
                             actions={
                                 <Button
                                     variant="vechainKitPrimary"
-                                    onClick={onClose}
+                                    onClick={handleClose}
                                     w="full"
                                 >
                                     {t('Done')}
                                 </Button>
                             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx`
around lines 165 - 183, Update the success StatusScreen action in
TransakCheckoutModal to invoke handleClose instead of onClose, matching the
error footer and overlay dismissal paths so the modal resets to idle before
closing.

116-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The ready-state checkout panel is duplicated across two components. Both sites render the same explanatory comment block, the same 'Continue in the new tab to complete your purchase with Transak, then come back here to confirm.' copy, and the same Button as="a" with href={widgetUrl ?? undefined}, target="_blank", rel="noopener noreferrer", and isDisabled={!widgetUrl}. The matching "I've completed my purchase" footers are duplicated as well. A future change to the external-tab handling must be applied twice, and the two panels can drift.

Extract one shared component, for example TransakReadyPanel, that accepts widgetUrl and onMarkCompleted.

  • packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx#L116-L163: replace the inline ready block and the ready footer at Lines 229-239 with the shared component.
  • packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx#L150-L180: replace the inline ready block and the ready footer at Lines 231-241 with the same shared component.

Move the iframe-versus-new-tab explanation into the shared component. Replace the reference to "the PR description" with the Transak error code and a durable link, because a PR description is not reachable from the source tree.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx`
around lines 116 - 163, Extract the duplicated ready-state panel and completion
footer into a shared TransakReadyPanel accepting widgetUrl and onMarkCompleted,
including the iframe-versus-new-tab explanation with the T-INF-102 code and a
durable link instead of the PR reference. Replace the inline ready content and
footer in
packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx
lines 116-163 and 229-239, and
packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx
lines 150-180 and 231-241, with the shared component.
packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx (1)

57-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The effect duplicates transitions that the hook callbacks already perform.

useTransakCheckout receives () => setStep('success') and () => setStep('error') at Lines 53-54. The hook invokes those callbacks on the same transitions this effect mirrors. Only the 'ready' transition needs the effect. Keeping all three creates two paths to the same state and makes future changes error prone.

♻️ Proposed narrowing
-    // Mirror the hook's status into this component's own step state (which
-    // additionally tracks the pre-checkout 'form' step the hook has no
-    // concept of).
+    // The hook drives 'success' and 'error' through the callbacks above.
+    // Only 'ready' has no callback, so mirror that one transition here.
     useEffect(() => {
-        if (status === 'ready') setStep('ready');
-        else if (status === 'error') setStep('error');
-        else if (status === 'success') setStep('success');
+        if (status === 'ready') setStep('ready');
     }, [status]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx`
around lines 57 - 64, Update the status synchronization effect in
TransakOnrampContent so it only handles the 'ready' status and calls
setStep('ready'). Remove its 'error' and 'success' branches, preserving the
existing hook callbacks as the sole handlers for those transitions.
packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts (1)

106-112: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Validate the URL scheme before you store widgetUrl.

widgetUrlBuilder is supplied by the integrating application. Its return value is rendered directly into an anchor href in TransakCheckoutModal.tsx (Line 152) and TransakOnrampContent.tsx (Line 168). If a builder returns a javascript: or data: URL, the link executes that scheme on click. A scheme check here protects both render sites from one place.

🛡️ Proposed scheme validation
                 if (genRef.current !== gen) {
                     // A newer open() call has superseded this one.
                     return;
                 }
 
+                const parsed = new URL(url);
+                if (parsed.protocol !== 'https:') {
+                    throw new Error(
+                        'transak.widgetUrlBuilder must return an https URL',
+                    );
+                }
+
                 setWidgetUrl(url);
                 setStatus('ready');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts` around lines
106 - 112, Validate the URL returned by widgetUrlBuilder before setWidgetUrl in
the current-generation branch, allowing only safe web URL schemes such as https:
(and the existing supported scheme, if applicable). Reject unsafe javascript:
and data: values without storing them or setting ready status, while preserving
the generation guard and valid-URL flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/playground/src/app/`(playground)/payments/page.tsx:
- Around line 30-39: Update the HOOK_SNIPPET return block so the button and
explanatory comments form valid JSX: wrap the top-level content in a fragment
and convert the // comments into a JSX comment, or move the explanation before
return. Preserve the existing open call and Transak completion guidance.

In `@packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts`:
- Around line 134-137: Update the close callback to increment genRef before
scheduling reset, invalidating any in-flight widgetUrlBuilder result when the
modal is dismissed. Preserve the existing setIsOpen(false) and delayed reset
behavior, and leave open’s generation handling unchanged.
- Around line 129-132: Update markCompleted to make completion idempotent under
rapid repeated invocations, ensuring the success state transition and
onSuccessRef callback occur only once. Use a functional setStatus update or a
completion ref, and reset that guard in reset and at the start of open if using
a ref.

---

Outside diff comments:
In
`@packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx`:
- Around line 84-87: Destructure reset from useTransakCheckout and update the
back-to-form flow to call reset before returning to the form, including the
onClick handler currently using setStep('form') and the “Try again” handler
around the existing backToForm logic. Ensure both paths cancel the pending
checkout and then transition to the form.

---

Nitpick comments:
In
`@packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx`:
- Around line 57-64: Update the status synchronization effect in
TransakOnrampContent so it only handles the 'ready' status and calls
setStep('ready'). Remove its 'error' and 'success' branches, preserving the
existing hook callbacks as the sole handlers for those transitions.

In
`@packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx`:
- Around line 165-183: Update the success StatusScreen action in
TransakCheckoutModal to invoke handleClose instead of onClose, matching the
error footer and overlay dismissal paths so the modal resets to idle before
closing.
- Around line 116-163: Extract the duplicated ready-state panel and completion
footer into a shared TransakReadyPanel accepting widgetUrl and onMarkCompleted,
including the iframe-versus-new-tab explanation with the T-INF-102 code and a
durable link instead of the PR reference. Replace the inline ready content and
footer in
packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx
lines 116-163 and 229-239, and
packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx
lines 150-180 and 231-241, with the shared component.

In `@packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts`:
- Around line 106-112: Validate the URL returned by widgetUrlBuilder before
setWidgetUrl in the current-generation branch, allowing only safe web URL
schemes such as https: (and the existing supported scheme, if applicable).
Reject unsafe javascript: and data: values without storing them or setting ready
status, while preserving the generation guard and valid-URL flow.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e76a2d5-b548-4fe9-8955-7b9c9eb6ada0

📥 Commits

Reviewing files that changed from the base of the PR and between 2c9bd8e and e554350.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (23)
  • examples/playground/src/app/(playground)/payments/page.tsx
  • packages/vechain-kit/package.json
  • packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx
  • packages/vechain-kit/src/components/PayWithTransakButton/PayWithTransakButton.tsx
  • packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx
  • packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts
  • packages/vechain-kit/src/languages/de.json
  • packages/vechain-kit/src/languages/en.json
  • packages/vechain-kit/src/languages/es.json
  • packages/vechain-kit/src/languages/fr.json
  • packages/vechain-kit/src/languages/hi.json
  • packages/vechain-kit/src/languages/it.json
  • packages/vechain-kit/src/languages/ja.json
  • packages/vechain-kit/src/languages/ko.json
  • packages/vechain-kit/src/languages/nl.json
  • packages/vechain-kit/src/languages/pt.json
  • packages/vechain-kit/src/languages/ro.json
  • packages/vechain-kit/src/languages/ru.json
  • packages/vechain-kit/src/languages/sv.json
  • packages/vechain-kit/src/languages/tr.json
  • packages/vechain-kit/src/languages/tw.json
  • packages/vechain-kit/src/languages/vi.json
  • packages/vechain-kit/src/languages/zh.json
💤 Files with no reviewable changes (1)
  • packages/vechain-kit/package.json

Comment thread examples/playground/src/app/(playground)/payments/page.tsx
Comment thread packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts
Comment thread packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces Transak’s failing iframe integration with a new-tab checkout and manual completion flow.

Changes:

  • Adds the ready checkout state, widget URL, and manual completion callback.
  • Updates both checkout interfaces and playground documentation.
  • Removes the Transak SDK dependency and localizes the new UI.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
yarn.lock Removes Transak SDK packages.
packages/vechain-kit/package.json Removes the SDK dependency.
packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts Implements the new checkout state machine.
packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx Adds new-tab checkout UI.
packages/vechain-kit/src/components/PayWithTransakButton/PayWithTransakButton.tsx Forwards the new hook API.
packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx Updates account on-ramp flow.
examples/playground/src/app/(playground)/payments/page.tsx Updates examples and guidance.
packages/vechain-kit/src/languages/en.json Adds English checkout copy.
packages/vechain-kit/src/languages/de.json Adds German checkout copy.
packages/vechain-kit/src/languages/es.json Adds Spanish checkout copy.
packages/vechain-kit/src/languages/fr.json Adds French checkout copy.
packages/vechain-kit/src/languages/hi.json Adds Hindi checkout copy.
packages/vechain-kit/src/languages/it.json Adds Italian checkout copy.
packages/vechain-kit/src/languages/ja.json Adds Japanese checkout copy.
packages/vechain-kit/src/languages/ko.json Adds Korean checkout copy.
packages/vechain-kit/src/languages/nl.json Adds Dutch checkout copy.
packages/vechain-kit/src/languages/pt.json Adds Portuguese checkout copy.
packages/vechain-kit/src/languages/ro.json Adds Romanian checkout copy.
packages/vechain-kit/src/languages/ru.json Adds Russian checkout copy.
packages/vechain-kit/src/languages/sv.json Adds Swedish checkout copy.
packages/vechain-kit/src/languages/tr.json Adds Turkish checkout copy.
packages/vechain-kit/src/languages/tw.json Adds Traditional Chinese checkout copy.
packages/vechain-kit/src/languages/vi.json Adds Vietnamese checkout copy.
packages/vechain-kit/src/languages/zh.json Adds Simplified Chinese checkout copy.
Suppressed comments (1)

packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts:137

  • Closing the checkout does not invalidate an in-flight widgetUrlBuilder call. If the modal is dismissed via Escape/mobile-sheet dismissal while processing, the old promise can still pass the generation check and set status back to ready, reopening the status-driven modal after it was closed. Invalidate the generation synchronously when closing.
    const close = useCallback(() => {
        setIsOpen(false);
        setTimeout(reset, 300);
    }, [reset]);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts
Comment thread examples/playground/src/app/(playground)/payments/page.tsx Outdated
…ab checkout

- useTransakCheckout: make markCompleted() idempotent against rapid repeat
  clicks (functional setStatus update, onSuccess fires once) [CodeRabbit].
- useTransakCheckout: bump genRef in close() so a widgetUrlBuilder() call
  still in flight when the user closes the modal can no longer resolve into
  'ready' after the fact [CodeRabbit].
- useTransakCheckout: track widgetUrl's 5-minute TTL / single-use contract
  (docs/recipes/transak-onramp.md) via a new widgetUrlExpired flag + timer,
  and a markWidgetUrlOpened() callers call on click -- surfaces a "get a new
  link" prompt instead of letting the user follow a dead link [Copilot].
- TransakCheckoutModal / TransakOnrampContent: render the expired/used state
  and wire markWidgetUrlOpened into the "Continue with Transak" link's
  onClick.
- TransakOnrampContent: stop mirroring hook status into `step` once the user
  has navigated back to the form, and cancel the in-flight checkout on Back
  -- previously a widgetUrlBuilder() promise resolving after Back was
  clicked could silently pull the user back into the 'ready' screen
  [Copilot].
- examples/playground: HOOK_SNIPPET was invalid JSX (a `//` comment as a
  sibling after `return`'s single child) [CodeRabbit]; CustomHookDemo never
  rendered a link or called markCompleted, so following it left the user
  stuck at 'ready' with no way to finish [Copilot]. Both now show the full,
  correct flow including the expired-link case.
- New copy translated to all 17 locales via `yarn translate`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts (1)

120-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use getConfig(network.type) for Transak network values.

Retrieve the Transak environment and network identifier through getConfig(network.type) instead of deriving them from hardcoded literals.

As per coding guidelines, use getConfig(networkType) to retrieve network-specific configurations instead of hardcoding network values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts` around lines
120 - 130, Update the Transak checkout configuration in the widget URL builder
to call getConfig(network.type), and use its environment and network identifier
values instead of deriving the environment from network.type or hardcoding
'vechain'. Preserve the existing wallet, fiat, and cryptocurrency parameters.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/playground/src/app/`(playground)/payments/page.tsx:
- Around line 26-50: The payment example around useTransakCheckout must handle
the single-use widget URL lifecycle. Destructure widgetUrlExpired and
markWidgetUrlOpened, invoke markWidgetUrlOpened when the Transak link is
clicked, and replace the expired-URL display with a new open() action so stale
URLs are not presented as usable.
- Around line 113-121: Update the custom-trigger purchase button in the payment
page to be disabled when status === 'processing', preventing its onClick handler
from invoking open() during processing while preserving the existing behavior
for other statuses.

In `@packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts`:
- Around line 78-84: Update reset and close in the checkout hook to invalidate
pending work by advancing genRef whenever reset() is called. Capture the current
generation when close() schedules its 300 ms delayed reset, and only perform
that reset if the generation is still unchanged; otherwise preserve the newer
checkout state.

---

Nitpick comments:
In `@packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts`:
- Around line 120-130: Update the Transak checkout configuration in the widget
URL builder to call getConfig(network.type), and use its environment and network
identifier values instead of deriving the environment from network.type or
hardcoding 'vechain'. Preserve the existing wallet, fiat, and cryptocurrency
parameters.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0767550c-3aa8-442b-8a95-ecca390e9443

📥 Commits

Reviewing files that changed from the base of the PR and between e554350 and d9bb745.

📒 Files selected for processing (22)
  • examples/playground/src/app/(playground)/payments/page.tsx
  • packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx
  • packages/vechain-kit/src/components/PayWithTransakButton/PayWithTransakButton.tsx
  • packages/vechain-kit/src/components/TransakCheckoutModal/TransakCheckoutModal.tsx
  • packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts
  • packages/vechain-kit/src/languages/de.json
  • packages/vechain-kit/src/languages/en.json
  • packages/vechain-kit/src/languages/es.json
  • packages/vechain-kit/src/languages/fr.json
  • packages/vechain-kit/src/languages/hi.json
  • packages/vechain-kit/src/languages/it.json
  • packages/vechain-kit/src/languages/ja.json
  • packages/vechain-kit/src/languages/ko.json
  • packages/vechain-kit/src/languages/nl.json
  • packages/vechain-kit/src/languages/pt.json
  • packages/vechain-kit/src/languages/ro.json
  • packages/vechain-kit/src/languages/ru.json
  • packages/vechain-kit/src/languages/sv.json
  • packages/vechain-kit/src/languages/tr.json
  • packages/vechain-kit/src/languages/tw.json
  • packages/vechain-kit/src/languages/vi.json
  • packages/vechain-kit/src/languages/zh.json
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/vechain-kit/src/languages/it.json
  • packages/vechain-kit/src/languages/ja.json
  • packages/vechain-kit/src/languages/fr.json
  • packages/vechain-kit/src/languages/en.json
  • packages/vechain-kit/src/languages/sv.json
  • packages/vechain-kit/src/languages/ko.json
  • packages/vechain-kit/src/languages/nl.json
  • packages/vechain-kit/src/languages/ro.json
  • packages/vechain-kit/src/components/AccountModal/Contents/TransakOnramp/TransakOnrampContent.tsx
  • packages/vechain-kit/src/languages/pt.json
  • packages/vechain-kit/src/components/PayWithTransakButton/PayWithTransakButton.tsx
  • packages/vechain-kit/src/languages/de.json
  • packages/vechain-kit/src/languages/es.json

Comment thread examples/playground/src/app/(playground)/payments/page.tsx
Comment thread examples/playground/src/app/(playground)/payments/page.tsx
Comment thread packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts
- useTransakCheckout: reset() now also bumps genRef, so a widgetUrlBuilder()
  call still in flight when reset() is called directly (not via close())
  can no longer resolve into 'ready' afterwards.
- useTransakCheckout: close()'s delayed reset() is now guarded by the
  generation captured at close() time -- if a new open() starts within the
  300ms delay, the stale reset can no longer clobber the newer checkout's
  state.
- examples/playground: HOOK_SNIPPET now shows the full widgetUrlExpired /
  markWidgetUrlOpened lifecycle (previously only CustomHookDemo had it,
  leaving the copyable snippet incomplete); the custom-trigger button is
  disabled while status === 'processing' in both the snippet and the live
  demo, preventing repeated open() calls from minting redundant Secure
  Widget URLs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated no new comments.

Suppressed comments (2)

packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts:173

  • The state updater performs the externally visible onSuccess side effect. React requires updater functions to be pure and may invoke them twice in Strict Mode, so consumers can receive duplicate success callbacks (and callbacks that set state can run during React's state calculation). Move the callback outside the updater and keep idempotency separately, such as with a ref that is reset for each checkout.
        setStatus((current) => {
            if (current === 'success') return current;
            onSuccessRef.current?.();
            return 'success';

packages/vechain-kit/src/languages/it.json:419

  • The Italian sentence is grammatically incomplete: “Ottieni un nuovo” uses an adjective without the noun/pronoun for “link.” Use “Ottienine uno nuovo” so the expiry prompt reads naturally.
    "This link has expired or was already opened. Get a new one to continue.": "Questo link è scaduto o è già stato aperto. Ottieni un nuovo per continuare.",

@Vombato
Vombato merged commit dc032d0 into main Aug 10, 2026
10 checks passed
@Vombato
Vombato deleted the fix/transak-open-new-tab branch August 10, 2026 07:42
@davidecarpini davidecarpini mentioned this pull request Aug 28, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants