@incodetech/web 2.0.0-alpha.1 → 2.0.0-alpha.3

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 (212) hide show
  1. package/dist/asset-manifest.json +18 -0
  2. package/dist/browser-ponyfill-B6W6hHVY.js +344 -0
  3. package/dist/button-DeMZ_34N.js +266 -0
  4. package/dist/button.css +335 -0
  5. package/dist/email/email.es.js +145 -0
  6. package/dist/email/styles.css +123 -0
  7. package/dist/flow/flow.es.js +9 -0
  8. package/dist/flow/styles.css +204 -0
  9. package/dist/flow-ZK6OBsa3.js +568 -0
  10. package/dist/incodeModule-BF5MX9GT.js +243 -0
  11. package/dist/incodeModule.css +119 -0
  12. package/dist/index.es.js +101 -0
  13. package/dist/otpInput-BUrV4IAF.js +151 -0
  14. package/dist/otpInput.css +167 -0
  15. package/dist/phone/phone.es.js +3442 -0
  16. package/dist/phone/styles.css +305 -0
  17. package/dist/selfie/selfie.es.js +893 -0
  18. package/dist/selfie/styles.css +1163 -0
  19. package/dist/selfieTutorial-C-u5GufD.js +29 -0
  20. package/dist/styles/core.css +1013 -0
  21. package/dist/styles/tokens.css +543 -0
  22. package/dist/successIcon.css +4 -0
  23. package/dist/title-ng7q7YDh.js +2161 -0
  24. package/dist/title.css +38 -0
  25. package/dist/types/core.d.ts +1 -0
  26. package/dist/types/email/email.d.ts +2 -0
  27. package/dist/types/email.d.ts +50 -0
  28. package/dist/types/flow/flow.d.ts +2 -0
  29. package/dist/types/flow.d.ts +46 -0
  30. package/dist/types/index.d.ts +223 -0
  31. package/dist/types/phone/phone.d.ts +2 -0
  32. package/dist/types/phone.d.ts +51 -0
  33. package/dist/types/selfie/selfie.d.ts +2 -0
  34. package/dist/types/selfie.d.ts +24 -0
  35. package/dist/types/styles/core.d.ts +1 -0
  36. package/dist/vendor-preact-CK0WeTOR.js +584 -0
  37. package/package.json +20 -17
  38. package/dev/README.md +0 -163
  39. package/dev/getToken.ts +0 -36
  40. package/dev/headless.html +0 -875
  41. package/dev/index.html +0 -366
  42. package/dev/main-headless.tsx +0 -1332
  43. package/dev/main-orchestrated-flow.tsx +0 -1158
  44. package/dev/main-preact.tsx +0 -323
  45. package/dev/main-simplified.tsx +0 -123
  46. package/dev/main-web-component.tsx +0 -256
  47. package/dev/main.tsx +0 -332
  48. package/dev/manual.html +0 -27
  49. package/dev/orchestrated-flow.html +0 -64
  50. package/dev/simplified.html +0 -64
  51. package/dev/tiktok-logo.svg +0 -7
  52. package/src/defineCustomElement.tsx +0 -30
  53. package/src/email/email.test.tsx +0 -368
  54. package/src/email/email.tsx +0 -255
  55. package/src/email/emailInput.test.tsx +0 -264
  56. package/src/email/emailInput.tsx +0 -85
  57. package/src/email/styles.css +0 -59
  58. package/src/flow/flow.test.tsx +0 -796
  59. package/src/flow/flow.tsx +0 -292
  60. package/src/flow/flowCompleted.css +0 -30
  61. package/src/flow/flowCompleted.test.tsx +0 -331
  62. package/src/flow/flowCompleted.tsx +0 -121
  63. package/src/flow/flowInit.test.ts +0 -264
  64. package/src/flow/flowInit.ts +0 -94
  65. package/src/flow/flowStart.css +0 -58
  66. package/src/flow/flowStart.test.tsx +0 -49
  67. package/src/flow/flowStart.tsx +0 -41
  68. package/src/flow/incode-logo.svg +0 -8
  69. package/src/flow/index.ts +0 -7
  70. package/src/flow/preloadFlow.test.ts +0 -421
  71. package/src/flow/preloadFlow.ts +0 -171
  72. package/src/flow/styles.css +0 -9
  73. package/src/flow/unsupportedModule.css +0 -21
  74. package/src/flow/unsupportedModule.tsx +0 -39
  75. package/src/flow/useFlowInitialization.test.tsx +0 -292
  76. package/src/flow/useFlowInitialization.ts +0 -128
  77. package/src/flow/useModuleLoader.test.tsx +0 -212
  78. package/src/flow/useModuleLoader.ts +0 -92
  79. package/src/hooks/index.ts +0 -1
  80. package/src/hooks/useManager.test.ts +0 -91
  81. package/src/hooks/useManager.ts +0 -40
  82. package/src/i18n/index.ts +0 -3
  83. package/src/i18n/instance.ts +0 -16
  84. package/src/i18n/setup.ts +0 -184
  85. package/src/i18n/useTranslation.ts +0 -42
  86. package/src/index.ts +0 -27
  87. package/src/permissions/assets/android-dots-icon.svg +0 -7
  88. package/src/permissions/assets/android-settings-icon.svg +0 -16
  89. package/src/permissions/assets/android-toggle-icon.svg +0 -20
  90. package/src/permissions/assets/bank-card-icon.svg +0 -14
  91. package/src/permissions/assets/camera-icon.svg +0 -12
  92. package/src/permissions/assets/camera-ios.svg +0 -53
  93. package/src/permissions/assets/check-icon.svg +0 -8
  94. package/src/permissions/assets/chrome-icon.svg +0 -43
  95. package/src/permissions/assets/password-icon.svg +0 -11
  96. package/src/permissions/assets/permissions-img.svg +0 -51
  97. package/src/permissions/assets/safari-icon.svg +0 -37
  98. package/src/permissions/assets/settings-icon.svg +0 -33
  99. package/src/permissions/assets/toggle-icon.svg +0 -19
  100. package/src/permissions/assets/warning-icon.svg +0 -6
  101. package/src/permissions/boldWithArrow.css +0 -9
  102. package/src/permissions/boldWithArrow.tsx +0 -41
  103. package/src/permissions/denied.css +0 -37
  104. package/src/permissions/denied.tsx +0 -29
  105. package/src/permissions/deniedAndroid.tsx +0 -56
  106. package/src/permissions/deniedDesktop.css +0 -9
  107. package/src/permissions/deniedDesktop.tsx +0 -64
  108. package/src/permissions/deniedIOS.tsx +0 -73
  109. package/src/permissions/deniedInstructions.tsx +0 -19
  110. package/src/permissions/iconWrapper.css +0 -9
  111. package/src/permissions/iconWrapper.tsx +0 -15
  112. package/src/permissions/learnMore.css +0 -37
  113. package/src/permissions/learnMore.tsx +0 -85
  114. package/src/permissions/numberedStep.css +0 -13
  115. package/src/permissions/numberedStep.tsx +0 -14
  116. package/src/permissions/permissions.css +0 -13
  117. package/src/permissions/permissions.tsx +0 -68
  118. package/src/phone/phone.tsx +0 -246
  119. package/src/phone/phoneInput.test.tsx +0 -275
  120. package/src/phone/phoneInput.tsx +0 -249
  121. package/src/phone/styles.css +0 -158
  122. package/src/selfie/cameraButton.css +0 -13
  123. package/src/selfie/cameraButton.tsx +0 -35
  124. package/src/selfie/capture.css +0 -57
  125. package/src/selfie/capture.tsx +0 -232
  126. package/src/selfie/errorModal.tsx +0 -218
  127. package/src/selfie/errorModalContent.css +0 -33
  128. package/src/selfie/errorModalContent.tsx +0 -44
  129. package/src/selfie/faceOutline.css +0 -5
  130. package/src/selfie/faceOutline.tsx +0 -22
  131. package/src/selfie/loadingBorder.css +0 -12
  132. package/src/selfie/loadingBorder.tsx +0 -77
  133. package/src/selfie/manualCaptureButton.css +0 -13
  134. package/src/selfie/manualCaptureButton.tsx +0 -35
  135. package/src/selfie/noMoreAttemptsModal.tsx +0 -44
  136. package/src/selfie/notification.css +0 -9
  137. package/src/selfie/notification.tsx +0 -36
  138. package/src/selfie/retryErrorModal.tsx +0 -56
  139. package/src/selfie/selfie.test.tsx +0 -458
  140. package/src/selfie/selfie.tsx +0 -83
  141. package/src/selfie/selfieTutorial.json +0 -2626
  142. package/src/selfie/styles.css +0 -1
  143. package/src/selfie/tutorial.test.tsx +0 -200
  144. package/src/selfie/tutorial.tsx +0 -43
  145. package/src/setup.ts +0 -33
  146. package/src/shared/baseTutorial/baseTutorial.css +0 -21
  147. package/src/shared/baseTutorial/baseTutorial.test.tsx +0 -184
  148. package/src/shared/baseTutorial/baseTutorial.tsx +0 -55
  149. package/src/shared/baseTutorial/replaceBaseTutorial.test.ts +0 -267
  150. package/src/shared/baseTutorial/replaceBaseTutorial.ts +0 -68
  151. package/src/shared/button/button.css +0 -55
  152. package/src/shared/button/button.test.tsx +0 -101
  153. package/src/shared/button/button.tsx +0 -47
  154. package/src/shared/componentRoot/incodeComponent.tsx +0 -12
  155. package/src/shared/countries/countries.test.ts +0 -75
  156. package/src/shared/countries/countries.ts +0 -139
  157. package/src/shared/countries/index.ts +0 -6
  158. package/src/shared/icons/chevronDown.tsx +0 -22
  159. package/src/shared/icons/index.ts +0 -2
  160. package/src/shared/icons/successIcon.css +0 -5
  161. package/src/shared/icons/successIcon.test.tsx +0 -40
  162. package/src/shared/icons/successIcon.tsx +0 -26
  163. package/src/shared/loader/loadingIcon.css +0 -28
  164. package/src/shared/loader/loadingIcon.tsx +0 -67
  165. package/src/shared/lottie/lottie.tsx +0 -108
  166. package/src/shared/otpInput/otpInput.css +0 -85
  167. package/src/shared/otpInput/otpInput.test.tsx +0 -356
  168. package/src/shared/otpInput/otpInput.tsx +0 -241
  169. package/src/shared/page/incode-logo.svg +0 -3
  170. package/src/shared/page/page.css +0 -47
  171. package/src/shared/page/page.test.tsx +0 -97
  172. package/src/shared/page/page.tsx +0 -91
  173. package/src/shared/page/pageUiConfig.test.ts +0 -112
  174. package/src/shared/page/pageUiConfig.ts +0 -64
  175. package/src/shared/page/verifiedByIncode.css +0 -5
  176. package/src/shared/page/verifiedByIncode.tsx +0 -75
  177. package/src/shared/spacer/spacer.css +0 -149
  178. package/src/shared/spacer/spacer.test.tsx +0 -143
  179. package/src/shared/spacer/spacer.tsx +0 -88
  180. package/src/shared/spinner/index.ts +0 -2
  181. package/src/shared/spinner/spinner.css +0 -28
  182. package/src/shared/spinner/spinner.test.tsx +0 -82
  183. package/src/shared/spinner/spinner.tsx +0 -65
  184. package/src/shared/title/title.css +0 -7
  185. package/src/shared/title/title.tsx +0 -12
  186. package/src/shared/uiConfig/uiConfig.ts +0 -36
  187. package/src/shared/webComponent/incodeModule.ts +0 -29
  188. package/src/shared/webComponent/registerIncodeElement.ts +0 -15
  189. package/src/styles/__mocks__/fetchTheme.ts +0 -19
  190. package/src/styles/applyTheme.ts +0 -37
  191. package/src/styles/cn.test.tsx +0 -57
  192. package/src/styles/cn.tsx +0 -21
  193. package/src/styles/core.css +0 -12
  194. package/src/styles/fetchTheme.test.ts +0 -390
  195. package/src/styles/fetchTheme.ts +0 -88
  196. package/src/styles/generatePalette.ts +0 -111
  197. package/src/styles/reset.css +0 -65
  198. package/src/styles/resolveCssVariableToHex.ts +0 -28
  199. package/src/styles/tailwind.css +0 -291
  200. package/src/styles/themeTypes.ts +0 -18
  201. package/src/styles/tokens/colors.css +0 -190
  202. package/src/styles/tokens/components.css +0 -174
  203. package/src/styles/tokens/index.css +0 -4
  204. package/src/styles/tokens/primitives.css +0 -129
  205. package/src/styles/tokens/semantic.css +0 -51
  206. package/src/svg.d.ts +0 -4
  207. package/src/types/assets.d.ts +0 -1
  208. package/src/types/custom-elements.d.ts +0 -104
  209. package/tsconfig.json +0 -22
  210. package/vite.config.ts +0 -260
  211. package/vitest.config.ts +0 -40
  212. package/vitest.setup.ts +0 -16
