@greatapps/common 1.1.665 → 1.1.667

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 (106) hide show
  1. package/dist/components/account/ConfigurationsMyAccountModal.mjs +8 -6
  2. package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -1
  3. package/dist/components/account/ConfirmDeleteAccountModal.mjs +20 -19
  4. package/dist/components/account/ConfirmDeleteAccountModal.mjs.map +1 -1
  5. package/dist/components/account/ConfirmGlobalPreferencesModal.mjs +10 -11
  6. package/dist/components/account/ConfirmGlobalPreferencesModal.mjs.map +1 -1
  7. package/dist/components/account/DeleteAccountModal.mjs +34 -28
  8. package/dist/components/account/DeleteAccountModal.mjs.map +1 -1
  9. package/dist/components/account/DisableTwoFactorAuthModal.mjs +7 -5
  10. package/dist/components/account/DisableTwoFactorAuthModal.mjs.map +1 -1
  11. package/dist/components/account/IsntPossibleDeleteAccountModal.mjs +8 -9
  12. package/dist/components/account/IsntPossibleDeleteAccountModal.mjs.map +1 -1
  13. package/dist/components/account/TwoFactorAuthModal.mjs +22 -23
  14. package/dist/components/account/TwoFactorAuthModal.mjs.map +1 -1
  15. package/dist/components/account/hooks/useChangeEmailForm.mjs +5 -3
  16. package/dist/components/account/hooks/useChangeEmailForm.mjs.map +1 -1
  17. package/dist/components/account/hooks/useChangePasswordForm.mjs +26 -3
  18. package/dist/components/account/hooks/useChangePasswordForm.mjs.map +1 -1
  19. package/dist/components/account/hooks/useChangePhoneForm.mjs +18 -9
  20. package/dist/components/account/hooks/useChangePhoneForm.mjs.map +1 -1
  21. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs +26 -3
  22. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs.map +1 -1
  23. package/dist/components/account/hooks/useOtpVerification.mjs +19 -3
  24. package/dist/components/account/hooks/useOtpVerification.mjs.map +1 -1
  25. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs +27 -4
  26. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -1
  27. package/dist/components/account/sections/ChangeEmailModal.mjs +25 -28
  28. package/dist/components/account/sections/ChangeEmailModal.mjs.map +1 -1
  29. package/dist/components/account/sections/ChangePasswordSection.mjs +8 -6
  30. package/dist/components/account/sections/ChangePasswordSection.mjs.map +1 -1
  31. package/dist/components/account/sections/ChangePhoneModal.mjs +24 -27
  32. package/dist/components/account/sections/ChangePhoneModal.mjs.map +1 -1
  33. package/dist/components/account/sections/MyProfileSection.mjs +30 -21
  34. package/dist/components/account/sections/MyProfileSection.mjs.map +1 -1
  35. package/dist/components/account/sections/PreferencesSection.mjs +41 -23
  36. package/dist/components/account/sections/PreferencesSection.mjs.map +1 -1
  37. package/dist/components/account/sections/SecuritySection.mjs +11 -9
  38. package/dist/components/account/sections/SecuritySection.mjs.map +1 -1
  39. package/dist/components/account/sections/TokenSection.mjs +24 -8
  40. package/dist/components/account/sections/TokenSection.mjs.map +1 -1
  41. package/dist/components/embeds/CrispEmbed.mjs +9 -6
  42. package/dist/components/embeds/CrispEmbed.mjs.map +1 -1
  43. package/dist/i18n/config.mjs +13 -0
  44. package/dist/i18n/config.mjs.map +1 -0
  45. package/dist/i18n/country-language.mjs +74 -0
  46. package/dist/i18n/country-language.mjs.map +1 -0
  47. package/dist/i18n/messages/en-us.mjs +211 -0
  48. package/dist/i18n/messages/en-us.mjs.map +1 -0
  49. package/dist/i18n/messages/es-es.mjs +211 -0
  50. package/dist/i18n/messages/es-es.mjs.map +1 -0
  51. package/dist/i18n/messages/index.mjs +16 -0
  52. package/dist/i18n/messages/index.mjs.map +1 -0
  53. package/dist/i18n/messages/pt-br.mjs +216 -0
  54. package/dist/i18n/messages/pt-br.mjs.map +1 -0
  55. package/dist/i18n/normalize.mjs +12 -0
  56. package/dist/i18n/normalize.mjs.map +1 -0
  57. package/dist/i18n/resolve-locale.mjs +42 -0
  58. package/dist/i18n/resolve-locale.mjs.map +1 -0
  59. package/dist/i18n.mjs +14 -0
  60. package/dist/i18n.mjs.map +1 -0
  61. package/dist/providers/auth.provider.mjs +9 -1
  62. package/dist/providers/auth.provider.mjs.map +1 -1
  63. package/dist/server.mjs +2 -0
  64. package/dist/server.mjs.map +1 -1
  65. package/dist/utils/intl/cookie-domain.mjs +17 -0
  66. package/dist/utils/intl/cookie-domain.mjs.map +1 -0
  67. package/dist/utils/intl/locale-cookie.mjs +13 -0
  68. package/dist/utils/intl/locale-cookie.mjs.map +1 -0
  69. package/dist/utils/intl/locales.mjs +3 -2
  70. package/dist/utils/intl/locales.mjs.map +1 -1
  71. package/package.json +7 -1
  72. package/src/components/account/ConfigurationsMyAccountModal.tsx +10 -6
  73. package/src/components/account/ConfirmDeleteAccountModal.tsx +22 -13
  74. package/src/components/account/ConfirmGlobalPreferencesModal.tsx +12 -10
  75. package/src/components/account/DeleteAccountModal.tsx +34 -24
  76. package/src/components/account/DisableTwoFactorAuthModal.tsx +11 -6
  77. package/src/components/account/IsntPossibleDeleteAccountModal.tsx +8 -6
  78. package/src/components/account/TwoFactorAuthModal.tsx +33 -29
  79. package/src/components/account/hooks/useChangeEmailForm.tsx +5 -3
  80. package/src/components/account/hooks/useChangePasswordForm.tsx +20 -6
  81. package/src/components/account/hooks/useChangePhoneForm.tsx +16 -10
  82. package/src/components/account/hooks/useDisableTwoFactorAuthForm.tsx +20 -6
  83. package/src/components/account/hooks/useOtpVerification.tsx +16 -4
  84. package/src/components/account/hooks/useTwoFactorAuthForm.tsx +21 -7
  85. package/src/components/account/sections/ChangeEmailModal.tsx +35 -21
  86. package/src/components/account/sections/ChangePasswordSection.tsx +10 -6
  87. package/src/components/account/sections/ChangePhoneModal.tsx +50 -22
  88. package/src/components/account/sections/MyProfileSection.tsx +30 -22
  89. package/src/components/account/sections/PreferencesSection.tsx +45 -24
  90. package/src/components/account/sections/SecuritySection.tsx +19 -10
  91. package/src/components/account/sections/TokenSection.tsx +22 -10
  92. package/src/components/embeds/CrispEmbed.tsx +12 -8
  93. package/src/i18n/config.ts +17 -0
  94. package/src/i18n/country-language.ts +60 -0
  95. package/src/i18n/messages/en-us.ts +226 -0
  96. package/src/i18n/messages/es-es.ts +227 -0
  97. package/src/i18n/messages/index.ts +24 -0
  98. package/src/i18n/messages/pt-br.ts +236 -0
  99. package/src/i18n/normalize.ts +15 -0
  100. package/src/i18n/resolve-locale.ts +77 -0
  101. package/src/i18n.ts +10 -0
  102. package/src/providers/auth.provider.tsx +13 -1
  103. package/src/server.ts +2 -0
  104. package/src/utils/intl/cookie-domain.ts +29 -0
  105. package/src/utils/intl/locale-cookie.ts +18 -0
  106. package/src/utils/intl/locales.ts +3 -2
