@rhinestone/deposit-modal 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +193 -29
  2. package/dist/{chunk-4BLOPOVO.mjs → DepositModalReown-6KUAO5ZC.mjs} +56 -4
  3. package/dist/{chunk-5E67UM3V.cjs → DepositModalReown-XC5RIU7Z.cjs} +62 -10
  4. package/dist/{caip-C_ZYFIGa.d.cts → caip-523Zrc3r.d.cts} +1 -3
  5. package/dist/{caip-C_ZYFIGa.d.ts → caip-523Zrc3r.d.ts} +1 -3
  6. package/dist/{chunk-46BRQZ2P.cjs → chunk-22RV33LK.cjs} +2148 -1467
  7. package/dist/chunk-4WB2FHGC.mjs +865 -0
  8. package/dist/chunk-A223BLVC.mjs +801 -0
  9. package/dist/{chunk-3WIHTUQ6.cjs → chunk-DIX7M3SP.cjs} +232 -601
  10. package/dist/{chunk-XK5RH4FZ.mjs → chunk-E4G5BOZP.mjs} +167 -536
  11. package/dist/chunk-H577RJN5.cjs +801 -0
  12. package/dist/{chunk-RARNNVZO.mjs → chunk-ILT2PITN.mjs} +60 -3
  13. package/dist/{chunk-KGZU3BL2.mjs → chunk-KKHLNFSA.mjs} +2397 -1716
  14. package/dist/chunk-LGVJWVSS.mjs +81 -0
  15. package/dist/chunk-NEXUCY4B.cjs +11 -0
  16. package/dist/chunk-NMIMRASD.cjs +1785 -0
  17. package/dist/chunk-OSI2CFGL.mjs +11 -0
  18. package/dist/chunk-PAGMU4RQ.cjs +865 -0
  19. package/dist/chunk-T7LGQFP3.cjs +81 -0
  20. package/dist/chunk-VN4BLZYC.cjs +2273 -0
  21. package/dist/chunk-VOGZAZB2.mjs +2273 -0
  22. package/dist/chunk-WYXBY2MQ.mjs +1785 -0
  23. package/dist/{chunk-ORKEBFMR.cjs → chunk-YEBXUUH3.cjs} +63 -6
  24. package/dist/claim.cjs +10 -0
  25. package/dist/claim.d.cts +8 -0
  26. package/dist/claim.d.ts +8 -0
  27. package/dist/claim.mjs +10 -0
  28. package/dist/constants.cjs +3 -3
  29. package/dist/constants.d.cts +4 -2
  30. package/dist/constants.d.ts +4 -2
  31. package/dist/constants.mjs +5 -5
  32. package/dist/deposit.cjs +8 -6
  33. package/dist/deposit.d.cts +2 -2
  34. package/dist/deposit.d.ts +2 -2
  35. package/dist/deposit.mjs +7 -5
  36. package/dist/index.cjs +22 -12
  37. package/dist/index.d.cts +15 -2
  38. package/dist/index.d.ts +15 -2
  39. package/dist/index.mjs +21 -11
  40. package/dist/polymarket.cjs +5 -6
  41. package/dist/polymarket.mjs +2 -3
  42. package/dist/server.cjs +96 -0
  43. package/dist/server.d.cts +65 -0
  44. package/dist/server.d.ts +65 -0
  45. package/dist/server.mjs +96 -0
  46. package/dist/styles.css +149 -15
  47. package/dist/types-C0SGf-21.d.cts +606 -0
  48. package/dist/types-C0SGf-21.d.ts +606 -0
  49. package/dist/withdraw.cjs +6 -5
  50. package/dist/withdraw.d.cts +3 -6
  51. package/dist/withdraw.d.ts +3 -6
  52. package/dist/withdraw.mjs +5 -4
  53. package/package.json +53 -6
  54. package/dist/DepositModalReown-A2W6R27Q.cjs +0 -63
  55. package/dist/DepositModalReown-IB5WUXVT.mjs +0 -63
  56. package/dist/WithdrawModalReown-A4S4BJHG.mjs +0 -46
  57. package/dist/WithdrawModalReown-KUWNNA2X.cjs +0 -46
  58. package/dist/chunk-2O2FJYRY.mjs +0 -800
  59. package/dist/chunk-DSXMWQYB.cjs +0 -4542
  60. package/dist/chunk-P5T4XSN2.cjs +0 -800
  61. package/dist/chunk-RH6XQHMA.mjs +0 -295
  62. package/dist/chunk-TUSEN26M.mjs +0 -4542
  63. package/dist/chunk-UNVVUULP.cjs +0 -295
  64. package/dist/types--LQWg_4W.d.cts +0 -468
  65. package/dist/types--LQWg_4W.d.ts +0 -468
