tempest-react-sdk 0.83.0 → 0.85.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 (70) hide show
  1. package/README.md +1 -0
  2. package/dist/auth/create-tempest-auth.cjs +1 -1
  3. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  4. package/dist/auth/create-tempest-auth.js +294 -178
  5. package/dist/auth/create-tempest-auth.js.map +1 -1
  6. package/dist/components/Markdown/Markdown.cjs +1 -1
  7. package/dist/components/Markdown/Markdown.cjs.map +1 -1
  8. package/dist/components/Markdown/Markdown.js +29 -28
  9. package/dist/components/Markdown/Markdown.js.map +1 -1
  10. package/dist/components/Markdown/markdown-parse.cjs +6 -5
  11. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  12. package/dist/components/Markdown/markdown-parse.js +195 -160
  13. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  14. package/dist/http/resumable-upload.cjs +1 -1
  15. package/dist/http/resumable-upload.cjs.map +1 -1
  16. package/dist/http/resumable-upload.js +1 -1
  17. package/dist/http/resumable-upload.js.map +1 -1
  18. package/dist/http/upload-with-progress.cjs +1 -1
  19. package/dist/http/upload-with-progress.cjs.map +1 -1
  20. package/dist/http/upload-with-progress.js +47 -39
  21. package/dist/http/upload-with-progress.js.map +1 -1
  22. package/dist/payments/PixCheckout.cjs +2 -0
  23. package/dist/payments/PixCheckout.cjs.map +1 -0
  24. package/dist/payments/PixCheckout.js +171 -0
  25. package/dist/payments/PixCheckout.js.map +1 -0
  26. package/dist/payments/PixCheckout.module.cjs +3 -0
  27. package/dist/payments/PixCheckout.module.cjs.map +1 -0
  28. package/dist/payments/PixCheckout.module.js +22 -0
  29. package/dist/payments/PixCheckout.module.js.map +1 -0
  30. package/dist/payments/format.cjs +2 -0
  31. package/dist/payments/format.cjs.map +1 -0
  32. package/dist/payments/format.js +9 -0
  33. package/dist/payments/format.js.map +1 -0
  34. package/dist/payments/mercado-pago.cjs +2 -0
  35. package/dist/payments/mercado-pago.cjs.map +1 -0
  36. package/dist/payments/mercado-pago.js +27 -0
  37. package/dist/payments/mercado-pago.js.map +1 -0
  38. package/dist/payments/outcome.cjs +2 -0
  39. package/dist/payments/outcome.cjs.map +1 -0
  40. package/dist/payments/outcome.js +111 -0
  41. package/dist/payments/outcome.js.map +1 -0
  42. package/dist/payments/types.cjs +2 -0
  43. package/dist/payments/types.cjs.map +1 -0
  44. package/dist/payments/types.js +16 -0
  45. package/dist/payments/types.js.map +1 -0
  46. package/dist/payments/use-mercado-pago-card.cjs +2 -0
  47. package/dist/payments/use-mercado-pago-card.cjs.map +1 -0
  48. package/dist/payments/use-mercado-pago-card.js +128 -0
  49. package/dist/payments/use-mercado-pago-card.js.map +1 -0
  50. package/dist/payments/use-pix-charge.cjs +2 -0
  51. package/dist/payments/use-pix-charge.cjs.map +1 -0
  52. package/dist/payments/use-pix-charge.js +71 -0
  53. package/dist/payments/use-pix-charge.js.map +1 -0
  54. package/dist/query/offline-persistence.cjs +1 -1
  55. package/dist/query/offline-persistence.cjs.map +1 -1
  56. package/dist/query/offline-persistence.js +100 -54
  57. package/dist/query/offline-persistence.js.map +1 -1
  58. package/dist/styles/PixCheckout.css +2 -0
  59. package/dist/styles/component/PixCheckout.css +18 -0
  60. package/dist/styles/manifest.json +6 -0
  61. package/dist/styles/payments.css +18 -0
  62. package/dist/styles.css +1 -1
  63. package/dist/sw/cache.cjs +1 -1
  64. package/dist/sw/cache.cjs.map +1 -1
  65. package/dist/sw/cache.js +1 -1
  66. package/dist/sw/cache.js.map +1 -1
  67. package/dist/tempest-react-sdk.cjs +1 -1
  68. package/dist/tempest-react-sdk.d.ts +913 -29
  69. package/dist/tempest-react-sdk.js +104 -97
  70. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"upload-with-progress.js","names":["UPLOAD_REDIRECT_REFUSED","parseErrorBody","raw","settle","answer","TempestApiError","buildApiError","requestId","parser","originOf","resolved","resolveRequestUrl","url","sendWithoutRedirects","response","init","name","uploadWithProgress","options","signal","token","getToken","sentRequestId","randomId","finalHeaders","headers","scope","credentialOrigin","isTrustedCredentialTarget","trustedOrigins","reportSuppressedCredential","csrfHeaders","redirect","sized","body","total","onProgress","xhr","method","withCredentials","key","value","event","handleAbort","landed","err","reject"],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport {\n isTrustedCredentialTarget,\n reportSuppressedCredential,\n resolveRequestUrl,\n} from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport type { CsrfOptions } from \"./csrf\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n /** Bytes already uploaded. */\n loaded: number;\n /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n total: number;\n /** Fraction between 0 and 1, or null when total is unknown. */\n fraction: number | null;\n lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n url: string;\n body: FormData | Blob | File;\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n headers?: Record<string, string>;\n /** Returns the current bearer token. */\n getToken?: () => string | null | undefined;\n /**\n * Origin the `getToken` credential belongs to, normally the API's.\n * Defaults to the page's own origin (`location.origin`).\n *\n * Unlike `createApiClient`, this helper has no base URL to infer a scope\n * from — the caller names the destination on every call, and the `url` can\n * come from somewhere other than your own code (a signed URL the API handed\n * back, a value from a response body). Until 0.82.0 an unset scope sent the\n * token wherever `url` pointed; it now starts closed, like the CSRF token\n * always did, and an API on another origin sets this to receive it (#530).\n *\n * Outside a browsing context there is no page origin, and the scope falls\n * back to `url` itself.\n */\n credentialOrigin?: string;\n /**\n * Extra origins allowed to receive the credential, on top of\n * {@link credentialOrigin} (or the page origin it defaults to).\n */\n trustedOrigins?: readonly string[];\n /** Send cookies. Defaults to false. */\n withCredentials?: boolean;\n /**\n * Send a CSRF token with the upload, as `createApiClient`'s `csrf` does.\n * Off by default; `true` reads the `csrf_token` cookie into `X-CSRF-Token`.\n *\n * Scoped exactly like the bearer token: to {@link credentialOrigin} (the\n * page's own origin when unset) plus {@link trustedOrigins}. An API on\n * another origin sets `credentialOrigin` to receive the token.\n */\n csrf?: boolean | CsrfOptions;\n /** Called on every `progress` event from the XHR upload channel. */\n onProgress?: (event: UploadProgressEvent) => void;\n /** Abort the request. */\n signal?: AbortSignal;\n /** Override the JSON parser. Defaults to `JSON.parse`. */\n parser?: (raw: string) => unknown;\n /**\n * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n * Defaults to a generated id. Return an empty string to disable.\n */\n requestId?: () => string;\n /**\n * What to do when the server answers with a redirect. Default `\"follow\"`.\n *\n * - `\"follow\"` — the `XMLHttpRequest` path, with byte-level progress. The\n * browser follows every redirect and offers no way to stop it: measured\n * in Chrome 150 (#570), a same-origin upload answered `301`/`302`/`303`/\n * `307`/`308` towards another origin delivered `X-Api-Key` from\n * {@link headers} and the SDK's own `X-CSRF-Token` there on all five, and\n * the body on `307`/`308`; only `Authorization` was stripped. What this\n * path does enforce (since 0.83.0): an answer from another origin than\n * the one the request was sent to is never the result — it rejects with\n * `code: \"REDIRECT_REFUSED\"`, so that host cannot hand the app a URL to\n * store. Until 0.82.0 it resolved with the other host's body.\n * - `\"error\"` — goes out over `fetch` with `redirect: \"manual\"`, and a\n * redirect rejects with `code: \"REDIRECT_REFUSED\"` before anything is\n * sent to the other host. The cost: `fetch` reports no upload progress,\n * so `onProgress` fires once, when the server confirms the whole body. A\n * streamed `fetch` body would report progress, but Chrome refuses one\n * over HTTP/1.1 (`TypeError: Failed to fetch`, measured) and Firefox and\n * Safari do not support it.\n *\n * Use `\"error\"` when {@link headers} carry a credential (an `X-Api-Key`) and\n * the server behind `url` could redirect; sending the bearer through\n * {@link getToken} keeps `\"follow\"` safe for it, because the browser strips\n * `Authorization` on a cross-origin redirect.\n */\n redirect?: \"follow\" | \"error\";\n}\n\n/**\n * The `code` of the {@link TempestApiError} an upload rejects with when a\n * redirect is refused, or when the answer came from another origin — the code\n * `createApiClient` and `createResumableUpload` use for the same refusal.\n */\nconst UPLOAD_REDIRECT_REFUSED = \"REDIRECT_REFUSED\";\n\n/** What a finished upload answered, whichever transport carried it. */\ninterface UploadAnswer {\n status: number;\n text: string;\n header: (name: string) => string | null;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n if (!raw) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Turn a finished upload's answer into the promised value, or the error the\n * contract promises. Shared by both transports, so `redirect: \"follow\"` and\n * `redirect: \"error\"` resolve and reject the same way.\n *\n * @param answer - Status, raw text and a header reader.\n * @param parser - The JSON parser.\n * @param requestId - The `X-Request-ID` sent, for the error.\n * @returns The parsed JSON, the raw text, or `undefined` for an empty answer.\n * @throws {TempestApiError} On a non-2xx status.\n */\nfunction settle<T>(answer: UploadAnswer, parser: (raw: string) => unknown, requestId: string): T {\n if (answer.status < 200 || answer.status >= 300) {\n throw new TempestApiError(\n buildApiError(\n answer.status,\n parseErrorBody(answer.text),\n { get: answer.header },\n requestId,\n ),\n );\n }\n if (answer.status === 204 || !answer.text) return undefined as T;\n if ((answer.header(\"content-type\") ?? \"\").includes(\"application/json\")) {\n return parser(answer.text) as T;\n }\n return answer.text as unknown as T;\n}\n\n/**\n * The origin a URL resolves to, or `null` when it does not resolve.\n *\n * @param url - Absolute or relative URL.\n * @returns The origin.\n */\nfunction originOf(url: string): string | null {\n const resolved = resolveRequestUrl(url);\n return resolved === null ? null : new URL(resolved).origin;\n}\n\n/** What {@link sendWithoutRedirects} sends. */\ninterface ManualUpload {\n url: string;\n method: string;\n headers: Record<string, string>;\n body: FormData | Blob;\n withCredentials: boolean;\n signal?: AbortSignal;\n requestId: string;\n}\n\n/**\n * Send the upload over `fetch` with `redirect: \"manual\"` — the\n * `redirect: \"error\"` path of {@link uploadWithProgress}.\n *\n * A browser hands the stopped redirect back as an `opaqueredirect` with\n * status `0`; a server runtime hands back the real `3xx`. Both are refused;\n * `304` is a cache answer, not a redirect.\n *\n * @param init - Where, how, and what to send.\n * @returns The answer.\n * @throws {TempestApiError} `code: \"REDIRECT_REFUSED\"` on a redirect, status\n * `0` on a network failure.\n */\nasync function sendWithoutRedirects(init: ManualUpload): Promise<UploadAnswer> {\n let response: Response;\n try {\n response = await fetch(init.url, {\n method: init.method,\n headers: init.headers,\n body: init.body,\n credentials: init.withCredentials ? \"include\" : \"same-origin\",\n redirect: \"manual\",\n signal: init.signal,\n });\n } catch {\n if (init.signal?.aborted) throw new DOMException(\"Aborted\", \"AbortError\");\n throw new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: init.requestId || undefined,\n });\n }\n if (\n response.type === \"opaqueredirect\" ||\n (response.status >= 300 && response.status < 400 && response.status !== 304)\n ) {\n throw new TempestApiError({\n status: response.status,\n code: UPLOAD_REDIRECT_REFUSED,\n detail: \"Redirect não seguido no upload.\",\n requestId: init.requestId || undefined,\n });\n }\n return {\n status: response.status,\n text: await response.text(),\n header: (name) => response.headers.get(name),\n };\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * An `XMLHttpRequest` cannot refuse a redirect; see\n * {@link UploadWithProgressOptions.redirect} for what follows one, what the\n * helper still refuses, and the `fetch` path that trades progress for never\n * following.\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n const {\n url,\n body,\n method = \"POST\",\n headers = {},\n getToken,\n credentialOrigin,\n trustedOrigins,\n withCredentials = false,\n csrf,\n onProgress,\n signal,\n parser = JSON.parse,\n requestId,\n redirect = \"follow\",\n } = options;\n\n if (signal?.aborted) return Promise.reject(new DOMException(\"Aborted\", \"AbortError\"));\n\n const token = getToken?.();\n const sentRequestId = requestId ? requestId() : randomId();\n const finalHeaders: Record<string, string> = { ...headers };\n const scope = credentialOrigin ?? globalThis.location?.origin ?? url;\n if (token && !(\"Authorization\" in finalHeaders)) {\n if (isTrustedCredentialTarget(url, scope, trustedOrigins)) {\n finalHeaders.Authorization = `Bearer ${token}`;\n } else {\n reportSuppressedCredential(url, scope);\n }\n }\n Object.assign(\n finalHeaders,\n csrfHeaders({\n method,\n url,\n reference: scope,\n csrf,\n trustedOrigins,\n headers,\n }),\n );\n if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n finalHeaders[\"X-Request-ID\"] = sentRequestId;\n }\n\n if (redirect === \"error\") {\n return sendWithoutRedirects({\n url,\n method,\n headers: finalHeaders,\n body,\n withCredentials,\n signal,\n requestId: sentRequestId,\n }).then((answer) => {\n const sized = body instanceof Blob;\n const total = sized ? body.size : 0;\n onProgress?.({\n loaded: total,\n total,\n fraction: sized ? 1 : null,\n lengthComputable: sized,\n });\n return settle<T>(answer, parser, sentRequestId);\n });\n }\n\n return new Promise<T>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(method, url);\n xhr.withCredentials = withCredentials;\n for (const [key, value] of Object.entries(finalHeaders)) {\n xhr.setRequestHeader(key, value);\n }\n\n if (onProgress) {\n xhr.upload.onprogress = (event) => {\n onProgress({\n loaded: event.loaded,\n total: event.total,\n fraction: event.lengthComputable ? event.loaded / event.total : null,\n lengthComputable: event.lengthComputable,\n });\n };\n }\n\n function handleAbort(): void {\n xhr.abort();\n }\n signal?.addEventListener(\"abort\", handleAbort);\n\n xhr.onload = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n const landed = xhr.responseURL ? originOf(xhr.responseURL) : null;\n if (landed !== null && landed !== originOf(url)) {\n reject(\n new TempestApiError({\n status: xhr.status,\n code: UPLOAD_REDIRECT_REFUSED,\n detail: \"O upload foi redirecionado para outra origem.\",\n requestId: sentRequestId || undefined,\n }),\n );\n return;\n }\n try {\n resolve(\n settle<T>(\n {\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n },\n parser,\n sentRequestId,\n ),\n );\n } catch (err) {\n reject(err);\n }\n };\n\n xhr.onerror = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: sentRequestId || undefined,\n }),\n );\n };\n\n xhr.onabort = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n\n xhr.send(body);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"upload-with-progress.js","names":["UPLOAD_REDIRECT_REFUSED","HEADERS_THAT_KEEP_FOLLOWING","defaultRedirect","wantsProgress","headers","name","parseErrorBody","raw","settle","answer","TempestApiError","buildApiError","requestId","parser","originOf","resolved","resolveRequestUrl","url","sendWithoutRedirects","response","init","uploadWithProgress","options","signal","token","getToken","sentRequestId","randomId","finalHeaders","scope","credentialOrigin","isTrustedCredentialTarget","trustedOrigins","reportSuppressedCredential","csrfHeaders","redirect","sized","body","total","onProgress","xhr","method","withCredentials","key","value","event","handleAbort","landed","err","reject"],"sources":["../../src/http/upload-with-progress.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — XMLHttpRequest is used instead of fetch precisely\n * because it reports upload progress, and its API is event-based: the body wires\n * load, error, abort, timeout and progress against one request object, plus the\n * AbortSignal bridge.\n */\nimport { randomId } from \"../utils\";\nimport {\n isTrustedCredentialTarget,\n reportSuppressedCredential,\n resolveRequestUrl,\n} from \"./credential-scope\";\nimport { csrfHeaders } from \"./csrf\";\nimport type { CsrfOptions } from \"./csrf\";\nimport { buildApiError, TempestApiError } from \"./errors\";\n\nexport interface UploadProgressEvent {\n /** Bytes already uploaded. */\n loaded: number;\n /** Total payload size in bytes — only meaningful when `lengthComputable` is true. */\n total: number;\n /** Fraction between 0 and 1, or null when total is unknown. */\n fraction: number | null;\n lengthComputable: boolean;\n}\n\nexport interface UploadWithProgressOptions {\n url: string;\n body: FormData | Blob | File;\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n headers?: Record<string, string>;\n /** Returns the current bearer token. */\n getToken?: () => string | null | undefined;\n /**\n * Origin the `getToken` credential belongs to, normally the API's.\n * Defaults to the page's own origin (`location.origin`).\n *\n * Unlike `createApiClient`, this helper has no base URL to infer a scope\n * from — the caller names the destination on every call, and the `url` can\n * come from somewhere other than your own code (a signed URL the API handed\n * back, a value from a response body). Until 0.82.0 an unset scope sent the\n * token wherever `url` pointed; it now starts closed, like the CSRF token\n * always did, and an API on another origin sets this to receive it (#530).\n *\n * Outside a browsing context there is no page origin, and the scope falls\n * back to `url` itself.\n */\n credentialOrigin?: string;\n /**\n * Extra origins allowed to receive the credential, on top of\n * {@link credentialOrigin} (or the page origin it defaults to).\n */\n trustedOrigins?: readonly string[];\n /** Send cookies. Defaults to false. */\n withCredentials?: boolean;\n /**\n * Send a CSRF token with the upload, as `createApiClient`'s `csrf` does.\n * Off by default; `true` reads the `csrf_token` cookie into `X-CSRF-Token`.\n *\n * Scoped exactly like the bearer token: to {@link credentialOrigin} (the\n * page's own origin when unset) plus {@link trustedOrigins}. An API on\n * another origin sets `credentialOrigin` to receive the token.\n */\n csrf?: boolean | CsrfOptions;\n /** Called on every `progress` event from the XHR upload channel. */\n onProgress?: (event: UploadProgressEvent) => void;\n /** Abort the request. */\n signal?: AbortSignal;\n /** Override the JSON parser. Defaults to `JSON.parse`. */\n parser?: (raw: string) => unknown;\n /**\n * Per-request correlation id sent as `X-Request-ID` (Tempest convention).\n * Defaults to a generated id. Return an empty string to disable.\n */\n requestId?: () => string;\n /**\n * What to do when the server answers with a redirect. When unset, the\n * helper picks: `\"error\"` when {@link headers} carry anything besides the\n * headers it exempts (`Authorization`, `X-Request-ID`, `Content-Type` and\n * `X-CSRF-Token`) **and** no {@link onProgress} was given, `\"follow\"`\n * otherwise (since 0.84.0). A value passed here always wins.\n *\n * - `\"follow\"` — the `XMLHttpRequest` path, with byte-level progress. The\n * browser follows every redirect and offers no way to stop it: measured\n * in Chrome 150, and again in Chromium 153, Firefox 155 and WebKit 26.6\n * (#570), a same-origin upload answered `301`/`302`/`303`/`307`/`308`\n * towards another origin delivered `X-Api-Key` from {@link headers} and\n * the SDK's own `X-CSRF-Token` there on all five, and the body on\n * `307`/`308`; only `Authorization` was stripped. What this path does\n * enforce (since 0.83.0): an answer from another origin than the one the\n * request was sent to is never the result — it rejects with\n * `code: \"REDIRECT_REFUSED\"`, so that host cannot hand the app a URL to\n * store. Until 0.82.0 it resolved with the other host's body.\n * - `\"error\"` — goes out over `fetch` with `redirect: \"manual\"`, and a\n * redirect rejects with `code: \"REDIRECT_REFUSED\"` before anything is\n * sent to the other host. The cost: `fetch` reports no upload progress,\n * so `onProgress` fires once, when the server confirms the whole body,\n * and a same-origin redirect (FastAPI's trailing-slash `307`) is refused\n * too — a browser hands it back opaque, with no readable `Location`. A\n * streamed `fetch` body would report progress, but Chrome refuses one\n * over HTTP/1.1 (`TypeError: Failed to fetch`, measured) and Firefox and\n * Safari do not support it.\n *\n * Why the default depends on `onProgress`: progress is the only thing the\n * XHR path buys, and the leak is its price. A caller that asked for no\n * progress paid that price for nothing, so its fixed headers get the\n * treatment `createApiClient` (#560) and `createResumableUpload` (#568)\n * give theirs. A caller that wants progress keeps it, and the guarantee is\n * one `redirect: \"error\"` away. Sending the bearer through\n * {@link getToken} keeps `\"follow\"` safe for it, because the browser\n * strips `Authorization` on a cross-origin redirect.\n */\n redirect?: \"follow\" | \"error\";\n}\n\n/**\n * The `code` of the {@link TempestApiError} an upload rejects with when a\n * redirect is refused, or when the answer came from another origin — the code\n * `createApiClient` and `createResumableUpload` use for the same refusal.\n */\nconst UPLOAD_REDIRECT_REFUSED = \"REDIRECT_REFUSED\";\n\n/**\n * Fixed headers that do not switch an upload without `onProgress` to\n * `redirect: \"error\"`, lowercased.\n *\n * `authorization` is stripped by the browser on a cross-origin redirect\n * (measured in Chromium 153, Firefox 155 and WebKit 26.6). `x-request-id` and\n * `content-type` are not secrets. `x-csrf-token` does cross the redirect, and\n * is exempt for the reason `createApiClient` (#560) exempts it: it rides on\n * every `POST` of a cookie session, which is exactly the trailing-slash `307`\n * case, and on its own it authenticates nothing — using it takes the victim's\n * cookie and a custom header the API's CORS does not allow from another\n * origin. The header the `csrf` option adds is not part of `headers` and never\n * counts.\n */\nconst HEADERS_THAT_KEEP_FOLLOWING: ReadonlySet<string> = new Set([\n \"authorization\",\n \"x-request-id\",\n \"content-type\",\n \"x-csrf-token\",\n]);\n\n/**\n * The redirect mode an upload runs with when the caller did not pick one.\n *\n * @param headers - The caller's fixed headers.\n * @param wantsProgress - Whether an `onProgress` was given.\n * @returns `\"error\"` when a header that would leak rides along and progress\n * was not asked for, `\"follow\"` otherwise.\n */\nfunction defaultRedirect(\n headers: Record<string, string>,\n wantsProgress: boolean,\n): \"follow\" | \"error\" {\n if (wantsProgress) return \"follow\";\n const leaks = Object.keys(headers).some(\n (name) => !HEADERS_THAT_KEEP_FOLLOWING.has(name.toLowerCase()),\n );\n return leaks ? \"error\" : \"follow\";\n}\n\n/** What a finished upload answered, whichever transport carried it. */\ninterface UploadAnswer {\n status: number;\n text: string;\n header: (name: string) => string | null;\n}\n\nfunction parseErrorBody(raw: string): unknown {\n if (!raw) return null;\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n\n/**\n * Turn a finished upload's answer into the promised value, or the error the\n * contract promises. Shared by both transports, so `redirect: \"follow\"` and\n * `redirect: \"error\"` resolve and reject the same way.\n *\n * @param answer - Status, raw text and a header reader.\n * @param parser - The JSON parser.\n * @param requestId - The `X-Request-ID` sent, for the error.\n * @returns The parsed JSON, the raw text, or `undefined` for an empty answer.\n * @throws {TempestApiError} On a non-2xx status.\n */\nfunction settle<T>(answer: UploadAnswer, parser: (raw: string) => unknown, requestId: string): T {\n if (answer.status < 200 || answer.status >= 300) {\n throw new TempestApiError(\n buildApiError(\n answer.status,\n parseErrorBody(answer.text),\n { get: answer.header },\n requestId,\n ),\n );\n }\n if (answer.status === 204 || !answer.text) return undefined as T;\n if ((answer.header(\"content-type\") ?? \"\").includes(\"application/json\")) {\n return parser(answer.text) as T;\n }\n return answer.text as unknown as T;\n}\n\n/**\n * The origin a URL resolves to, or `null` when it does not resolve.\n *\n * @param url - Absolute or relative URL.\n * @returns The origin.\n */\nfunction originOf(url: string): string | null {\n const resolved = resolveRequestUrl(url);\n return resolved === null ? null : new URL(resolved).origin;\n}\n\n/** What {@link sendWithoutRedirects} sends. */\ninterface ManualUpload {\n url: string;\n method: string;\n headers: Record<string, string>;\n body: FormData | Blob;\n withCredentials: boolean;\n signal?: AbortSignal;\n requestId: string;\n}\n\n/**\n * Send the upload over `fetch` with `redirect: \"manual\"` — the\n * `redirect: \"error\"` path of {@link uploadWithProgress}.\n *\n * A browser hands the stopped redirect back as an `opaqueredirect` with\n * status `0`; a server runtime hands back the real `3xx`. Both are refused;\n * `304` is a cache answer, not a redirect.\n *\n * @param init - Where, how, and what to send.\n * @returns The answer.\n * @throws {TempestApiError} `code: \"REDIRECT_REFUSED\"` on a redirect, status\n * `0` on a network failure.\n */\nasync function sendWithoutRedirects(init: ManualUpload): Promise<UploadAnswer> {\n let response: Response;\n try {\n response = await fetch(init.url, {\n method: init.method,\n headers: init.headers,\n body: init.body,\n credentials: init.withCredentials ? \"include\" : \"same-origin\",\n redirect: \"manual\",\n signal: init.signal,\n });\n } catch {\n if (init.signal?.aborted) throw new DOMException(\"Aborted\", \"AbortError\");\n throw new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: init.requestId || undefined,\n });\n }\n if (\n response.type === \"opaqueredirect\" ||\n (response.status >= 300 && response.status < 400 && response.status !== 304)\n ) {\n throw new TempestApiError({\n status: response.status,\n code: UPLOAD_REDIRECT_REFUSED,\n detail: \"Redirect não seguido no upload.\",\n requestId: init.requestId || undefined,\n });\n }\n return {\n status: response.status,\n text: await response.text(),\n header: (name) => response.headers.get(name),\n };\n}\n\n/**\n * Upload a file (or any payload) with byte-level progress reporting.\n *\n * `fetch` cannot report upload progress in browsers, so this helper falls\n * back to `XMLHttpRequest`. It mirrors the error contract used by\n * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.\n *\n * An `XMLHttpRequest` cannot refuse a redirect; see\n * {@link UploadWithProgressOptions.redirect} for what follows one, what the\n * helper still refuses, the `fetch` path that trades progress for never\n * following, and when that path is the default (fixed headers and no\n * `onProgress`, since 0.84.0).\n *\n * @returns The parsed JSON response, or the raw text when the response is not JSON.\n */\nexport function uploadWithProgress<T = unknown>(options: UploadWithProgressOptions): Promise<T> {\n const {\n url,\n body,\n method = \"POST\",\n headers = {},\n getToken,\n credentialOrigin,\n trustedOrigins,\n withCredentials = false,\n csrf,\n onProgress,\n signal,\n parser = JSON.parse,\n requestId,\n redirect = defaultRedirect(options.headers ?? {}, options.onProgress !== undefined),\n } = options;\n\n if (signal?.aborted) return Promise.reject(new DOMException(\"Aborted\", \"AbortError\"));\n\n const token = getToken?.();\n const sentRequestId = requestId ? requestId() : randomId();\n const finalHeaders: Record<string, string> = { ...headers };\n const scope = credentialOrigin ?? globalThis.location?.origin ?? url;\n if (token && !(\"Authorization\" in finalHeaders)) {\n if (isTrustedCredentialTarget(url, scope, trustedOrigins)) {\n finalHeaders.Authorization = `Bearer ${token}`;\n } else {\n reportSuppressedCredential(url, scope);\n }\n }\n Object.assign(\n finalHeaders,\n csrfHeaders({\n method,\n url,\n reference: scope,\n csrf,\n trustedOrigins,\n headers,\n }),\n );\n if (sentRequestId && !(\"X-Request-ID\" in finalHeaders)) {\n finalHeaders[\"X-Request-ID\"] = sentRequestId;\n }\n\n if (redirect === \"error\") {\n return sendWithoutRedirects({\n url,\n method,\n headers: finalHeaders,\n body,\n withCredentials,\n signal,\n requestId: sentRequestId,\n }).then((answer) => {\n const sized = body instanceof Blob;\n const total = sized ? body.size : 0;\n onProgress?.({\n loaded: total,\n total,\n fraction: sized ? 1 : null,\n lengthComputable: sized,\n });\n return settle<T>(answer, parser, sentRequestId);\n });\n }\n\n return new Promise<T>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(method, url);\n xhr.withCredentials = withCredentials;\n for (const [key, value] of Object.entries(finalHeaders)) {\n xhr.setRequestHeader(key, value);\n }\n\n if (onProgress) {\n xhr.upload.onprogress = (event) => {\n onProgress({\n loaded: event.loaded,\n total: event.total,\n fraction: event.lengthComputable ? event.loaded / event.total : null,\n lengthComputable: event.lengthComputable,\n });\n };\n }\n\n function handleAbort(): void {\n xhr.abort();\n }\n signal?.addEventListener(\"abort\", handleAbort);\n\n xhr.onload = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n const landed = xhr.responseURL ? originOf(xhr.responseURL) : null;\n if (landed !== null && landed !== originOf(url)) {\n reject(\n new TempestApiError({\n status: xhr.status,\n code: UPLOAD_REDIRECT_REFUSED,\n detail: \"O upload foi redirecionado para outra origem.\",\n requestId: sentRequestId || undefined,\n }),\n );\n return;\n }\n try {\n resolve(\n settle<T>(\n {\n status: xhr.status,\n text: xhr.responseText,\n header: (name) => xhr.getResponseHeader(name),\n },\n parser,\n sentRequestId,\n ),\n );\n } catch (err) {\n reject(err);\n }\n };\n\n xhr.onerror = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(\n new TempestApiError({\n status: 0,\n detail: \"Falha de rede no upload.\",\n requestId: sentRequestId || undefined,\n }),\n );\n };\n\n xhr.onabort = () => {\n signal?.removeEventListener(\"abort\", handleAbort);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n\n xhr.send(body);\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../utils/cn.cjs"),t=require("../components/Alert/Alert.cjs"),n=require("../hooks/use-countdown.cjs"),r=require("../components/QRCode/QRCode.cjs"),i=require("../components/CopyButton/CopyButton.cjs"),a=require("../components/Money/Money.cjs"),o=require("./format.cjs"),s=require("./PixCheckout.module.cjs");let c=require("react/jsx-runtime");var l={qr:`QR code do Pix`,copyHint:`Ou copie o código Pix e cole no app do seu banco`,copy:`Copiar código`,copied:`Copiado`,expiresIn:`Expira em`,confirming:`O prazo acabou. Estamos confirmando com o banco se o pagamento chegou.`,states:{pending:{title:`Aguardando pagamento`,description:`Abra o app do seu banco, escolha Pix e escaneie o QR code.`},authorized:{title:`Pagamento autorizado`,description:`Estamos concluindo o pagamento.`},in_analysis:{title:`Pagamento em análise`,description:`Avisaremos assim que for concluído.`},paid:{title:`Pagamento confirmado`,description:`Recebemos o seu Pix. Obrigado!`},expired:{title:`Pix expirado`,description:`O prazo para pagar acabou. Gere um novo Pix.`},cancelled:{title:`Pix cancelado`,description:`Esta cobrança foi cancelada e não pode mais ser paga.`},refunded:{title:`Pix devolvido`,description:`O valor foi devolvido para a sua conta.`},charged_back:{title:`Pix contestado`,description:`O pagamento foi contestado junto ao banco.`},failed:{title:`Pix não concluído`,description:`Não foi possível concluir o pagamento. Gere um novo Pix.`},unknown:{title:`Verificando o pagamento`,description:`Ainda estamos confirmando este Pix.`}}},u={pending:`info`,authorized:`info`,in_analysis:`info`,paid:`success`,expired:`warning`,cancelled:`neutral`,refunded:`neutral`,charged_back:`warning`,failed:`danger`,unknown:`neutral`},d=/^[A-Za-z0-9+/]+={0,2}$/,f=`data:image/png;base64,`;function p(e){if(e===null)return null;let t=(e.startsWith(f)?e.slice(22):e).replace(/\s+/g,``);return t.length>0&&t.length%4==0&&d.test(t)?f+t:null}function m({charge:d,qrSize:f=220,labels:m,formatRemaining:h=o.formatPixRemaining,onCopied:g,footer:_,className:v,...y}){let b={...l,...m,states:{...l.states,...m?.states}},x=d.expires_at===null?NaN:Date.parse(d.expires_at),S=Number.isFinite(x),C=n.useCountdown(S?x:0,0),w=Object.hasOwn(u,d.status)?d.status:`unknown`,T=S&&C<=0,E=w===`pending`&&!T&&d.br_code!==null,D=p(d.qr_code_base64),O=b.states[w];return(0,c.jsxs)(`div`,{className:e.cn(s.default.checkout,v),"data-status":w,...y,children:[(0,c.jsx)(a.Money,{className:s.default.amount,cents:d.amount_cents,currency:d.currency}),w===`pending`?(0,c.jsxs)(`div`,{role:`status`,className:s.default.state,children:[(0,c.jsx)(`p`,{className:s.default.title,children:O.title}),(0,c.jsx)(`p`,{className:s.default.description,children:T?b.confirming:O.description})]}):(0,c.jsx)(t.Alert,{className:s.default.state,variant:u[w],title:O.title,description:O.description}),E&&S&&(0,c.jsxs)(`p`,{className:s.default.timer,role:`timer`,"aria-live":`off`,children:[b.expiresIn,` `,(0,c.jsx)(`strong`,{children:h(C)})]}),E&&d.br_code!==null&&(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(`div`,{className:s.default.symbol,children:D===null?(0,c.jsx)(r.QRCode,{value:d.br_code,size:f,label:b.qr,margin:2}):(0,c.jsx)(`img`,{src:D,alt:b.qr,width:f,height:f,className:s.default.image})}),(0,c.jsx)(`p`,{className:s.default.hint,children:b.copyHint}),(0,c.jsxs)(`div`,{className:s.default.copy,children:[(0,c.jsx)(`code`,{className:s.default.code,"data-testid":`pix-checkout-code`,children:d.br_code}),(0,c.jsxs)(i.CopyButton,{value:d.br_code,onCopied:g,children:[(0,c.jsx)(`span`,{className:s.default.copyIdle,children:b.copy}),(0,c.jsx)(`span`,{className:s.default.copyDone,children:b.copied})]})]})]}),_!==void 0&&(0,c.jsx)(`div`,{className:s.default.footer,children:_})]})}exports.PixCheckout=m;
