@smileid/web-components 10.0.6 → 11.0.1

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 (142) hide show
  1. package/README.md +15 -15
  2. package/dist/README.md +15 -0
  3. package/dist/components/README.md +14 -0
  4. package/dist/components/document/src/README.md +111 -0
  5. package/dist/components/document/src/document-capture/README.md +90 -0
  6. package/dist/components/document/src/document-capture-instructions/README.md +56 -0
  7. package/dist/components/document/src/document-capture-review/README.md +79 -0
  8. package/dist/components/selfie/README.md +225 -0
  9. package/dist/components/smart-camera-web/src/README.md +207 -0
  10. package/dist/domain/camera/src/README.md +38 -0
  11. package/dist/domain/file-upload/README.md +35 -0
  12. package/dist/esm/{DocumentCaptureScreens-BjATTDqu.js → DocumentCaptureScreens-DmH2JZDA.js} +3 -3
  13. package/dist/esm/DocumentCaptureScreens-DmH2JZDA.js.map +1 -0
  14. package/dist/esm/EndUserConsent-D4fd1ovG.js.map +1 -1
  15. package/dist/esm/Navigation-CTjK6tLU.js.map +1 -1
  16. package/dist/esm/PoweredBySmileId-CxbaihMu.js.map +1 -1
  17. package/dist/esm/SelfieCaptureScreens-DbdN2zNk.js +7901 -0
  18. package/dist/esm/SelfieCaptureScreens-DbdN2zNk.js.map +1 -0
  19. package/dist/esm/SignaturePad-C7MtmT8m.js.map +1 -1
  20. package/dist/esm/TotpConsent-CQU5jQi4.js.map +1 -1
  21. package/dist/esm/combobox.js.map +1 -1
  22. package/dist/esm/document.js +1 -1
  23. package/dist/esm/main.js +2 -2
  24. package/dist/esm/{package-CZlW6BZn.js → package-bgeQiff6.js} +2 -2
  25. package/dist/esm/package-bgeQiff6.js.map +1 -0
  26. package/dist/esm/selfie.js +1 -1
  27. package/dist/esm/smart-camera-web.js +3 -3
  28. package/dist/esm/smart-camera-web.js.map +1 -1
  29. package/dist/esm/styles-BOEZtbuc.js.map +1 -1
  30. package/dist/package-lock.json +4948 -0
  31. package/dist/package.json +59 -0
  32. package/dist/smart-camera-web.js +72 -98
  33. package/dist/smart-camera-web.js.gz +0 -0
  34. package/dist/smart-camera-web.js.map +1 -1
  35. package/dist/src/components/combobox/src/index.js +2 -0
  36. package/dist/src/components/combobox/src/index.js.map +7 -0
  37. package/dist/src/components/document/src/index.js +2 -0
  38. package/dist/src/components/document/src/index.js.map +7 -0
  39. package/dist/src/components/end-user-consent/src/index.js +14 -0
  40. package/dist/src/components/end-user-consent/src/index.js.map +7 -0
  41. package/dist/src/components/selfie/src/index.js +2 -0
  42. package/dist/src/components/selfie/src/index.js.map +7 -0
  43. package/dist/src/components/signature-pad/src/index.js +10 -0
  44. package/dist/src/components/signature-pad/src/index.js.map +7 -0
  45. package/dist/src/components/smart-camera-web/src/SmartCameraWeb.js +2 -0
  46. package/dist/src/components/smart-camera-web/src/SmartCameraWeb.js.map +7 -0
  47. package/dist/src/components/totp-consent/src/index.js +14 -0
  48. package/dist/src/components/totp-consent/src/index.js.map +7 -0
  49. package/dist/src/index.js.map +7 -0
  50. package/dist/styles/README.md +3 -0
  51. package/dist/types/combobox.d.ts +19 -19
  52. package/dist/types/document.d.ts +19 -19
  53. package/dist/types/end-user-consent.d.ts +19 -19
  54. package/dist/types/main.d.ts +24 -20
  55. package/dist/types/navigation.d.ts +19 -19
  56. package/dist/types/selfie.d.ts +19 -19
  57. package/dist/types/signature-pad.d.ts +19 -19
  58. package/dist/types/smart-camera-web.d.ts +19 -19
  59. package/dist/types/totp-consent.d.ts +19 -19
  60. package/lib/components/README.md +14 -14
  61. package/lib/components/attribution/PoweredBySmileId.js +42 -42
  62. package/lib/components/camera-permission/CameraPermission.js +139 -139
  63. package/lib/components/camera-permission/CameraPermission.stories.js +27 -27
  64. package/lib/components/combobox/src/Combobox.js +589 -589
  65. package/lib/components/combobox/src/index.js +1 -1
  66. package/lib/components/document/src/DocumentCaptureScreens.js +410 -409
  67. package/lib/components/document/src/DocumentCaptureScreens.stories.js +57 -57
  68. package/lib/components/document/src/README.md +111 -111
  69. package/lib/components/document/src/document-capture/DocumentCapture.js +760 -760
  70. package/lib/components/document/src/document-capture/DocumentCapture.stories.js +78 -78
  71. package/lib/components/document/src/document-capture/README.md +90 -90
  72. package/lib/components/document/src/document-capture/index.js +3 -3
  73. package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.js +545 -545
  74. package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.stories.js +24 -24
  75. package/lib/components/document/src/document-capture-instructions/README.md +56 -56
  76. package/lib/components/document/src/document-capture-instructions/index.js +3 -3
  77. package/lib/components/document/src/document-capture-review/DocumentCaptureReview.js +360 -360
  78. package/lib/components/document/src/document-capture-review/DocumentCaptureReview.stories.js +24 -24
  79. package/lib/components/document/src/document-capture-review/README.md +79 -79
  80. package/lib/components/document/src/document-capture-review/index.js +3 -3
  81. package/lib/components/document/src/index.js +3 -3
  82. package/lib/components/end-user-consent/src/EndUserConsent.js +795 -795
  83. package/lib/components/end-user-consent/src/EndUserConsent.stories.js +29 -29
  84. package/lib/components/end-user-consent/src/index.js +4 -4
  85. package/lib/components/navigation/src/Navigation.js +171 -171
  86. package/lib/components/navigation/src/Navigation.stories.js +24 -24
  87. package/lib/components/navigation/src/index.js +3 -3
  88. package/lib/components/selfie/README.md +225 -225
  89. package/lib/components/selfie/src/SelfieCaptureScreens.js +420 -431
  90. package/lib/components/selfie/src/SelfieCaptureScreens.stories.js +29 -29
  91. package/lib/components/selfie/src/index.js +3 -3
  92. package/lib/components/selfie/src/selfie-capture/SelfieCapture.js +1099 -1084
  93. package/lib/components/selfie/src/selfie-capture/SelfieCapture.stories.js +36 -36
  94. package/lib/components/selfie/src/selfie-capture/index.js +3 -3
  95. package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.js +689 -689
  96. package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.stories.js +23 -23
  97. package/lib/components/selfie/src/selfie-capture-instructions/index.js +3 -3
  98. package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.js +209 -209
  99. package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.stories.js +24 -24
  100. package/lib/components/selfie/src/selfie-capture-review/index.js +3 -3
  101. package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +256 -239
  102. package/lib/components/selfie/src/selfie-capture-wrapper/index.ts +1 -1
  103. package/lib/components/selfie/src/smartselfie-capture/OvalProgress.tsx +81 -81
  104. package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +265 -283
  105. package/lib/components/selfie/src/smartselfie-capture/components/AlertDisplay.tsx +34 -34
  106. package/lib/components/selfie/src/smartselfie-capture/components/CameraPreview.tsx +97 -97
  107. package/lib/components/selfie/src/smartselfie-capture/components/CaptureControls.tsx +78 -76
  108. package/lib/components/selfie/src/smartselfie-capture/components/index.ts +3 -3
  109. package/lib/components/selfie/src/smartselfie-capture/constants.ts +23 -23
  110. package/lib/components/selfie/src/smartselfie-capture/hooks/index.ts +2 -2
  111. package/lib/components/selfie/src/smartselfie-capture/hooks/useCamera.ts +238 -238
  112. package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +618 -617
  113. package/lib/components/selfie/src/smartselfie-capture/index.ts +1 -1
  114. package/lib/components/selfie/src/smartselfie-capture/utils/alertMessages.ts +13 -13
  115. package/lib/components/selfie/src/smartselfie-capture/utils/canvas.ts +105 -105
  116. package/lib/components/selfie/src/smartselfie-capture/utils/faceDetection.ts +129 -129
  117. package/lib/components/selfie/src/smartselfie-capture/utils/imageCapture.ts +64 -64
  118. package/lib/components/selfie/src/smartselfie-capture/utils/index.ts +4 -4
  119. package/lib/components/selfie/src/smartselfie-capture/utils/mediapipeManager.ts +77 -77
  120. package/lib/components/signature-pad/package-lock.json +3009 -3009
  121. package/lib/components/signature-pad/package.json +30 -30
  122. package/lib/components/signature-pad/src/SignaturePad.js +484 -484
  123. package/lib/components/signature-pad/src/SignaturePad.stories.js +32 -32
  124. package/lib/components/signature-pad/src/index.js +3 -3
  125. package/lib/components/smart-camera-web/src/README.md +206 -206
  126. package/lib/components/smart-camera-web/src/SmartCameraWeb.js +305 -305
  127. package/lib/components/smart-camera-web/src/SmartCameraWeb.stories.js +57 -57
  128. package/lib/components/totp-consent/src/TotpConsent.js +949 -949
  129. package/lib/components/totp-consent/src/index.js +4 -4
  130. package/lib/domain/camera/src/README.md +38 -38
  131. package/lib/domain/camera/src/SmartCamera.js +109 -109
  132. package/lib/domain/constants/src/Constants.js +27 -27
  133. package/lib/domain/file-upload/README.md +35 -35
  134. package/lib/domain/file-upload/src/SmartFileUpload.js +65 -65
  135. package/lib/styles/README.md +3 -3
  136. package/lib/styles/src/styles.js +372 -372
  137. package/lib/styles/src/typography.js +52 -52
  138. package/package.json +111 -112
  139. package/dist/esm/DocumentCaptureScreens-BjATTDqu.js.map +0 -1
  140. package/dist/esm/SelfieCaptureScreens-UUzZzl1A.js +0 -11361
  141. package/dist/esm/SelfieCaptureScreens-UUzZzl1A.js.map +0 -1
  142. package/dist/esm/package-CZlW6BZn.js.map +0 -1
