@greatapps/common 1.1.805 → 1.1.807
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/dist/components/account/sections/affiliates/ReceivingAccountModal.mjs +52 -18
- package/dist/components/account/sections/affiliates/ReceivingAccountModal.mjs.map +1 -1
- package/dist/components/account/sections/subscription/PixDetailsDialog.mjs +4 -6
- package/dist/components/account/sections/subscription/PixDetailsDialog.mjs.map +1 -1
- package/dist/components/account/sections/subscription/PixPaymentDialog.mjs +17 -24
- package/dist/components/account/sections/subscription/PixPaymentDialog.mjs.map +1 -1
- package/dist/hooks/useSecondsUntil.mjs +17 -0
- package/dist/hooks/useSecondsUntil.mjs.map +1 -0
- package/dist/i18n/messages/en-us.mjs +7 -0
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +7 -0
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +7 -0
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/index.mjs +6 -1
- package/dist/index.mjs.map +1 -1
- package/dist/modules/subscriptions/types/pending-pix-status.type.mjs +1 -0
- package/dist/modules/subscriptions/types/pending-pix-status.type.mjs.map +1 -1
- package/dist/testing/factories/affiliate.factory.mjs +24 -0
- package/dist/testing/factories/affiliate.factory.mjs.map +1 -0
- package/dist/testing/factories/index.mjs +2 -0
- package/dist/testing/factories/index.mjs.map +1 -1
- package/dist/utils/format/masks.mjs +6 -1
- package/dist/utils/format/masks.mjs.map +1 -1
- package/dist/utils/validators/pix-key.mjs +27 -0
- package/dist/utils/validators/pix-key.mjs.map +1 -0
- package/package.json +10 -9
- package/src/components/account/sections/affiliates/ReceivingAccountModal.tsx +71 -20
- package/src/components/account/sections/subscription/PixDetailsDialog.tsx +4 -9
- package/src/components/account/sections/subscription/PixPaymentDialog.tsx +18 -29
- package/src/hooks/useSecondsUntil.ts +17 -0
- package/src/i18n/messages/en-us.ts +8 -0
- package/src/i18n/messages/es-es.ts +8 -0
- package/src/i18n/messages/pt-br.ts +8 -0
- package/src/index.ts +2 -0
- package/src/modules/subscriptions/types/pending-pix-status.type.ts +1 -0
- package/src/testing/factories/affiliate.factory.ts +22 -0
- package/src/testing/factories/index.ts +1 -0
- package/src/utils/format/masks.ts +6 -2
- package/src/utils/validators/pix-key.ts +25 -0
|
@@ -12,7 +12,7 @@ import { useCurrencyFormatter } from '../../../../modules/accounts/hooks/use-cur
|
|
|
12
12
|
import { useRefreshChargePix } from '../../../../modules/charges/hooks/refresh-pix.hook';
|
|
13
13
|
import type { Charge } from '../../../../modules/charges/types/charge.type';
|
|
14
14
|
import type { PixRefreshData } from '../../../../modules/charges/types/refresh-pix.type';
|
|
15
|
-
import
|
|
15
|
+
import useSecondsUntil from '../../../../hooks/useSecondsUntil';
|
|
16
16
|
import { copyToClipboard } from '../../../../utils/browser/clipboard';
|
|
17
17
|
import { qrCodeToDataUrl } from '../../../../utils/qrcode/to-data-url';
|
|
18
18
|
|
|
@@ -82,12 +82,7 @@ export function PixDetailsDialog({ open, charge, onOpenChange }: PixDetailsDialo
|
|
|
82
82
|
const isRefreshing = needsRefresh && refreshPix.isPending;
|
|
83
83
|
const refreshNotice = refreshPix.data && !refreshPix.data.pix ? refreshPix.data.message : null;
|
|
84
84
|
|
|
85
|
-
const
|
|
86
|
-
expiresAtRaw && mountedAt
|
|
87
|
-
? Math.max(0, Math.floor((new Date(expiresAtRaw).getTime() - mountedAt) / 1000))
|
|
88
|
-
: null;
|
|
89
|
-
const shouldShowCountdown = typeof expiresInSeconds === 'number' && expiresInSeconds > 0;
|
|
90
|
-
const { seconds } = useCountdownTimer({ initialSeconds: shouldShowCountdown ? expiresInSeconds : 1 });
|
|
85
|
+
const secondsUntilExpiry = useSecondsUntil(expiresAtRaw);
|
|
91
86
|
|
|
92
87
|
useEffect(() => {
|
|
93
88
|
if (!qrCode) {
|
|
@@ -185,9 +180,9 @@ export function PixDetailsDialog({ open, charge, onOpenChange }: PixDetailsDialo
|
|
|
185
180
|
</div>
|
|
186
181
|
|
|
187
182
|
<div className="flex flex-col gap-6 p-5 border border-zinc-200 rounded-lg bg-white items-center">
|
|
188
|
-
{
|
|
183
|
+
{!!secondsUntilExpiry && (
|
|
189
184
|
<p className="paragraph-small-semibold text-zinc-950 text-center">
|
|
190
|
-
{translate('payIn')} <span className="text-red-600">{formatCountdown(
|
|
185
|
+
{translate('payIn')} <span className="text-red-600">{formatCountdown(secondsUntilExpiry)}</span>
|
|
191
186
|
</p>
|
|
192
187
|
)}
|
|
193
188
|
|
|
@@ -10,7 +10,7 @@ import { Toast } from '../../../ui/feedback/Toast';
|
|
|
10
10
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../ui/overlay/Dialog';
|
|
11
11
|
import { useCurrencyFormatter } from '../../../../modules/accounts/hooks/use-currency-formatter.hook';
|
|
12
12
|
import { usePendingPixStatus } from '../../../../modules/subscriptions/hooks/pending-pix-status.hook';
|
|
13
|
-
import
|
|
13
|
+
import useSecondsUntil from '../../../../hooks/useSecondsUntil';
|
|
14
14
|
import { copyToClipboard } from '../../../../utils/browser/clipboard';
|
|
15
15
|
import { qrCodeToDataUrl } from '../../../../utils/qrcode/to-data-url';
|
|
16
16
|
import { useSubscriptionFlows } from '../../../../store/useSubscriptionFlows';
|
|
@@ -39,20 +39,11 @@ export function PixPaymentDialog() {
|
|
|
39
39
|
const close = useSubscriptionFlows((state) => state.closePixPayment);
|
|
40
40
|
const openPlansCatalog = useSubscriptionFlows((state) => state.openPlansCatalog);
|
|
41
41
|
|
|
42
|
-
const [mountedAt, setMountedAt] = useState<number | null>(null);
|
|
43
42
|
const [generatedQrImage, setGeneratedQrImage] = useState<string | null>(null);
|
|
44
43
|
|
|
45
44
|
const { data: statusData } = usePendingPixStatus(pixPayment?.correlationId);
|
|
46
45
|
const phase = statusData?.data?.phase;
|
|
47
46
|
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (!pixPayment) {
|
|
50
|
-
setMountedAt(null);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
setMountedAt(Date.now());
|
|
54
|
-
}, [pixPayment]);
|
|
55
|
-
|
|
56
47
|
useEffect(() => {
|
|
57
48
|
const qrCode = pixPayment?.qrCode;
|
|
58
49
|
if (!qrCode) {
|
|
@@ -79,19 +70,22 @@ export function PixPaymentDialog() {
|
|
|
79
70
|
};
|
|
80
71
|
}, [pixPayment?.qrCode]);
|
|
81
72
|
|
|
82
|
-
const
|
|
73
|
+
const isCompleted = phase === 'completed';
|
|
74
|
+
const isAuthorizedOnly = statusData?.data?.payment_collected === false;
|
|
75
|
+
const completedTitle = isAuthorizedOnly ? translate('authorizedTitle') : translate('paidTitle');
|
|
76
|
+
const completedMessage = isAuthorizedOnly ? translate('authorizedMessage') : translate('paidToast');
|
|
83
77
|
const isRejected = phase === 'rejected';
|
|
84
78
|
const isExpired = phase === 'expired';
|
|
85
79
|
|
|
86
80
|
// Pago avisa uma vez; a janela fica com a confirmação na tela, como a página do gapps fazia.
|
|
87
81
|
const paidNotifiedRef = useRef(false);
|
|
88
82
|
useEffect(() => {
|
|
89
|
-
if (!
|
|
83
|
+
if (!isCompleted || paidNotifiedRef.current) return;
|
|
90
84
|
paidNotifiedRef.current = true;
|
|
91
85
|
toast.custom((toastId) => (
|
|
92
|
-
<Toast variant="success" message={
|
|
86
|
+
<Toast variant="success" message={completedMessage} toastId={toastId} />
|
|
93
87
|
));
|
|
94
|
-
}, [
|
|
88
|
+
}, [isCompleted, completedMessage]);
|
|
95
89
|
|
|
96
90
|
useEffect(() => {
|
|
97
91
|
if (!pixPayment) paidNotifiedRef.current = false;
|
|
@@ -102,12 +96,7 @@ export function PixPaymentDialog() {
|
|
|
102
96
|
openPlansCatalog();
|
|
103
97
|
};
|
|
104
98
|
|
|
105
|
-
const
|
|
106
|
-
pixPayment?.expiresAt && mountedAt
|
|
107
|
-
? Math.max(0, Math.floor((new Date(pixPayment.expiresAt).getTime() - mountedAt) / 1000))
|
|
108
|
-
: null;
|
|
109
|
-
const shouldShowCountdown = typeof expiresInSeconds === 'number' && expiresInSeconds > 0;
|
|
110
|
-
const { seconds } = useCountdownTimer({ initialSeconds: shouldShowCountdown ? expiresInSeconds : 1 });
|
|
99
|
+
const secondsUntilExpiry = useSecondsUntil(pixPayment?.expiresAt);
|
|
111
100
|
|
|
112
101
|
const handleCopyCode = async () => {
|
|
113
102
|
if (!pixPayment?.qrCode) return;
|
|
@@ -141,15 +130,15 @@ export function PixPaymentDialog() {
|
|
|
141
130
|
|
|
142
131
|
{/* Fechado o ciclo (pago, recusado ou expirado), o QR perde a função: a janela vira o
|
|
143
132
|
desfecho, como a página de pix do gapps. */}
|
|
144
|
-
{
|
|
133
|
+
{isCompleted || isRejected || isExpired ? (
|
|
145
134
|
<div className="flex flex-col items-center justify-center gap-4 p-8 text-center">
|
|
146
135
|
<div
|
|
147
136
|
className={cn(
|
|
148
137
|
'size-12 rounded-full flex items-center justify-center',
|
|
149
|
-
|
|
138
|
+
isCompleted ? 'bg-green-50' : 'bg-red-50'
|
|
150
139
|
)}
|
|
151
140
|
>
|
|
152
|
-
{
|
|
141
|
+
{isCompleted ? (
|
|
153
142
|
<IconCircleCheck size={24} className="text-green-500" />
|
|
154
143
|
) : (
|
|
155
144
|
<IconAlertTriangle size={24} className="text-red-500" />
|
|
@@ -158,18 +147,18 @@ export function PixPaymentDialog() {
|
|
|
158
147
|
|
|
159
148
|
<div className="flex flex-col gap-1">
|
|
160
149
|
<span className="paragraph-xlarge-semibold text-zinc-950">
|
|
161
|
-
{
|
|
162
|
-
?
|
|
150
|
+
{isCompleted
|
|
151
|
+
? completedTitle
|
|
163
152
|
: isRejected
|
|
164
153
|
? translate('rejectedTitle')
|
|
165
154
|
: translate('expiredTitle')}
|
|
166
155
|
</span>
|
|
167
156
|
<span className="paragraph-small-regular text-zinc-500">
|
|
168
|
-
{
|
|
157
|
+
{isCompleted ? completedMessage : translate('rejectedOrExpiredDescription')}
|
|
169
158
|
</span>
|
|
170
159
|
</div>
|
|
171
160
|
|
|
172
|
-
{
|
|
161
|
+
{isCompleted ? (
|
|
173
162
|
<Button variant="secondary" className="h-10" onClick={close}>
|
|
174
163
|
{translateActions('common.actions.close')}
|
|
175
164
|
</Button>
|
|
@@ -189,9 +178,9 @@ export function PixPaymentDialog() {
|
|
|
189
178
|
)}
|
|
190
179
|
|
|
191
180
|
<div className="flex flex-col gap-6 p-5 border border-zinc-200 rounded-lg bg-white items-center">
|
|
192
|
-
{
|
|
181
|
+
{!!secondsUntilExpiry && (
|
|
193
182
|
<p className="paragraph-small-semibold text-zinc-950 text-center">
|
|
194
|
-
{translate('payIn')} <span className="text-red-600">{formatCountdown(
|
|
183
|
+
{translate('payIn')} <span className="text-red-600">{formatCountdown(secondsUntilExpiry)}</span>
|
|
195
184
|
</p>
|
|
196
185
|
)}
|
|
197
186
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function useSecondsUntil(deadline: string | null | undefined): number | null {
|
|
6
|
+
const [now, setNow] = useState<number | null>(null);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (!deadline) return;
|
|
10
|
+
setNow(Date.now());
|
|
11
|
+
const interval = setInterval(() => setNow(Date.now()), 1000);
|
|
12
|
+
return () => clearInterval(interval);
|
|
13
|
+
}, [deadline]);
|
|
14
|
+
|
|
15
|
+
if (!deadline || now === null) return null;
|
|
16
|
+
return Math.max(0, Math.floor((new Date(deadline).getTime() - now) / 1000));
|
|
17
|
+
}
|
|
@@ -558,6 +558,12 @@ const messages = {
|
|
|
558
558
|
validationCnpjChecksum: 'Invalid CNPJ — check the verification digits',
|
|
559
559
|
validationPixDocumentMismatch:
|
|
560
560
|
'The Pix key is a tax ID that differs from the account holder document. Use a key for the same document or fix the holder tax ID.',
|
|
561
|
+
validationPixInvalid:
|
|
562
|
+
'Invalid Pix key. Use a CPF, CNPJ, mobile number with area code, email or random key.',
|
|
563
|
+
pixKeyPreview: 'Will be saved as: {value}',
|
|
564
|
+
pixAmbiguousQuestion: 'Is this number a mobile phone or a CPF?',
|
|
565
|
+
pixAmbiguousMobile: 'Mobile',
|
|
566
|
+
pixAmbiguousCpf: 'CPF',
|
|
561
567
|
pixCnpjInvoiceNotice:
|
|
562
568
|
'Company account: withdrawals require a PDF invoice and do not use the individual monthly limit.',
|
|
563
569
|
},
|
|
@@ -1092,6 +1098,8 @@ const messages = {
|
|
|
1092
1098
|
paidToast: 'Payment processed successfully.',
|
|
1093
1099
|
waitingPayment: 'Waiting for payment confirmation...',
|
|
1094
1100
|
paidTitle: 'PIX paid',
|
|
1101
|
+
authorizedTitle: 'Automatic PIX authorized',
|
|
1102
|
+
authorizedMessage: 'The charge will be made automatically on the due date.',
|
|
1095
1103
|
rejectedTitle: 'PIX rejected',
|
|
1096
1104
|
expiredTitle: 'PIX code expired',
|
|
1097
1105
|
rejectedOrExpiredDescription: 'Click below to generate a new PIX code',
|
|
@@ -561,6 +561,12 @@ const messages = {
|
|
|
561
561
|
validationCnpjChecksum: 'CNPJ inválido — verifica los dígitos verificadores',
|
|
562
562
|
validationPixDocumentMismatch:
|
|
563
563
|
'La clave Pix es un CPF/CNPJ distinto del documento del titular. Usa una clave del mismo documento o corrige el CPF/CNPJ del titular.',
|
|
564
|
+
validationPixInvalid:
|
|
565
|
+
'Clave Pix inválida. Usa CPF, CNPJ, celular con código de área, correo electrónico o clave aleatoria.',
|
|
566
|
+
pixKeyPreview: 'Se guardará como: {value}',
|
|
567
|
+
pixAmbiguousQuestion: '¿Este número es un celular o un CPF?',
|
|
568
|
+
pixAmbiguousMobile: 'Celular',
|
|
569
|
+
pixAmbiguousCpf: 'CPF',
|
|
564
570
|
pixCnpjInvoiceNotice:
|
|
565
571
|
'Cuenta CNPJ: el retiro exige factura en PDF y no usa el límite mensual de CPF.',
|
|
566
572
|
},
|
|
@@ -1098,6 +1104,8 @@ const messages = {
|
|
|
1098
1104
|
paidToast: 'Pago realizado exitosamente.',
|
|
1099
1105
|
waitingPayment: 'Esperando la confirmación del pago...',
|
|
1100
1106
|
paidTitle: 'PIX pagado',
|
|
1107
|
+
authorizedTitle: 'PIX automático autorizado',
|
|
1108
|
+
authorizedMessage: 'El cobro se realizará automáticamente en la fecha de vencimiento.',
|
|
1101
1109
|
rejectedTitle: 'PIX rechazado',
|
|
1102
1110
|
expiredTitle: 'Código PIX expirado',
|
|
1103
1111
|
rejectedOrExpiredDescription: 'Haz clic abajo para generar un nuevo código PIX',
|
|
@@ -569,6 +569,12 @@ const messages = {
|
|
|
569
569
|
validationCnpjChecksum: 'CNPJ inválido — confira os dígitos verificadores',
|
|
570
570
|
validationPixDocumentMismatch:
|
|
571
571
|
'A chave Pix informada é um CPF/CNPJ diferente do documento do titular. Use uma chave do mesmo documento ou corrija o CPF/CNPJ do titular.',
|
|
572
|
+
validationPixInvalid:
|
|
573
|
+
'Chave Pix inválida. Use CPF, CNPJ, celular com DDD, e-mail ou chave aleatória.',
|
|
574
|
+
pixKeyPreview: 'Será salva como: {value}',
|
|
575
|
+
pixAmbiguousQuestion: 'Esse número é um celular ou um CPF?',
|
|
576
|
+
pixAmbiguousMobile: 'Celular',
|
|
577
|
+
pixAmbiguousCpf: 'CPF',
|
|
572
578
|
pixCnpjInvoiceNotice:
|
|
573
579
|
'Conta CNPJ: o saque passa a exigir nota fiscal em PDF e não usa o limite mensal de CPF.',
|
|
574
580
|
},
|
|
@@ -1105,6 +1111,8 @@ const messages = {
|
|
|
1105
1111
|
paidToast: 'Pagamento efetuado com sucesso.',
|
|
1106
1112
|
waitingPayment: 'Aguardando a confirmação do pagamento...',
|
|
1107
1113
|
paidTitle: 'PIX pago',
|
|
1114
|
+
authorizedTitle: 'PIX automático autorizado',
|
|
1115
|
+
authorizedMessage: 'A cobrança será feita automaticamente na data de vencimento.',
|
|
1108
1116
|
rejectedTitle: 'Pix recusado',
|
|
1109
1117
|
expiredTitle: 'Código PIX expirado',
|
|
1110
1118
|
rejectedOrExpiredDescription: 'Clique abaixo para gerar um novo código PIX',
|
package/src/index.ts
CHANGED
|
@@ -559,7 +559,9 @@ export {
|
|
|
559
559
|
formatCPF,
|
|
560
560
|
formatCNPJ,
|
|
561
561
|
formatCardNumber,
|
|
562
|
+
formatPixKey,
|
|
562
563
|
} from "./utils/format/masks";
|
|
564
|
+
export { AMBIGUOUS_PIX_KEY, normalizePixKeyForStorage } from "./utils/validators/pix-key";
|
|
563
565
|
export {
|
|
564
566
|
formatCurrency,
|
|
565
567
|
formatCurrencyNumber,
|
|
@@ -16,6 +16,7 @@ export const PendingPixStatusDataSchema = z
|
|
|
16
16
|
payment_method: z.number().nullable().optional(),
|
|
17
17
|
subscription_id: z.union([z.number(), z.string()]).optional(),
|
|
18
18
|
completed_at: z.string().nullable().optional(),
|
|
19
|
+
payment_collected: z.boolean().optional(),
|
|
19
20
|
pix: z
|
|
20
21
|
.object({
|
|
21
22
|
qr_code: z.string().nullable().optional(),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { Affiliate } from '../../modules/affiliates/types/affiliate.type';
|
|
3
|
+
import { TEST_IDS } from '../constants';
|
|
4
|
+
import { createMock } from './create-mock';
|
|
5
|
+
|
|
6
|
+
export const createAffiliateMock = createMock<Affiliate, Partial<Affiliate>>((overrides) => ({
|
|
7
|
+
id: overrides?.id ?? 1,
|
|
8
|
+
id_wl: overrides?.id_wl ?? TEST_IDS.wl,
|
|
9
|
+
id_account: overrides?.id_account ?? TEST_IDS.account,
|
|
10
|
+
active: overrides?.active ?? true,
|
|
11
|
+
deleted: overrides?.deleted ?? false,
|
|
12
|
+
balance: overrides?.balance ?? 0,
|
|
13
|
+
datetime_add: overrides?.datetime_add ?? null,
|
|
14
|
+
datetime_alt: overrides?.datetime_alt ?? null,
|
|
15
|
+
datetime_del: overrides?.datetime_del ?? null,
|
|
16
|
+
holder_name: overrides?.holder_name || faker.person.fullName(),
|
|
17
|
+
pix: overrides?.pix ?? null,
|
|
18
|
+
holder_document: overrides?.holder_document ?? null,
|
|
19
|
+
bank: overrides?.bank ?? null,
|
|
20
|
+
agency: overrides?.agency ?? null,
|
|
21
|
+
account: overrides?.account ?? null,
|
|
22
|
+
}));
|
|
@@ -8,3 +8,4 @@ export { createPlanMock, createPlanItemMock } from './plan.factory';
|
|
|
8
8
|
export { createProjectMock } from './project.factory';
|
|
9
9
|
export { createProjectUserMock } from './project-user.factory';
|
|
10
10
|
export { createCouponMock } from './coupon.factory';
|
|
11
|
+
export { createAffiliateMock } from './affiliate.factory';
|
|
@@ -92,8 +92,12 @@ function formatIfCpfCnpjShaped(value: string): string {
|
|
|
92
92
|
/** Documento do titular: CPF/CNPJ mascarado; qualquer outro conteúdo volta intacto. */
|
|
93
93
|
export const formatDocument = formatIfCpfCnpjShaped;
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
export function formatPixKey(value: string): string {
|
|
96
|
+
if (/^\+55\d{11}$/.test(value)) return `+55 ${formatPhone(value.slice(3))}`;
|
|
97
|
+
if (/^\d{11}$/.test(value)) return formatCPF(value);
|
|
98
|
+
if (/^\d{14}$/.test(value)) return formatCNPJ(value);
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
97
101
|
|
|
98
102
|
/** Tira a máscara antes de gravar; valor que não é CPF/CNPJ volta como veio. */
|
|
99
103
|
export function normalizeCpfCnpjForStorage(value: string): string {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isValidCNPJ, isValidCPF, isValidEmail } from './common';
|
|
2
|
+
|
|
3
|
+
const RANDOM_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
const PIX_KEY_CHARS = /^[\d ().\/+-]+$/;
|
|
5
|
+
const MOBILE_WITH_COUNTRY_CODE = /^55[1-9]{2}9\d{8}$/;
|
|
6
|
+
const MOBILE = /^[1-9]{2}9\d{8}$/;
|
|
7
|
+
|
|
8
|
+
export const AMBIGUOUS_PIX_KEY = Symbol('ambiguous-pix-key');
|
|
9
|
+
|
|
10
|
+
export function normalizePixKeyForStorage(value: string, holderDocument: string): string | typeof AMBIGUOUS_PIX_KEY | null {
|
|
11
|
+
const key = value.trim();
|
|
12
|
+
if (isValidEmail(key) || RANDOM_KEY.test(key)) return key;
|
|
13
|
+
if (!PIX_KEY_CHARS.test(key)) return null;
|
|
14
|
+
const digits = key.replace(/\D/g, '');
|
|
15
|
+
if (key.startsWith('+')) return MOBILE_WITH_COUNTRY_CODE.test(digits) ? `+${digits}` : null;
|
|
16
|
+
if (digits.length === 14) return isValidCNPJ(digits) ? digits : null;
|
|
17
|
+
if (digits.length === 13) return MOBILE_WITH_COUNTRY_CODE.test(digits) ? `+${digits}` : null;
|
|
18
|
+
if (digits.length !== 11) return null;
|
|
19
|
+
if (digits === holderDocument.replace(/\D/g, '')) return digits;
|
|
20
|
+
const isMobile = MOBILE.test(digits);
|
|
21
|
+
const isCpf = isValidCPF(digits);
|
|
22
|
+
if (isMobile && isCpf) return AMBIGUOUS_PIX_KEY;
|
|
23
|
+
if (isMobile) return `+55${digits}`;
|
|
24
|
+
return isCpf ? digits : null;
|
|
25
|
+
}
|