@12-apps/payments-frontend 3.39.0 → 3.40.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.39.0",
3
+ "version": "3.40.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Browser half of the vendor-agnostic payments platform: plug-and-play MUI components for the per-provider settings page (credential form from each provider's schema, masked hints, verify/enable) and the checkout page (PIX QR + polling, card tokenization, hosted-checkout redirect), plus the headless hooks and fetch clients they build on. Talks only to the host's payments HTTP surface — never to a provider directly. Microfrontend-ready: no app coupling, host injects theme and auth.",
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Opening the buyer's bank app from the Pix face (FUT-3612).
3
+ *
4
+ * There is no standard Pix link that pays a copia-e-cola code, so this only
5
+ * OPENS the app; the buyer pastes the code the button already copied. Android
6
+ * opens an app through an `intent:` URL naming its package; iOS only through a
7
+ * URL scheme the bank's app registers. Neither tells the page whether it
8
+ * worked, so {@link openBankApp} watches the page: an app that opened takes
9
+ * the screen within a moment, and a page still visible after
10
+ * {@link OPEN_WAIT_MS} is a bank that did not open — not installed, or a link
11
+ * this phone does not know.
12
+ */
13
+ import type { PixBankId } from "./types";
14
+
15
+ /** How long a page may stay visible before the bank counts as not opened. */
16
+ const OPEN_WAIT_MS = 1500;
17
+
18
+ interface BankApp {
19
+ /** The Play Store package of the bank's personal app — each one checked on its Play listing (2026-10-16). */
20
+ android: string;
21
+ /**
22
+ * The URL scheme its app answers on iOS (without "://"). Only Nubank's
23
+ * (`nuapp`) is documented — in Nubank's own deep links. The other five
24
+ * publish no scheme we could find: theirs are the likeliest name, and a
25
+ * phone that does not know one is a bank that "did not open", which the face
26
+ * says (owner, 2026-10-16: Android and iOS, and say so when it does not open).
27
+ */
28
+ ios: string;
29
+ /** A scheme Android opens the app with, when one is known; else the app's launcher, by package. */
30
+ scheme?: string;
31
+ }
32
+
33
+ /** The six, in the prototype's order. */
34
+ export const PIX_BANKS: readonly PixBankId[] = ["nubank", "itau", "bradesco", "bb", "caixa", "santander"];
35
+
36
+ const APPS: Record<PixBankId, BankApp> = {
37
+ nubank: { android: "com.nu.production", ios: "nuapp", scheme: "nuapp" },
38
+ itau: { android: "com.itau", ios: "itau" },
39
+ bradesco: { android: "com.bradesco", ios: "bradesco" },
40
+ bb: { android: "br.com.bb.android", ios: "bbapp" },
41
+ caixa: { android: "br.gov.caixa.CAIXA", ios: "caixa" },
42
+ santander: { android: "com.santander.app", ios: "santanderpf" },
43
+ };
44
+
45
+ export type PhoneOs = "android" | "ios";
46
+
47
+ /** The phone this page runs on, or `null` for a computer (or no browser at all). */
48
+ export function phoneOs(nav: Pick<Navigator, "userAgent" | "maxTouchPoints"> | undefined = typeof navigator === "undefined" ? undefined : navigator): PhoneOs | null {
49
+ if (!nav) return null;
50
+ const ua = nav.userAgent;
51
+ if (/Android/i.test(ua)) return "android";
52
+ if (/iPhone|iPad|iPod/i.test(ua)) return "ios";
53
+ // iPadOS asks for the desktop site: a "Macintosh" with a touch screen.
54
+ if (/Macintosh/i.test(ua) && nav.maxTouchPoints > 1) return "ios";
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * The phone the host's bank flow runs on: its own word when it gave one
60
+ * (`phone`), else the browser's. `null` ⇒ a computer: no banks.
61
+ */
62
+ export function bankFlowOs(phone: boolean | undefined): PhoneOs | null {
63
+ if (phone === false) return null;
64
+ if (phone === true) return phoneOs() ?? "android";
65
+ return phoneOs();
66
+ }
67
+
68
+ /**
69
+ * The URL that opens `bank`'s app on `os`. Android: an `intent:` URL naming the
70
+ * package — through the app's scheme when one is known, else its launcher; a
71
+ * phone without the app gets the bank's Play Store page from the browser.
72
+ */
73
+ export function bankAppUrl(bank: PixBankId, os: PhoneOs): string {
74
+ const app = APPS[bank];
75
+ if (os === "ios") return `${app.ios}://`;
76
+ if (app.scheme) return `intent://#Intent;scheme=${app.scheme};package=${app.android};end`;
77
+ return `intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;package=${app.android};end`;
78
+ }
79
+
80
+ /**
81
+ * Open `bank`'s app, and say whether it opened: `true` when the page lost the
82
+ * screen (hidden, or left) within {@link OPEN_WAIT_MS}.
83
+ */
84
+ export function openBankApp(
85
+ bank: PixBankId,
86
+ os: PhoneOs,
87
+ env: { win: Window; doc: Document; waitMs?: number } = { win: window, doc: document },
88
+ ): Promise<boolean> {
89
+ const { win, doc } = env;
90
+ return new Promise((resolve) => {
91
+ let left = false;
92
+ const onVisibility = (): void => {
93
+ if (doc.visibilityState === "hidden") left = true;
94
+ };
95
+ const onLeave = (): void => {
96
+ left = true;
97
+ };
98
+ doc.addEventListener("visibilitychange", onVisibility);
99
+ win.addEventListener("pagehide", onLeave);
100
+ win.addEventListener("blur", onLeave);
101
+ win.setTimeout(() => {
102
+ doc.removeEventListener("visibilitychange", onVisibility);
103
+ win.removeEventListener("pagehide", onLeave);
104
+ win.removeEventListener("blur", onLeave);
105
+ resolve(left);
106
+ }, env.waitMs ?? OPEN_WAIT_MS);
107
+ try {
108
+ win.location.href = bankAppUrl(bank, os);
109
+ } catch {
110
+ // A scheme the browser refuses outright is a bank that did not open.
111
+ }
112
+ });
113
+ }
@@ -0,0 +1,26 @@
1
+ import type { PixBankCopy } from "./types";
2
+
3
+ /** The Pix-to-bank flow's words, en-US (FUT-3612). */
4
+ export const EN_US_PIX_BANK_COPY: PixBankCopy = {
5
+ names: {
6
+ nubank: { name: "Nubank", the: "" },
7
+ itau: { name: "Itaú", the: "" },
8
+ bradesco: { name: "Bradesco", the: "" },
9
+ bb: { name: "Banco do Brasil", the: "" },
10
+ caixa: { name: "Caixa", the: "" },
11
+ santander: { name: "Santander", the: "" },
12
+ },
13
+ generateAndCopy: "Create and copy the Pix",
14
+ generateCopyAndOpen: (bank) => `Create, copy and open ${bank.name}`,
15
+ copyAndOpen: (bank) => `Copy and open ${bank.name}`,
16
+ openOther: "Open another bank",
17
+ listTitle: "Open your bank and paste in Pix copia e cola",
18
+ open: (bank) => `Open ${bank.name}`,
19
+ opening: (bank) => `Opening **${bank.name}**…`,
20
+ other: "Another bank",
21
+ notOpened: (bank) => `We couldn't open ${bank.name}. Is the app installed on this phone?`,
22
+ retry: "Try again",
23
+ copied: "Pix code copied",
24
+ copiedPick: "Pix code copied. Choose your bank.",
25
+ copiedOpening: (bank) => `Pix code copied. Opening ${bank.name}…`,
26
+ };
@@ -0,0 +1,35 @@
1
+ import { Box } from "@mui/material";
2
+ import type { JSX } from "react";
3
+
4
+ import type { PixBankId } from "./types";
5
+
6
+ /**
7
+ * The six banks' own marks (FUT-3612, owner 2026-10-15: "coloca as logos dos
8
+ * bancos"), drawn at 32px beside each name so a buyer finds their bank by its
9
+ * mark. Inline as data URIs — no remote asset (ADR 0074's sibling rule, §5.1
10
+ * item 5) — and only to identify the bank. Sources: Nubank and Itaú from
11
+ * Wikimedia Commons (PD-textlogo, trademarked); Bradesco, Banco do Brasil,
12
+ * Caixa and Santander from the Bancos-em-SVG collection (the banks' symbols).
13
+ * Each is the bank's trademark.
14
+ */
15
+ const LOGOS: Record<PixBankId, string> = {
16
+ nubank: "data:image/svg+xml;base64,PHN2ZyB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIiB2aWV3Qm94PSIwIDAgMjYzLjg0MjA3IDE0NS41NjczNyIgdmVyc2lvbj0iMS4xIiBpZD0ic3ZnNiIgc29kaXBvZGk6ZG9jbmFtZT0iTnViYW5rIGxvZ28gMjAyMS5zdmciIHdpZHRoPSIyNjMuODQyMDciIGhlaWdodD0iMTQ1LjU2NzM3IiBpbmtzY2FwZTpleHBvcnQtZmlsZW5hbWU9IkM6XFVzZXJzXGFsZXhcc2l0ZXNcbnViYW5rLWxvZ28tMC5wbmciIGlua3NjYXBlOmV4cG9ydC14ZHBpPSI1NjEuNzM3MTIiIGlua3NjYXBlOmV4cG9ydC15ZHBpPSI1NjEuNzM3MTIiIGlua3NjYXBlOnZlcnNpb249IjEuMC4yIChlODZjODcwODc5LCAyMDIxLTAxLTE1LCBjdXN0b20pIj4gPG1ldGFkYXRhIGlkPSJtZXRhZGF0YTEyIj4gPHJkZjpSREY+IDxjYzpXb3JrIHJkZjphYm91dD0iIj4gPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+IDxkYzp0eXBlIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+IDxkYzp0aXRsZT48L2RjOnRpdGxlPiA8L2NjOldvcms+IDwvcmRmOlJERj4gPC9tZXRhZGF0YT4gPGRlZnMgaWQ9ImRlZnMxMCIgLz4gPHNvZGlwb2RpOm5hbWVkdmlldyBwYWdlY29sb3I9IiNmZmZmZmYiIGJvcmRlcmNvbG9yPSIjNjY2NjY2IiBib3JkZXJvcGFjaXR5PSIxIiBvYmplY3R0b2xlcmFuY2U9IjEwIiBncmlkdG9sZXJhbmNlPSIxMCIgZ3VpZGV0b2xlcmFuY2U9IjEwIiBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxMzY2IiBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI3MDUiIGlkPSJuYW1lZHZpZXc4IiBzaG93Z3JpZD0iZmFsc2UiIGlua3NjYXBlOnpvb209IjAuMjE0NTk3NTkiIGlua3NjYXBlOmN4PSIyMDguODkzNiIgaW5rc2NhcGU6Y3k9IjEyOC41MTMxNSIgaW5rc2NhcGU6d2luZG93LXg9Ii04IiBpbmtzY2FwZTp3aW5kb3cteT0iLTgiIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2ZzYiIGlua3NjYXBlOmRvY3VtZW50LXJvdGF0aW9uPSIwIiBmaXQtbWFyZ2luLXRvcD0iMCIgZml0LW1hcmdpbi1sZWZ0PSIwIiBmaXQtbWFyZ2luLXJpZ2h0PSIwIiBmaXQtbWFyZ2luLWJvdHRvbT0iMCIgLz4gPHBhdGggZD0iTSA0Ny42Mjc1OTgsMTMuODg1NjUgQyA1NS45NzA5MTgsNS4xMjAyMiA2Ny4wMzgxODgsMCA4MC4wNDAxNTgsMCBjIDI1LjMyOTMwMiwwIDQyLjEyMjg2MiwxOC40OTM2MSA0NS4zMzU4MTIsNDYuMTk4MzEgMS4wNDE3Myw4Ljk5NzAyIDEuMDMyMTcsMjEuNjgxNDYgMS4wMjE3MiwzNi4zNzMzOCAtMC4wMDIsMS41MDY2MiAtMC4wMDIsMy4wMzQ2NCAtMC4wMDIsNC41ODE3NSB2IDU1LjAzODM5IEggOTEuNTYzMDY4IHYgLTQwLjcwODEyIGMgMCwwIC0wLjA3MSwtMzQuODQwMzYgLTAuMjgzODYsLTQxLjM1MTggLTAuOTM0ODIsLTI4LjM1OTQ1IC0xNy43MjM4MywtNDYuMTc5MTEgLTQzLjY1NDk3LC00Ni4xOTgyNiAtNy44MjM2LDguMjY1NzcgLTEyLjAxNTU5LDE4LjM3MjEgLTEyLjYzMzgxLDMzLjgyNDk4IC0wLjA4NSwyLjE1NTMxIC0wLjA2MzYsOS43OTQ5MSAtMC4wMzU0LDE5Ljg2MzY3IDAuMDE0Niw1LjIxNjggMC4wMzEsMTEuMDg1OTIgMC4wMzU0LDE3LjE4MjA0IDAuMDI1OCwyNi41MzM4OCAwLDU3LjM5MzQxIDAsNTcuMzkzNDEgSCAwLjE1NzcwMTQ1IFYgNzkuNTQ4NDIgYyAwLC0yLjE0NjY4IC0wLjAzOTE1LC00LjMxMTU1IC0wLjA3ODM5LC02LjQ4NTA2IC0wLjA3ODk5OTk5NzEsLTQuMzcyMDUgLTAuMTU4NjI2LC04Ljc3Nzc1IDAuMDc4MzksLTEzLjE0MDI1IEMgMC41NTEyNTI0NSw1Mi42NTA2MiAxLjgwOTMyMjUsNDUuNTAxNCA1LjIwMjg2ODUsMzguODU5OTUgMTIuOTcwNjU4LDIzLjY0NDgxIDI4Ljg4Njg4OCwxMy44NDgzNSA0NS44NTQ4NDgsMTMuODQ4MzUgYyAwLjU5Mjc0LDAgMS4xODU5MywwLjAxMzcgMS43NzI3NSwwLjAzNjQgeiIgY2xhc3M9IlBhdGgteHJ0a3BkLTAgZXFmWWlSIiBpZD0icGF0aDIiIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIHN0eWxlPSJmaWxsOiM4MjBhZDE7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlLXdpZHRoOjQuNTQ5IiBpbmtzY2FwZTpleHBvcnQtZmlsZW5hbWU9IkM6XFVzZXJzXGFsZXhcc2l0ZXNcbnViYW5rLWxvZ28tNS5wbmciIGlua3NjYXBlOmV4cG9ydC14ZHBpPSIxMzIuNzY1MzgiIGlua3NjYXBlOmV4cG9ydC15ZHBpPSIxMzIuNzY1MzgiIC8+IDxwYXRoIGQ9Im0gMjYzLjY4NDMzLDg1LjY0NDk5IGMgMC4yMzcsLTQuMzYyNDkgMC4xNTczOSwtOC43Njg2NiAwLjA3ODcsLTEzLjE0MDI1IC0wLjAzOTYsLTIuMTczNTEgLTAuMDc4NywtNC4zMzgzOCAtMC4wNzg3LC02LjQ4NTA1IFYgMy4zNzAyMiBoIC0zNC44MzIxOCBjIDAsMCAtMC4wMjYsMzAuODU5NTMgMCw1Ny4zOTMwOSAwLjAwNCw2LjA5NjEyIDAuMDIwNSwxMS45NjQ3OSAwLjAzNSwxNy4xODE1OSAwLjAyODIsMTAuMDY5MjEgMC4wNDk2LDE3LjcwODgxIC0wLjAzNSwxOS44NjQxMyAtMC42MTgyMSwxNS40NTM0MiAtNC44MTA1NiwyNS41NTk0OCAtMTIuNjMzOTQsMzMuODI1NDggLTI1LjkzMDY5LC0wLjAxODIgLTQyLjcxOTIzLC0xNy44MzkzOCAtNDMuNjU0NTEsLTQ2LjE5ODMyIC0wLjIxMjksLTYuNTExNDQgLTAuMzAyOTcsLTIyLjY2OTk1IC0wLjMwMjk3LC00MS4zODQ1MSBWIDMuMzQzNjEgbCAtMzQuODA2NjksMC4wMzE4IHYgNTUuMDM4NTIgYyAwLDEuNTQ3MTIgLTAuMDAyLDMuMDc0NjggLTAuMDAyLDQuNTgxNzYgLTAuMDExLDE0LjY5MTkxIC0wLjAyLDI3LjM3NTkgMS4wMjE3LDM2LjM3MzM4IDMuMjA2MTUsMjcuNzA0NzkgMTkuOTk5Nyw0Ni4xOTgzIDQ1LjMyOSw0Ni4xOTgzIDEzLjAwMjQyLDAgMjQuMDY5NjksLTUuMTIwMzUgMzIuNDEzMDEsLTEzLjg4NTgyIDAuNTg2MzcsMC4wMjI3IDEuMTc5MTEsMC4wMzY0IDEuNzcxODQsMC4wMzY0IDE2Ljk2Nzc4LDAgMzIuODg0MjksLTkuNzk2MjggNDAuNjUyMTcsLTI1LjAxMTMzIDMuMzkzNTUsLTYuNjQyIDQuNjUxMzUsLTEzLjc5MDc2IDUuMDQ0ODQsLTIxLjA2MzI1IHoiIGNsYXNzPSJQYXRoLXhydGtwZC0wIGVxZllpUiIgaWQ9InBhdGg0IiBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiBzdHlsZT0iZmlsbDojODIwYWQxO2ZpbGwtb3BhY2l0eToxO3N0cm9rZS13aWR0aDo0LjU0OSIgaW5rc2NhcGU6ZXhwb3J0LWZpbGVuYW1lPSJDOlxVc2Vyc1xhbGV4XHNpdGVzXG51YmFuay1sb2dvLTUucG5nIiBpbmtzY2FwZTpleHBvcnQteGRwaT0iMTMyLjc2NTM4IiBpbmtzY2FwZTpleHBvcnQteWRwaT0iMTMyLjc2NTM4IiAvPiA8cGF0aCBkPSJtIDExOC41ODczMSw3OTQuNjAwOTMgYyAxNC4xNTg0NiwtMTQuOTQ0NjQgMzIuOTM5NDIsLTIzLjY3NDM1IDU1LjAwMzc4LC0yMy42NzQzNSA0Mi45ODMzMSwwIDcxLjQ4MTY1LDMxLjUzMDY1IDc2LjkzNDAxLDc4Ljc2NTY1IDEuNzY3NzcsMTUuMzM5NSAxLjc1MTYsMzYuOTY1OCAxLjczMzg5LDYyLjAxNDcgMCwyLjU2ODggMCw1LjE3MzkgMCw3LjgxMTcgdiA5My44Mzc0NyBoIC01OS4xMTA0NCB2IC02OS40MDUxNyBjIDAsMCAtMC4xMjA1NSwtNTkuNDAwOSAtMC40ODE4NiwtNzAuNTAyNiAtMS41ODYxOCwtNDguMzUxNCAtMzAuMDc2NzgsLTc4LjczMyAtNzQuMDgxNjksLTc4Ljc2NTYgLTEzLjI3NjQyLDE0LjA5MjcgLTIwLjM5MDM3MiwzMS4zMjM0IC0yMS40Mzk0MDIsNTcuNjY5OCAtMC4xNDQxMiwzLjY3NDcgLTAuMTA3OTEsMTYuNjk5OCAtMC4wNjA2LDMzLjg2NjUgMC4wMjQ4LDguODk0NCAwLjA1MzMsMTguOTAwOSAwLjA2MDYsMjkuMjk0NSAwLjA0NSw0NS4yMzg5IDAsOTcuODUyNjcgMCw5Ny44NTI2NyBIIDM4LjAzNTA2OCBWIDkwNi41NTI0MyBjIDAsLTMuNjU5OSAtMC4wNjcsLTcuMzUwOSAtMC4xMzMsLTExLjA1NjcgLTAuMTMzOTMsLTcuNDU0MSAtMC4yNjkwMywtMTQuOTY1NiAwLjEzMywtMjIuNDAzNCAwLjY2ODAzLC0xMi4zOTkyIDIuODAyOTMsLTI0LjU4ODMgOC41NjE4LC0zNS45MTE1IDEzLjE4MTYzLC0yNS45NDExIDQwLjE5MTM0LC00Mi42NDM1IDY4Ljk4NTcyMiwtNDIuNjQzNSAxLjAwNTc3LDAgMi4wMTI0NCwwLjAyMyAzLjAwODIxLDAuMDYyIHoiIGNsYXNzPSJQYXRoLXhydGtwZC0wIGVxZllpUiIgaWQ9InBhdGgyLTYiIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiIHN0eWxlPSJmaWxsOiNmZmZmZmY7c3Ryb2tlLXdpZHRoOjcuNzM3NjgiIGlua3NjYXBlOmV4cG9ydC1maWxlbmFtZT0iQzpcVXNlcnNcYWxleFxzaXRlc1xudWJhbmstbG9nby00LnBuZyIgaW5rc2NhcGU6ZXhwb3J0LXhkcGk9IjkyLjYxODA5NSIgaW5rc2NhcGU6ZXhwb3J0LXlkcGk9IjkyLjYxODA5NSIgLz4gPHBhdGggZD0ibSA0ODUuMjMyMTcsOTE2Ljk0NjgzIGMgMC40MDIyMSwtNy40Mzc5IDAuMjY3MTQsLTE0Ljk1MDEgMC4xMzM2LC0yMi40MDM1IC0wLjA2NzIsLTMuNzA1NyAtMC4xMzM2LC03LjM5NjcgLTAuMTMzNiwtMTEuMDU2NyB2IC0xMDYuODE0IGggLTU5LjEwOTYyIGMgMCwwIC0wLjA0NDMsNTIuNjEzOSAwLDk3Ljg1MjIgMC4wMSwxMC4zOTM2IDAuMDM0NiwyMC4zOTkzIDAuMDU5MywyOS4yOTM3IDAuMDQ3OCwxNy4xNjc1IDAuMDg0MSwzMC4xOTI2IC0wLjA1OTMsMzMuODY3MyAtMS4wNDkxMSwyNi4zNDczIC04LjE2MzQzLDQzLjU3NzYgLTIxLjQzOTU5LDU3LjY3MDcgLTQ0LjAwMzk4LC0wLjAzMSAtNzIuNDkzODUsLTMwLjQxNTMgLTc0LjA4MTA1LC03OC43NjU3IC0wLjM2MTI1LC0xMS4xMDE3IC0wLjUxNDE4LC0zOC42NTExIC0wLjUxNDE4LC03MC41NTg0IHYgLTY5LjQwNTE2IGwgLTU5LjA2NjM1LDAuMDU0MSB2IDkzLjgzNzg2IGMgMCwyLjYzNzcgMCw1LjI0MjEgMCw3LjgxMTYgLTAuMDE5MiwyNS4wNDkgLTAuMDMzLDQ2LjY3NDUgMS43MzM4Nyw2Mi4wMTQ3IDUuNDQwNzEsNDcuMjM1MiAzMy45MzkwNSw3OC43NjU2NyA3Ni45MjI1NSw3OC43NjU2NyAyMi4wNjUwMSwwIDQwLjg0NTg3LC04LjcyOTkgNTUuMDA0MzYsLTIzLjY3NDU3IDAuOTk1MDgsMC4wMzkgMi4wMDA5NCwwLjA2MiAzLjAwNjc5LDAuMDYyIDI4Ljc5NDA4LDAgNTUuODA0MTIsLTE2LjcwMjEgNjguOTg2MDUsLTQyLjY0MyA1Ljc1ODg5LC0xMS4zMjQzIDcuODkzMjgsLTIzLjUxMjUgOC41NjEwNSwtMzUuOTExNyB6IiBjbGFzcz0iUGF0aC14cnRrcGQtMCBlcWZZaVIiIGlkPSJwYXRoNC01IiBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiBzdHlsZT0iZmlsbDojZmZmZmZmO3N0cm9rZS13aWR0aDo3LjczNzY4IiBpbmtzY2FwZTpleHBvcnQtZmlsZW5hbWU9IkM6XFVzZXJzXGFsZXhcc2l0ZXNcbnViYW5rLWxvZ28tNC5wbmciIGlua3NjYXBlOmV4cG9ydC14ZHBpPSI5Mi42MTgwOTUiIGlua3NjYXBlOmV4cG9ydC15ZHBpPSI5Mi42MTgwOTUiIC8+IDwvc3ZnPg==",
17
+ itau: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MDAiIGhlaWdodD0iNTAwIiB2aWV3Qm94PSIwIDAgNTAwIDUwMCI+PHBhdGggZD0iTTIxOC42NzYgMTAuNTc0Yy0xMy43OTcuMzI4LTIzLjkxOC44MTMtNDIuNDczIDEuOTU3LTguNTgyLjUxMi0xNy45NSAxLjI2Ni0yNC41NDcgMS45MjYtMS42NTYuMTgtNS4xMi41MTItNy42OC43NTQtMTAuNzUzIDEuMDU1LTI2LjcxOCAzLjA3LTMzLjQzMyA0LjIxNS0xLjQxOC4yNDItMy40OTYuNTc0LTQuNjY4Ljc1NC0xLjE0NS4xNTItMy4xOTUuNTExLTQuNTIuNzU0LTEuMzI0LjI0Mi0zLjU1NC42NjQtNC45NjguOTAyLTQuOTczLjg0NC0xMy40MzQgMi41NjItMTguMjIzIDMuNzA3LTEuMzI4LjMzMi0zLjcwNy45MDItNS4yNzMgMS4yNjYtMS41NjcuMzU5LTIuOTguNjkxLTMuMTYuNzIyLTguODI5IDIuMTA2LTE3LjU5NCA2LjM1Ni0yNC4zNCAxMS44NjdDMzguODI0IDQ0LjczIDMyLjk1MyA1My4zNDQgMzAgNjEuODk4Yy0uNDggMS4zMjUtLjk2NSAyLjY4LTEuMTEzIDMuMDEyLS41MTIgMS4xNDUtMi43NDIgMTAuNzgxLTQuMzQgMTguNjc2LS4yMzggMS4zMjQtLjY2IDMuMjgxLS45MDIgNC4zNjctLjI0MyAxLjA4Mi0uNTcgMi45OC0uNzgyIDQuMjE1LS4xODMgMS4yMzQtLjQyMiAyLjc0Mi0uNTc0IDMuMzEyLS40NTMgMS43Mi0yLjI4OSAxMy40NjUtMy40NjUgMjEuOTg5LTEuMTQ0IDguMjU0LTEuODk4IDE0LjI1LTIuNTU4IDIwLjMzMi0uMzMyIDIuOTgtLjY2NCA1LjkwMi0uNzIzIDYuNDc2LS4wOS41Ny0uMzYzIDMuNDkzLS42MzMgNi40NzctLjI0MiAyLjk4LS42MDEgNi42MjUtLjc4NSA4LjEzMy0uNjMzIDUuNTM5LTIuMiAyOS42NjgtMi45OCA0Ni42ODctLjYwMiAxMi42MTctLjk5MyAzOS42MzctLjc4MiA1NC4zNjcuMjExIDE4LjM3Mi42NiAzNC43OSAxLjA4MiA0MC41MTIuMTUzIDIuMzE3LjQ1MyA3LjIuNjMzIDEwLjg0NC4xOCAzLjY0NC40MjIgOC4zMTIuNTc0IDEwLjM5LjY5MiAxMC40MjIgMS4yMzUgMTcuMjU4IDEuNTA0IDE5LjQyNi4xNTMgMS4zMjguNTEyIDQuODUyLjc1NCA3LjgwMS4yNDIgMi45NTMuNjAyIDYuNDc3Ljc1NCA3LjgzMi4xOCAxLjM1Ni41MTIgNC4yNzcuNzUgNi41MDguNzU0IDcuMTM3IDEuMzI4IDExLjgwNSAxLjgwOSAxNC45MS4yNDIgMS42NTYuNjY0IDQuNDg4LjkwMiA2LjMyNC44NzUgNi42MjUgMS44OTggMTMuMDcgMy4wMTIgMTguOTc3LjI0MiAxLjMyNC42MzMgMy41NTUuOTA2IDQuOTY5LjUxMiAzLjAxMSAxLjY4NyA5LjAwNyAyLjUzMSAxMi44IDIuODkgMTMuMjg2IDQuMTU2IDE3LjI1OCA3LjU1OSAyMy43OTcuOTMzIDEuODA1IDIuMTM3IDMuOTE0IDIuNjggNC42NjguNTExLjc1NCAxLjI2NSAxLjgzNiAxLjY1NiAyLjQxIDEuNzE4IDIuNTU5IDYuNjI5IDcuNTU5IDkuMzQgOS40ODkuNDQ5LjMyOCAxLjMyNC45NiAxLjg5NCAxLjQxNC42MDYuNDggMS4xNzYuODQzIDEuMzI4Ljg0My4xMjEgMCAuNDIyLjIxMS42Ni40NTQgMS4xNzYgMS4xNDQgOS40NjEgNS4wOSAxMy4wMTIgNi4xNDQuNjY0LjIxMSAxLjQ3Ny40OCAxLjgwOS42MzMuNzg1LjM2IDUuMjEgMS40NzMgMTAuNTQzIDIuNjggMi4zMi41NDMgNC42OTkgMS4xMTMgNS4yNyAxLjI2NSAxLjM4Ni4zNiAxMC4xMiAyLjEwNiAxNi43MTggMy4zMTMgMy43OTMuNjkxIDExLjE3NiAxLjg2NyAxNS44MTIgMi41MzEgMS43NDcuMjM4IDMuOTc3LjU3IDQuOTcuNzUgMy42NDQuNjMzIDE4LjcwNiAyLjM4MyAyOC4xNjMgMy4yODUgMy40MDMuMzMyIDcuNTkuNzIzIDkuMzM2LjkwMiAyLjgzMi4zMDEgNS43MjMuNTQzIDE5LjU3OCAxLjUzNiAyLjA3OC4xNTIgNi44MDkuNDIyIDEwLjU0My42MDUgMy43MzUuMTggOC45NDYuNDUgMTEuNTk4LjYwMiAyMS40NDUgMS4yMzQgODIuNzcgMS4yMzQgMTAzLjkxNCAwIDIuNjUyLS4xOCA3Ljg2My0uNDIyIDExLjU5OC0uNjAyIDYuNDE0LS4zIDguNzY1LS40NTMgMTguODI0LTEuMjA3IDEyLjg5LS45OTIgMzEuODQtMi44MzIgMzguMjU0LTMuNzM0IDEuODM2LS4yNyA1LS42OTIgNy4wNzgtLjk2NSA3LjY4LS45OTIgMTguMjIzLTIuNTkgMjQuMjQ2LTMuNzAzIDYuNTk4LTEuMjA3IDE1LjMzMi0yLjk1MyAxNi43MTktMy4zMTMuNTctLjE1MiAyLjk0OS0uNzIyIDUuMjctMS4yNjUgNS4yMS0xLjE3NiA5LjczLTIuMzIgMTAuNTQyLTIuNjhhMTc0LjggMTc0LjggMCAwIDEgMy4wMTItMS4wNTVjMi40NjktLjgxMiAxMC42NjQtNC42OTkgMTEuMzU1LTUuMzYzLjIxMS0uMjExLjgxMy0uNjYgMS4zMjUtLjk2NSAyLjM1MS0xLjQ3MyAzLjQzMy0yLjMxNiA1LjU3NC00LjI0NiAyLjQxLTIuMiA1Ljc1NC01LjgxMiA2LjU2Ni03LjA0Ny4yNy0uNDIyLjk5My0xLjQ0NSAxLjU5NC0yLjI1OC42MzMtLjg0MyAxLjE0NS0xLjU5NyAxLjE0NS0xLjcxOCAwLS4xMjIuNDg0LS45NjUgMS4wNTQtMS44NjguNTc1LS45MDIgMS4yNjYtMi4yMyAxLjU0LTIuOTUzLjMtLjY5MS42LTEuNDQ1LjcyMi0xLjU5My41Ny0uNzU0IDIuNjgtNi43NSAzLjU4Mi0xMC4xNTMgMS40MTgtNS4yNyA0LjA2Ni0xNy43MSA1LjAzMS0yMy41ODYuMjQyLTEuNTA0LjUxMi0yLjkxOC42MDItMy4xNi4yNzMtLjY2NCAxLjkzLTEwLjk5NiAzLjQzMy0yMS41MzUuMzMyLTIuMzIuNjY0LTQuNjQuNzU0LTUuMTIxLjI0My0xLjM4NyAxLjMyNS0xMC42MzMgMS44MDktMTUuNTEyLjIzOC0yLjQxLjU3LTUuMTgzLjc1LTYuMTc2LjE4NC0uOTk2LjUxMi00LjI0Ni43NTQtNy4yMy4yNDItMi45OC41NzQtNi42NTYuNzIzLTguMTMzLjE4LTEuNTA0LjQ1My00LjkzNy42MzItNy42OC4xOC0yLjc0Mi40NTMtNi4xMTMuNjAyLTcuNTMuMTgzLTEuNDE1LjUxMi02LjYyNi43NTQtMTEuNTk1LjI3My00Ljk3Mi41NzQtMTEuMDg2Ljc1NC0xMy41NTQgMS4zNTUtMjEuODk5IDEuMzU1LTc5LjM2OCAwLTEwMS45NTctLjE1My0yLjU2My0uNDIyLTcuNTktLjYwMi0xMS4xNDUtLjE4My0zLjU1NS0uNDUzLTguMjU0LS42MDUtMTAuMzk0LS4xOC0yLjE2OC0uNTctNy41LS45MDMtMTEuODk1LS4zMzItNC4zOTgtLjY2NC04LjUyNy0uNzU0LTkuMTg3LS4wOS0uNjY1LS4zNTktMy41ODYtLjYwMS02LjQ3Ny0uMjQyLTIuODktLjUxMi01LjYwMi0uNjAyLTYuMDIzLS4wOS0uNDIyLS40MjItMy4xOTYtLjc1NC02LjE3Ni0uNzgtNy4yLTEuNzQ2LTE1LjE0OS0xLjk1Ny0xNi4wODItLjA5LS40MjItLjQyMi0yLjgzMi0uNzU0LTUuMzA1LTEuNDc2LTExLjI2Mi00LjIxNC0yNi45NTctNi41NjYtMzcuNjQ4LS4zMzItMS41MDgtLjg3MS0zLjk0Ni0xLjIwMy01LjQyMi0uODQ0LTMuODU2LTEuODQtNy43NDItMi4xNjgtOC40MzQtLjE1Mi0uMzMyLS40MjItMS4xNDQtLjYzMy0xLjgwOC0uNjY0LTIuMjI3LTQuMDA4LTkuNjY4LTQuODUxLTEwLjg0NC0uMTIyLS4xOC0xLjA4Mi0xLjU5NC0yLjExLTMuMTYtMi4xMzctMy4yMjMtNy42MTctOC45NDYtMTAuOTAyLTExLjM1Ni02LjUwNC00Ljc4OS0xMy43MzUtOC4wNDMtMjMuMDctMTAuMzMyYTQ0MC45MzIgNDQwLjkzMiAwIDAgMS00LjgyLTEuMjA3Yy0xLjExNC0uMzI4LTMuMzQ1LS44MTItOC4xMzQtMS44MDVhMzk2LjMxNyAzOTYuMzE3IDAgMCAxLTQuMDY2LS44NzVjLTUuODEzLTEuNDE0LTI0LjczLTQuNDg4LTM3LjA0Ny02LjA1NC0yLjQ3My0uMzAxLTQuOTEtLjYzMy01LjQyMi0uNzUtLjkwNi0uMTg0LTUuMjczLS42MzMtMTQuMTU2LTEuNTA4LTIuNDczLS4yNDItNS44NzUtLjYwMi03LjUzMS0uNzU0LTEuNjU2LS4xOC00LjY0LS40NS02LjYyNS0uNjMzLTEuOTg5LS4xNDgtNS4yNDItLjQyMi03LjIzLS41Ny02Ljc0Ny0uNTc0LTI1LjQ4MS0xLjY1Ni0zNS44NDQtMi4xMS0yMC4wMzItLjg3NC01Ny4wMi0xLjE0NC03OS45Ny0uNTdaTTg1Ljk2NSAyMTQuMDk4Yy4xOC4xNDggMS4wNTUuNDIyIDEuOTI2LjYzMiAxLjYyOS4zOTEgMy45MTggMS40NDYgNS42MzIgMi41OSAxLjM1Ni45MDMgNC41NTEgNC4zMzYgNS4xOCA1LjU0My4zMDUuNjAyLjYzMyAxLjE0NS43MjMgMS4yMzUuMDk0LjA5LjM2My42OTEuNjA1IDEuMzU1LjIxMS42Ni40OCAxLjM1Ni42MzMgMS41MDQuMzkuNTc0Ljg0NCA0LjE1Ni44NDQgNi44MDkgMCAyLjU5LS4zOTUgNS42MzItLjkwNiA2Ljc0Ni0xLjYyNSAzLjczNC0yLjQxIDQuOTY4LTQuNDU3IDcuMTQtMS44OTkgMS45NTctMi42OCAyLjU1OS01LjI3IDMuODI1LTIuMTEgMS4wNTQtMy42MTcgMS41OTctNC44MiAxLjcxNC0uOTY1LjEyMS0xLjgwOS4zMzItMS44OTkuNDU0LS4yMS4zNjMtNi43NDYuMzYzLTYuNzQ2IDAgMC0uMTIyLS43NTQtLjMzMy0xLjcxOS0uNDU0LS45MzMtLjExNy0yLjEwNS0uMzktMi42MTctLjYwMWEzMC45OTggMzAuOTk4IDAgMCAwLTEuOTg4LS44MTNjLS41NzQtLjIxLTEuMTE3LS40NTMtMS4yMDctLjU0My0uMDktLjEyLS41NDMtLjM5LTEuMDU1LS42MzItMS43NDYtLjg3Mi01LjM2LTQuODgtNi40MTQtNy4wNzktLjU3NC0xLjE3NS0xLjExMy0yLjMyLTEuMjY1LTIuNTU4LTEuMDI0LTIuMDUtMS4yMDQtMTEuMzg3LS4yNDMtMTMuNTU1LjI3NC0uNTc0LjY2NC0xLjUzNS44NzUtMi4xMS4yMTEtLjU3LjQ1My0xLjExMy41NDMtMS4yMDIuMDktLjA5LjQ1LS42OTYuNzgyLTEuMzU2LjU3NC0xLjExNyA0LjE1Ni00Ljk3MiA0LjY2OC00Ljk3Mi4xMiAwIC4zOTQtLjIxMS42MDUtLjQ4LjU3LS42MzQgMy40OTItMi4wNDggNS4yNy0yLjUuODQzLS4yMTIgMS43NzctLjUxMyAyLjEwOS0uNjY1LjMzMi0uMTQ4IDEuMTQ0LS4zIDEuODA1LS4zOSAxLjc3Ny0uMTggOC4wMTUuMDkgOC40MDYuMzYzWm0zMjguMTkxIDIwLjk5MnYxNS4yMWgtNTguNDMzdi0yMC4zNTlsMy4yNTQtLjUxNWM3LjE5OS0xLjExMyAxMS42ODctMS44NjcgMTEuOTU3LTIuMDQ3LjE4LS4xMjEgMS42NTYtLjM2IDMuMzEyLS41NyAxLjY1Ni0uMjExIDMuMTY0LS40ODUgMy4zMTMtLjYwNi4xODMtLjExNyAxLjU5Ny0uMzkgMy4xNjQtLjYwMSAxLjU2Ni0uMTggMy42MTMtLjU0MyA0LjUyLS43ODIuOTAyLS4yMSAyLjgtLjU0MyA0LjIxNC0uNzIyIDEuNDE4LS4xODQgMi44MzItLjQ1MyAzLjE2NC0uNTc1LjMzMi0uMTQ4IDEuODM2LS40MjEgMy4zMTMtLjYzMiAxLjUwNy0uMTggMy4xOTEtLjQ1IDMuNzY1LS42MzMuNTctLjE0OSAyLjc0Mi0uNTcgNC44Mi0uOTAzIDIuMDc5LS4zMzIgNC43OS0uNzggNi4wMjQtLjk5MiAxLjIzNC0uMjQyIDIuNTU5LS40MjIgMi45NS0uNDUzbC42NjMtLjAzMVptLTI0OS4zOTggMTMuNDMzdjE4LjA0M2gzMC4xMnYyNi44MDlIMTY0LjczbC4xMTggMjcuOTIyYy4wOTMgMzAuMDkuMDMgMjguNjEzIDEuNjYgMzIuMzE2IDEuMjM0IDIuODY0IDQuNzI2IDUuNzg1IDcuNzM4IDYuNTA4LjUxMi4xMjEgMS4wODYuMzYgMS4zMjguNTQzLjU3LjQ1IDEyLjc3LjQ1IDEzLjMxMy0uMDMxLjIzOC0uMTggMS4zMjQtLjQyMiAyLjQ2OC0uNTQzbDIuMDItLjIxMXYyNi40MThsLS44MTYuMzZjLS43NS4zMzEtNC4yMTUuODc0LTguOTczIDEuNDQ1LTYuNzc3Ljc4NS0xOS4xMjkuODc1LTE5Ljg1Mi4xMi0uMTgtLjE0OC0uNjYtLjI2OS0xLjExMy0uMjY5LTEuMTc2IDAtNi4wNTUtLjkwNi02LjQxOC0xLjE3Ni0uMTgtLjEyLS45OTItLjQyMi0xLjgzNi0uNjAxLS44MTItLjIxMS0xLjYyNS0uNDg1LTEuODA4LS42MDItLjE0OS0uMTUyLS44NC0uNDIyLTEuNTA0LS42MzMtLjY2NC0uMjEtMS4yNjYtLjQ4NC0xLjM1Ni0uNTc0LS4wOS0uMDktLjYzMy0uNDIyLTEuMjA3LS43NTQtMi41LTEuMzI0LTMuMjUtMS44MDQtNC41NzgtMi45MjItLjc4MS0uNjMyLTEuNTA0LTEuMTcxLTEuNjU2LTEuMTcxLS4zMDEtLjAzMi0yLjU5LTIuNTMyLTQuMzk1LTQuODItMS4zODYtMS43NzgtMy40MzctNS4xOC00LjEyOS02LjkzLS4yNDItLjU3LS41MTEtMS4xMTQtLjYwMS0xLjIwNC0uMTIxLS4wOS0uNDIyLTEuMDIzLS43MjMtMi4xMDktLjI3My0xLjA4Mi0uNjMzLTIuMjI3LS43ODUtMi41NTktLjE0OC0uMzMyLS41MTItMS42ODctLjc1LTMuMDExYTE0NC4yODYgMTQ0LjI4NiAwIDAgMC0uODE2LTMuNzY2Yy0uMjExLS45MDItLjM2LTExLjUwOC0uNDUtMzEuNTY2bC0uMTItMzAuMThoLTE3LjQ0MnYtMjYuODA5aDE3LjQ3M3YtMzAuMzYzbDIuMDQ2LS4yMWMxLjExNC0uMTE4IDIuMTY4LS4zMjkgMi4zMi0uNDUuMTgtLjEyMSAxLjY1Ny0uMzkgMy4zMTMtLjYwMiAxLjY1Ni0uMjEgMy4xNjQtLjUxNSAzLjMxMy0uNjMyLjE4LS4xMjEgMi4wNzgtLjQ1NCA0LjIxOC0uNzIzIDIuMTY4LS4zMDUgNC4wNjctLjYzMyA0LjIxNS0uNzU0LjE4LS4xMjEgMS4xNzYtLjMzMiAyLjI1OC0uNDIyYTQ5LjcxOSA0OS43MTkgMCAwIDAgNC4yMTktLjY5MWMzLjIyMy0uNjY0IDguODU1LTEuNTQgOS4xMjUtMS4zODcuMTIxLjA1OS4yMSA4LjI1NC4yMSAxOC4xOTFabTk5Ljg1MSAxNS4xOGM0LjIxNS4yNDIgOC45NzcuNzg1IDkuMzM2IDEuMDU1LjE4LjEyLjkwMy4zMzIgMS42NTcuNDIyIDEuNzE4LjI3MyA1LjkwNiAxLjUwNyA4LjQzMyAyLjUzIDIuMTEuODQ1IDYuODA5IDMuMTkyIDcuNTMxIDMuNzk4LjI0My4xOC43ODIuNTM5IDEuMjA0LjcyMi40MjEuMjExIDEuNzUgMS4yNjIgMi45MjEgMi4yOSAxLjE3NiAxLjA1NCAyLjI2MiAxLjk1NyAyLjQxIDIuMDE1LjQ1NC4yMTEgNS44MTMgNi43MTkgNS44MTMgNy4wNzggMCAuMDkuMjc0LjU3NS42MDYgMS4wMjQuNjMyLjkwNiAzLjMxMiA2LjExNyAzLjMxMiA2LjQ0OSAwIC4xMTcuMjQyLjc4MS41NyAxLjQ3My4zMDEuNjk1LjY2NCAxLjc1Ljc4NiAyLjMyLjEyLjU3NC4zMzIgMS4xNzYuNDQ5IDEuMzU1LjEyLjE1My4zOTQgMS4xMTQuNjA1IDIuMTEuMTguOTkyLjQyMiAyLjAyLjUxMiAyLjI1OC4xMjEuMjQyLjM5IDEuNTM1LjYzMyAyLjg2My4zNiAxLjk4OC40NDkgOS40ODguNTcgNDIuNjhsLjEyMSA0MC4zaC0zMi44NmwtLjAzLTMuNTU0Yy0uMDMyLTEuOTI2LS4xMjItNC43MjctLjI0My02LjIwNGwtLjE4LTIuNzEtLjg3NCAxLjIwM2MtMS4zODMgMS44NC01LjkzNCA2LjE3NS03LjczOSA3LjM3OS0zLjk0OSAyLjU5My01IDMuMjIyLTYuMDg2IDMuNTg2LS42NjQuMTgtMS41MzUuNTQzLTEuOTU3Ljc4LS40NTMuMjEyLTEuNTY2LjU3NS0yLjQ3Mi43NTUtLjkwMy4yMS0xLjgwNS40ODQtMS45NTcuNjAxLTEuNTM1IDEuMTQ1LTIwLjUxMiAxLjExNy0yMS45ODktLjAyNy0uMTQ4LS4xMjEtMS4yMzQtLjM5LTIuNDEtLjYwMi0xLjE0NC0uMjEtMi41LS41NDMtMy4wMTEtLjc4NS0uNDgtLjIxLTEuNzc4LS43NTQtMi44MjktMS4xNzZhNDIuNDIgNDIuNDIgMCAwIDEtMy41NTQtMS43NDYgNDkuODY2IDQ5Ljg2NiAwIDAgMC0yLjQ0Mi0xLjQxNGMtMS4yNjUtLjY5NS0xLjY4Ny0xLjA4Ni01LjEyLTQuNTItMy41MjQtMy41MjMtMy45NzctNC4wMDMtMy45NzctNC4zMzUgMC0uMTIxLS4zOTEtLjc4NS0uODQtMS40NzctLjQ4NS0uNjkxLTEuMTc2LTEuOTU3LTEuNTQtMi43NzMtLjM1OS0uODQtLjg0My0xLjgzNi0xLjA1NC0yLjI1OC0uMjM4LS40MjItLjU3LTEuNTA0LS43ODEtMi40MS0uMjExLS45MDMtLjQ4LTEuODA1LS42MDItMS45NTctLjMzMi0uNDIyLS45MDYtNC43LS45MDYtNi41NjcgMC0uODctLjE0OC0xLjc0Ni0uMzI4LTEuOTI1LS4zNjMtLjM2NC0uMjc0LTEuNTY3LjY2LTguODI5LjE4LTEuNDE0LjQ1My0yLjcxLjU3NC0yLjg1OS4xMjEtLjE4LjM5LS45NjUuNTctMS43NzcuMzkxLTEuNTY3IDEuMjA4LTMuNjE0IDIuMDUxLTUuMTUzLjI3LS41MTEuNjMtMS4yMDMuODEzLTEuNTY2LjE4LS4zOS40MjItLjY5MS41Ny0uNjkxLjEyMSAwIC4yMTEtLjE4LjIxMS0uMzkxIDAtLjMzMiAzLjMxMy00LjY0IDMuODg3LTUuMDMxLjEyLS4wOS4zMzItLjM5LjQyMi0uNjkyLjEyLS4zMzIuNDgtLjYzMi44MTItLjcyMi4zMzItLjA5LjYwMi0uMjc0LjYwMi0uMzk1IDAtLjMgNS4wNjItNC4yNzcgNi43NzctNS4yNyAxLjIzNC0uNzUzIDQuMDA4LTIuMTY3IDguMDQzLTQuMDk3LjU3NC0uMyA2LjIwMy0yLjEwNiA4LjY3Ni0yLjguOTAyLS4yNyAyLjY4LS42MzQgMy45MTQtLjgxMyAxLjIzNC0uMjExIDIuNDEtLjQ1MyAyLjU2Mi0uNTcuMTgtLjEyMiAxLjk4NS0uMzk1IDQuMDY3LS41NzUgMi4wNzgtLjE4IDQuNjM3LS40OCA1LjcyMi0uNjMzIDQuMjc4LS42MzIgMTkuMzA1LS41MTEgMjAuOTAzLjE1My41NDMuMjM4LjU3LjEyLjM2LTIuNDQyLS4xMTgtMS40NzYtLjMyOS0yLjc0Mi0uNDUtMi44MzItLjE1Mi0uMDktLjMzMi0uNjYtLjQ1My0xLjI2NS0uMjExLTEuMTE0LTEuMzU2LTMuNTUxLTIuNDM4LTUuMjExLS42OTUtMS4wNTEtMy42MTctNC4wNjctMy45NDktNC4wNjctLjExNyAwLS42MjktLjMtMS4xNDQtLjY5MS0xLjMyNS0uOTY1LTMuNDYxLTEuOTI2LTQuNzU4LTIuMTY4LS42MzMtLjEyMS0xLjE0NS0uMzMyLTEuMTQ1LS40NTMgMC0xLjMyNC0xOS40NTctMS41MzUtMjUuMy0uMjM5LS43NTQuMTQ5LTIuNS40OC0zLjkxNS43MjMtMS40MTguMjM4LTIuODMyLjU3LTMuMTY0LjcyMy0uMzMyLjE0OC0xLjQxNC40MjItMi40MS42MDEtLjk5Mi4xOC0yLjA3OC40NTMtMi40MS42MDItLjMzMi4xNTItMS41MzUuNDg0LTIuNjguNzU0LTEuMTQ0LjI3LTIuMjMuNTctMi40MS43MjItLjE4LjEyMi0uOTkyLjQyMi0xLjgzNi42MzMtLjgxMi4yMTEtMS42NTYuNTEyLTEuODA4LjYzMy0uMTguMTIxLS45OTMuMzktMS44MDkuNjAyLS44NC4xOC0yLjAxNi42MDEtMi42MTcuOTAyLS42MzMuMzMyLTEuMjA3LjU3NC0xLjI5Ny41NzQtLjA5IDAtLjE1Mi02LjI5Ny0uMTUyLTEzLjk3NiAwLTEwLjg3NS4wOTMtMTQuMDM2LjM5NC0xNC4yNzguMjExLS4xOCAxLjA1NS0uNDggMS44NjctLjY5MS44NDQtLjE4IDEuNjU3LS40NTMgMS44MDUtLjU3NC4xODQtLjEyMSAxLjE3Ni0uNDggMi4yNjItLjc1NCAxLjA4Mi0uMzAxIDIuMTA1LS42MyAyLjI1OC0uNzUuMTgtLjE1MyAxLjExMy0uMzk1IDIuMDc4LS42MDIuOTk2LS4yMSAyLjAyLS40ODQgMi4yNTgtLjYzMy4yNzMtLjEyIDEuNTA3LS40NTMgMi43NDItLjcyMmE1OC45NSA1OC45NSAwIDAgMCAyLjg2My0uNjMzYy4zMjgtLjA5NCAyLjAxNi0uMzYzIDMuNzYyLS42MDYgMS43NS0uMjM4IDMuNTg2LS41NyA0LjA2Ni0uNzIyIDEuNzUtLjU0IDE3LjUzMS0xLjUzNSAyMS4wODYtMS4zMjQuOTkyLjA1OCAzLjE2NC4xOCA0LjgyLjI3Wk05OC40OTIgMzI2LjUwOHY1OS45MzdINjMuMjU0VjI2Ni41NjZoMzUuMjM4Wm0yNjYuMjY2LTIyLjU5NGMwIDM1LjY2NC4xNTIgNDIuNTYzLjg3NSA0My41MjcuMTQ4LjE0OS4zMzIuNzUuNDUzIDEuMzI1LjQyMiAyLjI1NyAyLjM3OSA1LjU0MyA0LjYwNSA3Ljc3IDEuMTc2IDEuMjA2IDIuNSAyLjI2IDIuOTIyIDIuMzc4LjQ1My4wOTQuNzg1LjI3NC43ODUuMzk1IDAgLjExNyAxLjA1NS41MzkgMi4zNDguOTYgMS45My42MDYgMi45NTMuNzIzIDUuOTY1LjcyMyAyLjExIDAgMy45NDUtLjE0OCA0LjM2Ny0uMzI4LjM5LS4yMSAxLjMyNC0uNTEyIDIuMDc4LS43MjMuNzU0LS4yMSAyLjExLS44NDMgMy4wMTItMS40NDkgMS42ODctMS4wODIgNC44Mi0zLjk3MiA0LjgyLTQuNDU3IDAtLjE0OC40MjItLjgxMi45MDMtMS40NDUuNTExLS42NjQuOTA2LTEuMzI0LjkwNi0xLjUwNCAwLS4xODQuMjctLjgxNi41Ny0xLjQxOC4zMzItLjYwMi42OTItMS42MjUuODEzLTIuMjU4LjA5LS42NjQuMy0xLjMyNC40NTMtMS40NzYuNzU0LS45NjUuODc1LTcuMDQ3Ljg3NS00Mi4zOHYtMzYuOTg4aDM1LjIzOHYxMTkuODhINDIwLjA2Yy0xMy4wNCAwLTE2Ljc3OC0uMDktMTYuOTU3LS4zOTEtLjEyMi0uMTgtLjI3LTMuMDQzLS4zMzItNi4zMjUtLjA2LTMuMjUzLS4yMTEtNS45MzMtLjMyOS01LjkzMy0uMTIgMC0uNDg0LjM2LS44NDMuODEyLTEuNjg4IDIuMjktNC4zNCA0Ljg4LTguNDk2IDguMzEzLS42MDIuNDgtMS44MzYgMS4xNDQtNC41NDcgMi40NjktMS4yMzUuNjAxLTIuNTkgMS4yOTYtMy4wMTIgMS41MDctLjQyMi4yMTEtMS41NjYuNTQzLTIuNTYzLjc1NC0uOTkyLjIxMS0xLjg2Ny40OC0xLjk1Ny42MDItLjMuNDUtNS42MzIuOTAyLTEwLjkwMi45MDItNS4xOCAwLTkuMzk4LS4zNi0xMC42MzMtLjg3NS0uMzMyLS4xNDgtMS40NzYtLjUxMS0yLjU2Mi0uNzgxLTEuMDgyLS4yNy0yLjA3OC0uNjMzLTIuMjU4LS43NTQtLjE0OC0uMTIxLS44NzEtLjQyMi0xLjU2Ni0uNjMzLS43MjMtLjE4LTEuMjkzLS40NDktMS4yOTMtLjU3IDAtLjEyMS0uNTQzLS4zOS0xLjIwNy0uNjAyLS42Ni0uMjEtMS4yMDQtLjQ4NC0xLjIwNC0uNjA1IDAtLjExNy0uMzMyLS4zNi0uNzIyLS41NC0yLjU5LTEuMTc1LTguNDY1LTYuNjg3LTEwLjkzNC0xMC4yNDEtMS41MDgtMi4xNjgtMy44MjgtNi41MDgtNC4zMDgtOC4xMDItLjE4LS42MzMtLjU0My0xLjU2Ni0uNzU0LTIuMDUtLjIxMS0uNTEyLS41Ny0xLjU5NC0uNzU0LTIuNDEtLjE4LS44NC0uNDUtMS43MTYtLjYwMi0yLjAxNi0uMTQ4LS4yNzQtLjQyMi0xLjY4OC0uNjAxLTMuMTY0LS4xOC0xLjQ0Ni0uNTEyLTIuOTIyLS42OTItMy4yNTQtLjI3My0uNDUtLjM5NC0xMS4yMDQtLjQ1My00My4zMTNsLS4wOS00Mi42OGgzNS4yN1ptMCAwIiBzdHlsZT0ic3Ryb2tlOm5vbmU7ZmlsbC1ydWxlOm5vbnplcm87ZmlsbDojRkY2MjAwO2ZpbGwtb3BhY2l0eToxIi8+PHBhdGggZD0iTTI2MC42OTEgMzMzLjE2NGMtMS40NzIuMTQ5LTIuOTguMzktMy4zMTIuNTQtLjMzMi4xODMtMS40MTQuNDUyLTIuNDEuNjYzLS45NjEuMTgtMi4yNTguNTQzLTIuODYuODEzLTQuNTIgMS45NTctNi43MTggMy4yNTQtOC4yMjIgNC44Mi0xLjQ0NiAxLjUwOC0yLjg5NSA0LjMwOS0zLjEzMyA2LjA1NS0uMDYzLjYwMS0uMjc0IDEuMzg2LS40NTMgMS43MTUtLjMzMi42OTUtLjIxMSAyLjgzMi4zMzIgNS42MzIuNjAxIDMuMDQzIDMuMDEyIDYuNzUgNS4xNDggNy44NjQgMy43OTcgMS45NTcgNC41MiAyLjI1NyA2LjYzIDIuNTkgMy43OTIuNTcgMTEuMDIzLjI0MiAxMi41ODktLjYwMi4yNy0uMTUyIDEuMzg3LS42NjQgMi40NjktMS4xNDUgMS4wODYtLjUxMSAyLjUtMS4zMjggMy4xNjQtMS44NC42NjQtLjUxMSAxLjQxNC0xLjExMyAxLjY4Ny0xLjMyNCAxLjcxNS0xLjI5MyA0Ljg4LTYuNTM1IDUuMzAxLTguNzAzLjA5LS41NDMuMzMyLTEuMTc2LjUxMi0xLjQxOC4yMS0uMjcuMzYtMy4wNDMuNDIyLTguMDEybC4xMi03LjU5LTEuNjI4LS4xNzljLTIuMi0uMjczLTEzLjA0LS4xOC0xNi4zNTYuMTIxWm0wIDAiIHN0eWxlPSJzdHJva2U6bm9uZTtmaWxsLXJ1bGU6bm9uemVybztmaWxsOiNGRjYyMDA7ZmlsbC1vcGFjaXR5OjEiLz48L3N2Zz4=",
18
+ bradesco: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMzc4Ljc2MjM0IDM3OC44MDYiIHZlcnNpb249IjEuMSIgaWQ9InN2ZzEwIiBzb2RpcG9kaTpkb2NuYW1lPSJicmFkZXNjby5zdmciIGlua3NjYXBlOnZlcnNpb249IjEuMi4yICg3MzJhMDFkYTYzLCAyMDIyLTEyLTA5KSIgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gPGRlZnMgaWQ9ImRlZnMxNCIgLz4gPHNvZGlwb2RpOm5hbWVkdmlldyBpZD0ibmFtZWR2aWV3MTIiIHBhZ2Vjb2xvcj0iIzUwNTA1MCIgYm9yZGVyY29sb3I9IiNlZWVlZWUiIGJvcmRlcm9wYWNpdHk9IjEiIGlua3NjYXBlOnNob3dwYWdlc2hhZG93PSIwIiBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIgaW5rc2NhcGU6ZGVza2NvbG9yPSIjNTA1MDUwIiBzaG93Z3JpZD0iZmFsc2UiIGlua3NjYXBlOnpvb209IjAuMTUzMzAwNzUiIGlua3NjYXBlOmN4PSIxODE5Ljk1MiIgaW5rc2NhcGU6Y3k9IjE1MjYuNDExMyIgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxOTIwIiBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDA5IiBpbmtzY2FwZTp3aW5kb3cteD0iLTgiIGlua3NjYXBlOndpbmRvdy15PSItOCIgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0iZzgiIC8+IDxnIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBpZD0iZzgiPiA8cGF0aCBkPSJNIC0wLjAxMTkyODE0LDAuMjA0NzA0MDggSCAzNzguNzE5NTUgViAzNzguOTEwNDQgSCAyMjIuMDc3NTQgViAyMjIuMDE5MjYgSCAxODcuNzQyNjggViAzNzguOTEwNDQgSCAxNjkuNTAzMDMgViAyNzEuNzk0MjkgSCAxNTIuMzM4NyBWIDM3OC45MTA0NCBIIC0wLjAxMTkyODE0IFogTSAxMi44NTk3NzUsMTMyLjIxMjEgQyAzMi44ODM3OTksMTIzLjE5OTM3IDUyLjkxMDI5OCwxMTQuMTgxNjQgNzIuOTQyOTg0LDEwNS4xNjE0MyAxNTMuMzYzMjksLTcuODgzNDU5NiAyNTcuODM1NDMsMzQuOTE0OSAyOTUuMDMyNDUsODguOTMxNTIyIDIwOC40ODE5NCwzMy4wMjk2MTQgMTQ0LjQ2MzcxLDYxLjUyNDQ3MiAxMTIuNjQwODQsOTkuNzUyMjkgMzU1LjgzODMxLDc0LjMxNTI1OSA0MTYuNjM1NTIsMjIzLjYzNDE0IDI4NS4zNzU1OCwyNzguMjgzNzYgNDAxLjYwMjAzLDE4NC4xNDc4MSAyNTMuOTAwNDEsMTA3LjMyNDU4IDk5Ljc2NzksMTE0Ljg5ODEzIDYyLjc0MTY0NSwxODMuNzgyNzIgNzQuODk1NjQyLDIzNC42MzgwOCAxMDkuNDIxMDUsMjgwLjQ0NjkyIDcyLjE5NDM0MSwyNTEuNzA3ODQgNDcuNTA1MjIyLDIwOS43MDMyMiA2Ny41Nzg3NDMsMTIwLjMwODUxIDQ5LjMzNzg0OSwxMjQuMjc5NyAzMS4wOTY5NTYsMTI4LjI0NTkgMTIuODU5Nzc1LDEzMi4yMTIxIFoiIGZpbGw9IiNjYzIyMjkiIGlkPSJwYXRoMiIgc3R5bGU9InN0cm9rZS13aWR0aDoxLjI0MTc0IiAvPiA8L2c+IDwvc3ZnPg==",
19
+ bb: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMTkyLjc1NiAxOTIuNzU2IiB2ZXJzaW9uPSIxLjEiIGlkPSJzdmcxMCIgc29kaXBvZGk6ZG9jbmFtZT0iYmFuY28tZG8tYnJhc2lsLXNlbS1mdW5kby5zdmciIGlua3NjYXBlOnZlcnNpb249IjEuMi4yICg3MzJhMDFkYTYzLCAyMDIyLTEyLTA5KSIgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gPGRlZnMgaWQ9ImRlZnMxNCIgLz4gPHNvZGlwb2RpOm5hbWVkdmlldyBpZD0ibmFtZWR2aWV3MTIiIHBhZ2Vjb2xvcj0iIzUwNTA1MCIgYm9yZGVyY29sb3I9IiNlZWVlZWUiIGJvcmRlcm9wYWNpdHk9IjEiIGlua3NjYXBlOnNob3dwYWdlc2hhZG93PSIwIiBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIgaW5rc2NhcGU6ZGVza2NvbG9yPSIjNTA1MDUwIiBzaG93Z3JpZD0iZmFsc2UiIGlua3NjYXBlOnpvb209IjAuMjE2OCIgaW5rc2NhcGU6Y3g9IjEyNTAiIGlua3NjYXBlOmN5PSIxMjUwIiBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEwMDkiIGlua3NjYXBlOndpbmRvdy14PSItOCIgaW5rc2NhcGU6d2luZG93LXk9Ii04IiBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIiBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmcxMCIgLz4gPGcgZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGlkPSJnOCI+IDxwYXRoIGZpbGw9IiMzMzM0OGUiIGQ9Ik05Ni4zOTYgMTYuOTI1bC03OC44NSA1Mi44NSAzMi44MTkgMjEuMzExTDg5LjU3NyA2NC42NiA3NC4wMjMgNTQuNDQzbDIxLjMwNS0xNC4yNzQgNDYuMjMxIDMwLjY3OS03NS44NDUgNTAuOTE4IDE2LjQwNSAxMC44NjUgOTQuMzc5LTYyLjg0OC0uMjEzLS4yMTMtNzkuODg5LTUyLjY0NXpNMTc1LjAyNyAxNi45NDdsLTMyLjgwOCAyMS43MzEgMTYuNDA0IDExLjA3OCAxNi40MDQtMTEuMDc4VjE2Ljk0N3pNMTcuMDA1IDE3NS44MzJsMzIuODA5LTIxLjczLTE2LjQwNC0xMS4wNzktMTYuNDA1IDExLjA3OXYyMS43M3oiIGlkPSJwYXRoNCIgLz4gPHBhdGggZmlsbD0iIzMzMzQ4ZSIgZD0iTTk2LjM2IDE3NS43ODdsNzguODQ5LTUyLjg1MS0zMi44MTgtMjEuMzExLTM5LjIxMSAyNi40MjYgMTUuNTUyIDEwLjIxNy0yMS4zMDQgMTQuMjc1LTQ2LjIzMS0zMC42OCA3NS44NDQtNTAuOTE3LTE2LjQwNC0xMC44NjYtOTQuMzggNjIuODQ4LjIxMy4yMTUgNzkuODkgNTIuNjQ0eiIgaWQ9InBhdGg2IiAvPiA8L2c+IDwvc3ZnPg==",
20
+ caixa: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMTkyLjc1NiAxOTIuNzU2IiB2ZXJzaW9uPSIxLjEiIGlkPSJzdmc3NDI5IiBzb2RpcG9kaTpkb2NuYW1lPSJjYWl4YS1lY29ub21pY2EtZmVkZXJhbC1YLnN2ZyIgaW5rc2NhcGU6dmVyc2lvbj0iMS4yLjIgKDczMmEwMWRhNjMsIDIwMjItMTItMDkpIiB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPiA8ZGVmcyBpZD0iZGVmczc0MzMiIC8+IDxzb2RpcG9kaTpuYW1lZHZpZXcgaWQ9Im5hbWVkdmlldzc0MzEiIHBhZ2Vjb2xvcj0iIzUwNTA1MCIgYm9yZGVyY29sb3I9IiNlZWVlZWUiIGJvcmRlcm9wYWNpdHk9IjEiIGlua3NjYXBlOnNob3dwYWdlc2hhZG93PSIwIiBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIgaW5rc2NhcGU6ZGVza2NvbG9yPSIjNTA1MDUwIiBzaG93Z3JpZD0iZmFsc2UiIGlua3NjYXBlOnpvb209IjAuMTUzMzAwNzUiIGlua3NjYXBlOmN4PSIyOTMuNTQwNjQiIGlua3NjYXBlOmN5PSIxNDgwLjc0OTQiIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTAwOSIgaW5rc2NhcGU6d2luZG93LXg9Ii04IiBpbmtzY2FwZTp3aW5kb3cteT0iLTgiIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2Zzc0MjkiIC8+IDxnIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBpZD0iZzc0MjciIHRyYW5zZm9ybT0ibWF0cml4KDQuNDMzNTkwOCwwLDAsNC40MzM1OTA4LC00NDguMTk1NzEsLTMyNi4wNzg1OCkiPiA8cGF0aCBkPSJtIDExNC43MjUsOTcuNjU2IGggMTQuNjM2IGwgMTAuMDMsMTYuNjEyIGggLTE0LjYzOSB6IiBmaWxsPSIjMGQ2ZmFiIiBpZD0icGF0aDc0MjEiIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2MiIC8+IDxwYXRoIGZpbGw9IiNkYjgxMzUiIGQ9Im0gMTE2LjI0Niw5NS4xMDQgaCAxNS4xMjkgbCAxMy4xMzksLTE2LjU0IGggLTE1LjEyOSB6IG0gLTE1LjEyOSwxOS4yNjUgaCAxNS4xMjkgbCAxMy4xMzksLTE2Ljc0NCBoIC0xNS4xMjkgeiIgaWQ9InBhdGg3NDIzIiAvPiA8cGF0aCBmaWxsPSIjMGQ2ZmFiIiBkPSJtIDEwNi41NzIsNzguMzYyIGggMTQuMTIxIGwgOS42NzQsMTYuNzQyIGggLTE0LjEyMSB6IiBpZD0icGF0aDc0MjUiIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2NjY2MiIC8+IDwvZz4gPC9zdmc+",
21
+ santander: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI1MDAiIHZpZXdCb3g9IjAgMC40IDIzOC4yIDI0MC43MTkyNiIgd2lkdGg9IjI1MDAiIHZlcnNpb249IjEuMSIgaWQ9InN2ZzQiIHNvZGlwb2RpOmRvY25hbWU9ImJhbmNvLXNhbnRhbmRlci1sb2dvLnN2ZyIgaW5rc2NhcGU6dmVyc2lvbj0iMS4yLjIgKDczMmEwMWRhNjMsIDIwMjItMTItMDkpIiB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPiA8ZGVmcyBpZD0iZGVmczgiIC8+IDxzb2RpcG9kaTpuYW1lZHZpZXcgaWQ9Im5hbWVkdmlldzYiIHBhZ2Vjb2xvcj0iIzUwNTA1MCIgYm9yZGVyY29sb3I9IiNlZWVlZWUiIGJvcmRlcm9wYWNpdHk9IjEiIGlua3NjYXBlOnNob3dwYWdlc2hhZG93PSIwIiBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMCIgaW5rc2NhcGU6cGFnZWNoZWNrZXJib2FyZD0iMCIgaW5rc2NhcGU6ZGVza2NvbG9yPSIjNTA1MDUwIiBzaG93Z3JpZD0iZmFsc2UiIGlua3NjYXBlOnpvb209IjAuMTczMzgyNTgiIGlua3NjYXBlOmN4PSItNDAuMzczMTQ0IiBpbmtzY2FwZTpjeT0iMTEwMS42MTAxIiBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjE5MjAiIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEwMDkiIGlua3NjYXBlOndpbmRvdy14PSItOCIgaW5rc2NhcGU6d2luZG93LXk9Ii04IiBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIiBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJzdmc0IiAvPiA8cGF0aCBkPSJtIDE3MS43MzY1LDExMS43MjI0NyBjIC0wLjU0Nzk4LC04LjY2Mjc2IC0yLjczOTg2LC0xNy4zMjU1MTkgLTcuMTIzNjUsLTI0LjgzMzI0OCBMIDEyNy4zNTA2OSwxOC4xNjQ2NDEgYyAtMi43Mzk4NSwtNS4xOTc2NTYgLTQuOTMxNzUsLTEwLjk3MjgzMDMgLTYuMDI3NywtMTYuNzQ4MDA2IGwgLTEuNjQzOTIsMi44ODc1ODc1IGMgLTkuMzE1NTMsMTYuNzQ4MDA4NSAtOS4zMTU1MywzOC4xMTYxNTQ1IDAsNTQuODY0MTYxNSBsIDMwLjEzODUsNTQuODY0MTU2IGMgOS4zMTU1NSwxNi43NDggOS4zMTU1NSwzOC4xMTYxNiAwLDU0Ljg2NDE2IGwgLTEuNjQzOSwyLjg4NzU4IGMgLTEuMDk1OTQsLTUuNzc1MTcgLTMuMjg3ODQsLTExLjU1MDM1IC02LjAyNzcxLC0xNi43NDgwMSBMIDExNC43NDczMiwxMDQuNzkyMjYgOTcuMjEyMTg2LDcyLjQ1MTI4NSBDIDk0LjQ3MjMyMSw2Ny4yNTM2MjggOTIuMjgwNDI4LDYxLjQ3ODQ1NCA5MS4xODQ0ODMsNTUuNzAzMjc5IGwgLTEuNjQzOTE5LDIuODg3NTg4IGMgLTkuMzE1NTQxLDE2Ljc0ODAwNiAtOS4zMTU1NDEsMzcuNTM4NjM2IDAsNTQuODY0MTczIGwgMzAuMTM4NTA2LDU0Ljg2NDE0IGMgOS4zMTU1NCwxNi43NDggOS4zMTU1NCwzOC4xMTYxNSAwLDU0Ljg2NDE2IGwgLTEuNjQzOTEsMi44ODc1OSBjIC0xLjA5NTk0LC01Ljc3NTE4IC0zLjI4Nzg0LC0xMS41NTAzNiAtNi4wMjc3MSwtMTYuNzQ4MDEgTCA3NC4xOTczMTksMTQwLjU5ODMzIGMgLTQuOTMxNzU0LC05LjI0MDI3IC03LjEyMzY0NywtMTkuNjM1NTcgLTcuMTIzNjQ3LC0zMC4wMzA4OSAtNDAuMDAyMDMsMTAuOTcyODMgLTY3Ljk0ODY1MDk1LDM0LjY1MTAzIC02Ny45NDg2NTA5NSw2MS43OTQzNSAwLDM4LjExNjE2IDUzLjcwMTM1MTk1LDY4LjcyNDU5IDEyMC4wMDYwNzg5NSw2OC43MjQ1OSA2Ni4zMDQ3MywwIDEyMC4wMDYwOSwtMzAuNjA4NDMgMTIwLjAwNjA5LC02OC43MjQ1OSAwLjU0Nzk2LC0yNS45ODgyOCAtMjYuODUwNjgsLTQ5LjY2NjQ5IC02Ny40MDA2OSwtNjAuNjM5MzIgeiIgZmlsbD0iI2VhMWQyNSIgaWQ9InBhdGgyIiBzb2RpcG9kaTpub2RldHlwZXM9ImNjY2NjY2NjY2NjY2NjY2NjY2NjY3NzY2MiIHN0eWxlPSJzdHJva2Utd2lkdGg6NS42MjU1MSIgLz4gPC9zdmc+",
22
+ };
23
+
24
+ export function BankLogo({ bank }: { bank: PixBankId }): JSX.Element {
25
+ return (
26
+ <Box
27
+ component="img"
28
+ src={LOGOS[bank]}
29
+ alt=""
30
+ aria-hidden="true"
31
+ data-bank={bank}
32
+ sx={{ width: 32, height: 32, flex: "none", objectFit: "contain", borderRadius: 1 }}
33
+ />
34
+ );
35
+ }
@@ -0,0 +1,26 @@
1
+ import type { PixBankCopy } from "./types";
2
+
3
+ /** The Pix-to-bank flow's words, pt-BR (FUT-3612). */
4
+ export const PT_BR_PIX_BANK_COPY: PixBankCopy = {
5
+ names: {
6
+ nubank: { name: "Nubank", the: "o " },
7
+ itau: { name: "Itaú", the: "o " },
8
+ bradesco: { name: "Bradesco", the: "o " },
9
+ bb: { name: "Banco do Brasil", the: "o " },
10
+ caixa: { name: "Caixa", the: "a " },
11
+ santander: { name: "Santander", the: "o " },
12
+ },
13
+ generateAndCopy: "Gerar e copiar o Pix",
14
+ generateCopyAndOpen: (bank) => `Gerar, copiar e abrir ${bank.the}${bank.name}`,
15
+ copyAndOpen: (bank) => `Copiar e abrir ${bank.the}${bank.name}`,
16
+ openOther: "Abrir outro banco",
17
+ listTitle: "Abra seu banco e cole em Pix copia e cola",
18
+ open: (bank) => `Abrir ${bank.name}`,
19
+ opening: (bank) => `Abrindo ${bank.the}**${bank.name}**…`,
20
+ other: "Outro banco",
21
+ notOpened: (bank) => `Não conseguimos abrir ${bank.the}${bank.name}. O app está instalado neste celular?`,
22
+ retry: "Tentar de novo",
23
+ copied: "Código Pix copiado",
24
+ copiedPick: "Código Pix copiado. Escolha o banco.",
25
+ copiedOpening: (bank) => `Código Pix copiado. Abrindo ${bank.the}${bank.name}…`,
26
+ };
@@ -84,9 +84,11 @@ export function lastUsedCard(saved: readonly SavedCard[]): SavedCard | undefined
84
84
  /**
85
85
  * What is chosen before the buyer touches anything (M7, owner 2026-10-15): a
86
86
  * saved card makes "Cartão de crédito" the choice with that card selected —
87
- * the one used last ({@link lastUsedCard}). Nothing else is ever
88
- * preselected: never Pix, never a door tender (FUT-1478), and not a store's
89
- * only method either (that pick used to raise an order with no tap at all).
87
+ * the one used last ({@link lastUsedCard}). The buyer's LAST online method
88
+ * (`lastMethod`, FUT-3611) wins when the store offers it: Pix as Pix, since
89
+ * choosing creates nothing any more. Never a door tender (FUT-1478), and not
90
+ * a store's only method by itself either (that pick used to raise an order
91
+ * with no tap at all).
90
92
  *
91
93
  * A buyer back here because a card was REFUSED (`fresh`) keeps the card
92
94
  * chosen, with no saved card selected for them — the one this would pick is
@@ -96,7 +98,10 @@ export function preselection(
96
98
  rows: readonly OnlineRow[],
97
99
  saved: readonly SavedCard[],
98
100
  fresh: boolean,
101
+ lastMethod: "PIX" | "CARD" | null = null,
99
102
  ): { choice: CheckoutListChoice; selection: string | null } | null {
103
+ const pixOffered = rows.some((row) => row.method === "PIX" && !row.disabled);
104
+ if (lastMethod === "PIX" && pixOffered && !fresh) return { choice: { kind: "online", method: "PIX" }, selection: null };
100
105
  const card = rows.find((row) => row.method === "CARD" && !row.disabled);
101
106
  if (!card) return null;
102
107
  if (fresh) return { choice: { kind: "online", method: "CARD" }, selection: saved.length > 0 ? null : NEW_CARD };
@@ -1,6 +1,19 @@
1
- import { createContext, useContext, useMemo, useState, type JSX, type ReactNode } from "react";
1
+ import { createContext, useContext, useMemo, useRef, useState, type JSX, type MutableRefObject, type ReactNode } from "react";
2
2
 
3
- import type { CheckoutPaymentList } from "./types";
3
+ import type { DeferredCopy } from "../pix-parts";
4
+
5
+ import type { CheckoutPaymentList, PixBankId } from "./types";
6
+
7
+ /**
8
+ * What the bar's Pix press asked of the code it is raising (FUT-3612): copy it
9
+ * the moment it arrives (the clipboard was opened inside the tap), then open
10
+ * `bank` — or show the six banks when `pick`. Consumed once by the Pix face.
11
+ */
12
+ export interface PixIntent {
13
+ copy: DeferredCopy;
14
+ bank: PixBankId | null;
15
+ pick: boolean;
16
+ }
4
17
 
5
18
  /**
6
19
  * What the one list shares with the panes it mounts (FUT-3591/3592): its
@@ -23,6 +36,15 @@ export interface PaymentListMode {
23
36
  * default — because the list skips that step.
24
37
  */
25
38
  consent: ListConsent;
39
+ /** The Pix press's intent, waiting for its code — see {@link PixIntent}. */
40
+ pixIntent: MutableRefObject<PixIntent | null>;
41
+ /**
42
+ * The armed charge (`armedCharge`) for this step: when it was armed and
43
+ * whether it was spent. Held HERE, above the faces, so a list face that
44
+ * remounts (after a Pix, "Prefere pagar com cartão?") can never fire it
45
+ * again — the 1-tap tap consented to ONE charge, right away.
46
+ */
47
+ armed: MutableRefObject<{ since: number; spent: boolean }>;
26
48
  }