@@ -2,9 +2,13 @@
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
4
  import { useForm } from 'react-hook-form';
5
+ import { useTranslations, useLocale } from 'next-intl';
6
+ import { useRouter } from 'next/navigation';
5
7
  import { toast } from 'sonner';
6
8
  import { IconLock } from '@tabler/icons-react';
7
9
  import { cn } from '../../../infra/utils/clsx';
10
+ import type { Locale } from '../../../i18n/config';
11
+ import { setLocaleCookie } from '../../../utils/intl/locale-cookie';
8
12
  import { Button } from '../../ui/buttons/Button';
9
13
  import { Separator } from '../../ui/data-display/Separator';
10
14
  import { Toast } from '../../ui/feedback/Toast';
@@ -13,6 +17,7 @@ import { ComboboxField } from '../../ui/form/ComboboxField';
13
17
  import { SelectField } from '../../ui/form/SelectField';
14
18
  import ConfirmGlobalPreferencesModal from '../ConfirmGlobalPreferencesModal';
15
19
  import { useAuth } from '../../../providers/auth.provider';
20
+ import { useWhitelabel } from '../../../providers/whitelabel.provider';
16
21
  import { UserProfile } from '../../../modules/users/schema';
17
22
  import type { UpdateAccountRequest } from '../../../modules/accounts/types';
