@incodetech/web 2.0.0-alpha.2 → 2.0.0-alpha.4

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 (273) hide show
  1. package/dist/{styles/core.css → base.css} +53 -57
  2. package/dist/email/email.es.js +1 -1
  3. package/dist/email/styles.css +11 -11
  4. package/dist/flow/styles.css +688 -1
  5. package/dist/{incodeModule-BF5MX9GT.js → incodeModule-Ct9jp5k6.js} +22 -19
  6. package/dist/index.es.js +18 -18
  7. package/dist/phone/phone.es.js +1 -1
  8. package/dist/phone/styles.css +31 -31
  9. package/dist/selfie/selfie.es.js +16 -16
  10. package/dist/selfie/styles.css +0 -51
  11. package/dist/themes/dark.css +652 -0
  12. package/dist/types/dark.d.ts +1 -0
  13. package/dist/types/light.d.ts +1 -0
  14. package/dist/types/themes/light.d.ts +1 -0
  15. package/package.json +12 -6
  16. package/.turbo/turbo-build.log +0 -58
  17. package/.turbo/turbo-coverage.log +0 -23
  18. package/.turbo/turbo-format.log +0 -6
  19. package/.turbo/turbo-lint$colon$fix.log +0 -6
  20. package/.turbo/turbo-lint.log +0 -6
  21. package/.turbo/turbo-test.log +0 -1033
  22. package/.turbo/turbo-typecheck.log +0 -5
  23. package/coverage/base.css +0 -224
  24. package/coverage/block-navigation.js +0 -87
  25. package/coverage/email/email.tsx.html +0 -850
  26. package/coverage/email/emailInput.tsx.html +0 -340
  27. package/coverage/email/index.html +0 -131
  28. package/coverage/favicon.png +0 -0
  29. package/coverage/flow/flow.tsx.html +0 -961
  30. package/coverage/flow/flowCompleted.tsx.html +0 -448
  31. package/coverage/flow/flowInit.ts.html +0 -367
  32. package/coverage/flow/flowStart.tsx.html +0 -208
  33. package/coverage/flow/index.html +0 -221
  34. package/coverage/flow/preloadFlow.ts.html +0 -598
  35. package/coverage/flow/unsupportedModule.tsx.html +0 -202
  36. package/coverage/flow/useFlowInitialization.ts.html +0 -469
  37. package/coverage/flow/useModuleLoader.ts.html +0 -361
  38. package/coverage/hooks/index.html +0 -116
  39. package/coverage/hooks/useManager.ts.html +0 -205
  40. package/coverage/index.html +0 -401
  41. package/coverage/permissions/boldWithArrow.tsx.html +0 -208
  42. package/coverage/permissions/denied.tsx.html +0 -172
  43. package/coverage/permissions/deniedAndroid.tsx.html +0 -253
  44. package/coverage/permissions/deniedDesktop.tsx.html +0 -277
  45. package/coverage/permissions/deniedIOS.tsx.html +0 -304
  46. package/coverage/permissions/deniedInstructions.tsx.html +0 -142
  47. package/coverage/permissions/iconWrapper.tsx.html +0 -130
  48. package/coverage/permissions/index.html +0 -251
  49. package/coverage/permissions/learnMore.tsx.html +0 -340
  50. package/coverage/permissions/numberedStep.tsx.html +0 -127
  51. package/coverage/permissions/permissions.tsx.html +0 -289
  52. package/coverage/phone/index.html +0 -116
  53. package/coverage/phone/phoneInput.tsx.html +0 -832
  54. package/coverage/prettify.css +0 -1
  55. package/coverage/prettify.js +0 -2
  56. package/coverage/selfie/index.html +0 -131
  57. package/coverage/selfie/selfie.tsx.html +0 -334
  58. package/coverage/selfie/tutorial.tsx.html +0 -214
  59. package/coverage/shared/baseTutorial/baseTutorial.tsx.html +0 -250
  60. package/coverage/shared/baseTutorial/index.html +0 -131
  61. package/coverage/shared/baseTutorial/replaceBaseTutorial.ts.html +0 -289
  62. package/coverage/shared/button/button.tsx.html +0 -226
  63. package/coverage/shared/button/index.html +0 -116
  64. package/coverage/shared/componentRoot/incodeComponent.tsx.html +0 -121
  65. package/coverage/shared/componentRoot/index.html +0 -116
  66. package/coverage/shared/countries/countries.ts.html +0 -502
  67. package/coverage/shared/countries/index.html +0 -116
  68. package/coverage/shared/icons/chevronDown.tsx.html +0 -151
  69. package/coverage/shared/icons/index.html +0 -131
  70. package/coverage/shared/icons/successIcon.tsx.html +0 -163
  71. package/coverage/shared/loader/index.html +0 -116
  72. package/coverage/shared/loader/loadingIcon.tsx.html +0 -286
  73. package/coverage/shared/otpInput/index.html +0 -116
  74. package/coverage/shared/otpInput/otpInput.tsx.html +0 -808
  75. package/coverage/shared/page/index.html +0 -146
  76. package/coverage/shared/page/page.tsx.html +0 -358
  77. package/coverage/shared/page/pageUiConfig.ts.html +0 -277
  78. package/coverage/shared/page/verifiedByIncode.tsx.html +0 -310
  79. package/coverage/shared/spacer/index.html +0 -116
  80. package/coverage/shared/spacer/spacer.tsx.html +0 -349
  81. package/coverage/shared/spinner/index.html +0 -116
  82. package/coverage/shared/spinner/spinner.tsx.html +0 -280
  83. package/coverage/shared/title/index.html +0 -116
  84. package/coverage/shared/title/title.tsx.html +0 -121
  85. package/coverage/shared/uiConfig/index.html +0 -116
  86. package/coverage/shared/uiConfig/uiConfig.ts.html +0 -193
  87. package/coverage/shared/webComponent/incodeModule.ts.html +0 -172
  88. package/coverage/shared/webComponent/index.html +0 -131
  89. package/coverage/shared/webComponent/registerIncodeElement.ts.html +0 -130
  90. package/coverage/sort-arrow-sprite.png +0 -0
  91. package/coverage/sorter.js +0 -210
  92. package/coverage/styles/cn.tsx.html +0 -148
  93. package/coverage/styles/fetchTheme.ts.html +0 -349
  94. package/coverage/styles/index.html +0 -131
  95. package/dev/README.md +0 -163
  96. package/dev/getToken.ts +0 -36
  97. package/dev/headless.html +0 -875
  98. package/dev/index.html +0 -366
  99. package/dev/main-headless.tsx +0 -1332
  100. package/dev/main-orchestrated-flow.tsx +0 -1158
  101. package/dev/main-preact.tsx +0 -323
  102. package/dev/main-simplified.tsx +0 -123
  103. package/dev/main-web-component.tsx +0 -256
  104. package/dev/main.tsx +0 -332
  105. package/dev/manual.html +0 -27
  106. package/dev/orchestrated-flow.html +0 -64
  107. package/dev/simplified.html +0 -64
  108. package/dev/tiktok-logo.svg +0 -7
  109. package/dist/asset-manifest.json +0 -18
  110. package/src/defineCustomElement.tsx +0 -30
  111. package/src/email/email.test.tsx +0 -368
  112. package/src/email/email.tsx +0 -255
  113. package/src/email/emailInput.test.tsx +0 -264
  114. package/src/email/emailInput.tsx +0 -85
  115. package/src/email/styles.css +0 -59
  116. package/src/flow/flow.test.tsx +0 -796
  117. package/src/flow/flow.tsx +0 -292
  118. package/src/flow/flowCompleted.css +0 -30
  119. package/src/flow/flowCompleted.test.tsx +0 -331
  120. package/src/flow/flowCompleted.tsx +0 -121
  121. package/src/flow/flowInit.test.ts +0 -264
  122. package/src/flow/flowInit.ts +0 -94
  123. package/src/flow/flowStart.css +0 -58
  124. package/src/flow/flowStart.test.tsx +0 -49
  125. package/src/flow/flowStart.tsx +0 -41
  126. package/src/flow/incode-logo.svg +0 -8
  127. package/src/flow/index.ts +0 -7
  128. package/src/flow/preloadFlow.test.ts +0 -421
  129. package/src/flow/preloadFlow.ts +0 -171
  130. package/src/flow/styles.css +0 -9
  131. package/src/flow/unsupportedModule.css +0 -21
  132. package/src/flow/unsupportedModule.tsx +0 -39
  133. package/src/flow/useFlowInitialization.test.tsx +0 -292
  134. package/src/flow/useFlowInitialization.ts +0 -128
  135. package/src/flow/useModuleLoader.test.tsx +0 -212
  136. package/src/flow/useModuleLoader.ts +0 -92
  137. package/src/hooks/index.ts +0 -1
  138. package/src/hooks/useManager.test.ts +0 -91
  139. package/src/hooks/useManager.ts +0 -40
  140. package/src/i18n/index.ts +0 -3
  141. package/src/i18n/instance.ts +0 -16
  142. package/src/i18n/setup.ts +0 -184
  143. package/src/i18n/useTranslation.ts +0 -42
  144. package/src/index.ts +0 -27
  145. package/src/permissions/assets/android-dots-icon.svg +0 -7
  146. package/src/permissions/assets/android-settings-icon.svg +0 -16
  147. package/src/permissions/assets/android-toggle-icon.svg +0 -20
  148. package/src/permissions/assets/bank-card-icon.svg +0 -14
  149. package/src/permissions/assets/camera-icon.svg +0 -12
  150. package/src/permissions/assets/camera-ios.svg +0 -53
  151. package/src/permissions/assets/check-icon.svg +0 -8
  152. package/src/permissions/assets/chrome-icon.svg +0 -43
  153. package/src/permissions/assets/password-icon.svg +0 -11
  154. package/src/permissions/assets/permissions-img.svg +0 -51
  155. package/src/permissions/assets/safari-icon.svg +0 -37
  156. package/src/permissions/assets/settings-icon.svg +0 -33
  157. package/src/permissions/assets/toggle-icon.svg +0 -19
  158. package/src/permissions/assets/warning-icon.svg +0 -6
  159. package/src/permissions/boldWithArrow.css +0 -9
  160. package/src/permissions/boldWithArrow.tsx +0 -41
  161. package/src/permissions/denied.css +0 -37
  162. package/src/permissions/denied.tsx +0 -29
  163. package/src/permissions/deniedAndroid.tsx +0 -56
  164. package/src/permissions/deniedDesktop.css +0 -9
  165. package/src/permissions/deniedDesktop.tsx +0 -64
  166. package/src/permissions/deniedIOS.tsx +0 -73
  167. package/src/permissions/deniedInstructions.tsx +0 -19
  168. package/src/permissions/iconWrapper.css +0 -9
  169. package/src/permissions/iconWrapper.tsx +0 -15
  170. package/src/permissions/learnMore.css +0 -37
  171. package/src/permissions/learnMore.tsx +0 -85
  172. package/src/permissions/numberedStep.css +0 -13
  173. package/src/permissions/numberedStep.tsx +0 -14
  174. package/src/permissions/permissions.css +0 -13
  175. package/src/permissions/permissions.tsx +0 -68
  176. package/src/phone/phone.tsx +0 -246
  177. package/src/phone/phoneInput.test.tsx +0 -275
  178. package/src/phone/phoneInput.tsx +0 -249
  179. package/src/phone/styles.css +0 -158
  180. package/src/selfie/cameraButton.css +0 -13
  181. package/src/selfie/cameraButton.tsx +0 -35
  182. package/src/selfie/capture.css +0 -57
  183. package/src/selfie/capture.tsx +0 -232
  184. package/src/selfie/errorModal.tsx +0 -218
  185. package/src/selfie/errorModalContent.css +0 -33
  186. package/src/selfie/errorModalContent.tsx +0 -44
  187. package/src/selfie/faceOutline.css +0 -5
  188. package/src/selfie/faceOutline.tsx +0 -22
  189. package/src/selfie/loadingBorder.css +0 -12
  190. package/src/selfie/loadingBorder.tsx +0 -77
  191. package/src/selfie/manualCaptureButton.css +0 -13
  192. package/src/selfie/manualCaptureButton.tsx +0 -35
  193. package/src/selfie/noMoreAttemptsModal.tsx +0 -44
  194. package/src/selfie/notification.css +0 -9
  195. package/src/selfie/notification.tsx +0 -36
  196. package/src/selfie/retryErrorModal.tsx +0 -56
  197. package/src/selfie/selfie.test.tsx +0 -458
  198. package/src/selfie/selfie.tsx +0 -83
  199. package/src/selfie/selfieTutorial.json +0 -2626
  200. package/src/selfie/styles.css +0 -1
  201. package/src/selfie/tutorial.test.tsx +0 -200
  202. package/src/selfie/tutorial.tsx +0 -43
  203. package/src/setup.ts +0 -33
  204. package/src/shared/baseTutorial/baseTutorial.css +0 -21
  205. package/src/shared/baseTutorial/baseTutorial.test.tsx +0 -184
  206. package/src/shared/baseTutorial/baseTutorial.tsx +0 -55
  207. package/src/shared/baseTutorial/replaceBaseTutorial.test.ts +0 -267
  208. package/src/shared/baseTutorial/replaceBaseTutorial.ts +0 -68
  209. package/src/shared/button/button.css +0 -55
  210. package/src/shared/button/button.test.tsx +0 -101
  211. package/src/shared/button/button.tsx +0 -47
  212. package/src/shared/componentRoot/incodeComponent.tsx +0 -12
  213. package/src/shared/countries/countries.test.ts +0 -75
  214. package/src/shared/countries/countries.ts +0 -139
  215. package/src/shared/countries/index.ts +0 -6
  216. package/src/shared/icons/chevronDown.tsx +0 -22
  217. package/src/shared/icons/index.ts +0 -2
  218. package/src/shared/icons/successIcon.css +0 -5
  219. package/src/shared/icons/successIcon.test.tsx +0 -40
  220. package/src/shared/icons/successIcon.tsx +0 -26
  221. package/src/shared/loader/loadingIcon.css +0 -28
  222. package/src/shared/loader/loadingIcon.tsx +0 -67
  223. package/src/shared/lottie/lottie.tsx +0 -108
  224. package/src/shared/otpInput/otpInput.css +0 -85
  225. package/src/shared/otpInput/otpInput.test.tsx +0 -356
  226. package/src/shared/otpInput/otpInput.tsx +0 -241
  227. package/src/shared/page/incode-logo.svg +0 -3
  228. package/src/shared/page/page.css +0 -47
  229. package/src/shared/page/page.test.tsx +0 -97
  230. package/src/shared/page/page.tsx +0 -91
  231. package/src/shared/page/pageUiConfig.test.ts +0 -112
  232. package/src/shared/page/pageUiConfig.ts +0 -64
  233. package/src/shared/page/verifiedByIncode.css +0 -5
  234. package/src/shared/page/verifiedByIncode.tsx +0 -75
  235. package/src/shared/spacer/spacer.css +0 -149
  236. package/src/shared/spacer/spacer.test.tsx +0 -143
  237. package/src/shared/spacer/spacer.tsx +0 -88
  238. package/src/shared/spinner/index.ts +0 -2
  239. package/src/shared/spinner/spinner.css +0 -28
  240. package/src/shared/spinner/spinner.test.tsx +0 -82
  241. package/src/shared/spinner/spinner.tsx +0 -65
  242. package/src/shared/title/title.css +0 -7
  243. package/src/shared/title/title.tsx +0 -12
  244. package/src/shared/uiConfig/uiConfig.ts +0 -36
  245. package/src/shared/webComponent/incodeModule.ts +0 -29
  246. package/src/shared/webComponent/registerIncodeElement.ts +0 -15
  247. package/src/styles/__mocks__/fetchTheme.ts +0 -19
  248. package/src/styles/applyTheme.ts +0 -37
  249. package/src/styles/cn.test.tsx +0 -57
  250. package/src/styles/cn.tsx +0 -21
  251. package/src/styles/core.css +0 -12
  252. package/src/styles/fetchTheme.test.ts +0 -390
  253. package/src/styles/fetchTheme.ts +0 -88
  254. package/src/styles/generatePalette.ts +0 -111
  255. package/src/styles/reset.css +0 -65
  256. package/src/styles/resolveCssVariableToHex.ts +0 -28
  257. package/src/styles/tailwind.css +0 -291
  258. package/src/styles/themeTypes.ts +0 -18
  259. package/src/styles/tokens/colors.css +0 -190
  260. package/src/styles/tokens/components.css +0 -174
  261. package/src/styles/tokens/index.css +0 -4
  262. package/src/styles/tokens/primitives.css +0 -129
  263. package/src/styles/tokens/semantic.css +0 -51
  264. package/src/svg.d.ts +0 -4
  265. package/src/types/assets.d.ts +0 -1
  266. package/src/types/custom-elements.d.ts +0 -104
  267. package/tsconfig.json +0 -22
  268. package/vite.config.ts +0 -260
  269. package/vitest.config.ts +0 -40
  270. package/vitest.setup.ts +0 -16
  271. /package/dist/{styles/tokens.css → themes/light.css} +0 -0
  272. /package/dist/types/{core.d.ts → base.d.ts} +0 -0
  273. /package/dist/types/{styles/core.d.ts → themes/dark.d.ts} +0 -0