27
49
 
28
50
  export interface ListConsent {
@@ -45,7 +67,9 @@ export function PaymentListProvider({
45
67
  children: ReactNode;
46
68
  }): JSX.Element {
47
69
  const [qrOpen, setQrOpen] = useState(false);
48
- const value = useMemo(() => ({ list, qrOpen, setQrOpen, onLeavePix, consent }), [list, qrOpen, onLeavePix, consent]);
70
+ const pixIntent = useRef<PixIntent | null>(null);
71
+ const armed = useRef({ since: Date.now(), spent: false });
72
+ const value = useMemo(() => ({ list, qrOpen, setQrOpen, onLeavePix, consent, pixIntent, armed }), [list, qrOpen, onLeavePix, consent]);
49
73
  return <PaymentListContext.Provider value={value}>{children}</PaymentListContext.Provider>;
50
74
  }
51
75
 
@@ -10,7 +10,11 @@ import type { BuyerContact, CheckoutProviderConfig } from "../types";
10
10
  import type { useCheckoutController } from "../use-checkout-controller";
11
11
  import { useCheckoutComponents } from "../ui";
12
12
 
13
+
13
14
  import { actionLabel, blockedReason, cardName } from "./choice";
15
+ import { usePaymentListMode, type PaymentListMode } from "./context";
16
+ import { armPixCopy, armedReady, otherBankShown, pixBankLabel, useArmedCharge, useDropRefusedCopy } from "./list-memory";
17
+ import { OtherBankLink } from "./list-pix";
14
18
  import { ChoiceList } from "./choice-list";
15
19
  import { CardRun, WalletRun, type ListChargeFailure } from "./charge-runners";
16
20
  import { ActionButton, ChargeBar, GooglePayAction, ListBar } from "./list-bar";
@@ -75,9 +79,10 @@ function onlineTotal(props: ListFaceProps): string {
75
79
  }
76
80
 
77
81
  /** An online choice's press: the card goes through the bar, Pix and the hand-off raise at once. */
78
- function actOnline(props: ListFaceProps, method: CheckoutOnlineChoice): void {
82
+ function actOnline(props: ListFaceProps, method: CheckoutOnlineChoice, mode: PaymentListMode | null, pick = false): void {
79
83
  const { c, charge, cap } = props;
80
84
  saveAsked(props);
85
+ if (method === "PIX" && mode) armPixCopy(props.list, mode, pick);
81
86
  if (method === "CARD") return charge.start("CARD", onlineTotal(props));
82
87
  const raised = method === "PIX" ? "PIX" : handOffMethod(cap.offered);
83
88
  if (method === "HAND_OFF") props.onHandOff();
@@ -90,13 +95,14 @@ function actOnline(props: ListFaceProps, method: CheckoutOnlineChoice): void {
90
95
  * or a charge. A door option hands the press to the host; the card counts five
91
96
  * seconds first; everything else raises at once.
92
97
  */
93
- function useAct(props: ListFaceProps, clearFailure: () => void): () => void {
94
- return () => {
98
+ function useAct(props: ListFaceProps, clearFailure: () => void): (pick?: boolean) => void {
99
+ const mode = usePaymentListMode();
100
+ return (pick = false) => {
95
101
  const { choice } = props.state;
96
102
  if (!choice || inFlight(props)) return;
97
103
  if (choice.kind === "door") return chosenDoor(props)?.onSend();
98
104
  clearFailure();
99
- actOnline(props, choice.method);
105
+ actOnline(props, choice.method, mode, pick);
100
106
  };
101
107
  }
102
108
 
@@ -176,7 +182,7 @@ function buttonFacts(props: ListFaceProps): { label: string; total: string; busy
176
182
  const door = chosenDoor(props);
177
183
  const online = state.choice?.kind === "online";
178
184
  return {
179
- label: list.hold ? list.hold.action : actionLabel(list.copy, state.choice, door, props.provider),
185
+ label: list.hold ? list.hold.action : pixBankLabel(list, state) ?? actionLabel(list.copy, state.choice, door, props.provider),
180
186
  total: door ? (door.totalLabel ?? props.total) : online ? onlineTotal(props) : props.total,
181
187
  busy: inFlight(props),
182
188
  };
@@ -198,27 +204,58 @@ function googlePayLive(props: ListFaceProps, reason: string | null, busy: boolea
198
204
  return choice?.kind === "online" && choice.method === "GOOGLE_PAY" && props.cap.googlePay !== null && reason === null && !busy;
199
205
  }
200
206
 
207
+ /** The 5-second bar (or the charge going out), and the host's footer while it counts. */
208
+ function ChargingBar({ props, onCancel }: { props: ListFaceProps; onCancel: () => void }): JSX.Element {
209
+ const { charge, list } = props;
210
+ const counting = charge.phase === "counting";
211
+ const footer = counting ? list.chargeFooter?.(charge.cancel) : null;
212
+ return (
213
+ <>
214
+ <ChargeBar text={chargingText(props)} full={!counting} cancelLabel={list.copy.cancel} onCancel={onCancel} />
215
+ {footer ? <Box data-testid="list-charge-footer">{footer}</Box> : null}
216
+ </>
217
+ );
218
+ }
219
+
220
+ /** The button, and "Abrir outro banco" under it when a remembered bank would open. */
221
+ function PressButton({ props, act, reason, returnFocus }: {
222
+ props: ListFaceProps;
223
+ act: (pick?: boolean) => void;
224
+ reason: string | null;
225
+ returnFocus: boolean;
226
+ }): JSX.Element {
227
+ const { list, state } = props;
228
+ const { label, total, busy } = buttonFacts(props);
229
+ const other = list.pixBank && otherBankShown(list, state, reason === null && !busy) ? list.pixBank.copy.openOther : null;
230
+ return (
231
+ <>
232
+ <ActionButton label={label} total={total} disabled={reason !== null || busy} loading={busy} onClick={() => act()} focusOnMount={returnFocus} />
233
+ {other ? <OtherBankLink label={other} onClick={() => act(true)} /> : null}
234
+ </>
235
+ );
236
+ }
237
+
201
238
  /** The button, the 5-second bar, Google's own button, or nothing for a charge nobody can confirm yet. */
202
239
  function BarAction({ props, failure, act, onGooglePay, onCancel, reason, returnFocus }: {
203
240
  props: ListFaceProps;
204
241
  failure: ListChargeFailure | null;
205
- act: () => void;
242
+ act: (pick?: boolean) => void;
206
243
  onGooglePay: () => void;
207
244
  onCancel: () => void;
208
245
  reason: string | null;
209
246
  returnFocus: boolean;
210
247
  }): JSX.Element | null {
211
- const { charge, list, cap } = props;
212
- if (cardCharging(charge)) {
213
- return <ChargeBar text={chargingText(props)} full={charge.phase !== "counting"} cancelLabel={list.copy.cancel} onCancel={onCancel} />;
214
- }
248
+ const { charge, cap } = props;
249
+ if (cardCharging(charge)) return <ChargingBar props={props} onCancel={onCancel} />;
215
250
  // An UNRESOLVED charge may be holding the buyer's money: no pay control at
216
251
  // all under "não pague de novo" (FUT-563), as the card pane does today.
217
252
  if (failure?.code === UNRESOLVED_CODE) return null;
218
- const { label, total, busy } = buttonFacts(props);
219
- if (googlePayUnpriced(props)) return <ActionButton label={label} total={total} disabled loading={false} onClick={act} />;
220
- if (googlePayLive(props, reason, busy) && cap.googlePay) return <GooglePayAction client={cap.googlePay.client} onPress={onGooglePay} />;
221
- return <ActionButton label={label} total={total} disabled={reason !== null || busy} loading={busy} onClick={act} focusOnMount={returnFocus} />;
253
+ if (googlePayUnpriced(props)) {
254
+ const { label, total } = buttonFacts(props);
255
+ return <ActionButton label={label} total={total} disabled loading={false} onClick={() => act()} />;
256
+ }
257
+ if (googlePayLive(props, reason, inFlight(props)) && cap.googlePay) return <GooglePayAction client={cap.googlePay.client} onPress={onGooglePay} />;
258
+ return <PressButton props={props} act={act} reason={reason} returnFocus={returnFocus} />;
222
259
  }
223
260
 
224
261
  /** The charge under way, once there is an order to run it against. */
@@ -318,6 +355,9 @@ export function ListFace(props: ListFaceProps): JSX.Element {
318
355
  const door = chosenDoor(props);
319
356
  const reason = reasonOf(props, door);
320
357
  useChargeGuard(props, reason);
358
+ const free = reason === null && props.charge.phase === "idle" && !c.creating;
359
+ useArmedCharge(armedReady(list, state, free), act);
360
+ useDropRefusedCopy(c.createError);
321
361
  const { returnFocus, onCancel } = useCancelWithFocus(charge);
322
362
  const shown = charge.phase !== "waiting";
323
363
  const facts = barFacts(props, door, reason);
@@ -327,7 +367,7 @@ export function ListFace(props: ListFaceProps): JSX.Element {
327
367
  <ChoiceList copy={list.copy} state={state} door={list.door ?? []} buyer={c.buyer} onBuyerChange={c.setBuyer} provider={props.provider} failure={failure} locked={facts.locked} covered={coveredByHost(list)} />
328
368
  ) : null}
329
369
  <Runners props={props} sheet={sheet.current} onFailed={onFailed} />
330
- <RaiseError c={c} onRetry={act} />
370
+ <RaiseError c={c} onRetry={() => act()} />
331
371
  {shown ? (
332
372
  <ListBar summary={list.summary} extras={facts.extras} payer={facts.charging ? null : <PayerLine props={props} />} reason={facts.underButton} live={facts.live}>
333
373
  <BarAction props={props} failure={failure} act={act} onGooglePay={onGooglePay} onCancel={onCancel} reason={reason} returnFocus={returnFocus} />
@@ -0,0 +1,96 @@
1
+ import { useEffect } from "react";
2
+
3
+ import { NEW_CARD } from "../../../card";
4
+ import { deferredCopy } from "../pix-parts";
5
+
6
+ import { bankFlowOs } from "./bank-apps";
7
+ import { usePaymentListMode, type PaymentListMode } from "./context";
8
+ import type { ListState } from "./use-list-state";
9
+ import type { CheckoutPaymentList, PixBankId } from "./types";
10
+
11
+ /**
12
+ * What the list does with the host's memory of the buyer (FUT-3611, FUT-3612):
13
+ * the bank a Pix opens in, and the charge "Pedir em 1 toque" armed.
14
+ */
15
+
16
+ /** How long after the step opens an armed charge may still start by itself. */
17
+ export const ARMED_WINDOW_MS = 15_000;
18
+
19
+ /** The bank flow's remembered bank, on a phone — what the Pix button opens. */
20
+ function rememberedBank(list: CheckoutPaymentList): PixBankId | null {
21
+ const bank = list.pixBank;
22
+ if (!bank?.remembered || !bankFlowOs(bank.phone)) return null;
23
+ return bank.remembered;
24
+ }
25
+
26
+ /**
27
+ * Pix with the host's bank flow (FUT-3612): the clipboard is opened HERE,
28
+ * inside the tap, for a code the server has not answered yet — the Pix face
29
+ * writes it the moment it arrives, then opens the remembered bank (or, for
30
+ * "Abrir outro banco", shows the six).
31
+ */
32
+ export function armPixCopy(list: CheckoutPaymentList, mode: PaymentListMode, pick: boolean): void {
33
+ if (!list.pixBank) return;
34
+ mode.pixIntent.current?.copy.fail();
35
+ const os = bankFlowOs(list.pixBank.phone);
36
+ mode.pixIntent.current = { copy: deferredCopy(), bank: os && !pick ? list.pixBank.remembered : null, pick };
37
+ }
38
+
39
+ function pixChosen(state: ListState): boolean {
40
+ return state.choice?.kind === "online" && state.choice.method === "PIX";
41
+ }
42
+
43
+ /** Pix with the bank flow: "Gerar e copiar o Pix", or "Gerar, copiar e abrir o Nubank". */
44
+ export function pixBankLabel(list: CheckoutPaymentList, state: ListState): string | null {
45
+ const bank = list.pixBank;
46
+ if (!bank || !pixChosen(state)) return null;
47
+ const remembered = rememberedBank(list);
48
+ return remembered ? bank.copy.generateCopyAndOpen(bank.copy.names[remembered]) : bank.copy.generateAndCopy;
49
+ }
50
+
51
+ /** "Abrir outro banco" under the button: Pix chosen, a bank remembered, a phone, and the button free to act. */
52
+ export function otherBankShown(list: CheckoutPaymentList, state: ListState, free: boolean): boolean {
53
+ return free && pixChosen(state) && rememberedBank(list) !== null;
54
+ }
55
+
56
+ /** A saved card — not the fields — is what the card choice will charge. */
57
+ function savedCardChosen(state: ListState): boolean {
58
+ const { choice, card } = state;
59
+ if (choice?.kind !== "online" || choice.method !== "CARD") return false;
60
+ return card.selection !== null && card.selection !== NEW_CARD;
61
+ }
62
+
63
+ /**
64
+ * The armed charge may start now: the host armed it, the saved card the
65
+ * consent named is the one chosen, and nothing (`free`) holds the button.
66
+ */
67
+ export function armedReady(list: CheckoutPaymentList, state: ListState, free: boolean): boolean {
68
+ if (!list.armedCharge || !free || !savedCardChosen(state)) return false;
69
+ return list.armedCardId === undefined || state.card.selection === list.armedCardId;
70
+ }
71
+
72
+ /**
73
+ * "Pedir em 1 toque" opened this checkout (FUT-3611): start the card's bar by
74
+ * itself, ONCE per step — as soon as {@link armedReady} holds, within
75
+ * {@link ARMED_WINDOW_MS}. Anything after Cancelar never charges by itself.
76
+ */
77
+ export function useArmedCharge(ready: boolean, act: () => void): void {
78
+ const mode = usePaymentListMode();
79
+ useEffect(() => {
80
+ const armed = mode?.armed.current;
81
+ if (!ready || !armed || armed.spent) return;
82
+ // Spent either way: fired now, or too late to be the tap's own charge.
83
+ armed.spent = true;
84
+ if (Date.now() - armed.since <= ARMED_WINDOW_MS) act();
85
+ }, [ready, act, mode]);
86
+ }
87
+
88
+ /** A Pix press whose code never came (the raise was refused) gives the clipboard up. */
89
+ export function useDropRefusedCopy(createError: unknown): void {
90
+ const mode = usePaymentListMode();
91
+ useEffect(() => {
92
+ if (!createError || !mode?.pixIntent.current) return;
93
+ mode.pixIntent.current.copy.fail();
94
+ mode.pixIntent.current = null;
95
+ }, [createError, mode]);
96
+ }
@@ -1,5 +1,5 @@
1
1
  import { Box, useTheme } from "@mui/material";
2
- import type { JSX, KeyboardEvent, MouseEvent, ReactNode } from "react";
2
+ import { useEffect, useRef, useState, type JSX, type KeyboardEvent, type MouseEvent, type ReactNode } from "react";
3
3
 
4
4
  import { usePaymentRefresh, usePaymentStale } from "../cart-changed";
5
5
  import { useCheckoutCopy } from "../copy-context";
@@ -9,8 +9,11 @@ import type { PixAfterCopyCopy } from "../screens-copy";
9
9
  import type { CheckoutOrder, PixCharge } from "../types";
10
10
  import { useCheckoutComponents } from "../ui";
11
11
 
12
+ import { bankFlowOs, type PhoneOs } from "./bank-apps";
12
13
  import type { PaymentListMode } from "./context";
13
14
  import { ListBar, ActionButton, WaitingLine } from "./list-bar";
15
+ import { PixBanks, usePixBankState, type PixBankState } from "./pix-banks";
16
+ import type { CheckoutPixBank, PixBankId } from "./types";
14
17
 
15
18
  /**
16
19
  * The Pix, in the one list (FUT-3592 #3): "Gerar Pix" already created the
@@ -146,6 +149,42 @@ interface ListPixFaceProps {
146
149
  onPreferCard?: () => void;
147
150
  /** The poll's stalled panel, when it has one. */
148
151
  footer: ReactNode;
152
+ /** The clipboard alone, saying whether it took the code — the bank flow's copy. */
153
+ writeClipboard: () => Promise<boolean>;
154
+ setAfterCopy: (afterCopy: boolean) => void;
155
+ }
156
+
157
+ /** A phone with a bank remembered: the copy opens it too, and keeps the way to another (FUT-3612). */
158
+ function rememberedOnPhone(flow: BankFlow | null): PixBankId | null {
159
+ return flow?.os ? flow.state.chosen : null;
160
+ }
161
+
162
+ /** "Copiar e abrir no Nubank" with a remembered bank on a phone, else the plain copy. */
163
+ function copyLabel(plain: string, flow: BankFlow | null): string {
164
+ const remembered = rememberedOnPhone(flow);
165
+ return remembered && flow ? flow.bank.copy.copyAndOpen(flow.bank.copy.names[remembered]) : plain;
166
+ }
167
+
168
+ /** "Abrir outro banco", when a remembered bank on a phone is what the copy opens. */
169
+ function otherBankLabel(flow: BankFlow | null): string | null {
170
+ return flow && rememberedOnPhone(flow) ? flow.bank.copy.openOther : null;
171
+ }
172
+
173
+ /** The bar's one control: held, "Atualizar pagamento", the wait after the copy, or the copy. */
174
+ function PixBarAction({ mode, afterCopy, onCopy, copyLabel }: {
175
+ mode: PaymentListMode;
176
+ afterCopy: boolean;
177
+ onCopy: () => void;
178
+ copyLabel: string;
179
+ }): JSX.Element {
180
+ const stale = usePaymentStale();
181
+ const refresh = usePaymentRefresh();
182
+ const refreshLabel = useCheckoutCopy().screens.cartChanged.refreshAction;
183
+ const { hold } = mode.list;
184
+ if (hold) return <ActionButton label={hold.action} total={null} disabled onClick={() => undefined} />;
185
+ if (stale && refresh) return <ActionButton label={refreshLabel} total={null} disabled={false} onClick={refresh} />;
186
+ if (afterCopy) return <WaitingLine text={mode.list.copy.pixFace.waiting} />;
187
+ return <ActionButton label={copyLabel} total={null} disabled={stale} onClick={onCopy} />;
149
188
  }
150
189
 
151
190
  /**
@@ -153,30 +192,126 @@ interface ListPixFaceProps {
153
192
  * code the cart has outgrown — "Atualizar pagamento" (FUT-3535, E20), then
154
193
  * the copy before it and the wait after it.
155
194
  */
156
- function ListPixBar({ mode, afterCopy, onCopy }: { mode: PaymentListMode; afterCopy: boolean; onCopy: () => void }): JSX.Element {
195
+ function ListPixBar({ mode, afterCopy, onCopy, bankFlow, onOtherBank }: {
196
+ mode: PaymentListMode;
197
+ afterCopy: boolean;
198
+ onCopy: () => void;
199
+ bankFlow: BankFlow | null;
200
+ onOtherBank: () => void;
201
+ }): JSX.Element {
157
202
  const copy = mode.list.copy.pixFace;
158
203
  const stale = usePaymentStale();
159
- const refresh = usePaymentRefresh();
160
- const refreshLabel = useCheckoutCopy().screens.cartChanged.refreshAction;
161
204
  const { hold } = mode.list;
162
- const action = (): JSX.Element => {
163
- if (hold) return <ActionButton label={hold.action} total={null} disabled onClick={() => undefined} />;
164
- if (stale && refresh) return <ActionButton label={refreshLabel} total={null} disabled={false} onClick={refresh} />;
165
- if (afterCopy) return <WaitingLine text={copy.waiting} />;
166
- return <ActionButton label={copy.copyCode} total={null} disabled={stale} onClick={onCopy} />;
167
- };
205
+ const settled = !hold && !stale;
206
+ const other = settled && !afterCopy ? otherBankLabel(bankFlow) : null;
168
207
  return (
169
- <ListBar summary={mode.list.summary} reason={hold?.reason ?? null} live={afterCopy && !hold && !stale ? copy.waiting : null}>
170
- {action()}
208
+ <ListBar summary={mode.list.summary} reason={hold?.reason ?? null} live={afterCopy && settled ? copy.waiting : null}>
209
+ <PixBarAction mode={mode} afterCopy={afterCopy} onCopy={onCopy} copyLabel={copyLabel(copy.copyCode, bankFlow)} />
210
+ {other ? <OtherBankLink label={other} onClick={onOtherBank} /> : null}
171
211
  </ListBar>
172
212
  );
173
213
  }
174
214
 
215
+ /** "Abrir outro banco" — a centred text link under the bar's button (FUT-3612). */
216
+ export function OtherBankLink({ label, onClick }: { label: string; onClick: () => void }): JSX.Element {
217
+ const { Button } = useCheckoutComponents();
218
+ return (
219
+ <Box sx={{ display: "flex", justifyContent: "center", "& button": { textDecoration: "underline", minHeight: (t) => t.spacing(5.5) } }}>
220
+ <Button variant="text" color="primary" size="sm" onClick={onClick} dataTestId="pix-open-other-bank">
221
+ {label}
222
+ </Button>
223
+ </Box>
224
+ );
225
+ }
226
+
227
+ /** The Pix-to-bank flow, when the host offers it (FUT-3612). */
228
+ interface BankFlow {
229
+ bank: CheckoutPixBank;
230
+ /** `null` on a computer: copy-and-say, no banks. */
231
+ os: PhoneOs | null;
232
+ state: PixBankState;
233
+ }
234
+
235
+ /** The toast after a copy: which bank opens, or that one is to be chosen. */
236
+ function copiedSentence(flow: BankFlow, opening: PixBankId | null): string {
237
+ const { copy } = flow.bank;
238
+ if (!flow.os) return copy.copied;
239
+ return opening ? copy.copiedOpening(copy.names[opening]) : copy.copiedPick;
240
+ }
241
+
242
+ /** A copy the clipboard took: the after-copy face, the grid when asked for, the toast, and the bank opened. */
243
+ function landCopy(at: { mode: PaymentListMode; flow: BankFlow; ports: CopyPorts; stale: boolean }, opening: PixBankId | null, grid: boolean): void {
244
+ const { mode, flow, ports } = at;
245
+ ports.setAfterCopy(true);
246
+ if (grid) flow.state.showGrid();
247
+ const target = flow.os ? opening : null;
248
+ mode.list.notify?.(copiedSentence(flow, target));
249
+ if (target && !at.stale) flow.state.open(target, false);
250
+ }
251
+
252
+ /**
253
+ * Every copy on the bank flow ends the same way: the after-copy face, the
254
+ * toast, and the bank opened — the one chosen, or none when the grid is asked
255
+ * for. A copy the clipboard refused does none of it (the code stays to copy by
256
+ * hand).
257
+ */
258
+ function useBankCopies(mode: PaymentListMode, flow: BankFlow | null, pix: PixCharge, ports: CopyPorts) {
259
+ const stale = usePaymentStale();
260
+ const live = useRef(true);
261
+ useEffect(() => {
262
+ live.current = true;
263
+ return () => {
264
+ live.current = false;
265
+ };
266
+ }, []);
267
+ const land = (ok: boolean, grid: boolean, target: PixBankId | null): void => {
268
+ if (ok && live.current && flow) landCopy({ mode, flow, ports, stale }, grid ? null : target, grid);
269
+ };
270
+ // The bar's press already opened the clipboard, waiting for this code.
271
+ useEffect(() => {
272
+ const intent = mode.pixIntent.current;
273
+ if (!intent) return;
274
+ mode.pixIntent.current = null;
275
+ intent.copy.resolve(pix.copyPaste);
276
+ void intent.copy.done.then((ok) => land(ok, intent.pick, intent.pick ? null : intent.bank));
277
+ // Once, as the code arrives.
278
+ }, []);
279
+ const copyThen = (grid: boolean) => () =>
280
+ void ports.writeClipboard().then((ok) => land(ok, grid, flow?.state.chosen ?? null));
281
+ return { copy: copyThen(false), other: copyThen(true) };
282
+ }
283
+
284
+ interface CopyPorts {
285
+ writeClipboard: () => Promise<boolean>;
286
+ setAfterCopy: (afterCopy: boolean) => void;
287
+ }
288
+
289
+ /** The bank flow's state, or `null` when the host does not offer it. */
290
+ function useBankFlow(mode: PaymentListMode): BankFlow | null {
291
+ const bank = mode.list.pixBank;
292
+ const [os] = useState(() => bankFlowOs(bank?.phone));
293
+ const stale = usePaymentStale();
294
+ const startOnGrid = mode.pixIntent.current?.pick ?? false;
295
+ const state = usePixBankState(bank?.remembered ?? null, bank?.onPick ?? noPick, os, stale, startOnGrid);
296
+ return bank ? { bank, os, state } : null;
297
+ }
298
+
299
+ function noPick(): void {
300
+ // No bank flow: nothing to remember.
301
+ }
302
+
175
303
  /** Both faces of the list's Pix, and the bar under them. */
176
304
  export function ListPixFace(props: ListPixFaceProps & { mode: PaymentListMode }): JSX.Element {
177
305
  const { Text } = useCheckoutComponents();
178
306
  const { mode, order, pix, afterCopy } = props;
179
307
  const copy = mode.list.copy.pixFace;
308
+ const flow = useBankFlow(mode);
309
+ const copies = useBankCopies(mode, flow, pix, props);
310
+ const stale = usePaymentStale();
311
+ // On a phone the bank flow takes the copy: "Copiar e abrir o Nubank", the
312
+ // toast, the app. Without it — or on a computer — the copy is today's.
313
+ const onCopy = flow ? copies.copy : props.onCopy;
314
+ const banks = flow?.os ? { node: <PixBanks bank={flow.bank} state={flow.state} stale={stale} />, replacesChecklist: flow.state.chosen === null } : undefined;
180
315
  return (
181
316
  <Box data-testid="list-pix" sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
182
317
  <Text variant="body" size="md" weight="bold" as="p" style={{ margin: 0 }} data-testid="list-pix-title">
@@ -189,18 +324,19 @@ export function ListPixFace(props: ListPixFaceProps & { mode: PaymentListMode })
189
324
  totalLabel={mode.list.amountDueLabel ?? order.totalLabel}
190
325
  deadline={props.time}
191
326
  justCopied={props.justCopied}
192
- onCopyAgain={props.onCopyAgain}
327
+ onCopyAgain={flow ? copies.copy : props.onCopyAgain}
193
328
  onShowQr={() => {
194
329
  mode.setQrOpen(true);
195
330
  props.onShowQr();
196
331
  }}
197
332
  onPreferCard={props.onPreferCard}
198
333
  footer={props.footer}
334
+ banks={banks}
199
335
  />
200
336
  ) : (
201
- <BeforeCopy mode={mode} pix={pix} time={props.time} onCopy={props.onCopy} onChangeMethod={mode.onLeavePix} footer={props.footer} />
337
+ <BeforeCopy mode={mode} pix={pix} time={props.time} onCopy={onCopy} onChangeMethod={mode.onLeavePix} footer={props.footer} />
202
338
  )}
203
- <ListPixBar mode={mode} afterCopy={afterCopy} onCopy={props.onCopy} />
339
+ <ListPixBar mode={mode} afterCopy={afterCopy} onCopy={onCopy} bankFlow={flow} onOtherBank={copies.other} />
204
340
  </Box>
205
341
  );
206
342
  }
@@ -130,6 +130,7 @@ export function PaymentListStep(props: PaymentListStepProps): JSX.Element {
130
130
  fresh: c.freshInstrument,
131
131
  arrivedWith: props.defaultBuyer,
132
132
  taxIdOnFile: props.taxIdOnFile,
133
+ lastMethod: list.lastMethod,
133
134
  onChoiceChange: list.onChoiceChange,
134
135
  });
135
136
  const charge = useListCharge({ ...c, cartTotal: displayTotals(props.totalOverride, props.cart).label, dueCents: list.totalCents });
@@ -0,0 +1,178 @@
1
+ import { Box } from "@mui/material";
2
+ import { useCallback, useEffect, useRef, useState, type JSX } from "react";
3
+
4
+ import { Emphasized } from "../pix-parts";
5
+ import { useCheckoutComponents } from "../ui";
6
+
7
+ import { BankLogo } from "./bank-logos";
8
+ import { openBankApp, PIX_BANKS, type PhoneOs } from "./bank-apps";
9
+ import type { CheckoutPixBank, PixBankId } from "./types";
10
+
11
+ /**
12
+ * The after-copy face's way to the bank app (FUT-3612), on a phone: with no
13
+ * bank chosen, the six as a 2×3 grid of large buttons under "Abra seu banco e
14
+ * cole em Pix copia e cola"; with one, "Abrindo o **Nubank**…" and "Outro
15
+ * banco" in one row — or, when the app did not open, the sentence that says so
16
+ * with "Tentar de novo".
17
+ */
18
+
19
+ export type BankStatus = "opening" | "failed";
20
+
21
+ export interface PixBankState {
22
+ /** The bank this face opens, or `null` while the grid is shown. */
23
+ chosen: PixBankId | null;
24
+ status: BankStatus | null;
25
+ /** Open `bank` (a pick from the grid remembers it). */
26
+ open: (bank: PixBankId, remember: boolean) => void;
27
+ /** "Outro banco": the grid again. The stored bank is replaced only by the next pick. */
28
+ showGrid: () => void;
29
+ }
30
+
31
+ /**
32
+ * The bank face's state. It starts on the remembered bank — unless the press
33
+ * asked for the grid ("Abrir outro banco") — and never opens anything while
34
+ * the code is veiled (FUT-3535: an app opened then would paste the old amount).
35
+ */
36
+ export function usePixBankState(
37
+ remembered: PixBankId | null,
38
+ onPick: (bank: PixBankId) => void,
39
+ os: PhoneOs | null,
40
+ stale: boolean,
41
+ startOnGrid: boolean,
42
+ ): PixBankState {
43
+ const [chosen, setChosen] = useState<PixBankId | null>(startOnGrid ? null : remembered);
44
+ const [status, setStatus] = useState<BankStatus | null>(null);
45
+ const live = useRef(true);
46
+ useEffect(() => {
47
+ // Set in the body too: a remount (StrictMode's, or a parent's) runs the
48
+ // cleanup first, and a flag left false would swallow every "não abriu".
49
+ live.current = true;
50
+ return () => {
51
+ live.current = false;
52
+ };
53
+ }, []);
54
+ const open = useCallback(
55
+ (next: PixBankId, remember: boolean) => {
56
+ if (stale || !os) return;
57
+ setChosen(next);
58
+ setStatus("opening");
59
+ if (remember) onPick(next);
60
+ void openBankApp(next, os).then((opened) => {
61
+ if (live.current && !opened) setStatus("failed");
62
+ });
63
+ },
64
+ [os, onPick, stale],
65
+ );
66
+ const showGrid = useCallback(() => {
67
+ setChosen(null);
68
+ setStatus(null);
69
+ }, []);
70
+ return { chosen, status, open, showGrid };
71
+ }
72
+
73
+ function OtherLink({ label, onClick, testId }: { label: string; onClick: () => void; testId: string }): JSX.Element {
74
+ const { Button } = useCheckoutComponents();
75
+ return (
76
+ <Box sx={{ ml: "auto", flex: "none", "& button": { textDecoration: "underline", minHeight: (t) => t.spacing(5.5), px: 0.5 } }}>
77
+ <Button variant="text" color="primary" size="sm" onClick={onClick} dataTestId={testId}>
78
+ {label}
79
+ </Button>
80
+ </Box>
81
+ );
82
+ }
83
+
84
+ /** "Abrindo o **Nubank**…" · "Outro banco" — or "Não conseguimos abrir…" · "Tentar de novo" · "Outro banco". */
85
+ function Opening({ bank, state, chosen }: { bank: CheckoutPixBank; state: PixBankState; chosen: PixBankId }): JSX.Element {
86
+ const { Button, Text } = useCheckoutComponents();
87
+ const name = bank.copy.names[chosen];
88
+ const failed = state.status === "failed";
89
+ return (
90
+ <Box data-testid="pix-bank-opening" data-status={state.status ?? "opening"} sx={{ display: "flex", alignItems: "center", flexWrap: "wrap", gap: 1 }}>
91
+ <Box component="span" role={failed ? "alert" : "status"} sx={{ minWidth: 0 }}>
92
+ <Text variant="body" size="sm" as="span">
93
+ {failed ? bank.copy.notOpened(name) : <Emphasized text={bank.copy.opening(name)} />}
94
+ </Text>
95
+ </Box>
96
+ {failed ? (
97
+ <Box sx={{ "& button": { minHeight: (t) => t.spacing(5.5) } }}>
98
+ <Button variant="outline" color="primary" size="sm" onClick={() => state.open(chosen, false)} dataTestId="pix-bank-retry">
99
+ {bank.copy.retry}
100
+ </Button>
101
+ </Box>
102
+ ) : null}
103
+ <OtherLink label={bank.copy.other} onClick={state.showGrid} testId="pix-bank-other" />
104
+ </Box>
105
+ );
106
+ }
107
+
108
+ /** One of the six: the bank's logo at 32px and its name, ≈56px tall. */
109
+ /** The grid's own width, never the window's: under it, a cell is too narrow for logo and name side by side. */
110
+ const NARROW_GRID = "@container pixbanks (max-width: 289.95px)";
111
+
112
+ function BankButton({ bank, id, onPick, disabled }: { bank: CheckoutPixBank; id: PixBankId; onPick: () => void; disabled: boolean }): JSX.Element {
113
+ const { Button } = useCheckoutComponents();
114
+ const name = bank.copy.names[id];
115
+ const label = bank.copy.open(name);
116
+ const lead = label.slice(0, label.lastIndexOf(name.name));
117
+ return (
118
+ <Box
119
+ sx={{
120
+ minWidth: 0,
121
+ "& button": {
122
+ width: "100%",
123
+ minHeight: (t) => t.spacing(7),
124
+ borderRadius: 1.5,
125
+ justifyContent: "flex-start",
126
+ textAlign: "left",
127
+ gap: 1.25,
128
+ px: 1.5,
129
+ color: "text.primary",
130
+ borderColor: "divider",
131
+ // A 320px phone gives the grid ~254px (each half ~123px): logo
132
+ // beside "Santander" no longer fits, so the logo goes above the name.
133
+ [NARROW_GRID]: {
134
+ flexDirection: "column",
135
+ justifyContent: "center",
136
+ textAlign: "center",
137
+ gap: 0.5,
138
+ px: 1,
139
+ py: 1,
140
+ "& .MuiButton-startIcon, & .MuiButton-start": { m: 0 },
141
+ },
142
+ },
143
+ "& button, & button *": { fontWeight: 600 },
144
+ }}
145
+ >
146
+ <Button variant="outline" color="neutral" size="md" fullWidth disabled={disabled} icon={<BankLogo bank={id} />} onClick={onPick} dataTestId={`pix-bank-${id}`}>
147
+ {/* The accessible name is "Abrir Nubank"; the eye reads the logo and "Nubank". */}
148
+ <Box component="span" sx={{ position: "absolute", width: "1px", height: "1px", overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap" }}>
149
+ {lead}
150
+ </Box>
151
+ {name.name}
152
+ </Button>
153
+ </Box>
154
+ );
155
+ }
156
+
157
+ function Grid({ bank, state, stale }: { bank: CheckoutPixBank; state: PixBankState; stale: boolean }): JSX.Element {
158
+ const { Text } = useCheckoutComponents();
159
+ return (
160
+ <Box data-testid="pix-bank-grid" sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
161
+ <Text variant="body" size="md" weight="semibold" as="p" style={{ margin: 0 }}>
162
+ {bank.copy.listTitle}
163
+ </Text>
164
+ <Box sx={{ containerType: "inline-size", containerName: "pixbanks" }}>
165
+ <Box sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 1 }}>
166
+ {PIX_BANKS.map((id) => (
167
+ <BankButton key={id} bank={bank} id={id} disabled={stale} onPick={() => state.open(id, true)} />
168
+ ))}
169
+ </Box>
170
+ </Box>
171
+ </Box>
172
+ );
173
+ }
174
+
175
+ /** The grid, or the bank being opened. */
176
+ export function PixBanks({ bank, state, stale }: { bank: CheckoutPixBank; state: PixBankState; stale: boolean }): JSX.Element {
177
+ return state.chosen ? <Opening bank={bank} state={state} chosen={state.chosen} /> : <Grid bank={bank} state={state} stale={stale} />;
178
+ }
@@ -131,6 +131,97 @@ export interface CheckoutPaymentList {
131
131
  * the list returns to its rows.
132
132
  */
133
133
  amountDueLabel?: string;
134
+ /**
135
+ * The buyer's last ONLINE way to pay, as the host remembers it (FUT-3611) —
136
+ * chosen for them before they touch anything, when the store offers it: Pix
137
+ * as Pix; a card as the saved card used last. Absent or `null` ⇒ the saved
138
+ * card rule alone. A door tender is never chosen for the buyer.
139
+ */
140
+ lastMethod?: "PIX" | "CARD" | null;
141
+ /**
142
+ * "Pedir em 1 toque" opened this checkout (FUT-3611): the tap on the
143
+ * cardápio was the buyer's consent, so the card's 5-second bar starts by
144
+ * itself once the saved card is chosen and nothing holds the button — ONCE
145
+ * per step, and only within {@link ARMED_WINDOW_MS} of the step opening: a
146
+ * checkout that lands held (a field to fill) never charges by itself minutes
147
+ * later. Without a saved card it does nothing.
148
+ */
149
+ armedCharge?: boolean;
150
+ /**
151
+ * The saved card the host's consent named (the 1-tap offer's). Given, the
152
+ * armed charge fires only on THAT card; a different selection never charges
153
+ * by itself.
154
+ */
155
+ armedCardId?: string;
156
+ /**
157
+ * Host content under the 5-second bar's Cancelar while it counts — the
158
+ * storefront's "Cancelar e desativar a compra em 1 toque". It is handed the
159
+ * bar's own cancel, so a link there stops the charge before doing its own.
160
+ */
161
+ chargeFooter?: (cancel: () => void) => ReactNode;
162
+ /**
163
+ * Pix straight to the buyer's bank app (FUT-3612) — on a phone only: the
164
+ * button generates AND copies the code, a short message says so through
165
+ * `notify`, and the after-copy face offers the six banks (or opens the one
166
+ * remembered). Absent ⇒ today's Pix: "Gerar Pix", then a tap to copy.
167
+ */
168
+ pixBank?: CheckoutPixBank;
169
+ /** Says a short sentence in the host's own toast — "Código Pix copiado". */
170
+ notify?: (message: string) => void;
171
+ }
172
+
173
+ /** The six banks the Pix face offers to open, in the prototype's order. */
174
+ export type PixBankId = "nubank" | "itau" | "bradesco" | "bb" | "caixa" | "santander";
175
+
176
+ /** `CheckoutPaymentList.pixBank`. */
177
+ export interface CheckoutPixBank {
178
+ /** The bank the buyer picked last time, as the host stored it — or `null`. */
179
+ remembered: PixBankId | null;
180
+ /** A bank was picked: the host remembers it for next time (a new pick replaces it). */
181
+ onPick: (bank: PixBankId) => void;
182
+ /** The words; ship with the list's pack (`PIX_BANK_COPY`). */
183
+ copy: PixBankCopy;
184
+ /**
185
+ * Whether this device can open a bank app — a phone. Absent ⇒ read from the
186
+ * browser (Android or iOS). A computer keeps today's face and gets only the
187
+ * generate-and-copy.
188
+ */
189
+ phone?: boolean;
190
+ }
191
+
192
+ /** A bank's name and the article pt-BR puts before it ("o Nubank", "a Caixa"). */
193
+ export interface PixBankName {
194
+ name: string;
195
+ /** "Abrir o Nubank" / "Abrir a Caixa" — the article with its trailing space, or "" for none. */
196
+ the: string;
197
+ }
198
+
199
+ export interface PixBankCopy {
200
+ names: Record<PixBankId, PixBankName>;
201
+ /** The bar's button with no bank remembered — "Gerar e copiar o Pix". */
202
+ generateAndCopy: string;
203
+ /** With one remembered — "Gerar, copiar e abrir o Nubank". */
204
+ generateCopyAndOpen: (bank: PixBankName) => string;
205
+ /** The before-copy face's button with one remembered — "Copiar e abrir o Nubank". */
206
+ copyAndOpen: (bank: PixBankName) => string;
207
+ /** The link under the button — "Abrir outro banco". */
208
+ openOther: string;
209
+ /** Over the grid — "Abra seu banco e cole em Pix copia e cola". */
210
+ listTitle: string;
211
+ /** A bank button's accessible name — "Abrir Nubank". */
212
+ open: (bank: PixBankName) => string;
213
+ /** "Abrindo o **Nubank**…" — `**` marks the bold span. */
214
+ opening: (bank: PixBankName) => string;
215
+ /** "Outro banco". */
216
+ other: string;
217
+ /** "Não conseguimos abrir o Nubank. O app está instalado neste celular?" */
218
+ notOpened: (bank: PixBankName) => string;
219
+ /** "Tentar de novo". */
220
+ retry: string;
221
+ /** The toasts. */
222
+ copied: string;
223
+ copiedPick: string;
224
+ copiedOpening: (bank: PixBankName) => string;
134
225
  }
135
226
 
136
227
  /** The list's words — a pack per locale ships at `@12-apps/payments-frontend/locales`. */
@@ -59,6 +59,7 @@ function usePreselect(
59
59
  rows: readonly OnlineRow[],
60
60
  saved: readonly SavedCard[],
61
61
  fresh: boolean,
62
+ lastMethod: "PIX" | "CARD" | null,
62
63
  apply: (choice: CheckoutListChoice, selection: string | null) => void,
63
64
  touched: { current: boolean },
64
65
  ): void {
@@ -66,9 +67,9 @@ function usePreselect(
66
67
  useEffect(() => {
67
68
  if (done.current || touched.current || !loaded) return;
68
69
  done.current = true;
69
- const pick = preselection(rows, saved, fresh);
70
+ const pick = preselection(rows, saved, fresh, lastMethod);
70
71
  if (pick) apply(pick.choice, pick.selection);
71
- }, [loaded, rows, saved, fresh, apply, touched]);
72
+ }, [loaded, rows, saved, fresh, lastMethod, apply, touched]);
72
73
  }
73
74
 
74
75
  /**
@@ -96,6 +97,8 @@ export function useListState(input: {
96
97
  fresh: boolean;
97
98
  arrivedWith: BuyerInfo | undefined;
98
99
  taxIdOnFile: boolean;
100
+ /** The buyer's last online method, from the host (FUT-3611). */
101
+ lastMethod?: "PIX" | "CARD" | null;
99
102
  onChoiceChange?: (choice: CheckoutListChoice | null) => void;
100
103
  }): ListState {
101
104
  const { capability } = input;
@@ -112,7 +115,7 @@ export function useListState(input: {
112
115
  },
113
116
  [setSelection],
114
117
  );
115
- usePreselect(loaded, rows, cards, input.fresh, apply, touched);
118
+ usePreselect(loaded, rows, cards, input.fresh, input.lastMethod ?? null, apply, touched);
116
119
  const choose = useCallback(
117
120
  (next: CheckoutListChoice) => {
118
121
  touched.current = true;
@@ -123,6 +123,12 @@ interface PixAfterCopyProps {
123
123
  onPreferCard?: () => void;
124
124
  /** The poll's line ("Verificando…", or the stalled panel that replaces it). */
125
125
  footer: ReactNode;
126
+ /**
127
+ * The way to the bank app, on a phone (FUT-3612): drawn where the heading
128
+ * was — the toast already said the code was copied and the bar says what it
129
+ * waits for — and, while it is the six banks, in place of the checklist.
130
+ */
131
+ banks?: { node: ReactNode; replacesChecklist: boolean };
126
132
  }
127
133
 
128
134
  /** The narrow layout's two secondaries side by side; wide, the card link joins the row. */
@@ -164,8 +170,8 @@ export function PixAfterCopy(props: PixAfterCopyProps): JSX.Element {
164
170
  return (
165
171
  <Box data-testid="pix-after-copy" sx={{ display: "flex", flexWrap: "wrap", gap: 4, alignItems: "flex-start" }}>
166
172
  <Box sx={{ flex: "999 1 420px", minWidth: 0, display: "flex", flexDirection: "column", gap: 2.5 }}>
167
- <Heading words={words} totalLabel={totalLabel} />
168
- <Checklist words={words} deadline={deadline} />
173
+ {props.banks ? props.banks.node : <Heading words={words} totalLabel={totalLabel} />}
174
+ {props.banks?.replacesChecklist ? null : <Checklist words={words} deadline={deadline} />}
169
175
  <Actions {...props} />
170
176
  {footer}
171
177
  {onPreferCard ? (
@@ -82,6 +82,62 @@ export function useClipboardCopy(text: string): { copy: () => Promise<boolean>;
82
82
  return { copy, justCopied };
83
83
  }
84
84
 
85
+ /** A copy opened inside a tap whose text arrives later (FUT-3612) — see {@link deferredCopy}. */
86
+ export interface DeferredCopy {
87
+ /** The text has arrived: write it. */
88
+ resolve: (text: string) => void;
89
+ /** There will be no text (the raise was refused): give the clipboard up. */
90
+ fail: () => void;
91
+ /** `true` once the clipboard holds the text; `false` when it refused or there was none. */
92
+ done: Promise<boolean>;
93
+ }
94
+
95
+ /**
96
+ * Open the clipboard NOW, inside the buyer's tap, for a text the server has
97
+ * not answered yet. Safari lets a page write to the clipboard only inside the
98
+ * gesture, so the write is started here with a promise of the text
99
+ * (`ClipboardItem`); where that is missing or refused, the plain write is
100
+ * tried once the text arrives — Chrome takes it from a focused page.
101
+ */
102
+ export function deferredCopy(): DeferredCopy {
103
+ let give: (text: string | null) => void = () => undefined;
104
+ const text = new Promise<string | null>((resolve) => {
105
+ give = resolve;
106
+ });
107
+ const plain = async (): Promise<boolean> => {
108
+ const value = await text;
109
+ if (value === null || !navigator.clipboard) return false;
110
+ try {
111
+ await navigator.clipboard.writeText(value);
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ };
117
+ const Item = typeof window === "undefined" ? undefined : (window as { ClipboardItem?: typeof ClipboardItem }).ClipboardItem;
118
+ let done: Promise<boolean> = Promise.resolve(false);
119
+ let opened = false;
120
+ if (Item && navigator.clipboard && typeof navigator.clipboard.write === "function") {
121
+ const blob = text.then((value) => {
122
+ if (value === null) throw new Error("no text");
123
+ return new Blob([value], { type: "text/plain" });
124
+ });
125
+ // An unanswered blob must not surface as an unhandled rejection.
126
+ blob.catch(() => undefined);
127
+ try {
128
+ done = navigator.clipboard.write([new Item({ "text/plain": blob })]).then(
129
+ () => true,
130
+ async () => (await text) !== null && plain(),
131
+ );
132
+ opened = true;
133
+ } catch {
134
+ // A browser whose ClipboardItem takes no promise throws here, in the tap.
135
+ }
136
+ }
137
+ if (!opened) done = plain();
138
+ return { resolve: (value) => give(value), fail: () => give(null), done };
139
+ }
140
+
85
141
  /**
86
142
  * A PIX's QR or code, veiled while the cart has changed under it (FUT-3535):
87
143
  * blurred past reading, out of reach of a pointer, a selection and a screen
@@ -287,6 +287,8 @@ function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; p
287
287
  onShowQr={() => setAfterCopy(false)}
288
288
  onPreferCard={stage.preferCard}
289
289
  footer={poll.error || poll.timedOut ? footerLine(copy.verifying) : null}
290
+ writeClipboard={writeClipboard}
291
+ setAfterCopy={setAfterCopy}
290
292
  />
291
293
  );
292
294
  }
package/src/index.ts CHANGED
@@ -56,7 +56,7 @@ export { PT_BR_CHECKOUT_PAYMENT_COPY } from './components/checkout-payment-pt-BR
56
56
  // ---------------------------------------------------------------------------
57
57
  export { CheckoutFlow, type CheckoutCartView, type CheckoutFlowProps } from './components/checkout/checkout-flow';
58
58
  // The Pagamento step as ONE list (FUT-3591/3592) — opt-in via `CheckoutFlowProps.paymentList`.
59
- export type { CheckoutDoorOption, CheckoutListChoice, CheckoutListHold, CheckoutOnlineChoice, CheckoutPaymentList, PaymentListCopy } from './components/checkout/list/types';
59
+ export type { CheckoutDoorOption, CheckoutListChoice, CheckoutListHold, CheckoutOnlineChoice, CheckoutPaymentList, CheckoutPixBank, PaymentListCopy, PixBankCopy, PixBankId, PixBankName } from './components/checkout/list/types';
60
60
  export { PT_BR_PAYMENT_LIST_COPY } from './components/checkout/list/pt-BR';
61
61
  export type { BackActionEmphasis } from './components/checkout/payment-status-parts';
62
62
  export type { CheckoutDeliverySummary, CheckoutSummaryLine } from './components/checkout/payment-summary';
package/src/locales.ts CHANGED
@@ -22,7 +22,9 @@ import type { CheckoutScreensCopy } from './components/checkout/screens-copy';
22
22
  import { PT_BR_CHECKOUT_SCREENS_COPY } from './components/checkout/screens-pt-BR';
23
23
  import { EN_US_PAYMENT_LIST_COPY } from './components/checkout/list/en-US';
24
24
  import { PT_BR_PAYMENT_LIST_COPY } from './components/checkout/list/pt-BR';
25
- import type { PaymentListCopy } from './components/checkout/list/types';
25
+ import { EN_US_PIX_BANK_COPY } from './components/checkout/list/bank-en-US';
26
+ import { PT_BR_PIX_BANK_COPY } from './components/checkout/list/bank-pt-BR';
27
+ import type { PaymentListCopy, PixBankCopy } from './components/checkout/list/types';
26
28
  import type { CheckoutViewCopy, PaymentStatusCopy } from './components/checkout/view-copy';
27
29
  import type { PlatformHomologacaoCopy } from './components/platform/copy';
28
30
  import { EN_US_PLATFORM_HOMOLOGACAO_COPY } from './components/platform/en-US';
@@ -88,6 +90,15 @@ export const PAYMENT_LIST_COPY = {
88
90
  'en-US': EN_US_PAYMENT_LIST_COPY,
89
91
  } as const satisfies LocalePack<PaymentListCopy>;
90
92
 
93
+ /**
94
+ * The Pix-to-bank flow's words (FUT-3612) — `CheckoutFlowProps.paymentList.pixBank.copy`.
95
+ * Read by the SHOPPER.
96
+ */
97
+ export const PIX_BANK_COPY = {
98
+ 'pt-BR': PT_BR_PIX_BANK_COPY,
99
+ 'en-US': EN_US_PIX_BANK_COPY,
100
+ } as const satisfies LocalePack<PixBankCopy>;
101
+
91
102
  export const CHECKOUT_COPY = {
92
103
  'pt-BR': PT_BR_CHECKOUT_COPY,
93
104
  'en-US': EN_US_CHECKOUT_COPY,