18
23
  import {
@@ -41,9 +46,21 @@ interface PreferencesSectionProps {
41
46
  }
42
47
 
43
48
  export function PreferencesSection({ onClose }: PreferencesSectionProps) {
49
+ // Padrão raiz: um único `translate` chamado pela chave inteira (`common.<modulo>.<chave>`).
50
+ const translate = useTranslations();
51
+ // Exceção: chave dinâmica (option.value) numa lista → namespace escopado.
52
+ const translateLanguage = useTranslations('common.languages');
53
+ const currentLocale = useLocale();
54
+ const router = useRouter();
44
55
  const { data: timezones = [] } = useTimezones();
45
56
  const { data: currencies = [] } = useCurrencies();
46
57
  const { user, account } = useAuth();
58
+ const { whitelabel } = useWhitelabel();
59
+ const languageOptions = LANGUAGE_OPTIONS.map((option) => ({
60
+ ...option,
61
+ label: translateLanguage(option.value as Locale),
62
+ displayValue: translateLanguage(option.value as Locale),
63
+ }));
47
64
  const isOwner = user?.profile === UserProfile.owner;
48
65
  const canEditPreferences = isOwner || user?.profile === UserProfile.admin;
49
66
  const updateAccountUser = useUpdateAccountUser();
@@ -74,7 +91,7 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
74
91
  companyName: account.name ?? '',
75
92
  language: LANGUAGE_OPTIONS.find(
76
93
  (o) => o.apiValue === user.language?.toLowerCase()
77
- )?.value ?? 'pt-BR',
94
+ )?.value ?? 'pt-br',
78
95
  currency: account.currency,
79
96
  timezone: account.timezone,
80
97
  timeFormat: '24h',
@@ -98,16 +115,20 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
98
115
  }
99
116
 
100
117
  reset(data);