@@ -0,0 +1,96 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/server.ts
2
+ var DEFAULT_PROCESSOR_URL = "https://v1.orchestrator.rhinestone.dev/deposit-processor";
3
+ function jsonError(message, status) {
4
+ return new Response(JSON.stringify({ error: message }), {
5
+ status,
6
+ headers: { "Content-Type": "application/json" }
7
+ });
8
+ }
9
+ function parseBody(value) {
10
+ if (typeof value !== "object" || value === null) return null;
11
+ const record = value;
12
+ const fields = ["chain", "txHash", "account", "token", "destination"];
13
+ for (const field of fields) {
14
+ if (typeof record[field] !== "string" || record[field] === "") return null;
15
+ }
16
+ return Object.fromEntries(
17
+ fields.map((field) => [field, record[field]])
18
+ );
19
+ }
20
+ function sameIdentifier(a, b) {
21
+ if (a === b) return true;
22
+ const isHex = (value) => /^0x[0-9a-fA-F]+$/.test(value);
23
+ return isHex(a) && isHex(b) && a.toLowerCase() === b.toLowerCase();
24
+ }
25
+ function createRefundHandler(options) {
26
+ const processorUrl = (_nullishCoalesce(options.processorUrl, () => ( DEFAULT_PROCESSOR_URL))).replace(
27
+ /\/$/,
28
+ ""
29
+ );
30
+ const headers = {
31
+ "Content-Type": "application/json",
32
+ "x-api-key": options.apiKey
33
+ };
34
+ return async function handleRefund(request) {
35
+ const body = parseBody(await request.json().catch(() => null));
36
+ if (!body) {
37
+ return jsonError("Malformed refund request", 400);
38
+ }
39
+ const authorization = await options.authorize(request, body);
40
+ if (!authorization) {
41
+ return jsonError("Unauthorized", 401);
42
+ }
43
+ const query = new URLSearchParams({
44
+ recipient: authorization.depositRecipient,
45
+ chain: body.chain,
46
+ txHash: body.txHash,
47
+ includeSpam: "true"
48
+ });
49
+ const lookup = await fetch(`${processorUrl}/deposits?${query}`, { headers });
50
+ if (!lookup.ok) {
51
+ return jsonError("Could not verify the deposit", 502);
52
+ }
53
+ const { deposits = [] } = await lookup.json();
54
+ const owned = deposits.some(
55
+ (deposit) => deposit.depositAddress !== void 0 && deposit.token !== void 0 && sameIdentifier(deposit.depositAddress, body.account) && sameIdentifier(deposit.token, body.token)
56
+ );
57
+ if (!owned) {
58
+ return jsonError("Forbidden", 403);
59
+ }
60
+ const destination = _nullishCoalesce(authorization.refundDestination, () => ( body.destination));
61
+ const upstream = await fetch(`${processorUrl}/deposits/refund`, {
62
+ method: "POST",
63
+ headers,
64
+ body: JSON.stringify({
65
+ chain: body.chain,
66
+ txHash: body.txHash,
67
+ account: body.account,
68
+ token: body.token,
69
+ recipient: destination
70
+ })
71
+ });
72
+ const text = await upstream.text();
73
+ if (!upstream.ok) {
74
+ return new Response(text, {
75
+ status: upstream.status,
76
+ headers: { "Content-Type": "application/json" }
77
+ });
78
+ }
79
+ let body_;
80
+ try {
81
+ body_ = JSON.parse(text);
82
+ } catch (e) {
83
+ return new Response(text, {
84
+ status: upstream.status,
85
+ headers: { "Content-Type": "application/json" }
86
+ });
87
+ }
88
+ return new Response(
89
+ JSON.stringify({ ...body_, destination }),
90
+ { status: upstream.status, headers: { "Content-Type": "application/json" } }
91
+ );
92
+ };
93
+ }
94
+
95
+
96
+ exports.createRefundHandler = createRefundHandler;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Server-side helper for the claim flow. Import from
3
+ * `@rhinestone/deposit-modal/server` — this module talks to the processor with
4
+ * your API key and must never reach the browser.
5
+ *
6
+ * Deposit accounts are service-managed, so no user wallet can sign for a
7
+ * refund, and an exchange-funded deposit has no signable sender either.
8
+ * Whether a deposit belongs to a given user is something only your app knows,
9
+ * so that one decision is yours (`authorize`) and everything else is here.
10
+ */
11
+ /** Body the ClaimModal POSTs to your endpoint. Never trusted on its own. */
12
+ interface RefundRequestBody {
13
+ chain: string;
14
+ txHash: string;
15
+ account: string;
16
+ token: string;
17
+ destination: string;
18
+ }
19
+ interface RefundAuthorization {
20
+ /**
21
+ * The deposit `recipient` this user owns — the same value your app passes
22
+ * `<DepositModal>`. Used *only* to prove the deposit belongs to them: the
23
+ * handler lists deposits settling to it and rejects a txHash that isn't
24
+ * among them.
25
+ *
26
+ * Not where the money goes. One recipient covers every deposit account the
27
+ * user has, since the account salt includes the target chain and token.
28
+ */
29
+ depositRecipient: string;
30
+ /**
31
+ * Where the refund is paid. Set this to override whatever the browser asked
32
+ * for, taking the choice away from the page. Omit it to honour the address
33
+ * the user typed.
34
+ */
35
+ refundDestination?: string;
36
+ }
37
+ interface CreateRefundHandlerOptions {
38
+ /** Write-scoped Rhinestone API key. Server-side only. */
39
+ apiKey: string;
40
+ /**
41
+ * Authenticate the caller and return the recipient they own, or `null` to
42
+ * reject with 401. This is the only part we can't write for you.
43
+ */
44
+ authorize: (request: Request, refund: RefundRequestBody) => Promise<RefundAuthorization | null> | RefundAuthorization | null;
45
+ /** Defaults to the production processor. */
46
+ processorUrl?: string;
47
+ }
48
+ /**
49
+ * Builds a `(Request) => Promise<Response>` you can mount in any fetch-based
50
+ * runtime (Next.js route handlers, Hono, Bun.serve, Cloudflare Workers).
51
+ *
52
+ * ```ts
53
+ * // app/api/refund/route.ts
54
+ * export const POST = createRefundHandler({
55
+ * apiKey: process.env.RHINESTONE_API_KEY!,
56
+ * async authorize(request) {
57
+ * const session = await getSession(request);
58
+ * return session ? { depositRecipient: session.depositRecipient } : null;
59
+ * },
60
+ * });
61
+ * ```
62
+ */
63
+ declare function createRefundHandler(options: CreateRefundHandlerOptions): (request: Request) => Promise<Response>;
64
+
65
+ export { type CreateRefundHandlerOptions, type RefundAuthorization, createRefundHandler };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Server-side helper for the claim flow. Import from
3
+ * `@rhinestone/deposit-modal/server` — this module talks to the processor with
4
+ * your API key and must never reach the browser.
5
+ *
6
+ * Deposit accounts are service-managed, so no user wallet can sign for a
7
+ * refund, and an exchange-funded deposit has no signable sender either.
8
+ * Whether a deposit belongs to a given user is something only your app knows,
9
+ * so that one decision is yours (`authorize`) and everything else is here.
10
+ */
11
+ /** Body the ClaimModal POSTs to your endpoint. Never trusted on its own. */
12
+ interface RefundRequestBody {
13
+ chain: string;
14
+ txHash: string;
15
+ account: string;
16
+ token: string;
17
+ destination: string;
18
+ }
19
+ interface RefundAuthorization {
20
+ /**
21
+ * The deposit `recipient` this user owns — the same value your app passes
22
+ * `<DepositModal>`. Used *only* to prove the deposit belongs to them: the
23
+ * handler lists deposits settling to it and rejects a txHash that isn't
24
+ * among them.
25
+ *
26
+ * Not where the money goes. One recipient covers every deposit account the
27
+ * user has, since the account salt includes the target chain and token.
28
+ */
29
+ depositRecipient: string;
30
+ /**
31
+ * Where the refund is paid. Set this to override whatever the browser asked
32
+ * for, taking the choice away from the page. Omit it to honour the address
33
+ * the user typed.
34
+ */
35
+ refundDestination?: string;
36
+ }
37
+ interface CreateRefundHandlerOptions {
38
+ /** Write-scoped Rhinestone API key. Server-side only. */
39
+ apiKey: string;
40
+ /**
41
+ * Authenticate the caller and return the recipient they own, or `null` to
42
+ * reject with 401. This is the only part we can't write for you.
43
+ */
44
+ authorize: (request: Request, refund: RefundRequestBody) => Promise<RefundAuthorization | null> | RefundAuthorization | null;
45
+ /** Defaults to the production processor. */
46
+ processorUrl?: string;
47
+ }
48
+ /**
49
+ * Builds a `(Request) => Promise<Response>` you can mount in any fetch-based
50
+ * runtime (Next.js route handlers, Hono, Bun.serve, Cloudflare Workers).
51
+ *
52
+ * ```ts
53
+ * // app/api/refund/route.ts
54
+ * export const POST = createRefundHandler({
55
+ * apiKey: process.env.RHINESTONE_API_KEY!,
56
+ * async authorize(request) {
57
+ * const session = await getSession(request);
58
+ * return session ? { depositRecipient: session.depositRecipient } : null;
59
+ * },
60
+ * });
61
+ * ```
62
+ */
63
+ declare function createRefundHandler(options: CreateRefundHandlerOptions): (request: Request) => Promise<Response>;
64
+
65
+ export { type CreateRefundHandlerOptions, type RefundAuthorization, createRefundHandler };
@@ -0,0 +1,96 @@
1
+ // src/server.ts
2
+ var DEFAULT_PROCESSOR_URL = "https://v1.orchestrator.rhinestone.dev/deposit-processor";
3
+ function jsonError(message, status) {
4
+ return new Response(JSON.stringify({ error: message }), {
5
+ status,
6
+ headers: { "Content-Type": "application/json" }
7
+ });
8
+ }
9
+ function parseBody(value) {
10
+ if (typeof value !== "object" || value === null) return null;
11
+ const record = value;
12
+ const fields = ["chain", "txHash", "account", "token", "destination"];
13
+ for (const field of fields) {
14
+ if (typeof record[field] !== "string" || record[field] === "") return null;
15
+ }
16
+ return Object.fromEntries(
17
+ fields.map((field) => [field, record[field]])
18
+ );
19
+ }
20
+ function sameIdentifier(a, b) {
21
+ if (a === b) return true;
22
+ const isHex = (value) => /^0x[0-9a-fA-F]+$/.test(value);
23
+ return isHex(a) && isHex(b) && a.toLowerCase() === b.toLowerCase();
24
+ }
25
+ function createRefundHandler(options) {
26
+ const processorUrl = (options.processorUrl ?? DEFAULT_PROCESSOR_URL).replace(
27
+ /\/$/,
28
+ ""
29
+ );
30
+ const headers = {
31
+ "Content-Type": "application/json",
32
+ "x-api-key": options.apiKey
33
+ };
34
+ return async function handleRefund(request) {
35
+ const body = parseBody(await request.json().catch(() => null));
36
+ if (!body) {
37
+ return jsonError("Malformed refund request", 400);
38
+ }
39
+ const authorization = await options.authorize(request, body);
40
+ if (!authorization) {
41
+ return jsonError("Unauthorized", 401);
42
+ }
43
+ const query = new URLSearchParams({
44
+ recipient: authorization.depositRecipient,
45
+ chain: body.chain,
46
+ txHash: body.txHash,
47
+ includeSpam: "true"
48
+ });
49
+ const lookup = await fetch(`${processorUrl}/deposits?${query}`, { headers });
50
+ if (!lookup.ok) {
51
+ return jsonError("Could not verify the deposit", 502);
52
+ }
53
+ const { deposits = [] } = await lookup.json();
54
+ const owned = deposits.some(
55
+ (deposit) => deposit.depositAddress !== void 0 && deposit.token !== void 0 && sameIdentifier(deposit.depositAddress, body.account) && sameIdentifier(deposit.token, body.token)
56
+ );
57
+ if (!owned) {
58
+ return jsonError("Forbidden", 403);
59
+ }
60
+ const destination = authorization.refundDestination ?? body.destination;
61
+ const upstream = await fetch(`${processorUrl}/deposits/refund`, {
62
+ method: "POST",
63
+ headers,
64
+ body: JSON.stringify({
65
+ chain: body.chain,
66
+ txHash: body.txHash,
67
+ account: body.account,
68
+ token: body.token,
69
+ recipient: destination
70
+ })
71
+ });
72
+ const text = await upstream.text();
73
+ if (!upstream.ok) {
74
+ return new Response(text, {
75
+ status: upstream.status,
76
+ headers: { "Content-Type": "application/json" }
77
+ });
78
+ }
79
+ let body_;
80
+ try {
81
+ body_ = JSON.parse(text);
82
+ } catch {
83
+ return new Response(text, {
84
+ status: upstream.status,
85
+ headers: { "Content-Type": "application/json" }
86
+ });
87
+ }
88
+ return new Response(
89
+ JSON.stringify({ ...body_, destination }),
90
+ { status: upstream.status, headers: { "Content-Type": "application/json" } }
91
+ );
92
+ };
93
+ }
94
+ export {
95
+ createRefundHandler
96
+ };
package/dist/styles.css CHANGED
@@ -327,7 +327,12 @@
327
327
  the previous step's height instead of jumping. Min-height is on