@@ -1,949 +1,949 @@
1
- import validate from 'validate.js';
2
-
3
- function postData(url, data) {
4
- return fetch(url, {
5
- body: JSON.stringify(data),
6
- cache: 'no-cache',
7
- headers: {
8
- Accept: 'application/json',
9
- 'Content-Type': 'application/json',
10
- },
11
- method: 'POST',
12
- mode: 'cors',
13
- });
14
- }
15
-
16
- function markup() {
17
- return `
18
- <style>
19
- *,
20
- *::before,
21
- *::after {
22
- box-sizing: border-box;
23
- margin: 0;
24
- padding: 0;
25
- }
26
-
27
- :host {
28
- --flow-space: 1.5rem;
29
-
30
- --color-dark: #404040;
31
- --color-grey: #555B69;
32
-
33
- --color-success: #1EB244;
34
- --color-failure: #FFEDEB;
35
- --color-failure-tint: #F86B58;
36
-
37
- --color-richblue: #043C93;
38
- --color-theme: ${this.themeColor};
39
-
40
- --color-active: #2D2B2A;
41
- --color-default: #001096;
42
- --color-disabled: #848282;
43
- }
44
-
45
- html {
46
- font-family: 'DM Sans', sans-serif;
47
- }
48
-
49
- [hidden] {
50
- display: none !important;
51
- }
52
-
53
- [disabled] {
54
- cursor: not-allowed !important;
55
- }
56
-
57
- .visually-hidden {
58
- border: 0;
59
- clip: rect(1px 1px 1px 1px);
60
- clip: rect(1px, 1px, 1px, 1px);
61
- height: auto;
62
- margin: 0;
63
- overflow: hidden;
64
- padding: 0;
65
- position: absolute;
66
- white-space: nowrap;
67
- width: 1px;
68
- }
69
-
70
- .color-dark {
71
- color: var(--color-dark);
72
- }
73
-
74
- .color-grey {
75
- color: var(--color-grey);
76
- }
77
-
78
- .flow > * + * {
79
- margin-top: var(--flow-space);
80
- }
81
-
82
- .center {
83
- margin-left: auto;
84
- margin-right: auto;
85
-
86
- text-align: center;
87
- }
88
-
89
- h1 {
90
- font-size: 1.5rem;
91
- font-weight: 700;
92
- }
93
-
94
- button, input, select, textarea {
95
- font: inherit
96
- }
97
-
98
- label,
99
- input,
100
- select,
101
- textarea {
102
- --flow-space: .5rem;
103
- display: block;
104
- width: 100%;
105
- }
106
-
107
- input,
108
- select,
109
- textarea {
110
- border: 1px solid #d1d8d6;
111
- border-radius: .5rem;
112
- padding: .75rem 1rem;
113
- }
114
-
115
- button {
116
- --button-color: var(--color-default);
117
- --flow-space: 3rem;
118
- -webkit-appearance: none;
119
- -moz-appearance: none;
120
- align-items: center;
121
- appearance: none;
122
- background-color: transparent;
123
- border-radius: 2.5rem;
124
- border: none;
125
- color: #ffffff;
126
- cursor: pointer;
127
- display: inline-flex;
128
- font-size: 20px;
129
- font-weight: 500;
130
- inline-size: 100%;
131
- justify-content: center;
132
- letter-spacing: .05ch;
133
- line-height: 1;
134
- padding: 1rem 2.5rem;
135
- text-align: center;
136
- text-decoration: none;
137
- }
138
-
139
- button[data-variant='solid'] {
140
- background-color: var(--button-color);
141
- border: 2px solid var(--button-color);
142
- }
143
-
144
- button[data-variant='outline'] {
145
- color: var(--button-color);
146
- border: 2px solid var(--button-color);
147
- }
148
-
149
- button[data-variant='ghost'] {
150
- color: var(--button-color);
151
- }
152
-
153
- button:hover,
154
- button:focus,
155
- button:active {
156
- --button-color: var(--color-active);
157
- }
158
-
159
- button:disabled {
160
- --button-color: var(--color-disabled);
161
- }
162
-
163
- button[data-type='icon'] {
164
- height: 2rem;
165
- padding: 0;
166
- width: 2rem;
167
- background: transparent;
168
- }
169
-
170
- input {
171
- font: inherit;
172
- }
173
-
174
- fieldset {
175
- margin: 0;
176
- border: none;
177
- }
178
-
179
- .font-weight:bold {
180
- font-weight: bold;
181
- }
182
-
183
- .justify-right {
184
- justify-content: end !important;
185
- }
186
- .nav {
187
- display: flex;
188
- justify-content: space-between;
189
- }
190
-
191
- .back-wrapper {
192
- display: flex;
193
- align-items: center;
194
- }
195
-
196
- .back-button-text {
197
- font-size: 11px;
198
- line-height: 11px;
199
- color: ${this.themeColor || 'rgb(21, 31, 114)'};
200
- }
201
-
202
- #error,
203
- .validation-message {
204
- color: red;
205
- text-transform: capitalize;
206
- }
207
-
208
- .input-group {
209
- --flow-space: 1.5rem;
210
- text-align: initial;
211
- }
212
-
213
- .input-radio {
214
- --flow-space: 1.5rem;
215
- background-color: #F8F8F8;
216
- border-radius: .5rem;
217
- padding: .625rem 1rem;
218
- display: flex;
219
- align-items: center;
220
- }
221
-
222
- .otp-mode {
223
- display: flex;
224
- align-items: center;
225
- text-align: initial;
226
- }
227
-
228
- .otp-mode :first-child {
229
- margin: 0;
230
- margin-inline-end: 1rem;
231
- }
232
-
233
- .otp-mode :nth-child(2n) {
234
- --flow-space: .5rem;
235
- }
236
-
237
- .input-radio [type='radio'] {
238
- border-radius: 50%;
239
- inline-size: 2rem;
240
- block-size: 2rem;
241
- margin-inline-end: .5rem;
242
- background-color: white;
243
- border: .125rem solid #f5f5f5;
244
- }
245
-
246
- #totp-token {
247
- block-size: 3rem;
248
- inline-size: 20rem;
249
- max-inline-size: 100%;
250
- background-color: #F5F5F5;
251
- border: none;
252
- border-bottom: 2px solid #2F718D;
253
- font-size: 1.5rem;
254
- text-align: center;
255
- font-weight: 700;
256
- letter-spacing: 2rem;
257
- padding: .5rem 1rem;
258
- margin-inline: auto;
259
- }
260
-
261
- @keyframes spin {
262
- 0% {
263
- transform: translate3d(-50%, -50%, 0) rotate(0deg);
264
- }
265
- 100% {
266
- transform: translate3d(-50%, -50%, 0) rotate(360deg);
267
- }
268
- }
269
-
270
- .spinner {
271
- animation: 1.5s linear infinite spin;
272
- animation-play-state: inherit;
273
- border: solid 5px #cfd0d1;
274
- border-bottom-color: var(--color-active);
275
- border-radius: 50%;
276
- content: "";
277
- display: block;
278
- height: 25px;
279
- width: 25px;
280
- will-change: transform;
281
- position: relative;
282
- top: .675rem;
283
- left: 1.25rem;
284
- }
285
- </style>
286
-
287
- <div class='flow center' id='id-entry'>
288
- <div class="nav">
289
- <div class="back-wrapper">
290
- <button type='button' data-type='icon' id="back-button" class="back-button">
291
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
292
- <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
293
- <path fill="${this.themeColor}" d="M15.5 11.25h-5.19l1.72-1.72c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-3 3c-.29.29-.29.77 0 1.06l3 3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-1.72-1.72h5.19c.41 0 .75-.34.75-.75s-.34-.75-.75-.75Z"/>
294
- </svg>
295
- </button>
296
- <div class="back-button-text">Back</div>
297
- </div>
298
- <button data-type='icon' type='button' class='close-iframe'>
299
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
300
- <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
301
- <path fill="#91190F" d="m13.06 12 2.3-2.3c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-2.3 2.3-2.3-2.3a.754.754 0 0 0-1.06 0c-.29.29-.29.77 0 1.06l2.3 2.3-2.3 2.3c-.29.29-.29.77 0 1.06.15.15.34.22.53.22s.38-.07.53-.22l2.3-2.3 2.3 2.3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-2.3-2.3Z"/>
302
- </svg>
303
- <span class='visually-hidden'>Close SmileIdentity Verification frame</span>
304
- </button>
305
- </div>
306
- <h1>
307
- Enter your ${this.idTypeLabel}
308
- </h1>
309
-
310
- <form name='id-entry-form' class='flow' novalidate style='--flow-space: 5.5rem'>
311
- <div id='id-number' class="input-group flow">
312
- <label class='required' for="id_number">
313
- ${this.idTypeLabel}
314
- </label>
315
-
316
- <input aria-required='true' id="id_number" name="id_number"
317
- maxlength='11' placeholder='' />
318
-
319
- <p>
320
- <small>${this.idHint}</small>
321
- </p>
322
- </div>
323
-
324
- <button data-variant='solid' id='query-otp-modes' type='submit'>
325
- <span class='text'>Continue</span>
326
- <svg aria-hidden='true' width="25" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
327
- <path d="M7 12h11m0 0-4.588-4M18 12l-4.588 4" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
328
- </svg>
329
- <span hidden class='spinner'></span>
330
- </button>
331
- </form>
332
- </div>
333
-
334
- <div hidden class='flow center' id='select-mode'>
335
- <div class="nav">
336
- <div class="back-wrapper">
337
- <button type='button' data-type='icon' id="back-to-entry-button" class="back-button">
338
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
339
- <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
340
- <path fill="${this.themeColor}" d="M15.5 11.25h-5.19l1.72-1.72c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-3 3c-.29.29-.29.77 0 1.06l3 3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-1.72-1.72h5.19c.41 0 .75-.34.75-.75s-.34-.75-.75-.75Z"/>
341
- </svg>
342
- </button>
343
- <div class="back-button-text">Back</div>
344
- </div>
345
- <button data-type='icon' type='button' class='close-iframe'>
346
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
347
- <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
348
- <path fill="#91190F" d="m13.06 12 2.3-2.3c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-2.3 2.3-2.3-2.3a.754.754 0 0 0-1.06 0c-.29.29-.29.77 0 1.06l2.3 2.3-2.3 2.3c-.29.29-.29.77 0 1.06.15.15.34.22.53.22s.38-.07.53-.22l2.3-2.3 2.3 2.3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-2.3-2.3Z"/>
349
- </svg>
350
- <span class='visually-hidden'>Close SmileIdentity Verification frame</span>
351
- </button>
352
- </div>
353
- <h1>
354
- Select contact method
355
- </h1>
356
-
357
- <form name='select-mode-form' novalidate style='--flow-space: 4.25rem' id='otp-entry' class='flow center'>
358
- <fieldset class='flow center'>
359
- <legend class='flow' style='--flow-space: 1.5rem'>
360
- <p>
361
- NIBSS, the data custodian of BVN,&nbsp;
362
- will send you a One-Time Password (OTP)
363
- </p>
364
-
365
- <p>
366
- <small>
367
- The request will be from Chams Plc, who is NIBSS' technical partner.
368
- </small>
369
- </p>
370
- </legend>
371
-
372
- <div class='flow center'>
373
- ${
374
- this.modes.length
375
- ? this.modes
376
- .map(
377
- (mode) => `<label class='input-radio'>
378
- <input type="radio" id="" name="mode" value="${Object.keys(mode)[0]}">
379
- <div class='otp-mode'>
380
- ${
381
- Object.keys(mode)[0].includes('sms')
382
- ? `
383
- <svg xmlns="http://www.w3.org/2000/svg" width="29" height="37" fill="none">
384
- <path stroke="#2F718D" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16.697 24.12c4.914 0 7.37 0 8.897-1.652 1.527-1.651 1.527-4.31 1.527-9.625 0-5.316 0-7.974-1.527-9.625-1.526-1.651-3.983-1.651-8.897-1.651h-5.211c-4.914 0-7.37 0-8.897 1.651-1.527 1.651-1.527 4.31-1.527 9.625 0 5.316 0 7.974 1.527 9.625.85.92 1.991 1.328 3.685 1.508"/>
385
- <g filter="url(#sms)">
386
- <path stroke="#2F718D" stroke-linecap="round" stroke-width="2" d="M16.697 24.12c-1.61 0-3.384.703-5.005 1.613-2.602 1.462-3.903 2.193-4.545 1.727-.64-.465-.52-1.91-.277-4.799l.055-.656" shape-rendering="crispEdges"/>
387
- </g>
388
- <defs>
389
- <filter id="sms" width="20.023" height="15.595" x="1.675" y="21.005" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
390
- <feFlood flood-opacity="0" result="BackgroundImageFix"/>
391
- <feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
392
- <feOffset dy="4"/>
393
- <feGaussianBlur stdDeviation="2"/>
394
- <feComposite in2="hardAlpha" operator="out"/>
395
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
396
- <feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2_404"/>
397
- <feBlend in="SourceGraphic" in2="effect1_dropShadow_2_404" result="shape"/>
398
- </filter>
399
- </defs>
400
- </svg>
401
- `
402
- : `
403
- <svg xmlns="http://www.w3.org/2000/svg" width="35" height="24" fill="none">
404
- <path stroke="#2F718D" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.062 4.367c0-1.437 1.221-2.603 2.727-2.603h21.815c1.506 0 2.727 1.166 2.727 2.603v15.62c0 1.438-1.221 2.604-2.727 2.604H6.789c-1.506 0-2.727-1.166-2.727-2.604V4.367Z"/>
405
- <g filter="url(#message)">
406
- <path stroke="#2F718D" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m5.426 3.066 8.647 7.338c2.067 1.754 5.18 1.754 7.247 0l8.648-7.338" shape-rendering="crispEdges"/>
407
- </g>
408
- <defs>
409
- <filter id="message" width="34.042" height="18.154" x=".676" y="2.316" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
410
- <feFlood flood-opacity="0" result="BackgroundImageFix"/>
411
- <feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
412
- <feOffset dy="4"/>
413
- <feGaussianBlur stdDeviation="2"/>
414
- <feComposite in2="hardAlpha" operator="out"/>
415
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
416
- <feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2_394"/>
417
- <feBlend in="SourceGraphic" in2="effect1_dropShadow_2_394" result="shape"/>
418
- </filter>
419
- </defs>
420
- </svg>
421
- `
422
- }
423
- <div class='flow'>
424
- <p>
425
- ${Object.values(mode)[0]}
426
- </p>
427
- <p>
428
- <small>
429
- An OTP will be sent by ${
430
- Object.keys(mode)[0].includes(
431
- 'sms',
432
- )
433
- ? 'sms'
434
- : 'email'
435
- } to verify your identity
436
- </small>
437
- </p>
438
- </div>
439
- </div>
440
- </label>`,
441
- )
442
- .join('\n')
443
- : 'No modes yet'
444
- }
445
- </div>
446
- </fieldset>
447
-
448
- <button data-variant='ghost' id='contact-methods-outdated' style='--flow-space: .5rem' class='' type='button'>
449
- I am no longer using any of these options
450
- </button>
451
-
452
- <button data-variant='solid' id='select-otp-mode' type='submit'>
453
- <span class='text'>Continue</span>
454
- <svg aria-hidden='true' width="25" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
455
- <path d="M7 12h11m0 0-4.588-4M18 12l-4.588 4" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
456
- </svg>
457
- <span hidden class='spinner'></span>
458
- </button>
459
- </form>
460
- </div>
461
-
462
- <div hidden class='flow center' id='otp-verification'>
463
- <div class="nav justify-right">
464
- <button data-type='icon' type='button' class='close-iframe'>
465
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
466
- <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
467
- <path fill="#91190F" d="m13.06 12 2.3-2.3c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-2.3 2.3-2.3-2.3a.754.754 0 0 0-1.06 0c-.29.29-.29.77 0 1.06l2.3 2.3-2.3 2.3c-.29.29-.29.77 0 1.06.15.15.34.22.53.22s.38-.07.53-.22l2.3-2.3 2.3 2.3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-2.3-2.3Z"/>
468
- </svg>
469
- <span class='visually-hidden'>Close SmileIdentity Verification frame</span>
470
- </button>
471
- </div>
472
- <h1>
473
- OTP Verification
474
- </h1>
475
-
476
- <div style='--flow-space: 4.25rem' id='otp-entry'>
477
- <form name='otp-submission-form' novalidate style='--flow-space: 1.5rem' class='flow center'>
478
- <label for='totp-token'>
479
- Enter the OTP sent to <span class='font-weight:bold'>${
480
- this.selectedOtpDeliveryMode
481
- }</span>
482
- </label>
483
- <input type='text' id='totp-token' maxlength='6' inputmode='numeric' autocomplete='one-time-code' />
484
-
485
- <p>
486
- Didn't receive the OTP${
487
- !this.selectedOtpDeliveryMode
488
- ? '?'
489
- : ` at <span class='font-weight:bold'>${this.selectedOtpDeliveryMode}</span>?`
490
- }
491
- </p>
492
-
493
- <button style='--flow-space: .5rem' data-variant='ghost' class='try-another-method' type='button'>
494
- Try another contact method
495
- </button>
496
-
497
- <button data-variant='solid' id='submit-otp' type='submit'>
498
- <span class='text'>Submit</span>
499
- <svg aria-hidden='true' width="25" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
500
- <path d="M7 12h11m0 0-4.588-4M18 12l-4.588 4" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
501
- </svg>
502
- <span hidden class='spinner'></span>
503
- </button>
504
- </form>
505
- </div>
506
- </div>
507
- `;
508
- }
509
-
510
- class TotpConsent extends HTMLElement {
511
- constructor() {
512
- super();
513
-
514
- this.templateString = markup.bind(this);
515
- this.render = () => this.templateString();
516
-
517
- this.attachShadow({ mode: 'open' });
518
-
519
- this.modes = [];
520
- this['otp-delivery-mode'] = '';
521
-
522
- this.queryOtpModes = this.queryOtpModes.bind(this);
523
- this.selectOtpMode = this.selectOtpMode.bind(this);
524
- this.submitOtp = this.submitOtp.bind(this);
525
- this.switchContactMethod = this.switchContactMethod.bind(this);
526
- this.handleTotpConsentGrant = this.handleTotpConsentGrant.bind(this);
527
- this.handleTotpConsentContactMethodsOutdated =
528
- this.handleTotpConsentContactMethodsOutdated.bind(this);
529
- this.pages = [];
530
- }
531
-
532
- static get observedAttributes() {
533
- return ['modes', 'otp-delivery-mode'];
534
- }
535
-
536
- attributeChangedCallback(name) {
537
- switch (name) {
538
- case 'modes':
539
- case 'otp-delivery-mode': {
540
- const updatedTemplate = document.createElement('template');
541
- updatedTemplate.innerHTML = this.render();
542
- const updatedNode = updatedTemplate.content
543
- .cloneNode(true)
544
- .querySelector(`#${this.activeScreen.id}`);
545
- updatedNode.hidden = false;
546
- this.shadowRoot.replaceChild(updatedNode, this.activeScreen);
547
- this.setUpEventListeners();
548
- this.setActiveScreen(updatedNode);
549
- break;
550
- }
551
- default:
552
- break;
553
- }
554
- }
555
-
556
- setUpEventListeners() {
557
- // Screens
558
- this.idEntryScreen = this.shadowRoot.querySelector('#id-entry');
559
- this.selectModeScreen = this.shadowRoot.querySelector('#select-mode');
560
- this.otpVerificationScreen =
561
- this.shadowRoot.querySelector('#otp-verification');
562
-
563
- if (!this.activeScreen) {
564
- this.activeScreen = this.idEntryScreen;
565
- }
566
-
567
- // Buttons
568
- this.queryOtpModesButton =
569
- this.idEntryScreen.querySelector('#query-otp-modes');
570
- this.backButton = this.idEntryScreen.querySelector('#back-button');
571
- this.selectOtpModeButton =
572
- this.selectModeScreen.querySelector('#select-otp-mode');
573
- this.entryBackbutton = this.selectModeScreen.querySelector(
574
- '#back-to-entry-button',
575
- );
576
- this.contactMethodsOutdatedButton = this.selectModeScreen.querySelector(
577
- '#contact-methods-outdated',
578
- );
579
- this.submitOtpButton =
580
- this.otpVerificationScreen.querySelector('#submit-otp');
581
- this.switchContactMethodButton = this.otpVerificationScreen.querySelector(
582
- '.try-another-method',
583
- );
584
- const CloseIframeButtons =
585
- this.shadowRoot.querySelectorAll('.close-iframe');
586
-
587
- // Input Elements
588
- this.idNumberInput = this.idEntryScreen.querySelector('#id_number');
589
- this.modeInputs = this.selectModeScreen.querySelectorAll('[name="mode"]');
590
- this.otpInput = this.otpVerificationScreen.querySelector('#totp-token');
591
-
592
- // Event Handlers
593
- this.queryOtpModesButton.addEventListener('click', (e) =>
594
- this.queryOtpModes(e),
595
- );
596
- this.selectOtpModeButton.addEventListener('click', (e) =>
597
- this.selectOtpMode(e),
598
- );
599
- this.submitOtpButton.addEventListener('click', (e) => this.submitOtp(e));
600
- this.switchContactMethodButton.addEventListener('click', (e) =>
601
- this.switchContactMethod(e),
602
- );
603
- this.contactMethodsOutdatedButton.addEventListener('click', (e) =>
604
- this.handleTotpConsentContactMethodsOutdated(e),
605
- );
606
-
607
- this.entryBackbutton.addEventListener('click', () => {
608
- this.handleBackClick();
609
- });
610
-
611
- this.backButton.addEventListener('click', () => {
612
- this.handleBackClick();
613
- });
614
-
615
- CloseIframeButtons.forEach((button) => {
616
- button.addEventListener(
617
- 'click',
618
- () => {
619
- this.closeWindow();
620
- },
621
- false,
622
- );
623
- });
624
- }
625
-
626
- closeWindow() {
627
- const referenceWindow = window.parent;
628
- [referenceWindow.parent, referenceWindow].forEach((win) => {
629
- win.postMessage('SmileIdentity::Close', '*');
630
- });
631
- }
632
-
633
- handleBackClick() {
634
- const page = this.pages.pop();
635
- if (page) {
636
- this.setActiveScreen(page);
637
- } else {
638
- this.dispatchEvent(
639
- new CustomEvent('end-user-consent.totp.cancelled', {}),
640
- );
641
- }
642
- }
643
-
644
- connectedCallback() {
645
- const template = document.createElement('template');
646
- template.innerHTML = this.render();
647
-
648
- this.shadowRoot.appendChild(template.content.cloneNode(true));
649
- this.setUpEventListeners();
650
- }
651
-
652
- switchContactMethod() {
653
- this.queryOtpModes();
654
- }
655
-
656
- resetForm() {
657
- const invalidElements =
658
- this.activeScreen.querySelectorAll('[aria-invalid]');
659
- invalidElements.forEach((el) => el.removeAttribute('aria-invalid'));
660
-
661
- const validationMessages = this.activeScreen.querySelectorAll(
662
- '.validation-message',
663
- );
664
- validationMessages.forEach((el) => el.remove());
665
- }
666
-
667
- handleIdNumberValidationErrors(errors) {
668
- const fields = Object.keys(errors);
669
-
670
- fields.forEach((field) => {
671
- const input = this.activeScreen.querySelector(`#${field}`);
672
- input.setAttribute('aria-invalid', 'true');
673
- input.setAttribute('aria-describedby', `${field}-hint`);
674
-
675
- const errorDiv = document.createElement('div');
676
- errorDiv.setAttribute('id', `${field}-hint`);
677
- errorDiv.setAttribute('class', 'validation-message');
678
- // eslint-disable-next-line prefer-destructuring
679
- errorDiv.textContent = errors[field][0];
680
-
681
- input.insertAdjacentElement('afterend', errorDiv);
682
- });
683
- }
684
-
685
- handleActiveScreenErrors(error) {
686
- const submitButton = this.activeScreen.querySelector('[type="submit"]');
687
- const errorDiv = document.createElement('div');
688
- errorDiv.setAttribute('class', 'validation-message');
689
- errorDiv.textContent = error;
690
- submitButton.insertAdjacentElement('beforebegin', errorDiv);
691
- }
692
-
693
- validateIdNumber(idNumber) {
694
- const validationConstraints = {
695
- id_number: {
696
- format: new RegExp(this.idRegex),
697
- presence: {
698
- allowEmpty: false,
699
- message: 'is required',
700
- },
701
- },
702
- };
703
-
704
- const errors = validate({ id_number: idNumber }, validationConstraints);
705
-
706
- if (errors) {
707
- this.handleIdNumberValidationErrors(errors);
708
- }
709
-
710
- return errors;
711
- }
712
-
713
- async queryOtpModes(event) {
714
- if (event) {
715
- // ACTION: disable another submission
716
- event.preventDefault();
717
-
718
- // ACTION: Reset any form validation errors'
719
- this.resetForm();
720
- }
721
-
722
- // ACTION: Validate idNumber
723
- const validationErrors = this.validateIdNumber(this.idNumberInput.value);
724
-
725
- // ACTION: Get and set idNumber
726
- localStorage.setItem('idNumber', this.idNumberInput.value || this.idNumber);
727
-
728
- if (!validationErrors) {
729
- const data = {
730
- country: this.country,
731
- id_number: this.idNumber,
732
- id_type: this.idType,
733
- partner_id: this.partnerId,
734
- token: this.token,
735
- };
736
- const url = `${this.baseUrl}/totp_consent`;
737
-
738
- try {
739
- this.toggleLoading();
740
- const response = await postData(url, data);
741
- const json = await response.json();
742
- this.toggleLoading();
743
-
744
- if (!response.ok) {
745
- this.handleActiveScreenErrors(json.error);
746
- } else {
747
- this.sessionId = json.session_id;
748
- this.modes = json.modes;
749
- this.setActiveScreen(this.selectModeScreen);
750
- this.setAttribute('modes', json.modes);
751
- }
752
- } catch (error) {
753
- this.toggleLoading();
754
- this.handleActiveScreenErrors(error.message);
755
- }
756
- }
757
- }
758
-
759
- async selectOtpMode(event) {
760
- // ACTION: disable another submission
761
- event.preventDefault();
762
-
763
- // ACTION: Reset any form validation errors'
764
- this.resetForm();
765
-
766
- // ACTION: Get mode
767
- this.mode = Array.prototype.find.call(
768
- this.modeInputs,
769
- (node) => node.checked,
770
- ).value;
771
- const data = {
772
- country: this.country,
773
- id_number: this.idNumber,
774
- id_type: this.idType,
775
- mode: this.mode,
776
- partner_id: this.partnerId,
777
- session_id: this.sessionId,
778
- token: this.token,
779
- };
780
- const url = `${this.baseUrl}/totp_consent/mode`;
781
-
782
- try {
783
- this.toggleLoading();
784
- const response = await postData(url, data);
785
- const json = await response.json();
786
- this.toggleLoading();
787
-
788
- if (!response.ok) {
789
- this.handleActiveScreenErrors(json.error);
790
- } else {
791
- this.selectedOtpDeliveryMode = this.modes.filter(
792
- (mode) => mode[this.mode],
793
- )[0][this.mode];
794
- this.setActiveScreen(this.otpVerificationScreen);
795
- this.setAttribute('otp-delivery-mode', this.selectedOtpDeliveryMode);
796
- }
797
- } catch (error) {
798
- this.toggleLoading();
799
- this.handleActiveScreenErrors(error.message);
800
- }
801
- }
802
-
803
- async submitOtp(event) {
804
- // ACTION: disable another submission
805
- event.preventDefault();
806
-
807
- // ACTION: Reset any form validation errors'
808
- this.resetForm();
809
-
810
- this.otp = this.otpInput.value;
811
-
812
- const data = {
813
- country: this.country,
814
- id_number: this.idNumber,
815
- id_type: this.idType,
816
- otp: this.otp,
817
- partner_id: this.partnerId,
818
- session_id: this.sessionId,
819
- token: this.token,
820
- };
821
- const url = `${this.baseUrl}/totp_consent/otp`;
822
-
823
- try {
824
- this.toggleLoading();
825
- const response = await postData(url, data);
826
- const json = await response.json();
827
- this.toggleLoading();
828
-
829
- if (!response.ok) {
830
- this.handleActiveScreenErrors(json.error);
831
- } else {
832
- this.handleTotpConsentGrant(event);
833
- }
834
- } catch (error) {
835
- this.toggleLoading();
836
- this.handleActiveScreenErrors(error.message);
837
- }
838
- }
839
-
840
- toggleLoading() {
841
- const button = this.activeScreen.querySelector('button[type="submit"]');
842
- const text = button.querySelector('.text');
843
- const arrow = button.querySelector('svg');
844
- const spinner = button.querySelector('.spinner');
845
-
846
- button.toggleAttribute('disabled');
847
- text.toggleAttribute('hidden');
848
- arrow.toggleAttribute('hidden');
849
- spinner.toggleAttribute('hidden');
850
- }
851
-
852
- setActiveScreen(screen) {
853
- this.activeScreen.hidden = true;
854
- screen.hidden = false;
855
- this.activeScreen = screen;
856
- }
857
-
858
- get baseUrl() {
859
- return this.getAttribute('base-url');
860
- }
861
-
862
- get country() {
863
- return this.getAttribute('country');
864
- }
865
-
866
- get idHint() {
867
- return this.getAttribute('id-hint') || 'Your BVN should be 11 digits long';
868
- }
869
-
870
- get idNumber() {
871
- return localStorage.getItem('idNumber');
872
- }
873
-
874
- get idRegex() {
875
- return this.getAttribute('id-regex');
876
- }
877
-
878
- get idType() {
879
- return this.getAttribute('id-type');
880
- }
881
-
882
- get idTypeLabel() {
883
- return this.getAttribute('id-type-label');
884
- }
885
-
886
- get partnerId() {
887
- return this.getAttribute('partner-id');
888
- }
889
-
890
- get partnerName() {
891
- return this.getAttribute('partner-name');
892
- }
893
-
894
- get token() {
895
- return this.getAttribute('token');
896
- }
897
-
898
- get themeColor() {
899
- return this.getAttribute('theme-color') || '#001096';
900
- }
901
-
902
- get hideBack() {
903
- return this.hasAttribute('hide-back');
904
- }
905
-
906
- get showNavigation() {
907
- return this.hasAttribute('show-navigation');
908
- }
909
-
910
- handleTotpConsentGrant() {
911
- const customEvent = new CustomEvent('end-user-consent.totp.granted', {
912
- detail: {
913
- consented: {
914
- contact_information: true,
915
- document_information: true,
916
- personal_details: true,
917
- },
918
- id_number: this.idNumber,
919
- session_id: this.sessionId,
920
- },
921
- });
922
-
923
- this.dispatchEvent(customEvent);
924
- }
925
-
926
- handleTotpConsentContactMethodsOutdated() {
927
- const tag = 'end-user-consent.totp.denied.contact-methods-outdated';
928
- const customEvent = new CustomEvent(tag, {
929
- detail: {
930
- data: {
931
- id_number: this.idNumber,
932
- session_id: this.sessionId,
933
- },
934
- message: tag,
935
- },
936
- });
937
-
938
- this.dispatchEvent(customEvent);
939
- }
940
- }
941
-
942
- if ('customElements' in window && !window.customElements.get('totp-consent')) {
943
- window.customElements.define('totp-consent', TotpConsent);
944
- }
945
-
946
- export {
947
- // eslint-disable-next-line import/prefer-default-export
948
- TotpConsent,
949
- };
1
+ import validate from 'validate.js';
2
+
3
+ function postData(url, data) {
4
+ return fetch(url, {
5
+ body: JSON.stringify(data),
6
+ cache: 'no-cache',
7
+ headers: {
8
+ Accept: 'application/json',
9
+ 'Content-Type': 'application/json',
10
+ },
11
+ method: 'POST',
12
+ mode: 'cors',
13
+ });
14
+ }
15
+
16
+ function markup() {
17
+ return `
18
+ <style>
19
+ *,
20
+ *::before,
21
+ *::after {
22
+ box-sizing: border-box;
23
+ margin: 0;
24
+ padding: 0;
25
+ }
26
+
27
+ :host {
28
+ --flow-space: 1.5rem;
29
+
30
+ --color-dark: #404040;
31
+ --color-grey: #555B69;
32
+
33
+ --color-success: #1EB244;
34
+ --color-failure: #FFEDEB;
35
+ --color-failure-tint: #F86B58;
36
+
37
+ --color-richblue: #043C93;
38
+ --color-theme: ${this.themeColor};
39
+
40
+ --color-active: #2D2B2A;
41
+ --color-default: #001096;
42
+ --color-disabled: #848282;
43
+ }
44
+
45
+ html {
46
+ font-family: 'DM Sans', sans-serif;
47
+ }
48
+
49
+ [hidden] {
50
+ display: none !important;
51
+ }
52
+
53
+ [disabled] {
54
+ cursor: not-allowed !important;
55
+ }
56
+
57
+ .visually-hidden {
58
+ border: 0;
59
+ clip: rect(1px 1px 1px 1px);
60
+ clip: rect(1px, 1px, 1px, 1px);
61
+ height: auto;
62
+ margin: 0;
63
+ overflow: hidden;
64
+ padding: 0;
65
+ position: absolute;
66
+ white-space: nowrap;
67
+ width: 1px;
68
+ }
69
+
70
+ .color-dark {
71
+ color: var(--color-dark);
72
+ }
73
+
74
+ .color-grey {
75
+ color: var(--color-grey);
76
+ }
77
+
78
+ .flow > * + * {
79
+ margin-top: var(--flow-space);
80
+ }
81
+
82
+ .center {
83
+ margin-left: auto;
84
+ margin-right: auto;
85
+
86
+ text-align: center;
87
+ }
88
+
89
+ h1 {
90
+ font-size: 1.5rem;
91
+ font-weight: 700;
92
+ }
93
+
94
+ button, input, select, textarea {
95
+ font: inherit
96
+ }
97
+
98
+ label,
99
+ input,
100
+ select,
101
+ textarea {
102
+ --flow-space: .5rem;
103
+ display: block;
104
+ width: 100%;
105
+ }
106
+
107
+ input,
108
+ select,
109
+ textarea {
110
+ border: 1px solid #d1d8d6;
111
+ border-radius: .5rem;
112
+ padding: .75rem 1rem;
113
+ }
114
+
115
+ button {
116
+ --button-color: var(--color-default);
117
+ --flow-space: 3rem;
118
+ -webkit-appearance: none;
119
+ -moz-appearance: none;
120
+ align-items: center;
121
+ appearance: none;
122
+ background-color: transparent;
123
+ border-radius: 2.5rem;
124
+ border: none;
125
+ color: #ffffff;
126
+ cursor: pointer;
127
+ display: inline-flex;
128
+ font-size: 20px;
129
+ font-weight: 500;
130
+ inline-size: 100%;
131
+ justify-content: center;
132
+ letter-spacing: .05ch;
133
+ line-height: 1;
134
+ padding: 1rem 2.5rem;
135
+ text-align: center;
136
+ text-decoration: none;
137
+ }
138
+
139
+ button[data-variant='solid'] {
140
+ background-color: var(--button-color);
141
+ border: 2px solid var(--button-color);
142
+ }
143
+
144
+ button[data-variant='outline'] {
145
+ color: var(--button-color);
146
+ border: 2px solid var(--button-color);
147
+ }
148
+
149
+ button[data-variant='ghost'] {
150
+ color: var(--button-color);
151
+ }
152
+
153
+ button:hover,
154
+ button:focus,
155
+ button:active {
156
+ --button-color: var(--color-active);
157
+ }
158
+
159
+ button:disabled {
160
+ --button-color: var(--color-disabled);
161
+ }
162
+
163
+ button[data-type='icon'] {
164
+ height: 2rem;
165
+ padding: 0;
166
+ width: 2rem;
167
+ background: transparent;
168
+ }
169
+
170
+ input {
171
+ font: inherit;
172
+ }
173
+
174
+ fieldset {
175
+ margin: 0;
176
+ border: none;
177
+ }
178
+
179
+ .font-weight:bold {
180
+ font-weight: bold;
181
+ }
182
+
183
+ .justify-right {
184
+ justify-content: end !important;
185
+ }
186
+ .nav {
187
+ display: flex;
188
+ justify-content: space-between;
189
+ }
190
+
191
+ .back-wrapper {
192
+ display: flex;
193
+ align-items: center;
194
+ }
195
+
196
+ .back-button-text {
197
+ font-size: 11px;
198
+ line-height: 11px;
199
+ color: ${this.themeColor || 'rgb(21, 31, 114)'};
200
+ }
201
+
202
+ #error,
203
+ .validation-message {
204
+ color: red;
205
+ text-transform: capitalize;
206
+ }
207
+
208
+ .input-group {
209
+ --flow-space: 1.5rem;
210
+ text-align: initial;
211
+ }
212
+
213
+ .input-radio {
214
+ --flow-space: 1.5rem;
215
+ background-color: #F8F8F8;
216
+ border-radius: .5rem;
217
+ padding: .625rem 1rem;
218
+ display: flex;
219
+ align-items: center;
220
+ }
221
+
222
+ .otp-mode {
223
+ display: flex;
224
+ align-items: center;
225
+ text-align: initial;
226
+ }
227
+
228
+ .otp-mode :first-child {
229
+ margin: 0;
230
+ margin-inline-end: 1rem;
231
+ }
232
+
233
+ .otp-mode :nth-child(2n) {
234
+ --flow-space: .5rem;
235
+ }
236
+
237
+ .input-radio [type='radio'] {
238
+ border-radius: 50%;
239
+ inline-size: 2rem;
240
+ block-size: 2rem;
241
+ margin-inline-end: .5rem;
242
+ background-color: white;
243
+ border: .125rem solid #f5f5f5;
244
+ }
245
+
246
+ #totp-token {
247
+ block-size: 3rem;
248
+ inline-size: 20rem;
249
+ max-inline-size: 100%;
250
+ background-color: #F5F5F5;
251
+ border: none;
252
+ border-bottom: 2px solid #2F718D;
253
+ font-size: 1.5rem;
254
+ text-align: center;
255
+ font-weight: 700;
256
+ letter-spacing: 2rem;
257
+ padding: .5rem 1rem;
258
+ margin-inline: auto;
259
+ }
260
+
261
+ @keyframes spin {
262
+ 0% {
263
+ transform: translate3d(-50%, -50%, 0) rotate(0deg);
264
+ }
265
+ 100% {
266
+ transform: translate3d(-50%, -50%, 0) rotate(360deg);
267
+ }
268
+ }
269
+
270
+ .spinner {
271
+ animation: 1.5s linear infinite spin;
272
+ animation-play-state: inherit;
273
+ border: solid 5px #cfd0d1;
274
+ border-bottom-color: var(--color-active);
275
+ border-radius: 50%;
276
+ content: "";
277
+ display: block;
278
+ height: 25px;
279
+ width: 25px;
280
+ will-change: transform;
281
+ position: relative;
282
+ top: .675rem;
283
+ left: 1.25rem;
284
+ }
285
+ </style>
286
+
287
+ <div class='flow center' id='id-entry'>
288
+ <div class="nav">
289
+ <div class="back-wrapper">
290
+ <button type='button' data-type='icon' id="back-button" class="back-button">
291
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
292
+ <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
293
+ <path fill="${this.themeColor}" d="M15.5 11.25h-5.19l1.72-1.72c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-3 3c-.29.29-.29.77 0 1.06l3 3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-1.72-1.72h5.19c.41 0 .75-.34.75-.75s-.34-.75-.75-.75Z"/>
294
+ </svg>
295
+ </button>
296
+ <div class="back-button-text">Back</div>
297
+ </div>
298
+ <button data-type='icon' type='button' class='close-iframe'>
299
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
300
+ <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
301
+ <path fill="#91190F" d="m13.06 12 2.3-2.3c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-2.3 2.3-2.3-2.3a.754.754 0 0 0-1.06 0c-.29.29-.29.77 0 1.06l2.3 2.3-2.3 2.3c-.29.29-.29.77 0 1.06.15.15.34.22.53.22s.38-.07.53-.22l2.3-2.3 2.3 2.3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-2.3-2.3Z"/>
302
+ </svg>
303
+ <span class='visually-hidden'>Close SmileIdentity Verification frame</span>
304
+ </button>
305
+ </div>
306
+ <h1>
307
+ Enter your ${this.idTypeLabel}
308
+ </h1>
309
+
310
+ <form name='id-entry-form' class='flow' novalidate style='--flow-space: 5.5rem'>
311
+ <div id='id-number' class="input-group flow">
312
+ <label class='required' for="id_number">
313
+ ${this.idTypeLabel}
314
+ </label>
315
+
316
+ <input aria-required='true' id="id_number" name="id_number"
317
+ maxlength='11' placeholder='' />
318
+
319
+ <p>
320
+ <small>${this.idHint}</small>
321
+ </p>
322
+ </div>
323
+
324
+ <button data-variant='solid' id='query-otp-modes' type='submit'>
325
+ <span class='text'>Continue</span>
326
+ <svg aria-hidden='true' width="25" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
327
+ <path d="M7 12h11m0 0-4.588-4M18 12l-4.588 4" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
328
+ </svg>
329
+ <span hidden class='spinner'></span>
330
+ </button>
331
+ </form>
332
+ </div>
333
+
334
+ <div hidden class='flow center' id='select-mode'>
335
+ <div class="nav">
336
+ <div class="back-wrapper">
337
+ <button type='button' data-type='icon' id="back-to-entry-button" class="back-button">
338
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
339
+ <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
340
+ <path fill="${this.themeColor}" d="M15.5 11.25h-5.19l1.72-1.72c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-3 3c-.29.29-.29.77 0 1.06l3 3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-1.72-1.72h5.19c.41 0 .75-.34.75-.75s-.34-.75-.75-.75Z"/>
341
+ </svg>
342
+ </button>
343
+ <div class="back-button-text">Back</div>
344
+ </div>
345
+ <button data-type='icon' type='button' class='close-iframe'>
346
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
347
+ <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
348
+ <path fill="#91190F" d="m13.06 12 2.3-2.3c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-2.3 2.3-2.3-2.3a.754.754 0 0 0-1.06 0c-.29.29-.29.77 0 1.06l2.3 2.3-2.3 2.3c-.29.29-.29.77 0 1.06.15.15.34.22.53.22s.38-.07.53-.22l2.3-2.3 2.3 2.3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-2.3-2.3Z"/>
349
+ </svg>
350
+ <span class='visually-hidden'>Close SmileIdentity Verification frame</span>
351
+ </button>
352
+ </div>
353
+ <h1>
354
+ Select contact method
355
+ </h1>
356
+
357
+ <form name='select-mode-form' novalidate style='--flow-space: 4.25rem' id='otp-entry' class='flow center'>
358
+ <fieldset class='flow center'>
359
+ <legend class='flow' style='--flow-space: 1.5rem'>
360
+ <p>
361
+ NIBSS, the data custodian of BVN,&nbsp;
362
+ will send you a One-Time Password (OTP)
363
+ </p>
364
+
365
+ <p>
366
+ <small>
367
+ The request will be from Chams Plc, who is NIBSS' technical partner.
368
+ </small>
369
+ </p>
370
+ </legend>
371
+
372
+ <div class='flow center'>
373
+ ${
374
+ this.modes.length
375
+ ? this.modes
376
+ .map(
377
+ (mode) => `<label class='input-radio'>
378
+ <input type="radio" id="" name="mode" value="${Object.keys(mode)[0]}">
379
+ <div class='otp-mode'>
380
+ ${
381
+ Object.keys(mode)[0].includes('sms')
382
+ ? `
383
+ <svg xmlns="http://www.w3.org/2000/svg" width="29" height="37" fill="none">
384
+ <path stroke="#2F718D" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16.697 24.12c4.914 0 7.37 0 8.897-1.652 1.527-1.651 1.527-4.31 1.527-9.625 0-5.316 0-7.974-1.527-9.625-1.526-1.651-3.983-1.651-8.897-1.651h-5.211c-4.914 0-7.37 0-8.897 1.651-1.527 1.651-1.527 4.31-1.527 9.625 0 5.316 0 7.974 1.527 9.625.85.92 1.991 1.328 3.685 1.508"/>
385
+ <g filter="url(#sms)">
386
+ <path stroke="#2F718D" stroke-linecap="round" stroke-width="2" d="M16.697 24.12c-1.61 0-3.384.703-5.005 1.613-2.602 1.462-3.903 2.193-4.545 1.727-.64-.465-.52-1.91-.277-4.799l.055-.656" shape-rendering="crispEdges"/>
387
+ </g>
388
+ <defs>
389
+ <filter id="sms" width="20.023" height="15.595" x="1.675" y="21.005" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
390
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
391
+ <feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
392
+ <feOffset dy="4"/>
393
+ <feGaussianBlur stdDeviation="2"/>
394
+ <feComposite in2="hardAlpha" operator="out"/>
395
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
396
+ <feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2_404"/>
397
+ <feBlend in="SourceGraphic" in2="effect1_dropShadow_2_404" result="shape"/>
398
+ </filter>
399
+ </defs>
400
+ </svg>
401
+ `
402
+ : `
403
+ <svg xmlns="http://www.w3.org/2000/svg" width="35" height="24" fill="none">
404
+ <path stroke="#2F718D" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.062 4.367c0-1.437 1.221-2.603 2.727-2.603h21.815c1.506 0 2.727 1.166 2.727 2.603v15.62c0 1.438-1.221 2.604-2.727 2.604H6.789c-1.506 0-2.727-1.166-2.727-2.604V4.367Z"/>
405
+ <g filter="url(#message)">
406
+ <path stroke="#2F718D" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m5.426 3.066 8.647 7.338c2.067 1.754 5.18 1.754 7.247 0l8.648-7.338" shape-rendering="crispEdges"/>
407
+ </g>
408
+ <defs>
409
+ <filter id="message" width="34.042" height="18.154" x=".676" y="2.316" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
410
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
411
+ <feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
412
+ <feOffset dy="4"/>
413
+ <feGaussianBlur stdDeviation="2"/>
414
+ <feComposite in2="hardAlpha" operator="out"/>
415
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
416
+ <feBlend in2="BackgroundImageFix" result="effect1_dropShadow_2_394"/>
417
+ <feBlend in="SourceGraphic" in2="effect1_dropShadow_2_394" result="shape"/>
418
+ </filter>
419
+ </defs>
420
+ </svg>
421
+ `
422
+ }
423
+ <div class='flow'>
424
+ <p>
425
+ ${Object.values(mode)[0]}
426
+ </p>
427
+ <p>
428
+ <small>
429
+ An OTP will be sent by ${
430
+ Object.keys(mode)[0].includes(
431
+ 'sms',
432
+ )
433
+ ? 'sms'
434
+ : 'email'
435
+ } to verify your identity
436
+ </small>
437
+ </p>
438
+ </div>
439
+ </div>
440
+ </label>`,
441
+ )
442
+ .join('\n')
443
+ : 'No modes yet'
444
+ }
445
+ </div>
446
+ </fieldset>
447
+
448
+ <button data-variant='ghost' id='contact-methods-outdated' style='--flow-space: .5rem' class='' type='button'>
449
+ I am no longer using any of these options
450
+ </button>
451
+
452
+ <button data-variant='solid' id='select-otp-mode' type='submit'>
453
+ <span class='text'>Continue</span>
454
+ <svg aria-hidden='true' width="25" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
455
+ <path d="M7 12h11m0 0-4.588-4M18 12l-4.588 4" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
456
+ </svg>
457
+ <span hidden class='spinner'></span>
458
+ </button>
459
+ </form>
460
+ </div>
461
+
462
+ <div hidden class='flow center' id='otp-verification'>
463
+ <div class="nav justify-right">
464
+ <button data-type='icon' type='button' class='close-iframe'>
465
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none">
466
+ <path fill="#DBDBC4" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z" opacity=".4"/>
467
+ <path fill="#91190F" d="m13.06 12 2.3-2.3c.29-.29.29-.77 0-1.06a.754.754 0 0 0-1.06 0l-2.3 2.3-2.3-2.3a.754.754 0 0 0-1.06 0c-.29.29-.29.77 0 1.06l2.3 2.3-2.3 2.3c-.29.29-.29.77 0 1.06.15.15.34.22.53.22s.38-.07.53-.22l2.3-2.3 2.3 2.3c.15.15.34.22.53.22s.38-.07.53-.22c.29-.29.29-.77 0-1.06l-2.3-2.3Z"/>
468
+ </svg>
469
+ <span class='visually-hidden'>Close SmileIdentity Verification frame</span>
470
+ </button>
471
+ </div>
472
+ <h1>
473
+ OTP Verification
474
+ </h1>
475
+
476
+ <div style='--flow-space: 4.25rem' id='otp-entry'>
477
+ <form name='otp-submission-form' novalidate style='--flow-space: 1.5rem' class='flow center'>
478
+ <label for='totp-token'>
479
+ Enter the OTP sent to <span class='font-weight:bold'>${
480
+ this.selectedOtpDeliveryMode
481
+ }</span>
482
+ </label>
483
+ <input type='text' id='totp-token' maxlength='6' inputmode='numeric' autocomplete='one-time-code' />
484
+
485
+ <p>
486
+ Didn't receive the OTP${
487
+ !this.selectedOtpDeliveryMode
488
+ ? '?'
489
+ : ` at <span class='font-weight:bold'>${this.selectedOtpDeliveryMode}</span>?`
490
+ }
491
+ </p>
492
+
493
+ <button style='--flow-space: .5rem' data-variant='ghost' class='try-another-method' type='button'>
494
+ Try another contact method
495
+ </button>
496
+
497
+ <button data-variant='solid' id='submit-otp' type='submit'>
498
+ <span class='text'>Submit</span>
499
+ <svg aria-hidden='true' width="25" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
500
+ <path d="M7 12h11m0 0-4.588-4M18 12l-4.588 4" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
501
+ </svg>
502
+ <span hidden class='spinner'></span>
503
+ </button>
504
+ </form>
505
+ </div>
506
+ </div>
507
+ `;
508
+ }
509
+
510
+ class TotpConsent extends HTMLElement {
511
+ constructor() {
512
+ super();
513
+
514
+ this.templateString = markup.bind(this);
515
+ this.render = () => this.templateString();
516
+
517
+ this.attachShadow({ mode: 'open' });
518
+
519
+ this.modes = [];
520
+ this['otp-delivery-mode'] = '';
521
+
522
+ this.queryOtpModes = this.queryOtpModes.bind(this);
523
+ this.selectOtpMode = this.selectOtpMode.bind(this);
524
+ this.submitOtp = this.submitOtp.bind(this);
525
+ this.switchContactMethod = this.switchContactMethod.bind(this);
526
+ this.handleTotpConsentGrant = this.handleTotpConsentGrant.bind(this);
527
+ this.handleTotpConsentContactMethodsOutdated =
528
+ this.handleTotpConsentContactMethodsOutdated.bind(this);
529
+ this.pages = [];
530
+ }
531
+
532
+ static get observedAttributes() {
533
+ return ['modes', 'otp-delivery-mode'];
534
+ }
535
+
536
+ attributeChangedCallback(name) {
537
+ switch (name) {
538
+ case 'modes':
539
+ case 'otp-delivery-mode': {
540
+ const updatedTemplate = document.createElement('template');
541
+ updatedTemplate.innerHTML = this.render();
542
+ const updatedNode = updatedTemplate.content
543
+ .cloneNode(true)
544
+ .querySelector(`#${this.activeScreen.id}`);
545
+ updatedNode.hidden = false;
546
+ this.shadowRoot.replaceChild(updatedNode, this.activeScreen);
547
+ this.setUpEventListeners();
548
+ this.setActiveScreen(updatedNode);
549
+ break;
550
+ }
551
+ default:
552
+ break;
553
+ }
554
+ }
555
+
556
+ setUpEventListeners() {
557
+ // Screens
558
+ this.idEntryScreen = this.shadowRoot.querySelector('#id-entry');
559
+ this.selectModeScreen = this.shadowRoot.querySelector('#select-mode');
560
+ this.otpVerificationScreen =
561
+ this.shadowRoot.querySelector('#otp-verification');
562
+
563
+ if (!this.activeScreen) {
564
+ this.activeScreen = this.idEntryScreen;
565
+ }
566
+
567
+ // Buttons
568
+ this.queryOtpModesButton =
569
+ this.idEntryScreen.querySelector('#query-otp-modes');
570
+ this.backButton = this.idEntryScreen.querySelector('#back-button');
571
+ this.selectOtpModeButton =
572
+ this.selectModeScreen.querySelector('#select-otp-mode');
573
+ this.entryBackbutton = this.selectModeScreen.querySelector(
574
+ '#back-to-entry-button',
575
+ );
576
+ this.contactMethodsOutdatedButton = this.selectModeScreen.querySelector(
577
+ '#contact-methods-outdated',
578
+ );
579
+ this.submitOtpButton =
580
+ this.otpVerificationScreen.querySelector('#submit-otp');
581
+ this.switchContactMethodButton = this.otpVerificationScreen.querySelector(
582
+ '.try-another-method',
583
+ );
584
+ const CloseIframeButtons =
585
+ this.shadowRoot.querySelectorAll('.close-iframe');
586
+
587
+ // Input Elements
588
+ this.idNumberInput = this.idEntryScreen.querySelector('#id_number');
589
+ this.modeInputs = this.selectModeScreen.querySelectorAll('[name="mode"]');
590
+ this.otpInput = this.otpVerificationScreen.querySelector('#totp-token');
591
+
592
+ // Event Handlers
593
+ this.queryOtpModesButton.addEventListener('click', (e) =>
594
+ this.queryOtpModes(e),
595
+ );
596
+ this.selectOtpModeButton.addEventListener('click', (e) =>
597
+ this.selectOtpMode(e),
598
+ );
599
+ this.submitOtpButton.addEventListener('click', (e) => this.submitOtp(e));
600
+ this.switchContactMethodButton.addEventListener('click', (e) =>
601
+ this.switchContactMethod(e),
602
+ );
603
+ this.contactMethodsOutdatedButton.addEventListener('click', (e) =>
604
+ this.handleTotpConsentContactMethodsOutdated(e),
605
+ );
606
+
607
+ this.entryBackbutton.addEventListener('click', () => {
608
+ this.handleBackClick();
609
+ });
610
+
611
+ this.backButton.addEventListener('click', () => {
612
+ this.handleBackClick();
613
+ });
614
+
615
+ CloseIframeButtons.forEach((button) => {
616
+ button.addEventListener(
617
+ 'click',
618
+ () => {
619
+ this.closeWindow();
620
+ },
621
+ false,
622
+ );
623
+ });
624
+ }
625
+
626
+ closeWindow() {
627
+ const referenceWindow = window.parent;
628
+ [referenceWindow.parent, referenceWindow].forEach((win) => {
629
+ win.postMessage('SmileIdentity::Close', '*');
630
+ });
631
+ }
632
+
633
+ handleBackClick() {
634
+ const page = this.pages.pop();
635
+ if (page) {
636
+ this.setActiveScreen(page);
637
+ } else {
638
+ this.dispatchEvent(
639
+ new CustomEvent('end-user-consent.totp.cancelled', {}),
640
+ );
641
+ }
642
+ }
643
+
644
+ connectedCallback() {
645
+ const template = document.createElement('template');
646
+ template.innerHTML = this.render();
647
+
648
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
649
+ this.setUpEventListeners();
650
+ }
651
+
652
+ switchContactMethod() {
653
+ this.queryOtpModes();
654
+ }
655
+
656
+ resetForm() {
657
+ const invalidElements =
658
+ this.activeScreen.querySelectorAll('[aria-invalid]');
659
+ invalidElements.forEach((el) => el.removeAttribute('aria-invalid'));
660
+
661
+ const validationMessages = this.activeScreen.querySelectorAll(
662
+ '.validation-message',
663
+ );
664
+ validationMessages.forEach((el) => el.remove());
665
+ }
666
+
667
+ handleIdNumberValidationErrors(errors) {
668
+ const fields = Object.keys(errors);
669
+
670
+ fields.forEach((field) => {
671
+ const input = this.activeScreen.querySelector(`#${field}`);
672
+ input.setAttribute('aria-invalid', 'true');
673
+ input.setAttribute('aria-describedby', `${field}-hint`);
674
+
675
+ const errorDiv = document.createElement('div');
676
+ errorDiv.setAttribute('id', `${field}-hint`);
677
+ errorDiv.setAttribute('class', 'validation-message');
678
+ // eslint-disable-next-line prefer-destructuring
679
+ errorDiv.textContent = errors[field][0];
680
+
681
+ input.insertAdjacentElement('afterend', errorDiv);
682
+ });
683
+ }
684
+
685
+ handleActiveScreenErrors(error) {
686
+ const submitButton = this.activeScreen.querySelector('[type="submit"]');
687
+ const errorDiv = document.createElement('div');
688
+ errorDiv.setAttribute('class', 'validation-message');
689
+ errorDiv.textContent = error;
690
+ submitButton.insertAdjacentElement('beforebegin', errorDiv);
691
+ }
692
+
693
+ validateIdNumber(idNumber) {
694
+ const validationConstraints = {
695
+ id_number: {
696
+ format: new RegExp(this.idRegex),
697
+ presence: {
698
+ allowEmpty: false,
699
+ message: 'is required',
700
+ },
701
+ },
702
+ };
703
+
704
+ const errors = validate({ id_number: idNumber }, validationConstraints);
705
+
706
+ if (errors) {
707
+ this.handleIdNumberValidationErrors(errors);
708
+ }
709
+
710
+ return errors;
711
+ }
712
+
713
+ async queryOtpModes(event) {
714
+ if (event) {
715
+ // ACTION: disable another submission
716
+ event.preventDefault();
717
+
718
+ // ACTION: Reset any form validation errors'
719
+ this.resetForm();
720
+ }
721
+
722
+ // ACTION: Validate idNumber
723
+ const validationErrors = this.validateIdNumber(this.idNumberInput.value);
724
+
725
+ // ACTION: Get and set idNumber
726
+ localStorage.setItem('idNumber', this.idNumberInput.value || this.idNumber);
727
+
728
+ if (!validationErrors) {
729
+ const data = {
730
+ country: this.country,
731
+ id_number: this.idNumber,
732
+ id_type: this.idType,
733
+ partner_id: this.partnerId,
734
+ token: this.token,
735
+ };
736
+ const url = `${this.baseUrl}/totp_consent`;
737
+
738
+ try {
739
+ this.toggleLoading();
740
+ const response = await postData(url, data);
741
+ const json = await response.json();
742
+ this.toggleLoading();
743
+
744
+ if (!response.ok) {
745
+ this.handleActiveScreenErrors(json.error);
746
+ } else {
747
+ this.sessionId = json.session_id;
748
+ this.modes = json.modes;
749
+ this.setActiveScreen(this.selectModeScreen);
750
+ this.setAttribute('modes', json.modes);
751
+ }
752
+ } catch (error) {
753
+ this.toggleLoading();
754
+ this.handleActiveScreenErrors(error.message);
755
+ }
756
+ }
757
+ }
758
+
759
+ async selectOtpMode(event) {
760
+ // ACTION: disable another submission
761
+ event.preventDefault();
762
+
763
+ // ACTION: Reset any form validation errors'
764
+ this.resetForm();
765
+
766
+ // ACTION: Get mode
767
+ this.mode = Array.prototype.find.call(
768
+ this.modeInputs,
769
+ (node) => node.checked,
770
+ ).value;
771
+ const data = {
772
+ country: this.country,
773
+ id_number: this.idNumber,
774
+ id_type: this.idType,
775
+ mode: this.mode,
776
+ partner_id: this.partnerId,
777
+ session_id: this.sessionId,
778
+ token: this.token,
779
+ };
780
+ const url = `${this.baseUrl}/totp_consent/mode`;
781
+
782
+ try {
783
+ this.toggleLoading();
784
+ const response = await postData(url, data);
785
+ const json = await response.json();
786
+ this.toggleLoading();
787
+
788
+ if (!response.ok) {
789
+ this.handleActiveScreenErrors(json.error);
790
+ } else {
791
+ this.selectedOtpDeliveryMode = this.modes.filter(
792
+ (mode) => mode[this.mode],
793
+ )[0][this.mode];
794
+ this.setActiveScreen(this.otpVerificationScreen);
795
+ this.setAttribute('otp-delivery-mode', this.selectedOtpDeliveryMode);
796
+ }
797
+ } catch (error) {
798
+ this.toggleLoading();
799
+ this.handleActiveScreenErrors(error.message);
800
+ }
801
+ }
802
+
803
+ async submitOtp(event) {
804
+ // ACTION: disable another submission
805
+ event.preventDefault();
806
+
807
+ // ACTION: Reset any form validation errors'
808
+ this.resetForm();
809
+
810
+ this.otp = this.otpInput.value;
811
+
812
+ const data = {
813
+ country: this.country,
814
+ id_number: this.idNumber,
815
+ id_type: this.idType,
816
+ otp: this.otp,
817
+ partner_id: this.partnerId,
818
+ session_id: this.sessionId,
819
+ token: this.token,
820
+ };
821
+ const url = `${this.baseUrl}/totp_consent/otp`;
822
+
823
+ try {
824
+ this.toggleLoading();
825
+ const response = await postData(url, data);
826
+ const json = await response.json();
827
+ this.toggleLoading();
828
+
829
+ if (!response.ok) {
830
+ this.handleActiveScreenErrors(json.error);
831
+ } else {
832
+ this.handleTotpConsentGrant(event);
833
+ }
834
+ } catch (error) {
835
+ this.toggleLoading();
836
+ this.handleActiveScreenErrors(error.message);
837
+ }
838
+ }
839
+
840
+ toggleLoading() {
841
+ const button = this.activeScreen.querySelector('button[type="submit"]');
842
+ const text = button.querySelector('.text');
843
+ const arrow = button.querySelector('svg');
844
+ const spinner = button.querySelector('.spinner');
845
+
846
+ button.toggleAttribute('disabled');
847
+ text.toggleAttribute('hidden');
848
+ arrow.toggleAttribute('hidden');
849
+ spinner.toggleAttribute('hidden');
850
+ }
851
+
852
+ setActiveScreen(screen) {
853
+ this.activeScreen.hidden = true;
854
+ screen.hidden = false;
855
+ this.activeScreen = screen;
856
+ }
857
+
858
+ get baseUrl() {
859
+ return this.getAttribute('base-url');
860
+ }
861
+
862
+ get country() {
863
+ return this.getAttribute('country');
864
+ }
865
+
866
+ get idHint() {
867
+ return this.getAttribute('id-hint') || 'Your BVN should be 11 digits long';
868
+ }
869
+
870
+ get idNumber() {
871
+ return localStorage.getItem('idNumber');
872
+ }
873
+
874
+ get idRegex() {
875
+ return this.getAttribute('id-regex');
876
+ }
877
+
878
+ get idType() {
879
+ return this.getAttribute('id-type');
880
+ }
881
+
882
+ get idTypeLabel() {
883
+ return this.getAttribute('id-type-label');
884
+ }
885
+
886
+ get partnerId() {
887
+ return this.getAttribute('partner-id');
888
+ }
889
+
890
+ get partnerName() {
891
+ return this.getAttribute('partner-name');
892
+ }
893
+
894
+ get token() {
895
+ return this.getAttribute('token');
896
+ }
897
+
898
+ get themeColor() {
899
+ return this.getAttribute('theme-color') || '#001096';
900
+ }
901
+
902
+ get hideBack() {
903
+ return this.hasAttribute('hide-back');
904
+ }
905
+
906
+ get showNavigation() {
907
+ return this.hasAttribute('show-navigation');
908
+ }
909
+
910
+ handleTotpConsentGrant() {
911
+ const customEvent = new CustomEvent('end-user-consent.totp.granted', {
912
+ detail: {
913
+ consented: {
914
+ contact_information: true,
915
+ document_information: true,
916
+ personal_details: true,
917
+ },
918
+ id_number: this.idNumber,
919
+ session_id: this.sessionId,
920
+ },
921
+ });
922
+
923
+ this.dispatchEvent(customEvent);
924
+ }
925
+
926
+ handleTotpConsentContactMethodsOutdated() {
927
+ const tag = 'end-user-consent.totp.denied.contact-methods-outdated';
928
+ const customEvent = new CustomEvent(tag, {
929
+ detail: {
930
+ data: {
931
+ id_number: this.idNumber,
932
+ session_id: this.sessionId,
933
+ },
934
+ message: tag,
935
+ },
936
+ });
937
+
938
+ this.dispatchEvent(customEvent);
939
+ }
940
+ }
941
+
942
+ if ('customElements' in window && !window.customElements.get('totp-consent')) {
943
+ window.customElements.define('totp-consent', TotpConsent);
944
+ }
945
+
946
+ export {
947
+ // eslint-disable-next-line import/prefer-default-export
948
+ TotpConsent,
949
+ };