101
- toast.custom((t) => (
102
- <Toast variant="success" message="Preferências salvas com sucesso" toastId={t} />
118
+ toast.custom((toastId) => (
119
+ <Toast variant="success" message={translate('common.preferences.savedSuccess')} toastId={toastId} />
103
120
  ));
104
121
  onClose();
122
+ if (data.language !== currentLocale) {
123
+ setLocaleCookie(data.language, whitelabel?.domain);
124
+ router.refresh();
125
+ }
105
126
  } catch (error) {
106
- toast.custom((t) => (
127
+ toast.custom((toastId) => (
107
128
  <Toast
108
129
  variant="error"
109
- message={error instanceof Error ? error.message : 'Erro ao salvar preferências'}
110
- toastId={t}
130
+ message={error instanceof Error ? error.message : translate('common.preferences.saveError')}
131
+ toastId={toastId}
111
132
  />
112
133
  ));
113
134
  } finally {
@@ -148,12 +169,12 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
148
169
  <>
149
170
  <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col h-full">
150
171
  <div className="overflow-y-auto overscroll-contain px-4 pb-4 lg:p-5 flex flex-col gap-5 lg:gap-6 flex-1">
151
- <span className="paragraph-medium-semibold text-gray-950">Preferências</span>
172
+ <span className="paragraph-medium-semibold text-gray-950">{translate('common.preferences.title')}</span>
152
173
  <div className="flex flex-col gap-8">
153
174
  <div className="flex flex-col lg:flex-row items-center gap-3">
154
175
  <FormField
155
- label="Nome da empresa"
156
- placeholder="Digite o nome da empresa"
176
+ label={translate('common.preferences.companyName')}
177
+ placeholder={translate('common.preferences.companyNamePlaceholder')}
157
178
  classnameContainer={cn('w-full', !isOwner && 'cursor-not-allowed opacity-50')}
158
179
  value={watch('companyName')}
159
180
  onChange={(e) => setValue('companyName', e.target.value, { shouldDirty: true })}
@@ -163,18 +184,18 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
163
184
 
164
185
  <div className="flex flex-col lg:flex-row items-center gap-3">
165
186
  <ComboboxField
166
- label="Idioma"
167
- placeholder="Selecione o idioma"
168
- searchPlaceholder="Buscar idioma..."
187
+ label={translate('common.preferences.language')}
188
+ placeholder={translate('common.preferences.languagePlaceholder')}
189
+ searchPlaceholder={translate('common.preferences.languageSearch')}
169
190
  containerClassName="w-full"
170
- options={LANGUAGE_OPTIONS}
191
+ options={languageOptions}
171
192
  value={watch('language')}
172
193
  onChange={(value) => setValue('language', value, { shouldDirty: true })}
173
194
  />
174
195
  <ComboboxField
175
- label="Moeda"
176
- placeholder="Selecione a moeda"
177
- searchPlaceholder="Buscar moeda..."
196
+ label={translate('common.preferences.currency')}
197
+ placeholder={translate('common.preferences.currencyPlaceholder')}
198
+ searchPlaceholder={translate('common.preferences.currencySearch')}
178
199
  containerClassName="w-full"
179
200
  options={currencies}
180
201
  value={watch('currency')}
@@ -187,12 +208,12 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
187
208
  <Separator />
188
209
 
189
210
  <div className="flex flex-col gap-4">
190
- <span className="paragraph-medium-semibold text-gray-950">Exibição hora</span>
211
+ <span className="paragraph-medium-semibold text-gray-950">{translate('common.preferences.timeDisplay')}</span>
191
212
  <div className="flex flex-col lg:flex-row items-center gap-3">
192
213
  <ComboboxField
193
- label="Fuso horário"
194
- placeholder="Selecione o fuso horário"
195
- searchPlaceholder="Buscar fuso horário..."
214
+ label={translate('common.preferences.timezone')}
215
+ placeholder={translate('common.preferences.timezonePlaceholder')}
216
+ searchPlaceholder={translate('common.preferences.timezoneSearch')}
196
217
  containerClassName="w-full"
197
218
  options={timezones}
198
219
  value={watch('timezone')}
@@ -201,8 +222,8 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
201
222
  disabled={!canEditPreferences}
202
223
  />
203
224
  <SelectField
204
- label="Formato hora"
205
- placeholder="Selecione o formato"
225
+ label={translate('common.preferences.timeFormat')}
226
+ placeholder={translate('common.preferences.timeFormatPlaceholder')}
206
227
  className="h-10!"
207
228
  containerClassName="w-full"
208
229
  options={TIME_FORMAT_OPTIONS}
@@ -230,10 +251,10 @@ export function PreferencesSection({ onClose }: PreferencesSectionProps) {
230
251
  onClose();
231
252
  }}
232
253
  >
233
- Cancelar
254
+ {translate('common.actions.cancel')}
234
255
  </Button>
235
256
  <Button className="h-10!" type="submit" disabled={isSaving}>
236
- {isSaving ? 'Salvando...' : 'Salvar alterações'}
257
+ {isSaving ? translate('common.actions.saving') : translate('common.actions.saveChanges')}
237
258
  </Button>
238
259
  </div>
239
260
  </form>
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { IconAlertTriangle, IconBrandGoogleFilled } from '@tabler/icons-react';
4
+ import { useTranslations } from 'next-intl';
4
5
  import { toast } from 'sonner';
5
6
  import { useQueryClient } from '@tanstack/react-query';
6
7
  import { useAuth } from '../../../providers/auth.provider';
@@ -18,6 +19,7 @@ interface SecuritySectionProps {
18
19
  export function SecuritySection({
19
20
  onDeleteAccount,
20
21
  }: SecuritySectionProps) {
22
+ const translate = useTranslations();
21
23
  const { user } = useAuth();
22
24
  const queryClient = useQueryClient();
23
25
  const { mutateAsync: unlinkGoogle, isPending: isUnlinking } = useUnlinkGoogle();
@@ -32,14 +34,16 @@ export function SecuritySection({
32
34
  queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });
33
35
  } catch (error) {
34
36
  const message =
35
- error instanceof Error ? error.message : 'Erro ao desvincular conta Google.';
36
- toast.custom((t) => <Toast variant="error" message={message} toastId={t} />);
37
+ error instanceof Error ? error.message : translate('common.account.security.unlinkError');
38
+ toast.custom((toastId) => <Toast variant="error" message={message} toastId={toastId} />);
37
39
  }
38
40
  };
39
41
 
40
42
  return (
41
43
  <div className="h-full overflow-y-auto overscroll-contain px-4 pb-20 lg:p-5 lg:pt-5 flex flex-col gap-5 lg:gap-6 lg:pb-31 lg:overscroll-auto">
42
- <span className="paragraph-medium-semibold text-gray-950 hidden lg:block">Segurança</span>
44
+ <span className="paragraph-medium-semibold text-gray-950 hidden lg:block">
45
+ {translate('common.account.security.title')}
46
+ </span>
43
47
 
44
48
  {hasGoogleLinked && (
45
49
  <div className="flex flex-col items-center lg:items-start gap-4">
@@ -48,10 +52,11 @@ export function SecuritySection({
48
52
  </div>
49
53
 
50
54
  <div className="flex flex-col gap-2">
51
- <span className="paragraph-medium-semibold text-gray-950">Conta Google vinculada</span>
55
+ <span className="paragraph-medium-semibold text-gray-950">
56
+ {translate('common.account.security.googleLinkedTitle')}
57
+ </span>
52
58
  <span className="paragraph-small-regular text-gray-600">
53
- Sua conta está vinculada ao Google. Se desvincular, use &quot;Esqueci minha senha&quot; para
54
- redefinir uma senha de acesso por e-mail.
59
+ {translate('common.account.security.googleLinkedDescription')}
55
60
  </span>
56
61
  </div>
57
62
 
@@ -61,7 +66,9 @@ export function SecuritySection({
61
66
  className="paragraph-small-semibold text-zinc-700 bg-zinc-100 rounded-lg px-3 py-2 cursor-pointer hover:bg-zinc-200 transition-colors w-fit disabled:opacity-50 disabled:cursor-not-allowed"
62
67
  onClick={handleUnlinkGoogle}
63
68
  >
64
- {isUnlinking ? 'Desvinculando...' : 'Desvincular Google'}
69
+ {isUnlinking
70
+ ? translate('common.account.security.unlinking')
71
+ : translate('common.account.security.unlinkGoogle')}
65
72
  </button>
66
73
  </div>
67
74
  )}
@@ -72,9 +79,11 @@ export function SecuritySection({
72
79
  </div>
73
80
 
74
81
  <div className="flex flex-col gap-2">
75
- <span className="paragraph-medium-semibold text-gray-950">Excluir conta</span>
82
+ <span className="paragraph-medium-semibold text-gray-950">
83
+ {translate('common.account.deleteAccount.title')}
84
+ </span>
76
85
  <span className="paragraph-small-regular text-gray-600">
77
- Esta ação é permanente e não pode ser desfeita. Todos os seus dados, projetos e configurações serão removidos definitivamente.
86
+ {translate('common.account.security.deleteAccountDescription')}
78
87
  </span>
79
88
  </div>
80
89
 
@@ -84,7 +93,7 @@ export function SecuritySection({
84
93
  className="paragraph-small-semibold text-red-600 bg-red-50 rounded-lg px-3 py-2 cursor-pointer hover:bg-red-100 transition-colors w-fit"
85
94
  onClick={onDeleteAccount}
86
95
  >
87
- Excluir minha conta
96
+ {translate('common.account.security.deleteMyAccount')}
88
97
  </button>
89
98
  )}
90
99
  </div>
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { IconCheck, IconCopy, IconInfoCircle } from '@tabler/icons-react';
4
+ import { useTranslations } from 'next-intl';
4
5
  import { toast } from 'sonner';
5
6
  import { Toast } from '../../ui/feedback/Toast';
6
7
  import { Button } from '../../ui/buttons/Button';
@@ -10,31 +11,42 @@ import { useAccountToken } from '../../../modules/accounts/hooks/use-account-tok
10
11
  import { useIsDefaultWhitelabel } from '../../../providers/whitelabel.provider';
11
12
 
12
13
  export function TokenSection() {
14
+ const translate = useTranslations();
13
15
  const { data: token = '', isLoading } = useAccountToken();
14
16
  const isDefaultWl = useIsDefaultWhitelabel();
15
17
 
16
18
  const { isCopied, copy } = useCopyToClipboard({
17
19
  onSuccess: () => {
18
- toast.custom((t) => (
19
- <Toast variant="success" message="Token copiado com sucesso" toastId={t} />
20
+ toast.custom((toastId) => (
21
+ <Toast
22
+ variant="success"
23
+ message={translate('common.account.token.copySuccess')}
24
+ toastId={toastId}
25
+ />
20
26
  ));
21
27
  },
22
28
  onError: () => {
23
- toast.custom((t) => (
24
- <Toast variant="error" message="Não foi possível copiar o token" toastId={t} />
29
+ toast.custom((toastId) => (
30
+ <Toast
31
+ variant="error"
32
+ message={translate('common.account.token.copyError')}
33
+ toastId={toastId}
34
+ />
25
35
  ));
26
36
  },
27
37
  });
28
38
 
29
39
  return (
30
40
  <div className="h-full overflow-y-auto overscroll-contain px-4 pb-20 lg:p-5 lg:pt-5 flex flex-col gap-5 lg:gap-6 lg:pb-31 lg:overscroll-auto">
31
- <span className="paragraph-medium-semibold text-gray-950">Token da conta</span>
41
+ <span className="paragraph-medium-semibold text-gray-950">
42
+ {translate('common.account.token.title')}
43
+ </span>
32
44
 
33
45
  <div className="flex flex-col gap-3">
34
46
  <div className="flex items-end gap-3">
35
47
  <FormField
36
- label="Token"
37
- value={isLoading ? 'Carregando...' : token}
48
+ label={translate('common.account.token.label')}
49
+ value={isLoading ? translate('common.actions.loading') : token}
38
50
  disabled
39
51
  readOnly
40
52
  classnameContainer="bg-gray-50 cursor-default opacity-100!"
@@ -48,14 +60,14 @@ export function TokenSection() {
48
60
  disabled={isLoading || !token}
49
61
  >
50
62
  {isCopied ? <IconCheck size={16} /> : <IconCopy size={16} />}
51
- {isCopied ? 'Copiado' : 'Copiar'}
63
+ {isCopied ? translate('common.actions.copied') : translate('common.actions.copy')}
52
64
  </Button>
53
65
  </div>
54
66
 
55
67
  <div className={`flex items-center gap-3 p-4 rounded-lg ${isDefaultWl ? 'bg-cyan-50' : 'bg-zinc-50'}`}>
56
68
  <IconInfoCircle className="size-4 text-zinc-950 shrink-0" />
57
69
  <span className="paragraph-small-regular text-zinc-950 max-w-[420px]">
58
- Este token é exclusivo da sua conta e permite acesso à API. Não compartilhe com terceiros.
70
+ {translate('common.account.token.info')}
59
71
  </span>
60
72
  </div>
61
73
 
@@ -65,7 +77,7 @@ export function TokenSection() {
65
77
  rel="noopener noreferrer"
66
78
  className="paragraph-small-medium text-gray-500 underline hover:text-gray-700 transition-colors w-fit"
67
79
  >
68
- Acessar a documentação
80
+ {translate('common.account.token.docsLink')}
69
81
  </a>
70
82
  </div>
71
83
  </div>
@@ -40,6 +40,10 @@ function crispSessionData(key: string, value: string) {
40
40
  safeCrispPush(['set', 'session:data', [[[key, value]]]]);
41
41
  }
42
42
 
43
+ function crispEvent(name: string, data?: Record<string, unknown>, color = 'blue') {
44
+ safeCrispPush(['set', 'session:event', [[[name, data ?? {}, color]]]]);
45
+ }
46
+
43
47
  // ── Public API ─────────────────────────────────────────────────
44
48
 
45
49
  export function openCrispHelpdesk() {
@@ -56,8 +60,8 @@ export function showCrisp() {
56
60
 
57
61
  const FREE_PLAN_ID = 5;
58
62
  const FREE_PLAN_SIGNUP_QUERY_FLAG = 'createNew';
59
- const FREE_PLAN_SIGNUP_SEGMENT = 'cadastro_plano_free';
60
- const FREE_PLAN_SIGNUP_TAGGED_KEY = 'crisp:free-plan-signup:tagged';
63
+ const FREE_PLAN_SIGNUP_EVENT = 'cadastro_plano_free';
64
+ const FREE_PLAN_SIGNUP_FIRED_KEY = 'crisp:free-plan-signup:fired';
61
65
 
62
66
  export function updateCrispUser(data: {
63
67
  email?: string;
@@ -173,17 +177,17 @@ export function CrispEmbed() {
173
177
  } catch {}
174
178
  }, []);
175
179
 
176
- // Marca o usuário com o segment de cadastro free para o trigger do Crisp.
180
+ // Dispara user event de cadastro free para o trigger do Crisp.
177
181
  // A mensagem em si é configurada lá no Crisp (Message Trigger com condição
178
- // session:segments contém 'cadastro_plano_free').
182
+ // "User Event = cadastro_plano_free").
179
183
  useEffect(() => {
180
184
  if (!user || !shouldLoadCrisp || !isFreshSignup) return;
181
185
  if (subscription?.id_plan !== FREE_PLAN_ID) return;
182
186
  try {
183
- const taggedKey = `${FREE_PLAN_SIGNUP_TAGGED_KEY}:${user.id}`;
184
- if (window.localStorage.getItem(taggedKey)) return;
185
- crispSegment(FREE_PLAN_SIGNUP_SEGMENT);
186
- window.localStorage.setItem(taggedKey, '1');
187
+ const firedKey = `${FREE_PLAN_SIGNUP_FIRED_KEY}:${user.id}`;
188
+ if (window.localStorage.getItem(firedKey)) return;
189
+ crispEvent(FREE_PLAN_SIGNUP_EVENT, { id_plan: FREE_PLAN_ID });
190
+ window.localStorage.setItem(firedKey, '1');
187
191
  } catch {}
188
192
  }, [user, shouldLoadCrisp, isFreshSignup, subscription?.id_plan]);
189
193
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Locales suportados pela plataforma GreatApps.
3
+ * Identificadores no padrão `idioma-REGIÃO` em minúsculas: `pt-br`, `en-us`, `es-es`.
4
+ */
5
+ export const LOCALES = ['pt-br', 'en-us', 'es-es'] as const;
6
+
7
+ export type Locale = (typeof LOCALES)[number];
8
+
9
+ /** Idioma padrão — fallback final (o produto nasce em pt-BR). */
10
+ export const DEFAULT_LOCALE: Locale = 'pt-br';
11
+
12
+ /** Nome do cookie onde o locale resolvido é persistido (convenção next-intl). */
13
+ export const LOCALE_COOKIE = 'NEXT_LOCALE';
14
+
15
+ export function isLocale(value: unknown): value is Locale {
16
+ return typeof value === 'string' && (LOCALES as readonly string[]).includes(value);
17
+ }
@@ -0,0 +1,60 @@
1
+ import { Locale } from './config';
2
+
3
+ /**
4
+ * Países de língua portuguesa (ISO 3166-1 alpha-2).
5
+ */
6
+ const PORTUGUESE_COUNTRIES = new Set<string>([
7
+ 'BR', // Brasil
8
+ 'PT', // Portugal
9
+ 'AO', // Angola
10
+ 'MZ', // Moçambique
11
+ 'CV', // Cabo Verde
12
+ 'GW', // Guiné-Bissau
13
+ 'ST', // São Tomé e Príncipe
14
+ 'TL', // Timor-Leste
15
+ ]);
16
+
17
+ /**
18
+ * Países de língua espanhola (ISO 3166-1 alpha-2).
19
+ */
20
+ const SPANISH_COUNTRIES = new Set<string>([
21
+ 'ES', // Espanha
22
+ 'MX', // México
23
+ 'AR', // Argentina
24
+ 'CO', // Colômbia
25
+ 'PE', // Peru
26
+ 'VE', // Venezuela
27
+ 'CL', // Chile
28
+ 'EC', // Equador
29
+ 'GT', // Guatemala
30
+ 'CU', // Cuba
31
+ 'BO', // Bolívia
32
+ 'DO', // República Dominicana
33
+ 'HN', // Honduras
34
+ 'PY', // Paraguai
35
+ 'SV', // El Salvador
36
+ 'NI', // Nicarágua
37
+ 'CR', // Costa Rica
38
+ 'PA', // Panamá
39
+ 'UY', // Uruguai
40
+ 'PR', // Porto Rico
41
+ 'GQ', // Guiné Equatorial
42
+ ]);
43
+
44
+ /**
45
+ * Infere o idioma a partir do código de país (ex.: header `CF-IPCountry` da Cloudflare
46
+ * ou `session.location.country` do JWT):
47
+ * - país lusófono → `pt`
48
+ * - país hispanófono → `es`
49
+ * - demais países → `en`
50
+ *
51
+ * Retorna `null` quando o país é desconhecido/ausente (deixa o fallback de locale decidir).
52
+ */
53
+ export function localeFromCountry(country?: string | null): Locale | null {
54
+ if (!country) return null;
55
+ const code = country.trim().toUpperCase();
56
+ if (code === 'XX' || code === 'T1') return null; // Cloudflare: desconhecido / Tor
57
+ if (PORTUGUESE_COUNTRIES.has(code)) return 'pt-br';
58
+ if (SPANISH_COUNTRIES.has(code)) return 'es-es';
59
+ return 'en-us';
60
+ }