328
328
  modal-content rather than modal-body so the transitioned property
329
329
  lives on an element that survives step swaps. */
330
- min-height 280ms cubic-bezier(0.22, 1, 0.36, 1);
330
+ min-height 280ms cubic-bezier(0.22, 1, 0.36, 1),
331
+ /* The Swapped iframe phase raises the shell corner to the widget's 28px
332
+ (see the sizing rules at the bottom of this file); animate it on the same
333
+ curve as max-width/min-height so the corner doesn't pop while the modal
334
+ is still resizing into the iframe. */
335
+ border-radius 240ms cubic-bezier(0.22, 1, 0.36, 1);
331
336
  }
332
337
 
333
338
  .rs-modal-content[data-theme="dark"] {
@@ -557,6 +562,19 @@
557
562
  gap: 6px;
558
563
  }
559
564
 
565
+ /* Chrome/Safari paint their own autofill background with !important UA
566
+ styles. Clip that paint to the glyphs so every themed input keeps its real
567
+ surface — including the transparent withdrawal inputs — without replacing
568
+ the focus/error box-shadows. The standard pseudo-class and WebKit alias are
569
+ both kept for current and older browser engines. */
570
+ .rs-modal
571
+ :is(.rs-input, .rs-withdraw-input):is(:autofill, :-webkit-autofill) {
572
+ -webkit-background-clip: text;
573
+ background-clip: text;
574
+ -webkit-text-fill-color: var(--rs-foreground);
575
+ caret-color: var(--rs-foreground);
576
+ }
577
+
560
578
  .rs-input-label {
561
579
  font-size: 13px;
562
580
  color: var(--rs-muted);
@@ -2555,7 +2573,8 @@
2555
2573
  the image is loading (`data-loading`), show a muted pulsing placeholder so the
2556
2574
  first paint isn't a blank/broken-image flash; it inherits the caller's size
2557
2575
  and border-radius, so there's no layout shift when the icon paints in. */
2558
- .rs-token-icon[data-loading] {
2576
+ .rs-token-icon[data-loading],
2577
+ .rs-cdn-icon[data-loading] {
2559
2578
  background-color: var(--rs-muted);
2560
2579
  animation: rs-token-icon-pulse 1.2s ease-in-out infinite;
2561
2580
  }
@@ -2571,7 +2590,8 @@
2571
2590
  }
2572
2591
 
2573
2592
  @media (prefers-reduced-motion: reduce) {
2574
- .rs-token-icon[data-loading] {
2593
+ .rs-token-icon[data-loading],
2594
+ .rs-cdn-icon[data-loading] {
2575
2595
  animation: none;
2576
2596
  }
2577
2597
  }
@@ -4867,6 +4887,53 @@
4867
4887
  object-fit: cover;
4868
4888
  }
4869
4889
 
4890
+ /* Provider-hosted fiat method icons (regional methods: GCash, Maya, PIX, …).
4891
+ When a dark-background variant is supplied, the theme toggles which img
4892
+ shows; otherwise the single (light) icon shows in both themes. */
4893
+ .rs-fiat-remote-icon {
4894
+ display: inline-flex;
4895
+ align-items: center;
4896
+ justify-content: center;
4897
+ width: 24px;
4898
+ height: 24px;
4899
+ }
4900
+
4901
+ .rs-fiat-remote-icon img {
4902
+ width: 24px;
4903
+ height: 24px;
4904
+ object-fit: contain;
4905
+ }
4906
+
4907
+ .rs-fiat-remote-icon-dark {
4908
+ display: none;
4909
+ }
4910
+
4911
+ .rs-modal[data-theme="dark"] .rs-fiat-remote-icon--has-dark .rs-fiat-remote-icon-light {
4912
+ display: none;
4913
+ }
4914
+
4915
+ .rs-modal[data-theme="dark"] .rs-fiat-remote-icon--has-dark .rs-fiat-remote-icon-dark {
4916
+ display: inline-block;
4917
+ }
4918
+
4919
+ /* Sits in the row's meta slot, so no inline-with-the-label wrapper is needed —
4920
+ `.rs-list-row-title` already handles its own truncation. */
4921
+ .rs-fiat-method-badge {
4922
+ flex-shrink: 0;
4923
+ padding: 2px 6px;
4924
+ border-radius: 999px;
4925
+ background: #a4f4cf;
4926
+ color: #004f3b;
4927
+ font-size: 11px;
4928
+ font-weight: 600;
4929
+ line-height: 1.2;
4930
+ }
4931
+
4932
+ .rs-modal[data-theme="dark"] .rs-fiat-method-badge {
4933
+ background: #006045;
4934
+ color: #d0fae5;
4935
+ }
4936
+
4870
4937
  .rs-list-row-body {
4871
4938
  flex: 1;
4872
4939
  min-width: 0;
@@ -4949,6 +5016,18 @@
4949
5016
  border-radius: 4px;
4950
5017
  }
4951
5018
 
5019
+ /* Chain with no bundled icon: short-label chip inside the badge circle. */
5020
+ .rs-chain-badge-fallback {
5021
+ display: inline-flex;
5022
+ width: 100%;
5023
+ height: 100%;
5024
+ align-items: center;
5025
+ justify-content: center;
5026
+ font-size: 9px;
5027
+ font-weight: 600;
5028
+ line-height: 1;
5029
+ }
5030
+
4952
5031
  /* Card-network chips: exported Figma tiles are all 38×24. */
4953
5032
  .rs-list-row-meta-icons--cards > .rs-card-network {
4954
5033
  position: relative;
@@ -5068,8 +5147,8 @@
5068
5147
  }
5069
5148
 
5070
5149
  /* The iframe phase owns the whole modal width. Normal screens keep the
5071
- 16px body inset, but Swapped now provides its own in-iframe back chrome so
5072
- the embedded sheet should not look like a padded card inside our modal. */
5150
+ 16px body inset, but the embedded sheet should not look like a padded card
5151
+ inside our modal. */
5073
5152
  .rs-modal-body[data-flow-mode="fiat-onramp"]:has(.rs-fiat-onramp),
5074
5153
  .rs-modal-body[data-flow-mode="exchange-connect"]:has(.rs-fiat-onramp) {
5075
5154
  padding-inline: 0;
@@ -5091,7 +5170,9 @@
5091
5170
  width: 100%;
5092
5171
  min-height: 586px;
5093
5172
  margin: 0;
5094
- border-radius: 0;
5173
+ /* Matches the iframe's 28px below so the loading / error overlays that sit
5174
+ on top of it share the same corner arc. */
5175
+ border-radius: 28px;
5095
5176
  overflow: hidden;
5096
5177
  }
5097
5178
 
@@ -5099,13 +5180,29 @@
5099
5180
  min-height: 585px;
5100
5181
  }