@@ -1,356 +0,0 @@
1
- import { fireEvent, render, screen } from '@testing-library/preact';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { OtpInput } from './otpInput';
4
-
5
- describe('OtpInput', () => {
6
- const defaultProps = {
7
- resendTimer: 0,
8
- canResend: true,
9
- onSubmit: vi.fn(),
10
- onResend: vi.fn(),
11
- onBack: vi.fn(),
12
- };
13
-
14
- beforeEach(() => {
15
- vi.clearAllMocks();
16
- });
17
-
18
- describe('Rendering', () => {
19
- it('renders 6 input boxes', () => {
20
- render(<OtpInput {...defaultProps} />);
21
-
22
- const inputs = screen.getAllByRole('textbox');
23
- expect(inputs.length).toBe(6);
24
- });
25
-
26
- it('renders Continue button', () => {
27
- render(<OtpInput {...defaultProps} />);
28
-
29
- expect(screen.getByTestId('otp-submit')).toBeTruthy();
30
- expect(screen.getByText('Continue')).toBeTruthy();
31
- });
32
-
33
- it('renders resend and change phone links when canResend is true', () => {
34
- render(<OtpInput {...defaultProps} canResend />);
35
-
36
- expect(screen.getByText('Resend code')).toBeTruthy();
37
- expect(screen.getByText('change phone number')).toBeTruthy();
38
- });
39
-
40
- it('renders timer when resendTimer > 0', () => {
41
- render(<OtpInput {...defaultProps} resendTimer={30} canResend={false} />);
42
-
43
- expect(screen.getByTestId('otp-timer')).toBeTruthy();
44
- expect(screen.getByText(/Resend code in/)).toBeTruthy();
45
- });
46
- });
47
-
48
- describe('Input behavior', () => {
49
- it('auto-focuses first input on mount', () => {
50
- render(<OtpInput {...defaultProps} />);
51
-
52
- const inputs = screen.getAllByRole('textbox');
53
- expect(document.activeElement).toBe(inputs[0]);
54
- });
55
-
56
- it('accepts single digit/character per box', () => {
57
- render(<OtpInput {...defaultProps} />);
58
-
59
- const input = screen.getByTestId('otp-input-0') as HTMLInputElement;
60
- fireEvent.input(input, { target: { value: 'A' } });
61
-
62
- expect(input.value).toBe('A');
63
- });
64
-
65
- it('converts input to uppercase', () => {
66
- render(<OtpInput {...defaultProps} />);
67
-
68
- const input = screen.getByTestId('otp-input-0') as HTMLInputElement;
69
- fireEvent.input(input, { target: { value: 'a' } });
70
-
71
- expect(input.value).toBe('A');
72
- });
73
-
74
- it('allows alphanumeric characters', () => {
75
- render(<OtpInput {...defaultProps} />);
76
-
77
- const input = screen.getByTestId('otp-input-0') as HTMLInputElement;
78
-
79
- // Test letter
80
- fireEvent.input(input, { target: { value: 'H' } });
81
- expect(input.value).toBe('H');
82
-
83
- // Test number
84
- const input2 = screen.getByTestId('otp-input-1') as HTMLInputElement;
85
- fireEvent.input(input2, { target: { value: '3' } });
86
- expect(input2.value).toBe('3');
87
- });
88
-
89
- it('blocks non-alphanumeric characters', () => {
90
- render(<OtpInput {...defaultProps} />);
91
-
92
- const input = screen.getByTestId('otp-input-0') as HTMLInputElement;
93
-
94
- fireEvent.keyDown(input, { key: '!' });
95
-
96
- expect(input.value).toBe('');
97
- });
98
-
99
- it('auto-advances to next input after entering character', () => {
100
- render(<OtpInput {...defaultProps} />);
101
-
102
- const input0 = screen.getByTestId('otp-input-0') as HTMLInputElement;
103
- const input1 = screen.getByTestId('otp-input-1') as HTMLInputElement;
104
-
105
- fireEvent.input(input0, { target: { value: 'A' } });
106
-
107
- expect(document.activeElement).toBe(input1);
108
- });
109
-
110
- it('moves focus back on backspace when current box is empty', () => {
111
- render(<OtpInput {...defaultProps} />);
112
-
113
- const input0 = screen.getByTestId('otp-input-0') as HTMLInputElement;
114
- const input1 = screen.getByTestId('otp-input-1') as HTMLInputElement;
115
-
116
- // Fill first input and move to second
117
- fireEvent.input(input0, { target: { value: 'A' } });
118
-
119
- // Press backspace on empty second input
120
- fireEvent.keyDown(input1, { key: 'Backspace' });
121
-
122
- expect(document.activeElement).toBe(input0);
123
- });
124
-
125
- it('clears previous box on backspace navigation', () => {
126
- render(<OtpInput {...defaultProps} />);
127
-
128
- const input0 = screen.getByTestId('otp-input-0') as HTMLInputElement;
129
- const input1 = screen.getByTestId('otp-input-1') as HTMLInputElement;
130
-
131
- fireEvent.input(input0, { target: { value: 'A' } });
132
- fireEvent.keyDown(input1, { key: 'Backspace' });
133
-
134
- expect(input0.value).toBe('');
135
- });
136
-
137
- it('supports arrow key navigation', () => {
138
- render(<OtpInput {...defaultProps} />);
139
-
140
- const input0 = screen.getByTestId('otp-input-0');
141
- const input1 = screen.getByTestId('otp-input-1');
142
- const input2 = screen.getByTestId('otp-input-2');
143
-
144
- input1.focus();
145
-
146
- fireEvent.keyDown(input1, { key: 'ArrowLeft' });
147
- expect(document.activeElement).toBe(input0);
148
-
149
- input1.focus();
150
- fireEvent.keyDown(input1, { key: 'ArrowRight' });
151
- expect(document.activeElement).toBe(input2);
152
- });
153
- });
154
-
155
- describe('Paste handling', () => {
156
- it('handles paste of full OTP code', () => {
157
- render(<OtpInput {...defaultProps} />);
158
-
159
- const input0 = screen.getByTestId('otp-input-0') as HTMLInputElement;
160
-
161
- fireEvent.paste(input0, {
162
- clipboardData: { getData: () => 'ABC123' },
163
- });
164
-
165
- expect(screen.getByTestId('otp-input-0')).toHaveProperty('value', 'A');
166
- expect(screen.getByTestId('otp-input-1')).toHaveProperty('value', 'B');
167
- expect(screen.getByTestId('otp-input-2')).toHaveProperty('value', 'C');
168
- expect(screen.getByTestId('otp-input-3')).toHaveProperty('value', '1');
169
- expect(screen.getByTestId('otp-input-4')).toHaveProperty('value', '2');
170
- expect(screen.getByTestId('otp-input-5')).toHaveProperty('value', '3');
171
- });
172
-
173
- it('filters non-alphanumeric from pasted content', () => {
174
- render(<OtpInput {...defaultProps} />);
175
-
176
- const input0 = screen.getByTestId('otp-input-0') as HTMLInputElement;
177
-
178
- fireEvent.paste(input0, {
179
- clipboardData: { getData: () => 'AB-12-34' },
180
- });
181
-
182
- expect(screen.getByTestId('otp-input-0')).toHaveProperty('value', 'A');
183
- expect(screen.getByTestId('otp-input-1')).toHaveProperty('value', 'B');
184
- expect(screen.getByTestId('otp-input-2')).toHaveProperty('value', '1');
185
- expect(screen.getByTestId('otp-input-3')).toHaveProperty('value', '2');
186
- expect(screen.getByTestId('otp-input-4')).toHaveProperty('value', '3');
187
- expect(screen.getByTestId('otp-input-5')).toHaveProperty('value', '4');
188
- });
189
-
190
- it('handles partial paste', () => {
191
- render(<OtpInput {...defaultProps} />);
192
-
193
- const input2 = screen.getByTestId('otp-input-2') as HTMLInputElement;
194
- input2.focus();
195
-
196
- fireEvent.paste(input2, {
197
- clipboardData: { getData: () => 'XYZ' },
198
- });
199
-
200
- expect(screen.getByTestId('otp-input-2')).toHaveProperty('value', 'X');
201
- expect(screen.getByTestId('otp-input-3')).toHaveProperty('value', 'Y');
202
- expect(screen.getByTestId('otp-input-4')).toHaveProperty('value', 'Z');
203
- });
204
- });
205
-
206
- describe('Submit behavior', () => {
207
- it('Continue button is disabled when OTP is incomplete', () => {
208
- render(<OtpInput {...defaultProps} />);
209
-
210
- const button = screen.getByTestId('otp-submit');
211
- expect(button.hasAttribute('disabled')).toBe(true);
212
- });
213
-
214
- it('Continue button is enabled when all 6 characters are entered', () => {
215
- render(<OtpInput {...defaultProps} />);
216
-
217
- // Fill all inputs
218
- for (let i = 0; i < 6; i++) {
219
- const input = screen.getByTestId(`otp-input-${i}`) as HTMLInputElement;
220
- fireEvent.input(input, { target: { value: String(i) } });
221
- }
222
-
223
- const button = screen.getByTestId('otp-submit');
224
- expect(button.hasAttribute('disabled')).toBe(false);
225
- });
226
-
227
- it('calls onSubmit when Continue is clicked with complete OTP', () => {
228
- const onSubmit = vi.fn();
229
- render(<OtpInput {...defaultProps} onSubmit={onSubmit} />);
230
-
231
- // Fill all inputs
232
- const chars = 'HH36LP';
233
- for (let i = 0; i < 6; i++) {
234
- const input = screen.getByTestId(`otp-input-${i}`) as HTMLInputElement;
235
- fireEvent.input(input, { target: { value: chars[i] } });
236
- }
237
-
238
- const button = screen.getByTestId('otp-submit');
239
- fireEvent.click(button);
240
-
241
- expect(onSubmit).toHaveBeenCalledWith('HH36LP');
242
- });
243
- });
244
-
245
- describe('Resend behavior', () => {
246
- it('calls onResend when resend link is clicked', () => {
247
- const onResend = vi.fn();
248
- render(<OtpInput {...defaultProps} onResend={onResend} canResend />);
249
-
250
- const resendLink = screen.getByText('Resend code');
251
- fireEvent.click(resendLink);
252
-
253
- expect(onResend).toHaveBeenCalled();
254
- });
255
-
256
- it('formats timer correctly', () => {
257
- render(<OtpInput {...defaultProps} resendTimer={90} canResend={false} />);
258
-
259
- expect(screen.getByText(/1:30/)).toBeTruthy();
260
- });
261
-
262
- it('shows timer as 0:30 for 30 seconds', () => {
263
- render(<OtpInput {...defaultProps} resendTimer={30} canResend={false} />);
264
-
265
- expect(screen.getByText(/0:30/)).toBeTruthy();
266
- });
267
- });
268
-
269
- describe('Back behavior', () => {
270
- it('calls onBack when change phone number link is clicked', () => {
271
- const onBack = vi.fn();
272
- render(<OtpInput {...defaultProps} onBack={onBack} canResend />);
273
-
274
- const backLink = screen.getByText('change phone number');
275
- fireEvent.click(backLink);
276
-
277
- expect(onBack).toHaveBeenCalled();
278
- });
279
- });
280
-
281
- describe('Error handling', () => {
282
- it('displays error message when error prop is set', () => {
283
- render(<OtpInput {...defaultProps} error="Invalid OTP code" />);
284
-
285
- expect(screen.getByTestId('otp-error')).toBeTruthy();
286
- expect(screen.getByText('Invalid OTP code')).toBeTruthy();
287
- });
288
-
289
- it('clears inputs when error occurs', () => {
290
- const { rerender } = render(<OtpInput {...defaultProps} />);
291
-
292
- // Fill inputs
293
- for (let i = 0; i < 6; i++) {
294
- const input = screen.getByTestId(`otp-input-${i}`) as HTMLInputElement;
295
- fireEvent.input(input, { target: { value: String(i) } });
296
- }
297
-
298
- // Rerender with error
299
- rerender(<OtpInput {...defaultProps} error="Invalid code" />);
300
-
301
- // Inputs should be cleared
302
- for (let i = 0; i < 6; i++) {
303
- const input = screen.getByTestId(`otp-input-${i}`) as HTMLInputElement;
304
- expect(input.value).toBe('');
305
- }
306
- });
307
-
308
- it('focuses first input after error', async () => {
309
- const { rerender } = render(<OtpInput {...defaultProps} />);
310
-
311
- // Fill inputs
312
- for (let i = 0; i < 6; i++) {
313
- const input = screen.getByTestId(`otp-input-${i}`) as HTMLInputElement;
314
- fireEvent.input(input, { target: { value: String(i) } });
315
- }
316
-
317
- // Rerender with error
318
- rerender(<OtpInput {...defaultProps} error="Invalid code" />);
319
-
320
- // Wait for focus to be set
321
- await vi.waitFor(() => {
322
- const input0 = screen.getByTestId('otp-input-0');
323
- expect(document.activeElement).toBe(input0);
324
- });
325
- });
326
-
327
- it('shows error styling on input boxes', () => {
328
- const { container } = render(
329
- <OtpInput {...defaultProps} error="Invalid code" />,
330
- );
331
-
332
- const errorBoxes = container.querySelector('.IncodeOtpInputBoxesError');
333
- expect(errorBoxes).toBeTruthy();
334
- });
335
- });
336
-
337
- describe('Accessibility', () => {
338
- it('each input has aria-label', () => {
339
- render(<OtpInput {...defaultProps} />);
340
-
341
- for (let i = 0; i < 6; i++) {
342
- const input = screen.getByTestId(`otp-input-${i}`);
343
- expect(input.getAttribute('aria-label')).toBeTruthy();
344
- }
345
- });
346
-
347
- it('inputs have appropriate attributes', () => {
348
- render(<OtpInput {...defaultProps} />);
349
-
350
- const input = screen.getByTestId('otp-input-0');
351
- expect(input.getAttribute('inputMode')).toBe('text');
352
- expect(input.getAttribute('maxLength')).toBe('1');
353
- expect(input.getAttribute('autocomplete')).toBe('one-time-code');
354
- });
355
- });
356
- });
@@ -1,241 +0,0 @@
1
- import type { FC } from 'preact/compat';
2
- import { useEffect, useRef, useState } from 'preact/hooks';
3
- import { Button } from '../button/button';
4
- import { Spacer } from '../spacer/spacer';
5
- import './otpInput.css';
6
-
7
- const OTP_LENGTH = 6;
8
-
9
- export type OtpInputProps = {
10
- resendTimer: number;
11
- canResend: boolean;
12
- error?: string;
13
- onSubmit: (code: string) => void;
14
- onResend: () => void;
15
- onBack: () => void;
16
- /**
17
- * Custom text for the back link (e.g., "change email address").
18
- * Defaults to "change phone number" if not provided.
19
- */
20
- backLinkText?: string;
21
- /** Whether the inputs and buttons should be disabled */
22
- disabled?: boolean;
23
- /** Whether the submit button should show loading state */
24
- isLoading?: boolean;
25
- };
26
-
27
- export const OtpInput: FC<OtpInputProps> = ({
28
- resendTimer,
29
- canResend,
30
- error,
31
- onSubmit,
32
- onResend,
33
- onBack,
34
- backLinkText = 'change phone number',
35
- disabled = false,
36
- isLoading = false,
37
- }) => {
38
- const [digits, setDigits] = useState<string[]>(Array(OTP_LENGTH).fill(''));
39
- const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
40
-
41
- // Auto-focus first input on mount
42
- useEffect(() => {
43
- inputRefs.current[0]?.focus();
44
- }, []);
45
-
46
- // Clear OTP and refocus when error occurs
47
- useEffect(() => {
48
- if (error) {
49
- setDigits(Array(OTP_LENGTH).fill(''));
50
- // Focus first input after clearing
51
- setTimeout(() => {
52
- inputRefs.current[0]?.focus();
53
- }, 0);
54
- }
55
- }, [error]);
56
-
57
- const handleChange = (index: number, e: Event) => {
58
- const target = e.target as HTMLInputElement;
59
- // Allow alphanumeric characters, convert to uppercase
60
- let value = target.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
61
-
62
- // Only take first character if multiple entered
63
- if (value.length > 1) {
64
- value = value.charAt(0);
65
- }
66
-
67
- const newDigits = [...digits];
68
- newDigits[index] = value;
69
- setDigits(newDigits);
70
-
71
- // Auto-advance to next input
72
- if (value && index < OTP_LENGTH - 1) {
73
- inputRefs.current[index + 1]?.focus();
74
- }
75
- };
76
-
77
- const handleKeyDown = (index: number, e: KeyboardEvent) => {
78
- const target = e.target as HTMLInputElement;
79
-
80
- // Allow: backspace, delete, tab, escape, enter, arrows
81
- const allowedKeys = [
82
- 'Backspace',
83
- 'Delete',
84
- 'Tab',
85
- 'Escape',
86
- 'Enter',
87
- 'ArrowLeft',
88
- 'ArrowRight',
89
- 'ArrowUp',
90
- 'ArrowDown',
91
- ];
92
-
93
- // Block non-alphanumeric keys (except allowed control keys)
94
- if (
95
- !allowedKeys.includes(e.key) &&
96
- !/^[a-zA-Z0-9]$/.test(e.key) &&
97
- !e.ctrlKey &&
98
- !e.metaKey
99
- ) {
100
- e.preventDefault();
101
- return;
102
- }
103
-
104
- if (e.key === 'Backspace') {
105
- if (!target.value && index > 0) {
106
- // Move to previous input if current is empty
107
- inputRefs.current[index - 1]?.focus();
108
- const newDigits = [...digits];
109
- newDigits[index - 1] = '';
110
- setDigits(newDigits);
111
- }
112
- } else if (e.key === 'ArrowLeft' && index > 0) {
113
- inputRefs.current[index - 1]?.focus();
114
- } else if (e.key === 'ArrowRight' && index < OTP_LENGTH - 1) {
115
- inputRefs.current[index + 1]?.focus();
116
- }
117
- };
118
-
119
- const handlePaste = (index: number, e: ClipboardEvent) => {
120
- e.preventDefault();
121
- const pastedData = e.clipboardData?.getData('text') || '';
122
- // Allow alphanumeric, convert to uppercase
123
- const pastedChars = pastedData
124
- .replace(/[^a-zA-Z0-9]/g, '')
125
- .toUpperCase()
126
- .slice(0, OTP_LENGTH);
127
-
128
- if (pastedChars) {
129
- const newDigits = [...digits];
130
- for (let i = 0; i < pastedChars.length && index + i < OTP_LENGTH; i++) {
131
- newDigits[index + i] = pastedChars[i];
132
- }
133
- setDigits(newDigits);
134
-
135
- // Focus the next empty input or last input
136
- const nextIndex = Math.min(index + pastedChars.length, OTP_LENGTH - 1);
137
- inputRefs.current[nextIndex]?.focus();
138
- }
139
- };
140
-
141
- const handleFocus = (e: FocusEvent) => {
142
- const target = e.target as HTMLInputElement;
143
- target.select();
144
- };
145
-
146
- const formatTimer = (seconds: number) => {
147
- const mins = Math.floor(seconds / 60);
148
- const secs = seconds % 60;
149
- return `${mins}:${secs.toString().padStart(2, '0')}`;
150
- };
151
-
152
- return (
153
- <div class="IncodeOtpInput">
154
- <div class="IncodeOtpInputContainer">
155
- <div
156
- class={`IncodeOtpInputBoxes ${error ? 'IncodeOtpInputBoxesError' : ''}`}
157
- >
158
- {digits.map((digit, index) => (
159
- <input
160
- key={index}
161
- type="text"
162
- inputMode="text"
163
- maxLength={1}
164
- value={digit}
165
- onInput={(e) => handleChange(index, e)}
166
- onKeyDown={(e) => handleKeyDown(index, e)}
167
- onPaste={(e) => handlePaste(index, e)}
168
- onFocus={handleFocus}
169
- ref={(el) => {
170
- inputRefs.current[index] = el;
171
- }}
172
- class="IncodeOtpInputBox"
173
- autocomplete="one-time-code"
174
- data-testid={`otp-input-${index}`}
175
- aria-label={`Character ${index + 1} of ${OTP_LENGTH}`}
176
- disabled={disabled || isLoading}
177
- />
178
- ))}
179
- </div>
180
-
181
- {error && (
182
- <>
183
- <Spacer size={8} />
184
- <p class="IncodeOtpInputError" data-testid="otp-error">
185
- {error}
186
- </p>
187
- </>
188
- )}
189
- </div>
190
-
191
- <Spacer />
192
-
193
- <Button
194
- onClick={() => {
195
- const code = digits.join('');
196
- if (code.length === OTP_LENGTH) {
197
- onSubmit(code);
198
- }
199
- }}
200
- disabled={digits.join('').length < OTP_LENGTH}
201
- isLoading={isLoading}
202
- data-testid="otp-submit"
203
- >
204
- Continue
205
- </Button>
206
-
207
- <Spacer size={16} />
208
-
209
- <div class="IncodeOtpInputActions">
210
- {canResend && !isLoading ? (
211
- <p class="IncodeOtpInputResendText" data-testid="otp-resend-section">
212
- Didn't get it?{' '}
213
- <button
214
- type="button"
215
- class="IncodeOtpInputLink"
216
- onClick={onResend}
217
- disabled={disabled || isLoading}
218
- data-testid="otp-resend"
219
- >
220
- Resend code
221
- </button>
222
- {' or '}
223
- <button
224
- type="button"
225
- class="IncodeOtpInputLink"
226
- onClick={onBack}
227
- disabled={disabled || isLoading}
228
- data-testid="otp-back"
229
- >
230
- {backLinkText}
231
- </button>
232
- </p>
233
- ) : !isLoading ? (
234
- <p class="IncodeOtpInputTimer" data-testid="otp-timer">
235
- Resend code in {formatTimer(resendTimer)}
236
- </p>
237
- ) : null}
238
- </div>
239
- </div>
240
- );
241
- };
@@ -1,3 +0,0 @@
1
- <svg width="76" height="20" viewBox="0 0 76 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <path fill-rule="evenodd" clip-rule="evenodd" d="M40.1687 6.01811C42.2312 6.01811 43.7927 6.59655 44.8446 7.76024C45.8919 8.91831 46.4126 10.5865 46.4126 12.7583C46.4126 14.93 45.8748 16.6105 44.794 17.7944C43.7102 18.9815 42.1729 19.5735 40.1937 19.5735C38.1479 19.5735 36.5973 18.99 35.5546 17.8187L35.4273 17.6701C34.4729 16.5101 33.9984 14.871 33.9984 12.7583C33.9984 8.28671 36.0703 6.01811 40.1687 6.01811ZM69.1127 6.01781C71.1144 6.01781 72.5953 6.66904 73.5373 7.97333C74.4288 9.2077 74.8837 10.9053 74.9066 13.0629L74.9058 13.5242H66.1149C66.2174 14.6334 66.5602 15.4734 67.141 16.0538C67.7488 16.662 68.6195 16.9679 69.7639 16.9679C70.5188 16.9679 71.2373 16.8742 71.921 16.6862C72.6048 16.4995 73.2105 16.2581 73.735 15.9646L73.9814 15.8267V18.666L73.8821 18.7093C72.5607 19.286 71.0464 19.5732 69.3379 19.5732C67.4088 19.5732 65.8853 18.986 64.7769 17.8093C63.6699 16.6348 63.1183 14.9722 63.1183 12.833C63.1183 10.6451 63.6389 8.96055 64.6862 7.78517C65.7359 6.60632 67.2157 6.01781 69.1127 6.01781ZM60.3976 1.06738V16.3832C60.3976 17.0998 60.0996 17.7092 59.5124 18.198C58.9425 18.6724 58.2282 19.0207 57.3718 19.2435C56.5257 19.4635 55.6531 19.5731 54.7534 19.5731C52.9298 19.5731 51.4237 19.056 50.2406 18.0202C49.0485 16.9752 48.4588 15.1678 48.4588 12.6072C48.4588 10.5545 48.9627 8.9421 49.9753 7.77411C50.9938 6.60175 52.4982 6.01774 54.4782 6.01774C54.9416 6.01774 55.4735 6.07665 56.0742 6.19449C56.582 6.296 57.0453 6.43849 57.4648 6.62252V1.06738H60.3976ZM27.911 6.04223C29.2828 6.04223 30.5881 6.29619 31.8264 6.80393L31.9291 6.84604V9.55797L31.6992 9.46109C30.4503 8.93473 29.3127 8.67257 28.2863 8.67257C27.0102 8.67257 26.0684 8.98809 25.4521 9.61162C24.8366 10.2352 24.5242 11.2397 24.5242 12.6331C24.5242 14.0959 24.8298 15.1535 25.4314 15.8092C26.029 16.4619 27.002 16.7923 28.3613 16.7923C29.4706 16.7923 30.589 16.5234 31.7164 15.9842L31.9534 15.8708V18.5903L31.8552 18.634C30.6349 19.1773 29.3283 19.4483 27.936 19.4483C26.2154 19.4483 24.7637 18.9753 23.5868 18.0285L23.4123 17.8829L23.2796 17.7636C22.1516 16.7067 21.5914 15.0592 21.5914 12.8332C21.5914 10.4557 22.186 8.72379 23.3849 7.64725C24.5757 6.57726 26.0871 6.04223 27.911 6.04223ZM4.11045 6.39262L4.13613 19.1987H1.15293V6.39262H4.11045ZM13.5766 6.0423C15.365 6.0423 16.6948 6.41948 17.5636 7.18368C18.4372 7.95269 18.8706 9.13902 18.8706 10.7331V19.1984H15.9127V11.0838C15.9127 9.42945 14.9988 8.62337 13.1013 8.62337C12.3086 8.62337 11.5458 8.73548 10.8133 8.95959L10.5401 9.04888V19.1984H7.58147V7.40579L7.67177 7.35974C8.43481 6.97061 9.36013 6.65415 10.4506 6.40815L10.8116 6.33134C11.7692 6.13867 12.6905 6.0423 13.5766 6.0423ZM40.1687 8.5477C39.0014 8.5477 38.1814 8.89722 37.6896 9.59114C37.1869 10.3012 36.932 11.3564 36.932 12.7583C36.932 14.1602 37.1948 15.223 37.7142 15.9499C38.2224 16.6613 39.0345 17.0182 40.1687 17.0182C42.3685 17.0182 43.4555 15.6249 43.4555 12.7583C43.4555 11.325 43.1888 10.2632 42.6636 9.57056C42.1472 8.89036 41.3203 8.5477 40.1687 8.5477ZM54.8535 8.59807C52.5835 8.59807 51.4659 9.9271 51.4659 12.6579C51.4659 14.0276 51.7253 15.0917 52.2391 15.8527C52.7378 16.5945 53.6185 16.9678 54.9035 16.9678C55.6556 16.9678 56.2834 16.8792 56.7876 16.7028C57.2156 16.553 57.4279 16.378 57.4604 16.186L57.4648 16.1332V9.25404L57.2778 9.15611C56.9611 8.99923 56.6189 8.87406 56.2512 8.78123C55.7705 8.65917 55.3045 8.59807 54.8535 8.59807ZM69.0377 8.49888C68.192 8.49888 67.5412 8.71795 67.0743 9.15366C66.6631 9.5374 66.3731 10.1684 66.2101 11.0498L66.1771 11.2425H71.8784L71.8489 11.0453C71.7111 10.208 71.4421 9.58986 71.0461 9.18626C70.5969 8.72981 69.9303 8.49888 69.0377 8.49888ZM4.21085 1.94156V4.64764H1.07715V1.94156H4.21085Z" fill="#006AFF"/>
3
- </svg>
@@ -1,47 +0,0 @@
1
- @reference "../../styles/tailwind.css";
2
-
3
- .IncodePageContainer {
4
- @apply flex h-full w-full bg-surface-neutral-0;
5
-
6
- .IncodePageInner {
7
- @apply flex flex-col flex-1 h-full w-full m-auto px-24;
8
- }
9
-
10
- @media screen and (min-width: 767px) {
11
- .IncodePageInner {
12
- @apply w-[800px] h-[840px] m-auto;
13
- }
14
- }
15
-
16
- .IncodePageHeader {
17
- @apply flex flex-col items-center justify-center px-8 min-h-[44px] h-[44px] w-full relative;
18
- }
19
-
20
- .IncodePageLogo {
21
- @apply max-h-full;
22
- }
23
-
24
- .IncodePageTitleContainer {
25
- @apply my-0 mx-auto;
26
- }
27
-
28
- .IncodePageTitle {
29
- @apply text-2xl font-bold text-center tracking-[-1px] leading-[125%] text-text-body-primary;
30
- }
31
-
32
- .IncodePageSubtitle {
33
- @apply text-base text-center text-text-body-secondary leading-[114%] tracking-[-0.5px] font-medium;
34
- }
35
-
36
- .IncodePageContent {
37
- @apply flex flex-1 flex-col;
38
- }
39
-
40
- .IncodePageFooter {
41
- @apply flex justify-center items-center pt-8 pb-12;
42
- }
43
-
44
- .IncodePageFooterInner {
45
- @apply max-h-40;
46
- }
47
- }