2
+ //# sourceMappingURL=PixCheckout.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PixCheckout.cjs","names":["formatPixRemaining","useCountdown","cn","styles","Money","Alert","_Fragment","QRCode","CopyButton","require_cn","require_Alert","require_use_countdown","require_QRCode","require_CopyButton","require_Money","require_format","require_PixCheckout_module","react_jsx_runtime","DEFAULT_LABELS","STATE_VARIANT","BASE64","PNG_PREFIX","qrImageSource","base64","compact","PixCheckout","rest","text","labels","expiresAt","charge","hasDeadline","remaining","status","pastDeadline","payable","image","state","className","formatRemaining","qrSize","footer"],"sources":["../../src/payments/PixCheckout.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — `charge` is the data, `qrSize` the symbol,\n * `labels`/`formatRemaining` the copy an app translates, and `onCopied` the one\n * intent the screen emits.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\n\nimport { Alert } from \"@/components/Alert\";\nimport type { AlertVariant } from \"@/components/Alert\";\nimport { CopyButton } from \"@/components/CopyButton\";\nimport { Money } from \"@/components/Money\";\nimport { QRCode } from \"@/components/QRCode\";\nimport { useCountdown } from \"@/hooks/use-countdown\";\nimport { cn } from \"@/utils/cn\";\n\nimport { formatPixRemaining } from \"./format\";\nimport type { PaymentStatus, PixCharge } from \"./types\";\nimport styles from \"./PixCheckout.module.css\";\n\n/** Strings the screen renders, so an app can translate them. */\nexport interface PixCheckoutLabels {\n /** Accessible name of the QR. */\n qr: string;\n /** Heading over the copia-e-cola line. */\n copyHint: string;\n /** Copy button, idle. */\n copy: string;\n /** Copy button, just clicked. */\n copied: string;\n /** Prefix of the countdown, e.g. \"Expira em\". */\n expiresIn: string;\n /** Shown when the deadline passed but the backend has not said \"expired\" yet. */\n confirming: string;\n /** Title and description per terminal or waiting state. */\n states: Record<PaymentStatus, { title: string; description: string }>;\n}\n\nconst DEFAULT_LABELS: PixCheckoutLabels = {\n qr: \"QR code do Pix\",\n copyHint: \"Ou copie o código Pix e cole no app do seu banco\",\n copy: \"Copiar código\",\n copied: \"Copiado\",\n expiresIn: \"Expira em\",\n confirming: \"O prazo acabou. Estamos confirmando com o banco se o pagamento chegou.\",\n states: {\n pending: {\n title: \"Aguardando pagamento\",\n description: \"Abra o app do seu banco, escolha Pix e escaneie o QR code.\",\n },\n authorized: {\n title: \"Pagamento autorizado\",\n description: \"Estamos concluindo o pagamento.\",\n },\n in_analysis: {\n title: \"Pagamento em análise\",\n description: \"Avisaremos assim que for concluído.\",\n },\n paid: { title: \"Pagamento confirmado\", description: \"Recebemos o seu Pix. Obrigado!\" },\n expired: {\n title: \"Pix expirado\",\n description: \"O prazo para pagar acabou. Gere um novo Pix.\",\n },\n cancelled: {\n title: \"Pix cancelado\",\n description: \"Esta cobrança foi cancelada e não pode mais ser paga.\",\n },\n refunded: {\n title: \"Pix devolvido\",\n description: \"O valor foi devolvido para a sua conta.\",\n },\n charged_back: {\n title: \"Pix contestado\",\n description: \"O pagamento foi contestado junto ao banco.\",\n },\n failed: {\n title: \"Pix não concluído\",\n description: \"Não foi possível concluir o pagamento. Gere um novo Pix.\",\n },\n unknown: {\n title: \"Verificando o pagamento\",\n description: \"Ainda estamos confirmando este Pix.\",\n },\n },\n};\n\nconst STATE_VARIANT: Record<PaymentStatus, AlertVariant> = {\n pending: \"info\",\n authorized: \"info\",\n in_analysis: \"info\",\n paid: \"success\",\n expired: \"warning\",\n cancelled: \"neutral\",\n refunded: \"neutral\",\n charged_back: \"warning\",\n failed: \"danger\",\n unknown: \"neutral\",\n};\n\nconst BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;\nconst PNG_PREFIX = \"data:image/png;base64,\";\n\n/**\n * The QR image source, or `null` to draw it locally.\n *\n * Only a well-formed Base64 payload becomes a `data:image/png` URL: the field\n * arrives from the network, and an image `src` built from an unchecked string\n * is a place where something other than a PNG can end up. A value that already\n * carries the `data:image/png;base64,` prefix is accepted once.\n */\nfunction qrImageSource(base64: string | null): string | null {\n if (base64 === null) return null;\n const body = base64.startsWith(PNG_PREFIX) ? base64.slice(PNG_PREFIX.length) : base64;\n const compact = body.replace(/\\s+/g, \"\");\n return compact.length > 0 && compact.length % 4 === 0 && BASE64.test(compact)\n ? PNG_PREFIX + compact\n : null;\n}\n\nexport interface PixCheckoutProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** The charge as the backend reports it — pair with {@link usePixCharge} to follow it. */\n charge: PixCharge;\n /** Rendered side of the QR in px. Default `220`. */\n qrSize?: number;\n labels?: Partial<Omit<PixCheckoutLabels, \"states\">> & {\n states?: Partial<PixCheckoutLabels[\"states\"]>;\n };\n /** Formats the countdown. Default {@link formatPixRemaining}. */\n formatRemaining?: (ms: number) => string;\n /** Called after the copia-e-cola reaches the clipboard. */\n onCopied?: () => void;\n /** Rendered under the status, e.g. a \"gerar novo Pix\" button for a final state. */\n footer?: ReactNode;\n}\n\n/**\n * The payer's Pix screen: amount, QR, copia-e-cola, countdown and the state the\n * backend reports.\n *\n * Presentational and controlled. It renders `charge` and never changes it, so\n * the decision that a Pix was paid stays where it can be made — on the backend,\n * re-reading the order. Feed it from {@link usePixCharge} or from your own\n * stream.\n *\n * The QR comes from `qr_code_base64` when the provider sent one (Mercado Pago),\n * and is otherwise drawn locally from `br_code` (OpenPix sends a URL instead,\n * and fetching it would hand the payload to an image host for nothing). The\n * copia-e-cola line is always next to it: a QR cannot be scanned by the phone\n * that is showing it, which is where a mobile checkout puts it.\n *\n * A status this version does not know renders as `unknown` (\"verificando\"). Known\n * means an own key of the state table, so a status named after an\n * `Object.prototype` member (`toString`, `constructor`) is unknown too, instead\n * of an empty state block.\n *\n * When the countdown reaches zero while the backend still says `pending`, the\n * screen hides the QR and says it is **confirming** — not \"expired\". A payment\n * sent in the last second can still settle, and only the backend knows.\n *\n * @example\n * const { charge } = usePixCharge(created, { refresh: (signal) => readCharge(created.provider_charge_id, signal) });\n * <PixCheckout charge={charge} />\n */\nexport function PixCheckout({\n charge,\n qrSize = 220,\n labels,\n formatRemaining = formatPixRemaining,\n onCopied,\n footer,\n className,\n ...rest\n}: PixCheckoutProps) {\n const text: PixCheckoutLabels = {\n ...DEFAULT_LABELS,\n ...labels,\n states: { ...DEFAULT_LABELS.states, ...labels?.states },\n };\n const expiresAt = charge.expires_at === null ? Number.NaN : Date.parse(charge.expires_at);\n const hasDeadline = Number.isFinite(expiresAt);\n const remaining = useCountdown(hasDeadline ? expiresAt : 0, 0);\n const status: PaymentStatus = Object.hasOwn(STATE_VARIANT, charge.status)\n ? charge.status\n : \"unknown\";\n const pastDeadline = hasDeadline && remaining <= 0;\n const payable = status === \"pending\" && !pastDeadline && charge.br_code !== null;\n const image = qrImageSource(charge.qr_code_base64);\n const state = text.states[status];\n\n return (\n <div className={cn(styles.checkout, className)} data-status={status} {...rest}>\n <Money\n className={styles.amount}\n cents={charge.amount_cents}\n currency={charge.currency}\n />\n {status === \"pending\" ? (\n <div role=\"status\" className={styles.state}>\n <p className={styles.title}>{state.title}</p>\n <p className={styles.description}>\n {pastDeadline ? text.confirming : state.description}\n </p>\n </div>\n ) : (\n <Alert\n className={styles.state}\n variant={STATE_VARIANT[status]}\n title={state.title}\n description={state.description}\n />\n )}\n {payable && hasDeadline && (\n <p className={styles.timer} role=\"timer\" aria-live=\"off\">\n {text.expiresIn} <strong>{formatRemaining(remaining)}</strong>\n </p>\n )}\n {payable && charge.br_code !== null && (\n <>\n <div className={styles.symbol}>\n {image !== null ? (\n <img\n src={image}\n alt={text.qr}\n width={qrSize}\n height={qrSize}\n className={styles.image}\n />\n ) : (\n <QRCode\n value={charge.br_code}\n size={qrSize}\n label={text.qr}\n margin={2}\n />\n )}\n </div>\n <p className={styles.hint}>{text.copyHint}</p>\n <div className={styles.copy}>\n <code className={styles.code} data-testid=\"pix-checkout-code\">\n {charge.br_code}\n </code>\n <CopyButton value={charge.br_code} onCopied={onCopied}>\n <span className={styles.copyIdle}>{text.copy}</span>\n <span className={styles.copyDone}>{text.copied}</span>\n </CopyButton>\n </div>\n </>\n )}\n {footer !== undefined && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ import { cn as e } from "../utils/cn.js";
2
+ import { Alert as t } from "../components/Alert/Alert.js";
3
+ import { useCountdown as n } from "../hooks/use-countdown.js";
4
+ import { QRCode as r } from "../components/QRCode/QRCode.js";
5
+ import { CopyButton as i } from "../components/CopyButton/CopyButton.js";
6
+ import { Money as a } from "../components/Money/Money.js";
7
+ import { formatPixRemaining as o } from "./format.js";
8
+ import s from "./PixCheckout.module.js";
9
+ import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
10
+ //#region src/payments/PixCheckout.tsx
11
+ var d = {
12
+ qr: "QR code do Pix",
13
+ copyHint: "Ou copie o código Pix e cole no app do seu banco",
14
+ copy: "Copiar código",
15
+ copied: "Copiado",
16
+ expiresIn: "Expira em",
17
+ confirming: "O prazo acabou. Estamos confirmando com o banco se o pagamento chegou.",
18
+ states: {
19
+ pending: {
20
+ title: "Aguardando pagamento",
21
+ description: "Abra o app do seu banco, escolha Pix e escaneie o QR code."
22
+ },
23
+ authorized: {
24
+ title: "Pagamento autorizado",
25
+ description: "Estamos concluindo o pagamento."
26
+ },
27
+ in_analysis: {
28
+ title: "Pagamento em análise",
29
+ description: "Avisaremos assim que for concluído."
30
+ },
31
+ paid: {
32
+ title: "Pagamento confirmado",
33
+ description: "Recebemos o seu Pix. Obrigado!"
34
+ },
35
+ expired: {
36
+ title: "Pix expirado",
37
+ description: "O prazo para pagar acabou. Gere um novo Pix."
38
+ },
39
+ cancelled: {
40
+ title: "Pix cancelado",
41
+ description: "Esta cobrança foi cancelada e não pode mais ser paga."
42
+ },
43
+ refunded: {
44
+ title: "Pix devolvido",
45
+ description: "O valor foi devolvido para a sua conta."
46
+ },
47
+ charged_back: {
48
+ title: "Pix contestado",
49
+ description: "O pagamento foi contestado junto ao banco."
50
+ },
51
+ failed: {
52
+ title: "Pix não concluído",
53
+ description: "Não foi possível concluir o pagamento. Gere um novo Pix."
54
+ },
55
+ unknown: {
56
+ title: "Verificando o pagamento",
57
+ description: "Ainda estamos confirmando este Pix."
58
+ }
59
+ }
60
+ }, f = {
61
+ pending: "info",
62
+ authorized: "info",
63
+ in_analysis: "info",
64
+ paid: "success",
65
+ expired: "warning",
66
+ cancelled: "neutral",
67
+ refunded: "neutral",
68
+ charged_back: "warning",
69
+ failed: "danger",
70
+ unknown: "neutral"
71
+ }, p = /^[A-Za-z0-9+/]+={0,2}$/, m = "data:image/png;base64,";
72
+ function h(e) {
73
+ if (e === null) return null;
74
+ let t = (e.startsWith(m) ? e.slice(22) : e).replace(/\s+/g, "");
75
+ return t.length > 0 && t.length % 4 == 0 && p.test(t) ? m + t : null;
76
+ }
77
+ function g({ charge: p, qrSize: m = 220, labels: g, formatRemaining: _ = o, onCopied: v, footer: y, className: b, ...x }) {
78
+ let S = {
79
+ ...d,
80
+ ...g,
81
+ states: {
82
+ ...d.states,
83
+ ...g?.states
84
+ }
85
+ }, C = p.expires_at === null ? NaN : Date.parse(p.expires_at), w = Number.isFinite(C), T = n(w ? C : 0, 0), E = Object.hasOwn(f, p.status) ? p.status : "unknown", D = w && T <= 0, O = E === "pending" && !D && p.br_code !== null, k = h(p.qr_code_base64), A = S.states[E];
86
+ return /* @__PURE__ */ u("div", {
87
+ className: e(s.checkout, b),
88
+ "data-status": E,
89
+ ...x,
90
+ children: [
91
+ /* @__PURE__ */ l(a, {
92
+ className: s.amount,
93
+ cents: p.amount_cents,
94
+ currency: p.currency
95
+ }),
96
+ E === "pending" ? /* @__PURE__ */ u("div", {
97
+ role: "status",
98
+ className: s.state,
99
+ children: [/* @__PURE__ */ l("p", {
100
+ className: s.title,
101
+ children: A.title
102
+ }), /* @__PURE__ */ l("p", {
103
+ className: s.description,
104
+ children: D ? S.confirming : A.description
105
+ })]
106
+ }) : /* @__PURE__ */ l(t, {
107
+ className: s.state,
108
+ variant: f[E],
109
+ title: A.title,
110
+ description: A.description
111
+ }),
112
+ O && w && /* @__PURE__ */ u("p", {
113
+ className: s.timer,
114
+ role: "timer",
115
+ "aria-live": "off",
116
+ children: [
117
+ S.expiresIn,
118
+ " ",
119
+ /* @__PURE__ */ l("strong", { children: _(T) })
120
+ ]
121
+ }),
122
+ O && p.br_code !== null && /* @__PURE__ */ u(c, { children: [
123
+ /* @__PURE__ */ l("div", {
124
+ className: s.symbol,
125
+ children: k === null ? /* @__PURE__ */ l(r, {
126
+ value: p.br_code,
127
+ size: m,
128
+ label: S.qr,
129
+ margin: 2
130
+ }) : /* @__PURE__ */ l("img", {
131
+ src: k,
132
+ alt: S.qr,
133
+ width: m,
134
+ height: m,
135
+ className: s.image
136
+ })
137
+ }),
138
+ /* @__PURE__ */ l("p", {
139
+ className: s.hint,
140
+ children: S.copyHint
141
+ }),
142
+ /* @__PURE__ */ u("div", {
143
+ className: s.copy,
144
+ children: [/* @__PURE__ */ l("code", {
145
+ className: s.code,
146
+ "data-testid": "pix-checkout-code",
147
+ children: p.br_code
148
+ }), /* @__PURE__ */ u(i, {
149
+ value: p.br_code,
150
+ onCopied: v,
151
+ children: [/* @__PURE__ */ l("span", {
152
+ className: s.copyIdle,
153
+ children: S.copy
154
+ }), /* @__PURE__ */ l("span", {
155
+ className: s.copyDone,
156
+ children: S.copied
157
+ })]
158
+ })]
159
+ })
160
+ ] }),
161
+ y !== void 0 && /* @__PURE__ */ l("div", {
162
+ className: s.footer,
163
+ children: y
164
+ })
165
+ ]
166
+ });
167
+ }
168
+ //#endregion
169
+ export { g as PixCheckout };
170
+
171
+ //# sourceMappingURL=PixCheckout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PixCheckout.js","names":["styles","_Fragment","PixCheckout_module_default","DEFAULT_LABELS","STATE_VARIANT","BASE64","PNG_PREFIX","qrImageSource","base64","compact","PixCheckout","formatPixRemaining","rest","text","labels","expiresAt","charge","hasDeadline","remaining","useCountdown","status","pastDeadline","payable","image","state","cn","className","Money","Alert","formatRemaining","Fragment","QRCode","qrSize","CopyButton","footer"],"sources":["../../src/payments/PixCheckout.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — `charge` is the data, `qrSize` the symbol,\n * `labels`/`formatRemaining` the copy an app translates, and `onCopied` the one\n * intent the screen emits.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\n\nimport { Alert } from \"@/components/Alert\";\nimport type { AlertVariant } from \"@/components/Alert\";\nimport { CopyButton } from \"@/components/CopyButton\";\nimport { Money } from \"@/components/Money\";\nimport { QRCode } from \"@/components/QRCode\";\nimport { useCountdown } from \"@/hooks/use-countdown\";\nimport { cn } from \"@/utils/cn\";\n\nimport { formatPixRemaining } from \"./format\";\nimport type { PaymentStatus, PixCharge } from \"./types\";\nimport styles from \"./PixCheckout.module.css\";\n\n/** Strings the screen renders, so an app can translate them. */\nexport interface PixCheckoutLabels {\n /** Accessible name of the QR. */\n qr: string;\n /** Heading over the copia-e-cola line. */\n copyHint: string;\n /** Copy button, idle. */\n copy: string;\n /** Copy button, just clicked. */\n copied: string;\n /** Prefix of the countdown, e.g. \"Expira em\". */\n expiresIn: string;\n /** Shown when the deadline passed but the backend has not said \"expired\" yet. */\n confirming: string;\n /** Title and description per terminal or waiting state. */\n states: Record<PaymentStatus, { title: string; description: string }>;\n}\n\nconst DEFAULT_LABELS: PixCheckoutLabels = {\n qr: \"QR code do Pix\",\n copyHint: \"Ou copie o código Pix e cole no app do seu banco\",\n copy: \"Copiar código\",\n copied: \"Copiado\",\n expiresIn: \"Expira em\",\n confirming: \"O prazo acabou. Estamos confirmando com o banco se o pagamento chegou.\",\n states: {\n pending: {\n title: \"Aguardando pagamento\",\n description: \"Abra o app do seu banco, escolha Pix e escaneie o QR code.\",\n },\n authorized: {\n title: \"Pagamento autorizado\",\n description: \"Estamos concluindo o pagamento.\",\n },\n in_analysis: {\n title: \"Pagamento em análise\",\n description: \"Avisaremos assim que for concluído.\",\n },\n paid: { title: \"Pagamento confirmado\", description: \"Recebemos o seu Pix. Obrigado!\" },\n expired: {\n title: \"Pix expirado\",\n description: \"O prazo para pagar acabou. Gere um novo Pix.\",\n },\n cancelled: {\n title: \"Pix cancelado\",\n description: \"Esta cobrança foi cancelada e não pode mais ser paga.\",\n },\n refunded: {\n title: \"Pix devolvido\",\n description: \"O valor foi devolvido para a sua conta.\",\n },\n charged_back: {\n title: \"Pix contestado\",\n description: \"O pagamento foi contestado junto ao banco.\",\n },\n failed: {\n title: \"Pix não concluído\",\n description: \"Não foi possível concluir o pagamento. Gere um novo Pix.\",\n },\n unknown: {\n title: \"Verificando o pagamento\",\n description: \"Ainda estamos confirmando este Pix.\",\n },\n },\n};\n\nconst STATE_VARIANT: Record<PaymentStatus, AlertVariant> = {\n pending: \"info\",\n authorized: \"info\",\n in_analysis: \"info\",\n paid: \"success\",\n expired: \"warning\",\n cancelled: \"neutral\",\n refunded: \"neutral\",\n charged_back: \"warning\",\n failed: \"danger\",\n unknown: \"neutral\",\n};\n\nconst BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;\nconst PNG_PREFIX = \"data:image/png;base64,\";\n\n/**\n * The QR image source, or `null` to draw it locally.\n *\n * Only a well-formed Base64 payload becomes a `data:image/png` URL: the field\n * arrives from the network, and an image `src` built from an unchecked string\n * is a place where something other than a PNG can end up. A value that already\n * carries the `data:image/png;base64,` prefix is accepted once.\n */\nfunction qrImageSource(base64: string | null): string | null {\n if (base64 === null) return null;\n const body = base64.startsWith(PNG_PREFIX) ? base64.slice(PNG_PREFIX.length) : base64;\n const compact = body.replace(/\\s+/g, \"\");\n return compact.length > 0 && compact.length % 4 === 0 && BASE64.test(compact)\n ? PNG_PREFIX + compact\n : null;\n}\n\nexport interface PixCheckoutProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** The charge as the backend reports it — pair with {@link usePixCharge} to follow it. */\n charge: PixCharge;\n /** Rendered side of the QR in px. Default `220`. */\n qrSize?: number;\n labels?: Partial<Omit<PixCheckoutLabels, \"states\">> & {\n states?: Partial<PixCheckoutLabels[\"states\"]>;\n };\n /** Formats the countdown. Default {@link formatPixRemaining}. */\n formatRemaining?: (ms: number) => string;\n /** Called after the copia-e-cola reaches the clipboard. */\n onCopied?: () => void;\n /** Rendered under the status, e.g. a \"gerar novo Pix\" button for a final state. */\n footer?: ReactNode;\n}\n\n/**\n * The payer's Pix screen: amount, QR, copia-e-cola, countdown and the state the\n * backend reports.\n *\n * Presentational and controlled. It renders `charge` and never changes it, so\n * the decision that a Pix was paid stays where it can be made — on the backend,\n * re-reading the order. Feed it from {@link usePixCharge} or from your own\n * stream.\n *\n * The QR comes from `qr_code_base64` when the provider sent one (Mercado Pago),\n * and is otherwise drawn locally from `br_code` (OpenPix sends a URL instead,\n * and fetching it would hand the payload to an image host for nothing). The\n * copia-e-cola line is always next to it: a QR cannot be scanned by the phone\n * that is showing it, which is where a mobile checkout puts it.\n *\n * A status this version does not know renders as `unknown` (\"verificando\"). Known\n * means an own key of the state table, so a status named after an\n * `Object.prototype` member (`toString`, `constructor`) is unknown too, instead\n * of an empty state block.\n *\n * When the countdown reaches zero while the backend still says `pending`, the\n * screen hides the QR and says it is **confirming** — not \"expired\". A payment\n * sent in the last second can still settle, and only the backend knows.\n *\n * @example\n * const { charge } = usePixCharge(created, { refresh: (signal) => readCharge(created.provider_charge_id, signal) });\n * <PixCheckout charge={charge} />\n */\nexport function PixCheckout({\n charge,\n qrSize = 220,\n labels,\n formatRemaining = formatPixRemaining,\n onCopied,\n footer,\n className,\n ...rest\n}: PixCheckoutProps) {\n const text: PixCheckoutLabels = {\n ...DEFAULT_LABELS,\n ...labels,\n states: { ...DEFAULT_LABELS.states, ...labels?.states },\n };\n const expiresAt = charge.expires_at === null ? Number.NaN : Date.parse(charge.expires_at);\n const hasDeadline = Number.isFinite(expiresAt);\n const remaining = useCountdown(hasDeadline ? expiresAt : 0, 0);\n const status: PaymentStatus = Object.hasOwn(STATE_VARIANT, charge.status)\n ? charge.status\n : \"unknown\";\n const pastDeadline = hasDeadline && remaining <= 0;\n const payable = status === \"pending\" && !pastDeadline && charge.br_code !== null;\n const image = qrImageSource(charge.qr_code_base64);\n const state = text.states[status];\n\n return (\n <div className={cn(styles.checkout, className)} data-status={status} {...rest}>\n <Money\n className={styles.amount}\n cents={charge.amount_cents}\n currency={charge.currency}\n />\n {status === \"pending\" ? (\n <div role=\"status\" className={styles.state}>\n <p className={styles.title}>{state.title}</p>\n <p className={styles.description}>\n {pastDeadline ? text.confirming : state.description}\n </p>\n </div>\n ) : (\n <Alert\n className={styles.state}\n variant={STATE_VARIANT[status]}\n title={state.title}\n description={state.description}\n />\n )}\n {payable && hasDeadline && (\n <p className={styles.timer} role=\"timer\" aria-live=\"off\">\n {text.expiresIn} <strong>{formatRemaining(remaining)}</strong>\n </p>\n )}\n {payable && charge.br_code !== null && (\n <>\n <div className={styles.symbol}>\n {image !== null ? (\n <img\n src={image}\n alt={text.qr}\n width={qrSize}\n height={qrSize}\n className={styles.image}\n />\n ) : (\n <QRCode\n value={charge.br_code}\n size={qrSize}\n label={text.qr}\n margin={2}\n />\n )}\n </div>\n <p className={styles.hint}>{text.copyHint}</p>\n <div className={styles.copy}>\n <code className={styles.code} data-testid=\"pix-checkout-code\">\n {charge.br_code}\n </code>\n <CopyButton value={charge.br_code} onCopied={onCopied}>\n <span className={styles.copyIdle}>{text.copy}</span>\n <span className={styles.copyDone}>{text.copied}</span>\n </CopyButton>\n </div>\n </>\n )}\n {footer !== undefined && <div className={styles.footer}>{footer}</div>}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ require("../styles/component/PixCheckout.css");
2
+ var e=`tempest_checkout_KESha`,t=`tempest_amount_7xCfs`,n=`tempest_state_I4M-g`,r=`tempest_title_wcrWG`,i=`tempest_description_6tRir`,a=`tempest_timer_KaP5v`,o=`tempest_symbol_860pD`,s=`tempest_image_wTRUA`,c=`tempest_hint_vdsWp`,l=`tempest_copy_7GUOj`,u=`tempest_code_KJufe`,d=`tempest_copyDone_MBlRT`,f=`tempest_copyIdle_MW0LU`,p=`tempest_footer_YnAyb`,m={checkout:e,amount:t,state:n,title:r,description:i,timer:a,symbol:o,image:s,hint:c,copy:l,code:u,copyDone:d,copyIdle:f,footer:p};exports.amount=t,exports.checkout=e,exports.code=u,exports.copy=l,exports.copyDone=d,exports.copyIdle=f,exports.default=m,exports.description=i,exports.footer=p,exports.hint=c,exports.image=s,exports.state=n,exports.symbol=o,exports.timer=a,exports.title=r;
3
+ //# sourceMappingURL=PixCheckout.module.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PixCheckout.module.cjs","names":["checkout","amount","state","title","description","timer","symbol","image","hint","copy","code","copyDone","copyIdle","footer","PixCheckout_module_default"],"sources":["../../src/payments/PixCheckout.module.css"],"sourcesContent":[".checkout {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--tempest-space-3);\n width: 100%;\n max-width: 360px;\n margin-inline: auto;\n font-family: var(--tempest-font-sans);\n color: var(--tempest-text);\n text-align: center;\n}\n\n.amount {\n font-size: var(--tempest-text-2xl);\n font-weight: var(--tempest-weight-semibold);\n font-variant-numeric: tabular-nums;\n}\n\n.state {\n width: 100%;\n text-align: start;\n}\n\ndiv.state[role=\"status\"] {\n text-align: center;\n}\n\n.title {\n margin: 0;\n font-size: var(--tempest-text-lg);\n font-weight: var(--tempest-weight-semibold);\n}\n\n.description {\n margin: var(--tempest-space-1) 0 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-snug);\n}\n\n.timer {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.timer strong {\n color: var(--tempest-text);\n font-variant-numeric: tabular-nums;\n}\n\n/* The QR stays black on white in both themes — see the note on `QRCode`. The\n white plate keeps the quiet zone intact on a dark page. */\n.symbol {\n display: inline-flex;\n padding: var(--tempest-space-2);\n background: #ffffff;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n}\n\n.image {\n display: block;\n max-width: 100%;\n height: auto;\n image-rendering: pixelated;\n}\n\n.hint {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.copy {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n}\n\n.copy > button {\n flex: none;\n white-space: nowrap;\n}\n\n.code {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n background: var(--tempest-surface-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text-muted);\n font-family: var(--tempest-font-mono);\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-snug);\n text-align: start;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Both labels render and CSS picks one, keyed on `CopyButton`'s `data-copied`;\n swapping the text node keeps the accessible name in sync. */\n.copyDone {\n display: none;\n}\n\n.copy button[data-copied] .copyIdle {\n display: none;\n}\n\n.copy button[data-copied] .copyDone {\n display: inline;\n}\n\n.footer {\n width: 100%;\n}\n"],"mappings":";"}
@@ -0,0 +1,22 @@
1
+ import "../styles/component/PixCheckout.css";
2
+ //#region src/payments/PixCheckout.module.css
3
+ var e = "tempest_checkout_KESha", t = "tempest_amount_7xCfs", n = "tempest_state_I4M-g", r = "tempest_title_wcrWG", i = "tempest_description_6tRir", a = "tempest_timer_KaP5v", o = "tempest_symbol_860pD", s = "tempest_image_wTRUA", c = "tempest_hint_vdsWp", l = "tempest_copy_7GUOj", u = "tempest_code_KJufe", d = "tempest_copyDone_MBlRT", f = "tempest_copyIdle_MW0LU", p = "tempest_footer_YnAyb", m = {
4
+ checkout: e,
5
+ amount: t,
6
+ state: n,
7
+ title: r,
8
+ description: i,
9
+ timer: a,
10
+ symbol: o,
11
+ image: s,
12
+ hint: c,
13
+ copy: l,
14
+ code: u,
15
+ copyDone: d,
16
+ copyIdle: f,
17
+ footer: p
18
+ };
19
+ //#endregion
20
+ export { t as amount, e as checkout, u as code, l as copy, d as copyDone, f as copyIdle, m as default, i as description, p as footer, c as hint, s as image, n as state, o as symbol, a as timer, r as title };
21
+
22
+ //# sourceMappingURL=PixCheckout.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PixCheckout.module.js","names":["checkout","amount","state","title","description","timer","symbol","image","hint","copy","code","copyDone","copyIdle","footer","PixCheckout_module_default"],"sources":["../../src/payments/PixCheckout.module.css"],"sourcesContent":[".checkout {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--tempest-space-3);\n width: 100%;\n max-width: 360px;\n margin-inline: auto;\n font-family: var(--tempest-font-sans);\n color: var(--tempest-text);\n text-align: center;\n}\n\n.amount {\n font-size: var(--tempest-text-2xl);\n font-weight: var(--tempest-weight-semibold);\n font-variant-numeric: tabular-nums;\n}\n\n.state {\n width: 100%;\n text-align: start;\n}\n\ndiv.state[role=\"status\"] {\n text-align: center;\n}\n\n.title {\n margin: 0;\n font-size: var(--tempest-text-lg);\n font-weight: var(--tempest-weight-semibold);\n}\n\n.description {\n margin: var(--tempest-space-1) 0 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-snug);\n}\n\n.timer {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.timer strong {\n color: var(--tempest-text);\n font-variant-numeric: tabular-nums;\n}\n\n/* The QR stays black on white in both themes — see the note on `QRCode`. The\n white plate keeps the quiet zone intact on a dark page. */\n.symbol {\n display: inline-flex;\n padding: var(--tempest-space-2);\n background: #ffffff;\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n}\n\n.image {\n display: block;\n max-width: 100%;\n height: auto;\n image-rendering: pixelated;\n}\n\n.hint {\n margin: 0;\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-sm);\n}\n\n.copy {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n width: 100%;\n}\n\n.copy > button {\n flex: none;\n white-space: nowrap;\n}\n\n.code {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n background: var(--tempest-surface-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-sm);\n color: var(--tempest-text-muted);\n font-family: var(--tempest-font-mono);\n font-size: var(--tempest-text-xs);\n line-height: var(--tempest-leading-snug);\n text-align: start;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Both labels render and CSS picks one, keyed on `CopyButton`'s `data-copied`;\n swapping the text node keeps the accessible name in sync. */\n.copyDone {\n display: none;\n}\n\n.copy button[data-copied] .copyIdle {\n display: none;\n}\n\n.copy button[data-copied] .copyDone {\n display: inline;\n}\n\n.footer {\n width: 100%;\n}\n"],"mappings":";"}
@@ -0,0 +1,2 @@
1
+ function e(e){let t=Math.max(0,Math.ceil(e/1e3)),n=Math.floor(t/3600),r=Math.floor(t%3600/60),i=t%60,a=`${String(r).padStart(2,`0`)}:${String(i).padStart(2,`0`)}`;return n>0?`${n}:${a}`:a}exports.formatPixRemaining=e;
2
+ //# sourceMappingURL=format.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.cjs","names":["formatPixRemaining","total","ms","hours","minutes","seconds","mmss"],"sources":["../../src/payments/format.ts"],"sourcesContent":["/**\n * `mm:ss` under an hour, `h:mm:ss` above — a Pix can live for 24 h.\n *\n * The default countdown format of `PixCheckout`, exported so an app that\n * renders its own Pix screen shows the same clock.\n *\n * @param ms - Milliseconds left. Rounded up, so the last second reads `00:01`.\n * @returns The formatted remaining time.\n *\n * @example\n * formatPixRemaining(65_000); // \"01:05\"\n * formatPixRemaining(3_725_000); // \"1:02:05\"\n */\nexport function formatPixRemaining(ms: number): string {\n const total = Math.max(0, Math.ceil(ms / 1000));\n const hours = Math.floor(total / 3600);\n const minutes = Math.floor((total % 3600) / 60);\n const seconds = total % 60;\n const mmss = `${String(minutes).padStart(2, \"0\")}:${String(seconds).padStart(2, \"0\")}`;\n return hours > 0 ? `${hours}:${mmss}` : mmss;\n}\n"],"mappings":"AAaA,SAAgBA,EAAmB,EAAoB,CACnD,IAAMC,EAAQ,KAAK,IAAI,EAAG,KAAK,KAAKC,EAAK,GAAI,CAAC,EACxCC,EAAQ,KAAK,MAAMF,EAAQ,IAAI,EAC/BG,EAAU,KAAK,MAAOH,EAAQ,KAAQ,EAAE,EACxCI,EAAUJ,EAAQ,GAClBK,EAAO,GAAG,OAAOF,CAAO,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,GAAG,OAAOC,CAAO,CAAC,CAAC,SAAS,EAAG,GAAG,IACnF,OAAOF,EAAQ,EAAI,GAAGA,EAAM,GAAGG,IAASA,CAC5C,C"}
@@ -0,0 +1,9 @@
1
+ //#region src/payments/format.ts
2
+ function e(e) {
3
+ let t = Math.max(0, Math.ceil(e / 1e3)), n = Math.floor(t / 3600), r = Math.floor(t % 3600 / 60), i = t % 60, a = `${String(r).padStart(2, "0")}:${String(i).padStart(2, "0")}`;
4
+ return n > 0 ? `${n}:${a}` : a;
5
+ }
6
+ //#endregion
7
+ export { e as formatPixRemaining };
8
+
9
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","names":["formatPixRemaining","total","ms","hours","minutes","seconds","mmss"],"sources":["../../src/payments/format.ts"],"sourcesContent":["/**\n * `mm:ss` under an hour, `h:mm:ss` above — a Pix can live for 24 h.\n *\n * The default countdown format of `PixCheckout`, exported so an app that\n * renders its own Pix screen shows the same clock.\n *\n * @param ms - Milliseconds left. Rounded up, so the last second reads `00:01`.\n * @returns The formatted remaining time.\n *\n * @example\n * formatPixRemaining(65_000); // \"01:05\"\n * formatPixRemaining(3_725_000); // \"1:02:05\"\n */\nexport function formatPixRemaining(ms: number): string {\n const total = Math.max(0, Math.ceil(ms / 1000));\n const hours = Math.floor(total / 3600);\n const minutes = Math.floor((total % 3600) / 60);\n const seconds = total % 60;\n const mmss = `${String(minutes).padStart(2, \"0\")}:${String(seconds).padStart(2, \"0\")}`;\n return hours > 0 ? `${hours}:${mmss}` : mmss;\n}\n"],"mappings":";AAaA,SAAgBA,EAAmB,GAAoB;CACnD,IAAMC,IAAQ,KAAK,IAAI,GAAG,KAAK,KAAKC,IAAK,GAAI,CAAC,GACxCC,IAAQ,KAAK,MAAMF,IAAQ,IAAI,GAC/BG,IAAU,KAAK,MAAOH,IAAQ,OAAQ,EAAE,GACxCI,IAAUJ,IAAQ,IAClBK,IAAO,GAAG,OAAOF,CAAO,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG,OAAOC,CAAO,CAAC,CAAC,SAAS,GAAG,GAAG;CACnF,OAAOF,IAAQ,IAAI,GAAGA,EAAM,GAAGG,MAASA;AAC5C"}
@@ -0,0 +1,2 @@
1
+ var e=`https://sdk.mercadopago.com/js/v2`,t=new WeakMap;function n(e){return e.defaultView?.MercadoPago}function r(e={}){let r=e.document??document,i=n(r);if(i!==void 0)return Promise.resolve(i);let a=t.get(r);if(a!==void 0)return a;let o=new Promise((i,a)=>{let o=r.createElement(`script`);o.src=e.src??`https://sdk.mercadopago.com/js/v2`,o.async=!0;let s=e=>{t.delete(r),o.remove(),a(Error(e))};o.addEventListener(`load`,()=>{let e=n(r);e===void 0?s(`MercadoPago.js loaded but did not define window.MercadoPago.`):(t.delete(r),i(e))}),o.addEventListener(`error`,()=>s(`MercadoPago.js failed to load from ${o.src}.`)),r.head.appendChild(o)});return t.set(r,o),o}exports.MERCADO_PAGO_SCRIPT_URL=e,exports.loadMercadoPago=r;
2
+ //# sourceMappingURL=mercado-pago.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mercado-pago.cjs","names":["MERCADO_PAGO_SCRIPT_URL","pending","readGlobal","doc","loadMercadoPago","options","ready","inflight","promise","script","fail","reject","message","loaded","resolve"],"sources":["../../src/payments/mercado-pago.ts"],"sourcesContent":["/**\n * The official MercadoPago.js v2 script.\n *\n * Loaded from Mercado Pago's own host, never bundled or vendored: the PCI DSS\n * self-assessment that keeps a merchant in the lightest scope (SAQ A) requires\n * the card fields to come straight from the processor, and Mercado Pago ships\n * the script unversioned so fixes reach every merchant without a rebuild. It is\n * also why this is not an npm dependency — there is no package to pin.\n *\n * A Content-Security-Policy needs `script-src https://sdk.mercadopago.com` and,\n * for the Secure Fields iframes and the API calls the script makes,\n * `frame-src https://secure-fields.mercadopago.com https://api-static.mercadopago.com`\n * plus `connect-src https://api.mercadopago.com https://api.mercadolibre.com`\n * (hosts read from the script itself, `js_version` 2.83.6, on 2026-11-14).\n */\nexport const MERCADO_PAGO_SCRIPT_URL = \"https://sdk.mercadopago.com/js/v2\";\n\n/** The Secure Fields this module mounts. */\nexport type MercadoPagoFieldType = \"cardNumber\" | \"expirationDate\" | \"securityCode\";\n\n/** Options for one Secure Field, as MercadoPago.js accepts them. */\nexport interface MercadoPagoFieldOptions {\n /** Placeholder shown inside the iframe. */\n placeholder?: string;\n /** Inline style applied inside the iframe (font, colour, size). */\n style?: Record<string, string>;\n}\n\n/** One mounted Secure Field — the subset of MercadoPago.js this module calls. */\nexport interface MercadoPagoFieldLike {\n /** Mounts the iframe into the element with this id. */\n mount(containerId: string): unknown;\n /** Removes the iframe. */\n unmount(): unknown;\n /** Subscribes to a field event (`binChange`, `validityChange`, `error`, …). */\n on(event: string, listener: (payload: unknown) => void): unknown;\n}\n\n/** One installment option, as `getInstallments` returns it. */\nexport interface MercadoPagoPayerCost {\n installments: number;\n installment_rate: number;\n installment_amount: number;\n total_amount: number;\n /** Ready-made text, e.g. `\"2 parcelas de R$ 54,82 (R$ 109,64)\"`. */\n recommended_message: string;\n}\n\n/** One entry of the `getInstallments` answer. */\nexport interface MercadoPagoInstallmentsEntry {\n payment_method_id: string;\n payment_type_id?: string;\n issuer?: { id?: string | number | null } | null;\n payer_costs: MercadoPagoPayerCost[];\n}\n\n/** Non-card data the token is bound to. */\nexport interface MercadoPagoCardTokenData {\n cardholderName: string;\n identificationType?: string;\n identificationNumber?: string;\n}\n\n/**\n * What `fields.createCardToken` resolves with — the subset this module reads.\n *\n * The Secure Fields iframe posts the card to `POST /v1/card_tokens` and answers\n * with that endpoint's JSON as is (read from the iframe served by\n * `secure-fields.mercadopago.com` on 2026-11-14), so the shape is the card token\n * resource: `CardTokenResponse` in Mercado Pago's own Node SDK (`mercadopago`\n * 3.6.2), where every field is optional. `first_six_digits` is the BIN of the\n * card that was actually tokenized, which is how {@link useMercadoPagoCard}\n * binds the token to the brand it quoted.\n */\nexport interface MercadoPagoCardToken {\n /** Single-use token id. */\n id: string;\n /** First six digits of the tokenized card. */\n first_six_digits?: string;\n}\n\n/**\n * The subset of a `new MercadoPago(publicKey)` instance this module calls.\n *\n * Declared here instead of depending on `@mercadopago/sdk-js`, matching how the\n * SDK treats every injected provider: tests pass a fake with this shape, and an\n * app never pays for typings it does not use.\n */\nexport interface MercadoPagoLike {\n fields: {\n create(type: MercadoPagoFieldType, options?: MercadoPagoFieldOptions): MercadoPagoFieldLike;\n createCardToken(data: MercadoPagoCardTokenData): Promise<MercadoPagoCardToken>;\n };\n getInstallments(params: {\n amount: string;\n bin: string;\n paymentTypeId?: string;\n }): Promise<MercadoPagoInstallmentsEntry[]>;\n}\n\n/** The `MercadoPago` global the script defines. */\nexport type MercadoPagoConstructor = new (\n publicKey: string,\n options?: { locale?: string },\n) => MercadoPagoLike;\n\n/** Options for {@link loadMercadoPago}. */\nexport interface LoadMercadoPagoOptions {\n /**\n * Script URL. Default {@link MERCADO_PAGO_SCRIPT_URL}; override only for a\n * proxy you control that serves the **identical** MercadoPago.js — SAQ A\n * depends on the fields coming from the processor unaltered. Not checked:\n * the script is unversioned, so there is no stable integrity hash to pin.\n */\n src?: string;\n /** Document to inject into. Default the global `document`. */\n document?: Document;\n}\n\nconst pending = new WeakMap<Document, Promise<MercadoPagoConstructor>>();\n\nfunction readGlobal(doc: Document): MercadoPagoConstructor | undefined {\n const view = doc.defaultView as (Window & { MercadoPago?: MercadoPagoConstructor }) | null;\n return view?.MercadoPago;\n}\n\n/**\n * Load MercadoPago.js once per document and resolve its constructor.\n *\n * Concurrent and repeated calls share one `<script>` tag and one promise, so a\n * checkout that remounts never injects the script twice. A failed load is\n * forgotten instead of cached — the next call tries again, which is what a\n * payer on a flaky connection needs — and the dead tag is removed.\n *\n * Trust: an existing `window.MercadoPago` is used as is, and `src` is not\n * checked. Neither is a boundary the page can enforce — whoever can define that\n * global already runs script here, and an unversioned script has no integrity\n * hash to pin — so the control is the page's CSP `script-src`, restricted to\n * `https://sdk.mercadopago.com` or the proxy `src` points at.\n *\n * @param options - Script URL and target document.\n * @returns The `MercadoPago` constructor.\n * @throws {Error} When the script fails to load, or loads without defining\n * `window.MercadoPago`.\n *\n * @example\n * const MercadoPago = await loadMercadoPago();\n * const mp = new MercadoPago(import.meta.env.VITE_MP_PUBLIC_KEY, { locale: \"pt-BR\" });\n */\nexport function loadMercadoPago(\n options: LoadMercadoPagoOptions = {},\n): Promise<MercadoPagoConstructor> {\n const doc = options.document ?? document;\n const ready = readGlobal(doc);\n if (ready !== undefined) return Promise.resolve(ready);\n const inflight = pending.get(doc);\n if (inflight !== undefined) return inflight;\n\n const promise = new Promise<MercadoPagoConstructor>((resolve, reject) => {\n const script = doc.createElement(\"script\");\n script.src = options.src ?? MERCADO_PAGO_SCRIPT_URL;\n script.async = true;\n const fail = (message: string): void => {\n pending.delete(doc);\n script.remove();\n reject(new Error(message));\n };\n script.addEventListener(\"load\", () => {\n const loaded = readGlobal(doc);\n if (loaded === undefined) {\n fail(\"MercadoPago.js loaded but did not define window.MercadoPago.\");\n return;\n }\n pending.delete(doc);\n resolve(loaded);\n });\n script.addEventListener(\"error\", () =>\n fail(`MercadoPago.js failed to load from ${script.src}.`),\n );\n doc.head.appendChild(script);\n });\n pending.set(doc, promise);\n return promise;\n}\n"],"mappings":"AAeA,IAAaA,EAA0B,oCAwGjCC,EAAU,IAAI,QAEpB,SAASC,EAAW,EAAmD,CAEnE,OADaC,EAAI,aACJ,WACjB,CAyBA,SAAgBC,EACZ,EAAkC,CAAC,EACJ,CAC/B,IAAMD,EAAME,EAAQ,UAAY,SAC1BC,EAAQJ,EAAWC,CAAG,EAC5B,GAAIG,IAAU,IAAA,GAAW,OAAO,QAAQ,QAAQA,CAAK,EACrD,IAAMC,EAAWN,EAAQ,IAAIE,CAAG,EAChC,GAAII,IAAa,IAAA,GAAW,OAAOA,EAEnC,IAAMC,EAAU,IAAI,SAAiC,EAAS,IAAW,CACrE,IAAMC,EAASN,EAAI,cAAc,QAAQ,EACzC,EAAO,IAAME,EAAQ,KAAA,oCACrB,EAAO,MAAQ,GACf,IAAMK,EAAQ,GAA0B,CACpCT,EAAQ,OAAOE,CAAG,EAClBM,EAAO,OAAO,EACdE,EAAW,MAAMC,CAAO,CAAC,CAC7B,EACAH,EAAO,iBAAiB,WAAc,CAClC,IAAMI,EAASX,EAAWC,CAAG,EACzBU,IAAW,IAAA,GACXH,EAAK,8DAA8D,GAGvET,EAAQ,OAAOE,CAAG,EAClBW,EAAQD,CAAM,EAClB,CAAC,EACDJ,EAAO,iBAAiB,YACpBC,EAAK,sCAAsCD,EAAO,IAAI,EAAE,CAC5D,EACAN,EAAI,KAAK,YAAYM,CAAM,CAC/B,CAAC,EAED,OADAR,EAAQ,IAAIE,EAAKK,CAAO,EACjBA,CACX,C"}
@@ -0,0 +1,27 @@
1
+ //#region src/payments/mercado-pago.ts
2
+ var e = "https://sdk.mercadopago.com/js/v2", t = /* @__PURE__ */ new WeakMap();
3
+ function n(e) {
4
+ return e.defaultView?.MercadoPago;
5
+ }
6
+ function r(e = {}) {
7
+ let r = e.document ?? document, i = n(r);
8
+ if (i !== void 0) return Promise.resolve(i);
9
+ let a = t.get(r);
10
+ if (a !== void 0) return a;
11
+ let o = new Promise((i, a) => {
12
+ let o = r.createElement("script");
13
+ o.src = e.src ?? "https://sdk.mercadopago.com/js/v2", o.async = !0;
14
+ let s = (e) => {
15
+ t.delete(r), o.remove(), a(Error(e));
16
+ };
17
+ o.addEventListener("load", () => {
18
+ let e = n(r);
19
+ e === void 0 ? s("MercadoPago.js loaded but did not define window.MercadoPago.") : (t.delete(r), i(e));
20
+ }), o.addEventListener("error", () => s(`MercadoPago.js failed to load from ${o.src}.`)), r.head.appendChild(o);
21
+ });
22
+ return t.set(r, o), o;
23
+ }
24
+ //#endregion
25
+ export { e as MERCADO_PAGO_SCRIPT_URL, r as loadMercadoPago };
26
+
27
+ //# sourceMappingURL=mercado-pago.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mercado-pago.js","names":["MERCADO_PAGO_SCRIPT_URL","pending","readGlobal","doc","loadMercadoPago","options","ready","inflight","promise","script","fail","reject","message","loaded","resolve"],"sources":["../../src/payments/mercado-pago.ts"],"sourcesContent":["/**\n * The official MercadoPago.js v2 script.\n *\n * Loaded from Mercado Pago's own host, never bundled or vendored: the PCI DSS\n * self-assessment that keeps a merchant in the lightest scope (SAQ A) requires\n * the card fields to come straight from the processor, and Mercado Pago ships\n * the script unversioned so fixes reach every merchant without a rebuild. It is\n * also why this is not an npm dependency — there is no package to pin.\n *\n * A Content-Security-Policy needs `script-src https://sdk.mercadopago.com` and,\n * for the Secure Fields iframes and the API calls the script makes,\n * `frame-src https://secure-fields.mercadopago.com https://api-static.mercadopago.com`\n * plus `connect-src https://api.mercadopago.com https://api.mercadolibre.com`\n * (hosts read from the script itself, `js_version` 2.83.6, on 2026-11-14).\n */\nexport const MERCADO_PAGO_SCRIPT_URL = \"https://sdk.mercadopago.com/js/v2\";\n\n/** The Secure Fields this module mounts. */\nexport type MercadoPagoFieldType = \"cardNumber\" | \"expirationDate\" | \"securityCode\";\n\n/** Options for one Secure Field, as MercadoPago.js accepts them. */\nexport interface MercadoPagoFieldOptions {\n /** Placeholder shown inside the iframe. */\n placeholder?: string;\n /** Inline style applied inside the iframe (font, colour, size). */\n style?: Record<string, string>;\n}\n\n/** One mounted Secure Field — the subset of MercadoPago.js this module calls. */\nexport interface MercadoPagoFieldLike {\n /** Mounts the iframe into the element with this id. */\n mount(containerId: string): unknown;\n /** Removes the iframe. */\n unmount(): unknown;\n /** Subscribes to a field event (`binChange`, `validityChange`, `error`, …). */\n on(event: string, listener: (payload: unknown) => void): unknown;\n}\n\n/** One installment option, as `getInstallments` returns it. */\nexport interface MercadoPagoPayerCost {\n installments: number;\n installment_rate: number;\n installment_amount: number;\n total_amount: number;\n /** Ready-made text, e.g. `\"2 parcelas de R$ 54,82 (R$ 109,64)\"`. */\n recommended_message: string;\n}\n\n/** One entry of the `getInstallments` answer. */\nexport interface MercadoPagoInstallmentsEntry {\n payment_method_id: string;\n payment_type_id?: string;\n issuer?: { id?: string | number | null } | null;\n payer_costs: MercadoPagoPayerCost[];\n}\n\n/** Non-card data the token is bound to. */\nexport interface MercadoPagoCardTokenData {\n cardholderName: string;\n identificationType?: string;\n identificationNumber?: string;\n}\n\n/**\n * What `fields.createCardToken` resolves with — the subset this module reads.\n *\n * The Secure Fields iframe posts the card to `POST /v1/card_tokens` and answers\n * with that endpoint's JSON as is (read from the iframe served by\n * `secure-fields.mercadopago.com` on 2026-11-14), so the shape is the card token\n * resource: `CardTokenResponse` in Mercado Pago's own Node SDK (`mercadopago`\n * 3.6.2), where every field is optional. `first_six_digits` is the BIN of the\n * card that was actually tokenized, which is how {@link useMercadoPagoCard}\n * binds the token to the brand it quoted.\n */\nexport interface MercadoPagoCardToken {\n /** Single-use token id. */\n id: string;\n /** First six digits of the tokenized card. */\n first_six_digits?: string;\n}\n\n/**\n * The subset of a `new MercadoPago(publicKey)` instance this module calls.\n *\n * Declared here instead of depending on `@mercadopago/sdk-js`, matching how the\n * SDK treats every injected provider: tests pass a fake with this shape, and an\n * app never pays for typings it does not use.\n */\nexport interface MercadoPagoLike {\n fields: {\n create(type: MercadoPagoFieldType, options?: MercadoPagoFieldOptions): MercadoPagoFieldLike;\n createCardToken(data: MercadoPagoCardTokenData): Promise<MercadoPagoCardToken>;\n };\n getInstallments(params: {\n amount: string;\n bin: string;\n paymentTypeId?: string;\n }): Promise<MercadoPagoInstallmentsEntry[]>;\n}\n\n/** The `MercadoPago` global the script defines. */\nexport type MercadoPagoConstructor = new (\n publicKey: string,\n options?: { locale?: string },\n) => MercadoPagoLike;\n\n/** Options for {@link loadMercadoPago}. */\nexport interface LoadMercadoPagoOptions {\n /**\n * Script URL. Default {@link MERCADO_PAGO_SCRIPT_URL}; override only for a\n * proxy you control that serves the **identical** MercadoPago.js — SAQ A\n * depends on the fields coming from the processor unaltered. Not checked:\n * the script is unversioned, so there is no stable integrity hash to pin.\n */\n src?: string;\n /** Document to inject into. Default the global `document`. */\n document?: Document;\n}\n\nconst pending = new WeakMap<Document, Promise<MercadoPagoConstructor>>();\n\nfunction readGlobal(doc: Document): MercadoPagoConstructor | undefined {\n const view = doc.defaultView as (Window & { MercadoPago?: MercadoPagoConstructor }) | null;\n return view?.MercadoPago;\n}\n\n/**\n * Load MercadoPago.js once per document and resolve its constructor.\n *\n * Concurrent and repeated calls share one `<script>` tag and one promise, so a\n * checkout that remounts never injects the script twice. A failed load is\n * forgotten instead of cached — the next call tries again, which is what a\n * payer on a flaky connection needs — and the dead tag is removed.\n *\n * Trust: an existing `window.MercadoPago` is used as is, and `src` is not\n * checked. Neither is a boundary the page can enforce — whoever can define that\n * global already runs script here, and an unversioned script has no integrity\n * hash to pin — so the control is the page's CSP `script-src`, restricted to\n * `https://sdk.mercadopago.com` or the proxy `src` points at.\n *\n * @param options - Script URL and target document.\n * @returns The `MercadoPago` constructor.\n * @throws {Error} When the script fails to load, or loads without defining\n * `window.MercadoPago`.\n *\n * @example\n * const MercadoPago = await loadMercadoPago();\n * const mp = new MercadoPago(import.meta.env.VITE_MP_PUBLIC_KEY, { locale: \"pt-BR\" });\n */\nexport function loadMercadoPago(\n options: LoadMercadoPagoOptions = {},\n): Promise<MercadoPagoConstructor> {\n const doc = options.document ?? document;\n const ready = readGlobal(doc);\n if (ready !== undefined) return Promise.resolve(ready);\n const inflight = pending.get(doc);\n if (inflight !== undefined) return inflight;\n\n const promise = new Promise<MercadoPagoConstructor>((resolve, reject) => {\n const script = doc.createElement(\"script\");\n script.src = options.src ?? MERCADO_PAGO_SCRIPT_URL;\n script.async = true;\n const fail = (message: string): void => {\n pending.delete(doc);\n script.remove();\n reject(new Error(message));\n };\n script.addEventListener(\"load\", () => {\n const loaded = readGlobal(doc);\n if (loaded === undefined) {\n fail(\"MercadoPago.js loaded but did not define window.MercadoPago.\");\n return;\n }\n pending.delete(doc);\n resolve(loaded);\n });\n script.addEventListener(\"error\", () =>\n fail(`MercadoPago.js failed to load from ${script.src}.`),\n );\n doc.head.appendChild(script);\n });\n pending.set(doc, promise);\n return promise;\n}\n"],"mappings":";AAeA,IAAaA,IAA0B,qCAwGjCC,oBAAU,IAAI,QAAmD;AAEvE,SAASC,EAAW,GAAmD;CAEnE,OADaC,EAAI,aACJ;AACjB;AAyBA,SAAgBC,EACZ,IAAkC,CAAC,GACJ;CAC/B,IAAMD,IAAME,EAAQ,YAAY,UAC1BC,IAAQJ,EAAWC,CAAG;CAC5B,IAAIG,MAAU,KAAA,GAAW,OAAO,QAAQ,QAAQA,CAAK;CACrD,IAAMC,IAAWN,EAAQ,IAAIE,CAAG;CAChC,IAAII,MAAa,KAAA,GAAW,OAAOA;CAEnC,IAAMC,IAAU,IAAI,SAAiC,GAAS,MAAW;EACrE,IAAMC,IAASN,EAAI,cAAc,QAAQ;EAEzC,AADA,EAAO,MAAME,EAAQ,OAAA,qCACrB,EAAO,QAAQ;EACf,IAAMK,KAAQ,MAA0B;GAGpC,AAFAT,EAAQ,OAAOE,CAAG,GAClBM,EAAO,OAAO,GACdE,EAAW,MAAMC,CAAO,CAAC;EAC7B;EAaA,AAZAH,EAAO,iBAAiB,cAAc;GAClC,IAAMI,IAASX,EAAWC,CAAG;GAC7B,AAAIU,MAAW,KAAA,IACXH,EAAK,8DAA8D,KAGvET,EAAQ,OAAOE,CAAG,GAClBW,EAAQD,CAAM;EAClB,CAAC,GACDJ,EAAO,iBAAiB,eACpBC,EAAK,sCAAsCD,EAAO,IAAI,EAAE,CAC5D,GACAN,EAAI,KAAK,YAAYM,CAAM;CAC/B,CAAC;CAED,OADAR,EAAQ,IAAIE,GAAKK,CAAO,GACjBA;AACX"}
@@ -0,0 +1,2 @@
1
+ var e=`O valor foi reservado no seu cartão e só será cobrado quando o pedido for confirmado.`,t={bad_filled_card_data:`Confira os dados do cartão: número, validade e código de segurança.`,invalid_card_token:`Não conseguimos validar o cartão. Preencha os dados de novo.`,high_risk:`O pagamento não foi aprovado. Tente outro cartão ou outro meio de pagamento.`,rejected_by_issuer:`O banco emissor recusou o pagamento. Use outro cartão ou fale com o banco.`,required_call_for_authorize:`O banco pede que você autorize este pagamento. Fale com o banco e tente de novo.`,max_attempts_exceeded:`Você atingiu o limite de tentativas com este cartão. Use outro cartão ou outro meio de pagamento.`,card_disabled:`O cartão está bloqueado ou inativo. Ative-o com o banco ou use outro cartão.`,insufficient_amount:`O cartão não tem limite suficiente. Use outro cartão ou outro meio de pagamento.`,card_insufficient_amount:`O cartão não tem limite suficiente. Use outro cartão ou outro meio de pagamento.`,amount_limit_exceeded:`O valor passa do limite permitido para este cartão. Use outro cartão.`,processing_error:`Não conseguimos processar o pagamento. Tente de novo em instantes.`,invalid_installments:`O cartão não aceita esse número de parcelas. Escolha outra opção.`,"3ds_challenge_expired":`O tempo para confirmar a compra no app do banco acabou. Tente de novo.`,waiting_capture:e,pending_challenge:`Confirme a compra no app do seu banco para concluir o pagamento.`,pending_review_manual:`O pagamento está em análise. Avisaremos assim que for concluído.`,in_review:`O pagamento está em análise. Avisaremos assim que for concluído.`,in_process:`O pagamento está sendo processado. Avisaremos assim que for concluído.`},n=new Set([`bad_filled_card_data`,`invalid_card_token`,`high_risk`,`rejected_by_issuer`,`required_call_for_authorize`,`max_attempts_exceeded`,`card_disabled`,`insufficient_amount`,`card_insufficient_amount`,`amount_limit_exceeded`,`processing_error`,`invalid_installments`,`3ds_challenge_expired`]),r={paid:{tone:`success`,title:`Pagamento aprovado`,message:`Recebemos o seu pagamento.`,canRetry:!1},authorized:{tone:`info`,title:`Pagamento autorizado`,message:e,canRetry:!1},pending:{tone:`info`,title:`Aguardando pagamento`,message:`O pagamento ainda não foi concluído.`,canRetry:!1},in_analysis:{tone:`info`,title:`Pagamento em análise`,message:`O pagamento está em análise. Avisaremos assim que for concluído.`,canRetry:!1},failed:{tone:`danger`,title:`Pagamento recusado`,message:`O pagamento não foi aprovado. Tente outro cartão ou outro meio de pagamento.`,canRetry:!0},expired:{tone:`warning`,title:`Pagamento expirado`,message:`O prazo para pagar acabou. Comece um novo pagamento.`,canRetry:!0},cancelled:{tone:`neutral`,title:`Pagamento cancelado`,message:`Esta cobrança foi cancelada e não será cobrada.`,canRetry:!1},refunded:{tone:`neutral`,title:`Pagamento estornado`,message:`O valor foi devolvido a você.`,canRetry:!1},charged_back:{tone:`warning`,title:`Pagamento contestado`,message:`O pagamento foi contestado junto ao banco.`,canRetry:!1},unknown:{tone:`neutral`,title:`Verificando o pagamento`,message:`Ainda estamos confirmando o estado deste pagamento.`,canRetry:!1}};function i(e,t){return Object.hasOwn(e,t)?e[t]:void 0}function a(e,a={}){let o=Object.hasOwn(r,e.status)?e.status:`unknown`,s=r[o],c=e.status_detail??``,l=c!==``&&o!==`paid`&&n.has(c)===(o===`failed`)?i(a,c)??i(t,c):void 0;return{status:o,...s,message:l??s.message}}exports.CARD_STATUS_DETAIL_MESSAGES=t,exports.describeCardCharge=a;
2
+ //# sourceMappingURL=outcome.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outcome.cjs","names":["WAITING_CAPTURE_MESSAGE","CARD_STATUS_DETAIL_MESSAGES","DECLINE_DETAILS","STATUS_OUTCOMES","ownText","table","key","describeCardCharge","status","charge","base","detail","refined","messages"],"sources":["../../src/payments/outcome.ts"],"sourcesContent":["import type { CardCharge, PaymentStatus } from \"./types\";\n\n/** Visual tone of an outcome, matching `Alert`'s variants. */\nexport type PaymentOutcomeTone = \"success\" | \"info\" | \"warning\" | \"danger\" | \"neutral\";\n\n/** What a card charge means to the payer, ready to render. */\nexport interface PaymentOutcome {\n /** The canonical state the outcome was derived from. */\n status: PaymentStatus;\n /** Tone for the surrounding `Alert`/`Badge`. */\n tone: PaymentOutcomeTone;\n /** Short headline. */\n title: string;\n /** One sentence telling the payer what happened and what to do. */\n message: string;\n /** Whether the payer can fix it from the checkout (another card, other data). */\n canRetry: boolean;\n}\n\nconst WAITING_CAPTURE_MESSAGE =\n \"O valor foi reservado no seu cartão e só será cobrado quando o pedido for confirmado.\";\n\n/**\n * PT-BR text for every `status_detail` Mercado Pago's Orders API documents.\n *\n * Ported from Mercado Pago's \"Status da transação\" table\n * (`developers/pt/docs/checkout-api-orders/payment-management/status/transaction-status`,\n * read on 2026-11-14), rewritten for the payer: the provider's descriptions are\n * addressed to the integrator. `rejected_by_issuer` is the decline the backend\n * pinned in the sandbox (holder `OTHE`, HTTP 402); the others come from the\n * table. `high_risk` deliberately does not say \"fraud\" — telling the payer the\n * anti-fraud system flagged them helps exactly the person it should not.\n *\n * A test pins every key, so a code dropped or renamed here fails instead of\n * silently falling back to the generic text.\n */\nexport const CARD_STATUS_DETAIL_MESSAGES: Readonly<Record<string, string>> = {\n bad_filled_card_data: \"Confira os dados do cartão: número, validade e código de segurança.\",\n invalid_card_token: \"Não conseguimos validar o cartão. Preencha os dados de novo.\",\n high_risk: \"O pagamento não foi aprovado. Tente outro cartão ou outro meio de pagamento.\",\n rejected_by_issuer:\n \"O banco emissor recusou o pagamento. Use outro cartão ou fale com o banco.\",\n required_call_for_authorize:\n \"O banco pede que você autorize este pagamento. Fale com o banco e tente de novo.\",\n max_attempts_exceeded:\n \"Você atingiu o limite de tentativas com este cartão. Use outro cartão ou outro meio de pagamento.\",\n card_disabled: \"O cartão está bloqueado ou inativo. Ative-o com o banco ou use outro cartão.\",\n insufficient_amount:\n \"O cartão não tem limite suficiente. Use outro cartão ou outro meio de pagamento.\",\n card_insufficient_amount:\n \"O cartão não tem limite suficiente. Use outro cartão ou outro meio de pagamento.\",\n amount_limit_exceeded: \"O valor passa do limite permitido para este cartão. Use outro cartão.\",\n processing_error: \"Não conseguimos processar o pagamento. Tente de novo em instantes.\",\n invalid_installments: \"O cartão não aceita esse número de parcelas. Escolha outra opção.\",\n \"3ds_challenge_expired\":\n \"O tempo para confirmar a compra no app do banco acabou. Tente de novo.\",\n waiting_capture: WAITING_CAPTURE_MESSAGE,\n pending_challenge: \"Confirme a compra no app do seu banco para concluir o pagamento.\",\n pending_review_manual: \"O pagamento está em análise. Avisaremos assim que for concluído.\",\n in_review: \"O pagamento está em análise. Avisaremos assim que for concluído.\",\n in_process: \"O pagamento está sendo processado. Avisaremos assim que for concluído.\",\n};\n\nconst DECLINE_DETAILS: ReadonlySet<string> = new Set([\n \"bad_filled_card_data\",\n \"invalid_card_token\",\n \"high_risk\",\n \"rejected_by_issuer\",\n \"required_call_for_authorize\",\n \"max_attempts_exceeded\",\n \"card_disabled\",\n \"insufficient_amount\",\n \"card_insufficient_amount\",\n \"amount_limit_exceeded\",\n \"processing_error\",\n \"invalid_installments\",\n \"3ds_challenge_expired\",\n]);\n\nconst STATUS_OUTCOMES: Readonly<Record<PaymentStatus, Omit<PaymentOutcome, \"status\">>> = {\n paid: {\n tone: \"success\",\n title: \"Pagamento aprovado\",\n message: \"Recebemos o seu pagamento.\",\n canRetry: false,\n },\n authorized: {\n tone: \"info\",\n title: \"Pagamento autorizado\",\n message: WAITING_CAPTURE_MESSAGE,\n canRetry: false,\n },\n pending: {\n tone: \"info\",\n title: \"Aguardando pagamento\",\n message: \"O pagamento ainda não foi concluído.\",\n canRetry: false,\n },\n in_analysis: {\n tone: \"info\",\n title: \"Pagamento em análise\",\n message: \"O pagamento está em análise. Avisaremos assim que for concluído.\",\n canRetry: false,\n },\n failed: {\n tone: \"danger\",\n title: \"Pagamento recusado\",\n message: \"O pagamento não foi aprovado. Tente outro cartão ou outro meio de pagamento.\",\n canRetry: true,\n },\n expired: {\n tone: \"warning\",\n title: \"Pagamento expirado\",\n message: \"O prazo para pagar acabou. Comece um novo pagamento.\",\n canRetry: true,\n },\n cancelled: {\n tone: \"neutral\",\n title: \"Pagamento cancelado\",\n message: \"Esta cobrança foi cancelada e não será cobrada.\",\n canRetry: false,\n },\n refunded: {\n tone: \"neutral\",\n title: \"Pagamento estornado\",\n message: \"O valor foi devolvido a você.\",\n canRetry: false,\n },\n charged_back: {\n tone: \"warning\",\n title: \"Pagamento contestado\",\n message: \"O pagamento foi contestado junto ao banco.\",\n canRetry: false,\n },\n unknown: {\n tone: \"neutral\",\n title: \"Verificando o pagamento\",\n message: \"Ainda estamos confirmando o estado deste pagamento.\",\n canRetry: false,\n },\n};\n\n/**\n * The text a table holds for `key` itself, or `undefined`.\n *\n * `status` and `status_detail` arrive from the network, and a plain object\n * literal answers `toString`, `constructor` or `__proto__` from its prototype:\n * an `in` check or a bare lookup would read those as known and hand back a\n * function. Only an own key counts.\n */\nfunction ownText(table: Readonly<Record<string, string>>, key: string): string | undefined {\n return Object.hasOwn(table, key) ? table[key] : undefined;\n}\n\n/**\n * Turn a card charge from the backend into what the payer should read.\n *\n * The **status** decides the outcome; `status_detail` only refines the sentence.\n * That ordering is the point: `waiting_capture` arrives as `authorized` and\n * reads as \"reserved, not charged\", never as paid, and a detail this table does\n * not know falls back to the status's generic text instead of guessing — and\n * \"know\" means an own key of the table, so a status or detail named after an\n * `Object.prototype` member (`toString`, `constructor`, `__proto__`) reads as\n * `unknown` / unknown detail instead of a half-empty outcome. A\n * decline reason only refines a `failed` charge, and a pending reason only a\n * charge that is not `failed`, so a detail that contradicts its status cannot\n * put \"recusado\" and \"reservado\" on the same screen.\n *\n * A decline is data here, not an exception. The backend returns a declined card\n * as a `CardCharge` with `status: \"failed\"` — Mercado Pago answers it with HTTP\n * 402 and the backend reads that as a result — so the checkout renders the\n * reason the same way it renders an approval. If your own endpoint chooses to\n * answer a decline with a non-2xx status, parse its body and pass it here; do\n * not show it as a network error, because it is not one.\n *\n * @param charge - The charge, or just its `status` and `status_detail`.\n * @param messages - Overrides per `status_detail`, merged over\n * {@link CARD_STATUS_DETAIL_MESSAGES} — for another language or tone.\n * @returns Tone, title, message and whether the payer can retry.\n *\n * @example\n * const outcome = describeCardCharge(charge);\n * <Alert variant={outcome.tone} title={outcome.title} description={outcome.message} />\n */\nexport function describeCardCharge(\n charge: Pick<CardCharge, \"status\" | \"status_detail\">,\n messages: Readonly<Record<string, string>> = {},\n): PaymentOutcome {\n const status: PaymentStatus = Object.hasOwn(STATUS_OUTCOMES, charge.status)\n ? charge.status\n : \"unknown\";\n const base = STATUS_OUTCOMES[status];\n const detail = charge.status_detail ?? \"\";\n const matches =\n detail !== \"\" && status !== \"paid\" && DECLINE_DETAILS.has(detail) === (status === \"failed\");\n const refined = matches\n ? (ownText(messages, detail) ?? ownText(CARD_STATUS_DETAIL_MESSAGES, detail))\n : undefined;\n return { status, ...base, message: refined ?? base.message };\n}\n"],"mappings":"AAmBA,IAAMA,EACF,wFAgBSC,EAAgE,CACzE,qBAAsB,sEACtB,mBAAoB,+DACpB,UAAW,+EACX,mBACI,6EACJ,4BACI,mFACJ,sBACI,oGACJ,cAAe,+EACf,oBACI,mFACJ,yBACI,mFACJ,sBAAuB,wEACvB,iBAAkB,qEAClB,qBAAsB,oEACtB,wBACI,yEACJ,gBAAiBD,EACjB,kBAAmB,mEACnB,sBAAuB,mEACvB,UAAW,mEACX,WAAY,wEAChB,EAEME,EAAuC,IAAI,IAAI,CACjD,uBACA,qBACA,YACA,qBACA,8BACA,wBACA,gBACA,sBACA,2BACA,wBACA,mBACA,uBACA,uBACJ,CAAC,EAEKC,EAAmF,CACrF,KAAM,CACF,KAAM,UACN,MAAO,qBACP,QAAS,6BACT,SAAU,EACd,EACA,WAAY,CACR,KAAM,OACN,MAAO,uBACP,QAASH,EACT,SAAU,EACd,EACA,QAAS,CACL,KAAM,OACN,MAAO,uBACP,QAAS,uCACT,SAAU,EACd,EACA,YAAa,CACT,KAAM,OACN,MAAO,uBACP,QAAS,mEACT,SAAU,EACd,EACA,OAAQ,CACJ,KAAM,SACN,MAAO,qBACP,QAAS,+EACT,SAAU,EACd,EACA,QAAS,CACL,KAAM,UACN,MAAO,qBACP,QAAS,uDACT,SAAU,EACd,EACA,UAAW,CACP,KAAM,UACN,MAAO,sBACP,QAAS,kDACT,SAAU,EACd,EACA,SAAU,CACN,KAAM,UACN,MAAO,sBACP,QAAS,gCACT,SAAU,EACd,EACA,aAAc,CACV,KAAM,UACN,MAAO,uBACP,QAAS,6CACT,SAAU,EACd,EACA,QAAS,CACL,KAAM,UACN,MAAO,0BACP,QAAS,sDACT,SAAU,EACd,CACJ,EAUA,SAASI,EAAQ,EAAyC,EAAiC,CACvF,OAAO,OAAO,OAAOC,EAAOC,CAAG,EAAID,EAAMC,GAAO,IAAA,EACpD,CAgCA,SAAgBC,EACZ,EACA,EAA6C,CAAC,EAChC,CACd,IAAMC,EAAwB,OAAO,OAAOL,EAAiBM,EAAO,MAAM,EACpEA,EAAO,OACP,UACAC,EAAOP,EAAgBK,GACvBG,EAASF,EAAO,eAAiB,GAGjCG,EADFD,IAAW,IAAMH,IAAW,QAAUN,EAAgB,IAAIS,CAAM,KAAOH,IAAW,UAE/EJ,EAAQS,EAAUF,CAAM,GAAKP,EAAQH,EAA6BU,CAAM,EACzE,IAAA,GACN,MAAO,CAAE,SAAQ,GAAGD,EAAM,QAASE,GAAWF,EAAK,OAAQ,CAC/D,C"}