5101
5182
 
5183
+ /* Swapped paints its widget as a card with a 28px corner radius and its drop
5184
+ shadow sits just outside that arc, so the embedding iframe has to clip at the
5185
+ same 28px — otherwise the shadow's corners bleed out past the card and read as
5186
+ grey smudges in our modal corners. Confirmed with Swapped: their iframe-
5187
+ initialization docs still show a stale `border-radius: 0.75rem`, 28px is the
5188
+ value that matches the current widget. */
5102
5189
  .rs-fiat-onramp-iframe {
5103
5190
  display: block;
5104
5191
  width: 100%;
5105
5192
  height: 586px;
5106
5193
  min-height: 586px;
5107
5194
  border: 0;
5195
+ border-radius: 28px;
5108
5196
  background: var(--rs-background);
5197
+ /* Swapped's widget paints its own page background behind a rounded card, and
5198
+ that card is rounder than our modal. Where their radius exceeds ours the
5199
+ modal's corner clip doesn't reach their page background, so a grey wedge
5200
+ shows in each corner (visible on their payment-method screen). Clipping the
5201
+ iframe at a radius at least as round as their card puts the modal's own
5202
+ background there instead.
5203
+ ponytail: one blunt radius rather than tracking Swapped's per-screen shape
5204
+ — if they round their card past this, raise it. */
5205
+ border-radius: 28px;
5109
5206
  /* Swapped's embedded widget has no theme URL param and follows
5110
5207
  `prefers-color-scheme`. Setting `color-scheme` on the iframe element
5111
5208
  propagates into its (cross-origin) document's `prefers-color-scheme`, so
@@ -5129,6 +5226,15 @@
5129
5226
  min-height: 585px;
5130
5227
  }
5131
5228
 
5229
+ /* Connect sits below our header (only fiat is immersive), so only its bottom
5230
+ corners meet the modal's rounded edge. Rounding the top too would leave the
5231
+ shell background showing through as light notches under the header. Applies to
5232
+ the wrap as well so the loading / error overlays share the same arc. */
5233
+ .rs-fiat-onramp[data-variant="connect"] .rs-fiat-onramp-iframe-wrap,
5234
+ .rs-fiat-onramp[data-variant="connect"] .rs-fiat-onramp-iframe {
5235
+ border-radius: 0 0 28px 28px;
5236
+ }
5237
+
5132
5238
  /* Finalising deposit — compact centered spinner screen (Swapped Connect). */
5133
5239
  .rs-finalising-body {
5134
5240
  display: flex;
@@ -5325,6 +5431,16 @@
5325
5431
  line-height: 1.35;
5326
5432
  }
5327
5433
 
5434
+ /* Pre-deposit "Exchange Fee" disclosure shown below the picker footer. */
5435
+ .rs-exchange-select-fee-note {
5436
+ font-size: 12px;
5437
+ font-weight: 500;
5438
+ line-height: 1.35;
5439
+ color: var(--rs-muted);
5440
+ text-align: center;
5441
+ margin: 0;
5442
+ }
5443
+
5328
5444
  /* =============================================================================
5329
5445
  Modal sizing for the Swapped iframe step
5330
5446
  =============================================================================
@@ -5368,16 +5484,32 @@
5368
5484
  min-height: 698px;
5369
5485
  }
5370
5486
 
5371
- /* Iframe phase: with no header/footer chrome the modal is sized to the
5372
- embedded iframe exactly (586 fiat / 585 connect) so it fills the modal with
5373
- no whitespace below. The taller setup values above pre-reserve the space
5374
- before SwappedIframeStep mounts. */
5487
+ /* Iframe phase: fiat is immersive (no header/footer chrome), so the modal is
5488
+ sized to the embedded iframe exactly — 586 — and fills top-to-bottom with no
5489
+ whitespace. The taller setup values above pre-reserve the space before
5490
+ SwappedIframeStep mounts. */
5375
5491
  .rs-modal-content:has(.rs-modal-body[data-flow-mode="fiat-onramp"] .rs-fiat-onramp) {
5376
5492
  min-height: 586px;
5377
5493
  }
5378
5494
 
5495
+ /* Connect keeps our header, so its exact height is the sum of the chrome above
5496
+ the widget: 16px top inset + 24px header row + 16px gap + 585px iframe + 2px
5497
+ borders (border-box). Matching it here means the setup → iframe hand-off
5498
+ settles on one height instead of overshooting and snapping back. */
5379
5499
  .rs-modal-content:has(.rs-modal-body[data-flow-mode="exchange-connect"] .rs-fiat-onramp) {
5380
- min-height: 585px;
5500
+ min-height: 643px;
5501
+ }
5502
+
5503
+ /* In the iframe phase the Swapped widget owns the modal's bottom edge (fiat owns
5504
+ all four corners; Connect owns the bottom two, under our header), so the shell
5505
+ corner takes the widget's 28px instead of our default 16px. Left at 16px, our
5506
+ border would trace a tighter arc than the widget inside it and those corners
5507
+ would show a sliver of modal background between the two curves — plus the
5508
+ widget's own drop shadow. Kept uniform so the shell stays one shape, and
5509
+ scoped to this phase only — every other screen keeps 16px. */
5510
+ .rs-modal-content:has(.rs-modal-body[data-flow-mode="fiat-onramp"] .rs-fiat-onramp),
5511
+ .rs-modal-content:has(.rs-modal-body[data-flow-mode="exchange-connect"] .rs-fiat-onramp) {
5512
+ border-radius: 28px;
5381
5513
  }
5382
5514
 
5383
5515
  /* =============================================================================
@@ -5621,15 +5753,17 @@
5621
5753
  }
5622
5754
 
5623
5755
  /* Swapped/fiat/exchange flows pre-reserve a tall desktop iframe height
5624
- (min-height 585-699px) plus a 445px max-width. On a phone that would force
5625
- the sheet taller than the screen and push its top chrome out of reach, so
5626
- cap it to the sheet height and let the body scroll. Specificity (0,4,0)
5627
- matches the tallest :has() sizing rule and, appended last, wins the tie. */
5756
+ (min-height 585-699px), a 445px max-width, and a 28px shell radius. On a
5757
+ phone that would force the sheet taller than the screen and round its
5758
+ bottom corners away from the viewport edge, so restore the bottom-sheet
5759
+ dimensions and top-only radius. Specificity (0,4,0) matches the iframe
5760
+ phase :has() rules and, appended last, wins the tie. */
5628
5761
  .rs-modal-overlay .rs-modal-content:has(.rs-modal-body[data-flow-mode="fiat-onramp"]),
5629
5762
  .rs-modal-overlay .rs-modal-content:has(.rs-modal-body[data-flow-mode="exchange-connect"]) {
5630
5763
  max-width: none;
5631
5764
  min-height: 0;
5632
5765
  max-height: min(92dvh, calc(100dvh - 24px));
5766
+ border-radius: 16px 16px 0 0;
5633
5767
  }
5634
5768
 
5635
5769
  /* The history panel is a long, scrollable list overlaid position:absolute, so