@@ -1,275 +0,0 @@
1
- import { fireEvent, render, screen } from '@testing-library/preact';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { PhoneInput } from './phoneInput';
4
-
5
- describe('PhoneInput', () => {
6
- const defaultProps = {
7
- countryCode: 'US',
8
- phonePrefix: '+1',
9
- onPhoneChange: vi.fn(),
10
- };
11
-
12
- beforeEach(() => {
13
- vi.clearAllMocks();
14
- });
15
-
16
- describe('Rendering', () => {
17
- it('renders phone input field', () => {
18
- render(<PhoneInput {...defaultProps} />);
19
-
20
- const input = screen.getByTestId('phone-input');
21
- expect(input).toBeTruthy();
22
- });
23
-
24
- it('renders country selector button', () => {
25
- render(<PhoneInput {...defaultProps} />);
26
-
27
- const selector = screen.getByTestId('phone-country-selector');
28
- expect(selector).toBeTruthy();
29
- });
30
-
31
- it('renders with initial country code', () => {
32
- render(<PhoneInput {...defaultProps} countryCode="MX" />);
33
-
34
- const selector = screen.getByTestId('phone-country-selector');
35
- expect(selector.textContent).toContain('+52');
36
- });
37
-
38
- it('renders US flag emoji for US country', () => {
39
- render(<PhoneInput {...defaultProps} countryCode="US" />);
40
-
41
- const selector = screen.getByTestId('phone-country-selector');
42
- expect(selector.textContent).toContain('🇺🇸');
43
- });
44
- });
45
-
46
- describe('Phone input', () => {
47
- it('calls onPhoneChange when input changes', () => {
48
- const onPhoneChange = vi.fn();
49
- render(<PhoneInput {...defaultProps} onPhoneChange={onPhoneChange} />);
50
-
51
- const input = screen.getByTestId('phone-input');
52
- fireEvent.input(input, { target: { value: '4155551234' } });
53
-
54
- expect(onPhoneChange).toHaveBeenCalled();
55
- });
56
-
57
- it('formats phone number as user types', () => {
58
- render(<PhoneInput {...defaultProps} />);
59
-
60
- const input = screen.getByTestId('phone-input') as HTMLInputElement;
61
- fireEvent.input(input, { target: { value: '4155551234' } });
62
-
63
- // Should be formatted with AsYouType
64
- expect(input.value).toMatch(/[\d\s()-]+/);
65
- });
66
-
67
- it('validates phone number and reports validity', () => {
68
- const onPhoneChange = vi.fn();
69
- render(<PhoneInput {...defaultProps} onPhoneChange={onPhoneChange} />);
70
-
71
- const input = screen.getByTestId('phone-input');
72
-
73
- // Invalid phone (too short)
74
- fireEvent.input(input, { target: { value: '123' } });
75
- expect(onPhoneChange).toHaveBeenLastCalledWith(expect.any(String), false);
76
-
77
- // Valid US phone
78
- fireEvent.input(input, { target: { value: '4155551234' } });
79
- expect(onPhoneChange).toHaveBeenLastCalledWith(expect.any(String), true);
80
- });
81
- });
82
-
83
- describe('Country selector', () => {
84
- it('opens dropdown on click', () => {
85
- render(<PhoneInput {...defaultProps} />);
86
-
87
- const selector = screen.getByTestId('phone-country-selector');
88
- fireEvent.click(selector);
89
-
90
- // Dropdown should be visible
91
- const dropdown = document.querySelector('.IncodePhoneCountryDropdown');
92
- expect(dropdown).toBeTruthy();
93
- });
94
-
95
- it('closes dropdown when clicking outside', () => {
96
- render(<PhoneInput {...defaultProps} />);
97
-
98
- const selector = screen.getByTestId('phone-country-selector');
99
- fireEvent.click(selector);
100
-
101
- // Click outside
102
- fireEvent.mouseDown(document.body);
103
-
104
- const dropdown = document.querySelector('.IncodePhoneCountryDropdown');
105
- expect(dropdown).toBeFalsy();
106
- });
107
-
108
- it('shows list of countries in dropdown', () => {
109
- render(<PhoneInput {...defaultProps} />);
110
-
111
- const selector = screen.getByTestId('phone-country-selector');
112
- fireEvent.click(selector);
113
-
114
- const options = document.querySelectorAll('.IncodePhoneCountryOption');
115
- expect(options.length).toBeGreaterThan(0);
116
- });
117
-
118
- it('changes country when option is selected', () => {
119
- const onPhoneChange = vi.fn();
120
- render(<PhoneInput {...defaultProps} onPhoneChange={onPhoneChange} />);
121
-
122
- const selector = screen.getByTestId('phone-country-selector');
123
- fireEvent.click(selector);
124
-
125
- // Find and click Mexico option
126
- const options = document.querySelectorAll('.IncodePhoneCountryOption');
127
- const mxOption = Array.from(options).find((opt) =>
128
- opt.textContent?.includes('Mexico'),
129
- );
130
-
131
- if (mxOption) {
132
- fireEvent.click(mxOption);
133
- }
134
-
135
- // Dropdown should close and country should change
136
- expect(selector.textContent).toContain('+52');
137
- });
138
-
139
- it('does not open dropdown when disabled', () => {
140
- render(<PhoneInput {...defaultProps} disabled />);
141
-
142
- const selector = screen.getByTestId('phone-country-selector');
143
- fireEvent.click(selector);
144
-
145
- const dropdown = document.querySelector('.IncodePhoneCountryDropdown');
146
- expect(dropdown).toBeFalsy();
147
- });
148
- });
149
-
150
- describe('Prefilled phone', () => {
151
- it('populates input with prefilled phone', () => {
152
- render(<PhoneInput {...defaultProps} prefilledPhone="+14155551234" />);
153
-
154
- const input = screen.getByTestId('phone-input') as HTMLInputElement;
155
- expect(input.value).toBeTruthy();
156
- });
157
-
158
- it('calls onPhoneChange with prefilled phone', () => {
159
- const onPhoneChange = vi.fn();
160
- render(
161
- <PhoneInput
162
- {...defaultProps}
163
- prefilledPhone="+14155551234"
164
- onPhoneChange={onPhoneChange}
165
- />,
166
- );
167
-
168
- // Should have called onPhoneChange with the prefilled value
169
- expect(onPhoneChange).toHaveBeenCalledWith('+14155551234', true);
170
- });
171
- });
172
-
173
- describe('Error state', () => {
174
- it('displays error message when phoneError is set', () => {
175
- render(
176
- <PhoneInput {...defaultProps} phoneError="Invalid phone number" />,
177
- );
178
-
179
- expect(screen.getByTestId('phone-error')).toBeTruthy();
180
- expect(screen.getByText('Invalid phone number')).toBeTruthy();
181
- });
182
-
183
- it('shows error styling on input wrapper', () => {
184
- const { container } = render(
185
- <PhoneInput {...defaultProps} phoneError="Error" />,
186
- );
187
-
188
- const wrapper = container.querySelector('.IncodePhoneInputWrapperError');
189
- expect(wrapper).toBeTruthy();
190
- });
191
- });
192
-
193
- describe('Opt-in checkbox', () => {
194
- it('does not show opt-in when optinEnabled is false', () => {
195
- render(<PhoneInput {...defaultProps} optinEnabled={false} />);
196
-
197
- expect(screen.queryByTestId('phone-optin')).toBeNull();
198
- });
199
-
200
- it('shows opt-in checkbox when optinEnabled is true', () => {
201
- render(<PhoneInput {...defaultProps} optinEnabled />);
202
-
203
- expect(screen.getByTestId('phone-optin')).toBeTruthy();
204
- });
205
-
206
- it('calls onOptInChange when checkbox is toggled', () => {
207
- const onOptInChange = vi.fn();
208
- render(
209
- <PhoneInput
210
- {...defaultProps}
211
- optinEnabled
212
- onOptInChange={onOptInChange}
213
- />,
214
- );
215
-
216
- const checkbox = screen.getByTestId('phone-optin');
217
- fireEvent.change(checkbox, { target: { checked: true } });
218
-
219
- expect(onOptInChange).toHaveBeenCalledWith(true);
220
- });
221
- });
222
-
223
- describe('Disabled state', () => {
224
- it('disables input when disabled prop is true', () => {
225
- render(<PhoneInput {...defaultProps} disabled />);
226
-
227
- const input = screen.getByTestId('phone-input') as HTMLInputElement;
228
- expect(input.disabled).toBe(true);
229
- });
230
-
231
- it('disables country selector when disabled', () => {
232
- render(<PhoneInput {...defaultProps} disabled />);
233
-
234
- const button = screen.getByTestId('phone-country-selector');
235
- expect(button.hasAttribute('disabled')).toBe(true);
236
- });
237
-
238
- it('shows disabled styling', () => {
239
- const { container } = render(<PhoneInput {...defaultProps} disabled />);
240
-
241
- const wrapper = container.querySelector(
242
- '.IncodePhoneInputWrapperDisabled',
243
- );
244
- expect(wrapper).toBeTruthy();
245
- });
246
- });
247
-
248
- describe('Accessibility', () => {
249
- it('country selector button is accessible', () => {
250
- render(<PhoneInput {...defaultProps} />);
251
-
252
- // Button should be accessible - it contains flag and country code
253
- const button = screen.getByTestId('phone-country-selector');
254
- expect(button.tagName.toLowerCase()).toBe('button');
255
- expect(button.textContent).toContain('+1'); // Country code is visible
256
- });
257
-
258
- it('has aria-expanded attribute on country selector', () => {
259
- render(<PhoneInput {...defaultProps} />);
260
-
261
- const selector = screen.getByTestId('phone-country-selector');
262
- expect(selector.getAttribute('aria-expanded')).toBe('false');
263
-
264
- fireEvent.click(selector);
265
- expect(selector.getAttribute('aria-expanded')).toBe('true');
266
- });
267
-
268
- it('has aria-haspopup attribute on country selector', () => {
269
- render(<PhoneInput {...defaultProps} />);
270
-
271
- const selector = screen.getByTestId('phone-country-selector');
272
- expect(selector.getAttribute('aria-haspopup')).toBe('listbox');
273
- });
274
- });
275
- });
@@ -1,249 +0,0 @@
1
- import {
2
- AsYouType,
3
- type CountryCode,
4
- parsePhoneNumberFromString,
5
- } from 'libphonenumber-js';
6
- import type { FC } from 'preact/compat';
7
- import { useEffect, useRef, useState } from 'preact/hooks';
8
- import {
9
- type CountryData,
10
- countries,
11
- getCountryByCode,
12
- } from '../shared/countries';
13
- import { ChevronDown } from '../shared/icons';
14
- import { Spacer } from '../shared/spacer/spacer';
15
- import './styles.css';
16
-
17
- export type PhoneInputProps = {
18
- countryCode: string;
19
- phonePrefix: string;
20
- prefilledPhone?: string;
21
- phoneError?: string;
22
- optinEnabled?: boolean;
23
- disabled?: boolean;
24
- onPhoneChange: (phone: string, isValid: boolean) => void;
25
- onOptInChange?: (granted: boolean) => void;
26
- };
27
-
28
- export const PhoneInput: FC<PhoneInputProps> = ({
29
- countryCode,
30
- prefilledPhone,
31
- phoneError,
32
- optinEnabled = false,
33
- disabled = false,
34
- onPhoneChange,
35
- onOptInChange,
36
- }) => {
37
- const [selectedCountry, setSelectedCountry] = useState<CountryData | null>(
38
- null,
39
- );
40
- const [isDropdownOpen, setIsDropdownOpen] = useState(false);
41
- const [phone, setPhone] = useState('');
42
- const [optInGranted, setOptInGranted] = useState(false);
43
- const dropdownRef = useRef<HTMLDivElement>(null);
44
-
45
- // Initialize selected country from countryCode prop
46
- useEffect(() => {
47
- const country = getCountryByCode(countryCode);
48
- if (country) {
49
- setSelectedCountry(country);
50
- } else {
51
- // Default to US if not found
52
- setSelectedCountry(
53
- countries.find((c) => c.code === 'US') || countries[0],
54
- );
55
- }
56
- }, [countryCode]);
57
-
58
- // Handle prefilled phone
59
- useEffect(() => {
60
- if (prefilledPhone && selectedCountry) {
61
- // Parse the prefilled phone to extract national number
62
- const phoneToFormat = prefilledPhone.startsWith('+')
63
- ? prefilledPhone
64
- : `+${prefilledPhone}`;
65
- const parsed = parsePhoneNumberFromString(phoneToFormat);
66
-
67
- if (parsed?.isValid()) {
68
- // Update country if different
69
- if (parsed.country && parsed.country !== selectedCountry.code) {
70
- const newCountry = getCountryByCode(parsed.country);
71
- if (newCountry) {
72
- setSelectedCountry(newCountry);
73
- }
74
- }
75
- // Format and set the national number
76
- const formatter = new AsYouType(parsed.country);
77
- const formatted = formatter.input(parsed.nationalNumber);
78
- setPhone(formatted);
79
- onPhoneChange(phoneToFormat, true);
80
- }
81
- }
82
- }, [prefilledPhone, selectedCountry]);
83
-
84
- // Close dropdown when clicking outside
85
- useEffect(() => {
86
- const handleClickOutside = (e: MouseEvent) => {
87
- if (
88
- dropdownRef.current &&
89
- !dropdownRef.current.contains(e.target as Node)
90
- ) {
91
- setIsDropdownOpen(false);
92
- }
93
- };
94
-
95
- if (isDropdownOpen) {
96
- document.addEventListener('mousedown', handleClickOutside);
97
- }
98
- return () => document.removeEventListener('mousedown', handleClickOutside);
99
- }, [isDropdownOpen]);
100
-
101
- const validateAndNotify = (nationalNumber: string, country: CountryData) => {
102
- const fullNumber = `${country.callingCode}${nationalNumber.replace(/\D/g, '')}`;
103
- try {
104
- const parsed = parsePhoneNumberFromString(
105
- fullNumber,
106
- country.code as CountryCode,
107
- );
108
- const valid = parsed?.isValid() ?? false;
109
- onPhoneChange(fullNumber, valid);
110
- } catch {
111
- onPhoneChange(fullNumber, false);
112
- }
113
- };
114
-
115
- const handlePhoneChange = (e: Event) => {
116
- const target = e.target as HTMLInputElement;
117
- const rawValue = target.value;
118
-
119
- if (selectedCountry) {
120
- // Format as user types
121
- const formatter = new AsYouType(selectedCountry.code);
122
- const formatted = formatter.input(rawValue);
123
- setPhone(formatted);
124
- validateAndNotify(rawValue, selectedCountry);
125
- } else {
126
- setPhone(rawValue);
127
- }
128
- };
129
-
130
- const handleCountrySelect = (country: CountryData) => {
131
- setSelectedCountry(country);
132
- setIsDropdownOpen(false);
133
-
134
- // Revalidate with new country
135
- if (phone) {
136
- validateAndNotify(phone, country);
137
- }
138
- };
139
-
140
- const handleOptInChange = (e: Event) => {
141
- const target = e.target as HTMLInputElement;
142
- const granted = target.checked;
143
- setOptInGranted(granted);
144
- onOptInChange?.(granted);
145
- };
146
-
147
- const toggleDropdown = () => {
148
- if (!disabled) {
149
- setIsDropdownOpen(!isDropdownOpen);
150
- }
151
- };
152
-
153
- return (
154
- <div class="IncodePhoneInput">
155
- <div class="IncodePhoneInputContainer">
156
- <div class="IncodePhoneInputRow">
157
- {/* Country Selector */}
158
- <div
159
- class={`IncodePhoneCountrySelector ${isDropdownOpen ? 'IncodePhoneCountrySelectorOpen' : ''} ${disabled ? 'IncodePhoneCountrySelectorDisabled' : ''}`}
160
- ref={dropdownRef}
161
- >
162
- <button
163
- type="button"
164
- class="IncodePhoneCountryButton"
165
- onClick={toggleDropdown}
166
- disabled={disabled}
167
- aria-expanded={isDropdownOpen}
168
- aria-haspopup="listbox"
169
- data-testid="phone-country-selector"
170
- >
171
- <span class="IncodePhoneCountryFlag" aria-hidden="true">
172
- {selectedCountry?.emoji}
173
- </span>
174
- <span class="IncodePhoneCountryCode">
175
- {selectedCountry?.callingCode}
176
- </span>
177
- <ChevronDown
178
- class={`IncodePhoneCountryArrow ${isDropdownOpen ? 'IncodePhoneCountryArrowUp' : ''}`}
179
- />
180
- </button>
181
-
182
- {/* Country Dropdown */}
183
- {isDropdownOpen && (
184
- <div class="IncodePhoneCountryDropdown">
185
- {countries.map((country) => (
186
- <button
187
- key={country.code}
188
- type="button"
189
- class={`IncodePhoneCountryOption ${selectedCountry?.code === country.code ? 'IncodePhoneCountryOptionSelected' : ''}`}
190
- onClick={() => handleCountrySelect(country)}
191
- >
192
- <span class="IncodePhoneCountryFlag" aria-hidden="true">
193
- {country.emoji}
194
- </span>
195
- <span class="IncodePhoneCountryOptionCode">
196
- {country.callingCode}
197
- </span>
198
- <span class="IncodePhoneCountryOptionName">
199
- {country.name}
200
- </span>
201
- </button>
202
- ))}
203
- </div>
204
- )}
205
- </div>
206
-
207
- {/* Phone Input */}
208
- <div
209
- class={`IncodePhoneInputWrapper ${phoneError ? 'IncodePhoneInputWrapperError' : ''} ${disabled ? 'IncodePhoneInputWrapperDisabled' : ''}`}
210
- >
211
- <input
212
- type="tel"
213
- value={phone}
214
- onInput={handlePhoneChange}
215
- placeholder="(123) 456-7890"
216
- disabled={disabled}
217
- class="IncodePhoneInputField"
218
- data-testid="phone-input"
219
- />
220
- </div>
221
- </div>
222
-
223
- {phoneError && (
224
- <>
225
- <Spacer size={8} />
226
- <p class="IncodePhoneInputError" data-testid="phone-error">
227
- {phoneError}
228
- </p>
229
- </>
230
- )}
231
-
232
- {optinEnabled && (
233
- <>
234
- <Spacer size={16} />
235
- <label class="IncodePhoneInputOptIn">
236
- <input
237
- type="checkbox"
238
- checked={optInGranted}
239
- onChange={handleOptInChange}
240
- data-testid="phone-optin"
241
- />
242
- <span>I agree to receive SMS notifications</span>
243
- </label>
244
- </>
245
- )}
246
- </div>
247
- </div>
248
- );
249
- };
@@ -1,158 +0,0 @@
1
- @reference "../styles/tailwind.css";
2
-
3
- /* Phone Page Layout */
4
- .IncodePhonePage .IncodePageContent {
5
- @apply pt-32 pb-16;
6
- }
7
-
8
- .IncodePhoneInput {
9
- @apply flex flex-col items-center w-full;
10
- }
11
-
12
- .IncodePhoneInputContainer {
13
- @apply flex flex-col items-center w-full;
14
- }
15
-
16
- /* Row container for country selector and input */
17
- .IncodePhoneInputRow {
18
- @apply relative flex items-stretch gap-8 w-full max-w-[340px];
19
- }
20
-
21
- /* Phone Input Wrapper (separate box) */
22
- .IncodePhoneInputWrapper {
23
- @apply flex-1 flex items-center border border-input-border-default rounded-small bg-input-surface-default transition-colors duration-200;
24
- }
25
-
26
- .IncodePhoneInputWrapper:focus-within {
27
- @apply border-input-border-active;
28
- }
29
-
30
- .IncodePhoneInputWrapper.IncodePhoneInputWrapperError {
31
- @apply border-input-border-negative;
32
- }
33
-
34
- .IncodePhoneInputWrapper.IncodePhoneInputWrapperDisabled {
35
- @apply border-input-border-disabled bg-input-surface-disabled;
36
- }
37
-
38
- /* Country Selector (separate box) */
39
- .IncodePhoneCountrySelector {
40
- @apply static shrink-0 w-[124px];
41
- @apply border border-input-border-default rounded-small bg-input-surface-default;
42
- @apply transition-colors duration-200;
43
- }
44
-
45
- .IncodePhoneCountrySelector.IncodePhoneCountrySelectorOpen {
46
- @apply border-input-border-active;
47
- }
48
-
49
- .IncodePhoneCountrySelector.IncodePhoneCountrySelectorDisabled {
50
- @apply border-input-border-disabled bg-input-surface-disabled;
51
- }
52
-
53
- .IncodePhoneCountryButton {
54
- @apply flex items-center justify-center w-full h-full;
55
- @apply p-8 pl-16;
56
- @apply bg-transparent border-none cursor-pointer;
57
- @apply text-18 font-medium text-text-body-primary;
58
- @apply rounded-small;
59
- font-family: inherit;
60
- }
61
-
62
- .IncodePhoneCountryButton:disabled {
63
- @apply cursor-not-allowed text-input-text-field-disabled;
64
- }
65
-
66
- .IncodePhoneCountryButton:focus {
67
- @apply outline-none;
68
- }
69
-
70
- .IncodePhoneCountryFlag {
71
- @apply text-24 leading-none;
72
- }
73
-
74
- .IncodePhoneCountryCode {
75
- @apply text-18 font-medium whitespace-nowrap text-text-body-primary ml-4;
76
- }
77
-
78
- .IncodePhoneCountryArrow {
79
- @apply w-24 h-24 text-icon-neutral-500 transition-transform duration-200 ml-8;
80
- }
81
-
82
- .IncodePhoneCountryArrowUp {
83
- @apply rotate-180;
84
- }
85
-
86
- .IncodePhoneCountryButton:disabled .IncodePhoneCountryArrow {
87
- @apply text-icon-neutral-300;
88
- }
89
-
90
- /* Country Dropdown */
91
- .IncodePhoneCountryDropdown {
92
- @apply absolute top-full left-0 right-0 z-50 mt-8;
93
- @apply max-h-[320px] overflow-y-auto;
94
- @apply bg-surface-neutral-0 border border-input-border-default rounded-small;
95
- @apply p-8;
96
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
97
- }
98
-
99
- .IncodePhoneCountryOption {
100
- @apply w-full flex items-center gap-12 py-12 px-12 cursor-pointer rounded-small;
101
- @apply text-16 font-medium text-text-body-primary;
102
- @apply bg-transparent border-none text-left;
103
- font-family: inherit;
104
- }
105
-
106
- .IncodePhoneCountryOption:hover {
107
- @apply bg-surface-neutral-100;
108
- }
109
-
110
- .IncodePhoneCountryOption:focus {
111
- @apply outline-none bg-surface-neutral-100;
112
- }
113
-
114
- .IncodePhoneCountryOptionSelected {
115
- @apply bg-surface-brand-50;
116
- }
117
-
118
- .IncodePhoneCountryOptionSelected:hover {
119
- @apply bg-surface-brand-50;
120
- }
121
-
122
- .IncodePhoneCountryOptionCode {
123
- @apply text-16 text-text-body-secondary min-w-[52px];
124
- }
125
-
126
- .IncodePhoneCountryOptionName {
127
- @apply text-16 text-text-body-primary flex-1 truncate;
128
- }
129
-
130
- /* Phone Input Field */
131
- .IncodePhoneInputField {
132
- @apply w-full py-16 px-16 text-18 font-medium border-none outline-none bg-transparent text-input-text-field-default;
133
- @apply rounded-small;
134
- }
135
-
136
- .IncodePhoneInputField::placeholder {
137
- @apply text-input-text-field-placeholder font-medium;
138
- }
139
-
140
- .IncodePhoneInputField:disabled {
141
- @apply cursor-not-allowed text-input-text-field-disabled;
142
- }
143
-
144
- .IncodePhoneInputError {
145
- @apply text-input-text-helper-negative text-14 text-center m-0 font-medium;
146
- }
147
-
148
- .IncodePhoneInputOptIn {
149
- @apply flex items-center gap-8 text-14 text-checkbox-text-default cursor-pointer;
150
- }
151
-
152
- .IncodePhoneInputOptIn input[type="checkbox"] {
153
- @apply w-[18px] h-[18px] cursor-pointer accent-checkbox-surface-selected rounded-checkbox;
154
- }
155
-
156
- .IncodePhoneInputOptIn span {
157
- @apply select-none font-medium;
158
- }
@@ -1,13 +0,0 @@
1
- @reference "../styles/tailwind.css";
2
-
3
- .IncodeCameraButton {
4
- @apply flex items-center justify-center;
5
-
6
- .IncodeCameraButtonPath {
7
- @apply fill-border-brand-500;
8
- }
9
-
10
- .IncodeCameraButtonBackground {
11
- @apply fill-surface-brand-50;
12
- }
13
- }