@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.
Files changed (40) hide show
  1. package/dist/components/account/sections/affiliates/ReceivingAccountModal.mjs +52 -18
  2. package/dist/components/account/sections/affiliates/ReceivingAccountModal.mjs.map +1 -1
  3. package/dist/components/account/sections/subscription/PixDetailsDialog.mjs +4 -6
  4. package/dist/components/account/sections/subscription/PixDetailsDialog.mjs.map +1 -1
  5. package/dist/components/account/sections/subscription/PixPaymentDialog.mjs +17 -24
  6. package/dist/components/account/sections/subscription/PixPaymentDialog.mjs.map +1 -1
  7. package/dist/hooks/useSecondsUntil.mjs +17 -0
  8. package/dist/hooks/useSecondsUntil.mjs.map +1 -0
  9. package/dist/i18n/messages/en-us.mjs +7 -0
  10. package/dist/i18n/messages/en-us.mjs.map +1 -1
  11. package/dist/i18n/messages/es-es.mjs +7 -0
  12. package/dist/i18n/messages/es-es.mjs.map +1 -1
  13. package/dist/i18n/messages/pt-br.mjs +7 -0
  14. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  15. package/dist/index.mjs +6 -1
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/modules/subscriptions/types/pending-pix-status.type.mjs +1 -0
  18. package/dist/modules/subscriptions/types/pending-pix-status.type.mjs.map +1 -1
  19. package/dist/testing/factories/affiliate.factory.mjs +24 -0
  20. package/dist/testing/factories/affiliate.factory.mjs.map +1 -0
  21. package/dist/testing/factories/index.mjs +2 -0
  22. package/dist/testing/factories/index.mjs.map +1 -1
  23. package/dist/utils/format/masks.mjs +6 -1
  24. package/dist/utils/format/masks.mjs.map +1 -1
  25. package/dist/utils/validators/pix-key.mjs +27 -0
  26. package/dist/utils/validators/pix-key.mjs.map +1 -0
  27. package/package.json +10 -9
  28. package/src/components/account/sections/affiliates/ReceivingAccountModal.tsx +71 -20
  29. package/src/components/account/sections/subscription/PixDetailsDialog.tsx +4 -9
  30. package/src/components/account/sections/subscription/PixPaymentDialog.tsx +18 -29
  31. package/src/hooks/useSecondsUntil.ts +17 -0
  32. package/src/i18n/messages/en-us.ts +8 -0
  33. package/src/i18n/messages/es-es.ts +8 -0
  34. package/src/i18n/messages/pt-br.ts +8 -0
  35. package/src/index.ts +2 -0
  36. package/src/modules/subscriptions/types/pending-pix-status.type.ts +1 -0
  37. package/src/testing/factories/affiliate.factory.ts +22 -0
  38. package/src/testing/factories/index.ts +1 -0
  39. package/src/utils/format/masks.ts +6 -2
  40. 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 useCountdownTimer from '../../../../hooks/useCountdownTimer';
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 expiresInSeconds =
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
- {shouldShowCountdown && (
183
+ {!!secondsUntilExpiry && (
189
184
  <p className="paragraph-small-semibold text-zinc-950 text-center">
190
- {translate('payIn')} <span className="text-red-600">{formatCountdown(seconds)}</span>
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 useCountdownTimer from '../../../../hooks/useCountdownTimer';
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 isPaid = phase === 'completed';
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 (!isPaid || paidNotifiedRef.current) return;
83
+ if (!isCompleted || paidNotifiedRef.current) return;
90
84
  paidNotifiedRef.current = true;
91
85
  toast.custom((toastId) => (
92
- <Toast variant="success" message={translate('paidToast')} toastId={toastId} />
86
+ <Toast variant="success" message={completedMessage} toastId={toastId} />
93
87
  ));
94
- }, [isPaid, translate]);
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 expiresInSeconds =
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
- {isPaid || isRejected || isExpired ? (
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
- isPaid ? 'bg-green-50' : 'bg-red-50'
138
+ isCompleted ? 'bg-green-50' : 'bg-red-50'
150
139
  )}
151
140
  >
152
- {isPaid ? (
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
- {isPaid
162
- ? translate('paidTitle')
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
- {isPaid ? translate('paidToast') : translate('rejectedOrExpiredDescription')}
157
+ {isCompleted ? completedMessage : translate('rejectedOrExpiredDescription')}
169
158
  </span>
170
159
  </div>
171
160
 
172
- {isPaid ? (
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
- {shouldShowCountdown && (
181
+ {!!secondsUntilExpiry && (
193
182
  <p className="paragraph-small-semibold text-zinc-950 text-center">
194
- {translate('payIn')} <span className="text-red-600">{formatCountdown(seconds)}</span>
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
- /** Chave PIX: só mascara quando a chave é o próprio CPF/CNPJ; e-mail, telefone e aleatória saem como estão. */
96
- export const formatPixKey = formatIfCpfCnpjShaped;
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
+ }