@glideidentity/web-client-sdk 5.1.2 → 6.0.0-beta.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 (203) hide show
  1. package/README.md +337 -526
  2. package/dist/browser/web-client-sdk.min.js +1 -1
  3. package/dist/cjs/adapters/index.js +15 -0
  4. package/dist/cjs/adapters/react.js +192 -0
  5. package/dist/cjs/adapters/vanilla.js +38 -0
  6. package/dist/cjs/adapters/vue.js +187 -0
  7. package/dist/cjs/browser.js +58 -0
  8. package/dist/cjs/client/http.js +159 -0
  9. package/dist/cjs/client/index.js +19 -0
  10. package/dist/cjs/client/logger.js +135 -0
  11. package/dist/cjs/client/phone-auth-client.js +439 -0
  12. package/dist/cjs/client/strategies/polling.js +177 -0
  13. package/dist/cjs/core/errors.js +204 -0
  14. package/dist/cjs/core/index.js +83 -0
  15. package/dist/cjs/core/type-guards.js +196 -0
  16. package/dist/cjs/core/types.js +25 -0
  17. package/dist/{core/phone-auth/validation-utils.js → cjs/core/validators.js} +70 -23
  18. package/dist/cjs/index.js +81 -0
  19. package/dist/cjs/ui/index.js +11 -0
  20. package/dist/{core/phone-auth → cjs}/ui/mobile-debug-console.js +149 -78
  21. package/dist/cjs/ui/modal.js +1122 -0
  22. package/dist/esm/adapters/index.js +11 -0
  23. package/dist/esm/adapters/react.js +182 -0
  24. package/dist/esm/adapters/vanilla.js +29 -0
  25. package/dist/esm/adapters/vue.js +177 -0
  26. package/dist/esm/browser.js +30 -11
  27. package/dist/esm/client/http.js +156 -0
  28. package/dist/esm/client/index.js +11 -0
  29. package/dist/esm/client/logger.js +131 -0
  30. package/dist/esm/client/phone-auth-client.js +435 -0
  31. package/dist/esm/client/strategies/polling.js +174 -0
  32. package/dist/esm/core/errors.js +193 -0
  33. package/dist/esm/core/index.js +60 -0
  34. package/dist/esm/core/type-guards.js +181 -0
  35. package/dist/esm/core/types.js +22 -1
  36. package/dist/esm/core/{phone-auth/validation-utils.js → validators.js} +66 -21
  37. package/dist/esm/index.js +45 -17
  38. package/dist/esm/ui/index.js +5 -0
  39. package/dist/esm/{core/phone-auth/ui → ui}/mobile-debug-console.js +149 -78
  40. package/dist/esm/ui/modal.js +1117 -0
  41. package/dist/types/adapters/index.d.ts +10 -0
  42. package/dist/types/adapters/index.d.ts.map +1 -0
  43. package/dist/types/adapters/react.d.ts +70 -0
  44. package/dist/types/adapters/react.d.ts.map +1 -0
  45. package/dist/types/adapters/vanilla.d.ts +29 -0
  46. package/dist/types/adapters/vanilla.d.ts.map +1 -0
  47. package/dist/types/adapters/vue.d.ts +71 -0
  48. package/dist/types/adapters/vue.d.ts.map +1 -0
  49. package/dist/types/browser.d.ts +27 -0
  50. package/dist/types/browser.d.ts.map +1 -0
  51. package/dist/types/client/http.d.ts +41 -0
  52. package/dist/types/client/http.d.ts.map +1 -0
  53. package/dist/types/client/index.d.ts +10 -0
  54. package/dist/types/client/index.d.ts.map +1 -0
  55. package/dist/types/client/logger.d.ts +36 -0
  56. package/dist/types/client/logger.d.ts.map +1 -0
  57. package/dist/types/client/phone-auth-client.d.ts +91 -0
  58. package/dist/types/client/phone-auth-client.d.ts.map +1 -0
  59. package/dist/types/client/strategies/polling.d.ts +36 -0
  60. package/dist/types/client/strategies/polling.d.ts.map +1 -0
  61. package/dist/types/core/errors.d.ts +71 -0
  62. package/dist/types/core/errors.d.ts.map +1 -0
  63. package/dist/types/core/index.d.ts +38 -0
  64. package/dist/types/core/index.d.ts.map +1 -0
  65. package/dist/types/core/type-guards.d.ts +118 -0
  66. package/dist/types/core/type-guards.d.ts.map +1 -0
  67. package/dist/types/core/types.d.ts +534 -0
  68. package/dist/types/core/types.d.ts.map +1 -0
  69. package/dist/types/core/validators.d.ts +63 -0
  70. package/dist/types/core/validators.d.ts.map +1 -0
  71. package/dist/types/index.d.ts +40 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/ui/index.d.ts +6 -0
  74. package/dist/types/ui/index.d.ts.map +1 -0
  75. package/dist/{esm/core/phone-auth → types}/ui/mobile-debug-console.d.ts +1 -0
  76. package/dist/types/ui/mobile-debug-console.d.ts.map +1 -0
  77. package/dist/types/ui/modal.d.ts +87 -0
  78. package/dist/types/ui/modal.d.ts.map +1 -0
  79. package/package.json +48 -34
  80. package/dist/adapters/angular/client.service.d.ts +0 -7
  81. package/dist/adapters/angular/client.service.js +0 -30
  82. package/dist/adapters/angular/index.d.ts +0 -3
  83. package/dist/adapters/angular/index.js +0 -18
  84. package/dist/adapters/angular/phone-auth.service.d.ts +0 -38
  85. package/dist/adapters/angular/phone-auth.service.js +0 -130
  86. package/dist/adapters/react/index.d.ts +0 -9
  87. package/dist/adapters/react/index.js +0 -28
  88. package/dist/adapters/react/useClient.d.ts +0 -26
  89. package/dist/adapters/react/useClient.js +0 -121
  90. package/dist/adapters/react/usePhoneAuth.d.ts +0 -23
  91. package/dist/adapters/react/usePhoneAuth.js +0 -95
  92. package/dist/adapters/vanilla/client.d.ts +0 -8
  93. package/dist/adapters/vanilla/client.js +0 -33
  94. package/dist/adapters/vanilla/index.d.ts +0 -3
  95. package/dist/adapters/vanilla/index.js +0 -18
  96. package/dist/adapters/vanilla/phone-auth.d.ts +0 -46
  97. package/dist/adapters/vanilla/phone-auth.js +0 -138
  98. package/dist/adapters/vue/index.d.ts +0 -10
  99. package/dist/adapters/vue/index.js +0 -36
  100. package/dist/adapters/vue/useClient.d.ts +0 -115
  101. package/dist/adapters/vue/useClient.js +0 -131
  102. package/dist/adapters/vue/usePhoneAuth.d.ts +0 -94
  103. package/dist/adapters/vue/usePhoneAuth.js +0 -103
  104. package/dist/browser.d.ts +0 -7
  105. package/dist/browser.js +0 -31
  106. package/dist/core/client.d.ts +0 -22
  107. package/dist/core/client.js +0 -77
  108. package/dist/core/logger.d.ts +0 -130
  109. package/dist/core/logger.js +0 -370
  110. package/dist/core/phone-auth/api-types.d.ts +0 -593
  111. package/dist/core/phone-auth/api-types.js +0 -215
  112. package/dist/core/phone-auth/client.d.ts +0 -189
  113. package/dist/core/phone-auth/client.js +0 -1441
  114. package/dist/core/phone-auth/error-utils.d.ts +0 -110
  115. package/dist/core/phone-auth/error-utils.js +0 -350
  116. package/dist/core/phone-auth/index.d.ts +0 -7
  117. package/dist/core/phone-auth/index.js +0 -50
  118. package/dist/core/phone-auth/status-types.d.ts +0 -107
  119. package/dist/core/phone-auth/status-types.js +0 -31
  120. package/dist/core/phone-auth/strategies/desktop.d.ts +0 -122
  121. package/dist/core/phone-auth/strategies/desktop.js +0 -596
  122. package/dist/core/phone-auth/strategies/index.d.ts +0 -11
  123. package/dist/core/phone-auth/strategies/index.js +0 -15
  124. package/dist/core/phone-auth/strategies/link.d.ts +0 -89
  125. package/dist/core/phone-auth/strategies/link.js +0 -384
  126. package/dist/core/phone-auth/strategies/ts43.d.ts +0 -32
  127. package/dist/core/phone-auth/strategies/ts43.js +0 -151
  128. package/dist/core/phone-auth/strategies/types.d.ts +0 -18
  129. package/dist/core/phone-auth/strategies/types.js +0 -6
  130. package/dist/core/phone-auth/type-guards.d.ts +0 -143
  131. package/dist/core/phone-auth/type-guards.js +0 -198
  132. package/dist/core/phone-auth/types.d.ts +0 -237
  133. package/dist/core/phone-auth/types.js +0 -93
  134. package/dist/core/phone-auth/ui/mobile-debug-console.d.ts +0 -25
  135. package/dist/core/phone-auth/ui/modal.d.ts +0 -88
  136. package/dist/core/phone-auth/ui/modal.js +0 -598
  137. package/dist/core/phone-auth/validation-utils.d.ts +0 -44
  138. package/dist/core/types.d.ts +0 -62
  139. package/dist/core/types.js +0 -2
  140. package/dist/core/version.d.ts +0 -1
  141. package/dist/core/version.js +0 -5
  142. package/dist/esm/adapters/angular/client.service.d.ts +0 -7
  143. package/dist/esm/adapters/angular/client.service.js +0 -27
  144. package/dist/esm/adapters/angular/index.d.ts +0 -3
  145. package/dist/esm/adapters/angular/index.js +0 -4
  146. package/dist/esm/adapters/angular/phone-auth.service.d.ts +0 -38
  147. package/dist/esm/adapters/angular/phone-auth.service.js +0 -127
  148. package/dist/esm/adapters/react/index.d.ts +0 -9
  149. package/dist/esm/adapters/react/index.js +0 -8
  150. package/dist/esm/adapters/react/useClient.d.ts +0 -26
  151. package/dist/esm/adapters/react/useClient.js +0 -116
  152. package/dist/esm/adapters/react/usePhoneAuth.d.ts +0 -23
  153. package/dist/esm/adapters/react/usePhoneAuth.js +0 -92
  154. package/dist/esm/adapters/vanilla/client.d.ts +0 -8
  155. package/dist/esm/adapters/vanilla/client.js +0 -29
  156. package/dist/esm/adapters/vanilla/index.d.ts +0 -3
  157. package/dist/esm/adapters/vanilla/index.js +0 -4
  158. package/dist/esm/adapters/vanilla/phone-auth.d.ts +0 -46
  159. package/dist/esm/adapters/vanilla/phone-auth.js +0 -134
  160. package/dist/esm/adapters/vue/index.d.ts +0 -10
  161. package/dist/esm/adapters/vue/index.js +0 -11
  162. package/dist/esm/adapters/vue/useClient.d.ts +0 -115
  163. package/dist/esm/adapters/vue/useClient.js +0 -127
  164. package/dist/esm/adapters/vue/usePhoneAuth.d.ts +0 -94
  165. package/dist/esm/adapters/vue/usePhoneAuth.js +0 -100
  166. package/dist/esm/browser.d.ts +0 -7
  167. package/dist/esm/core/client.d.ts +0 -22
  168. package/dist/esm/core/client.js +0 -70
  169. package/dist/esm/core/logger.d.ts +0 -130
  170. package/dist/esm/core/logger.js +0 -359
  171. package/dist/esm/core/phone-auth/api-types.d.ts +0 -593
  172. package/dist/esm/core/phone-auth/api-types.js +0 -203
  173. package/dist/esm/core/phone-auth/client.d.ts +0 -189
  174. package/dist/esm/core/phone-auth/client.js +0 -1404
  175. package/dist/esm/core/phone-auth/error-utils.d.ts +0 -110
  176. package/dist/esm/core/phone-auth/error-utils.js +0 -338
  177. package/dist/esm/core/phone-auth/index.d.ts +0 -7
  178. package/dist/esm/core/phone-auth/index.js +0 -8
  179. package/dist/esm/core/phone-auth/status-types.d.ts +0 -107
  180. package/dist/esm/core/phone-auth/status-types.js +0 -26
  181. package/dist/esm/core/phone-auth/strategies/desktop.d.ts +0 -122
  182. package/dist/esm/core/phone-auth/strategies/desktop.js +0 -590
  183. package/dist/esm/core/phone-auth/strategies/index.d.ts +0 -11
  184. package/dist/esm/core/phone-auth/strategies/index.js +0 -7
  185. package/dist/esm/core/phone-auth/strategies/link.d.ts +0 -89
  186. package/dist/esm/core/phone-auth/strategies/link.js +0 -380
  187. package/dist/esm/core/phone-auth/strategies/ts43.d.ts +0 -32
  188. package/dist/esm/core/phone-auth/strategies/ts43.js +0 -147
  189. package/dist/esm/core/phone-auth/strategies/types.d.ts +0 -18
  190. package/dist/esm/core/phone-auth/strategies/types.js +0 -5
  191. package/dist/esm/core/phone-auth/type-guards.d.ts +0 -143
  192. package/dist/esm/core/phone-auth/type-guards.js +0 -185
  193. package/dist/esm/core/phone-auth/types.d.ts +0 -237
  194. package/dist/esm/core/phone-auth/types.js +0 -76
  195. package/dist/esm/core/phone-auth/ui/modal.d.ts +0 -88
  196. package/dist/esm/core/phone-auth/ui/modal.js +0 -594
  197. package/dist/esm/core/phone-auth/validation-utils.d.ts +0 -44
  198. package/dist/esm/core/types.d.ts +0 -62
  199. package/dist/esm/core/version.d.ts +0 -1
  200. package/dist/esm/core/version.js +0 -2
  201. package/dist/esm/index.d.ts +0 -12
  202. package/dist/index.d.ts +0 -12
  203. package/dist/index.js +0 -55
@@ -0,0 +1,1117 @@
1
+ /**
2
+ * Modal UI Component for Phone Authentication
3
+ *
4
+ * This file creates the UI components (modals, buttons) that are shown
5
+ * for Desktop QR code authentication flow.
6
+ *
7
+ * Supports three view modes:
8
+ * - toggle: Single QR with iOS/Android toggle (default)
9
+ * - dual: Side-by-side QR codes for both platforms
10
+ * - pre-step: OS selection first, then QR
11
+ */
12
+ /**
13
+ * Creates and manages a modal dialog for authentication
14
+ */
15
+ export class AuthModal {
16
+ constructor(options) {
17
+ this.container = null;
18
+ this.backdrop = null;
19
+ this.isOpen = false;
20
+ // State for Pre-step mode
21
+ this.currentStep = 'os-choice';
22
+ this.qrCodeData = null;
23
+ this.statusMessage = '';
24
+ // Body scroll lock state
25
+ this.originalBodyOverflow = '';
26
+ // Close animation state
27
+ this.isClosing = false;
28
+ // Icons
29
+ this.iconApple = `<svg class="glide-icon glide-icon-os" viewBox="0 0 384 512" fill="currentColor"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 52.3-11.4 69.5-34.3z"/></svg>`;
30
+ this.iconAndroid = `<svg class="glide-icon glide-icon-os" viewBox="0 0 576 512" fill="currentColor"><path d="M420.55,301.93a24,24,0,1,1,24-24,24,24,0,0,1-24,24m-265.1,0a24,24,0,1,1,24-24,24,24,0,0,1-24,24m273.7-144.48,47.94-83a10,10,0,1,0-17.27-10h0l-48.54,84.07a301.25,301.25,0,0,0-246.56,0L116.18,64.45a10,10,0,1,0-17.27,10h0l47.94,83C64.53,202.22,8.24,285.55,0,384H576c-8.24-98.45-64.54-181.78-146.85-226.55"/></svg>`;
31
+ this.iconBack = `<svg class="glide-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>`;
32
+ this.options = options || {};
33
+ this.theme = this.options.theme || 'auto';
34
+ this.handleEscapeKey = this.handleEscapeKey.bind(this);
35
+ }
36
+ /**
37
+ * Determines if dark mode should be used based on theme setting
38
+ */
39
+ shouldUseDarkMode() {
40
+ if (this.theme === 'dark')
41
+ return true;
42
+ if (this.theme === 'light')
43
+ return false;
44
+ // Auto - use system preference
45
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
46
+ }
47
+ handleEscapeKey(event) {
48
+ if (event.key === 'Escape' && this.isOpen) {
49
+ if (this.options?.closeOnEscape !== false) {
50
+ this.closeCallback?.();
51
+ this.close();
52
+ }
53
+ }
54
+ }
55
+ escapeHtml(unsafe) {
56
+ return unsafe
57
+ .replace(/&/g, "&amp;")
58
+ .replace(/</g, "&lt;")
59
+ .replace(/>/g, "&gt;")
60
+ .replace(/"/g, "&quot;")
61
+ .replace(/'/g, "&#039;");
62
+ }
63
+ /**
64
+ * Shows the modal with a QR code
65
+ * @param qrCodeData - Either a string (single QR) or QRCodeData (separate iOS/Android)
66
+ * @param statusMessage - Optional status message to display
67
+ */
68
+ showQRCode(qrCodeData, statusMessage = 'Scan with your phone camera') {
69
+ this.qrCodeData = qrCodeData;
70
+ this.statusMessage = statusMessage;
71
+ // Determine mode
72
+ const mode = this.options.viewMode || 'toggle';
73
+ // If legacy string data, force toggle mode
74
+ if (typeof qrCodeData === 'string') {
75
+ this.renderToggleMode(qrCodeData, statusMessage);
76
+ }
77
+ else {
78
+ switch (mode) {
79
+ case 'dual':
80
+ this.renderDualMode(qrCodeData, statusMessage);
81
+ break;
82
+ case 'pre-step':
83
+ this.renderPreStepMode(qrCodeData, statusMessage);
84
+ break;
85
+ case 'toggle':
86
+ default:
87
+ this.renderToggleMode(qrCodeData, statusMessage);
88
+ break;
89
+ }
90
+ }
91
+ this.show();
92
+ }
93
+ /**
94
+ * Updates the status message shown in the modal
95
+ */
96
+ updateStatus(status, isError = false) {
97
+ this.statusMessage = status;
98
+ if (this.container) {
99
+ const el = this.container.querySelector('#glide-status');
100
+ if (el) {
101
+ el.textContent = status;
102
+ if (isError) {
103
+ el.style.color = '#ff3b30';
104
+ }
105
+ else {
106
+ el.style.color = '';
107
+ }
108
+ }
109
+ }
110
+ }
111
+ setCloseCallback(callback) {
112
+ this.closeCallback = callback;
113
+ }
114
+ // --- Renderers ---
115
+ renderToggleMode(data, msg) {
116
+ let iosQR = '';
117
+ let androidQR = '';
118
+ if (typeof data === 'string') {
119
+ iosQR = data;
120
+ androidQR = data;
121
+ }
122
+ else {
123
+ iosQR = data.iosQRCode;
124
+ androidQR = data.androidQRCode || data.iosQRCode;
125
+ }
126
+ const content = `
127
+ <div class="glide-content">
128
+ <div class="glide-toggle" id="glide-toggle" data-active="ios">
129
+ <div class="glide-toggle-slider"></div>
130
+ <button class="glide-btn glide-toggle-btn active" data-platform="ios">${this.iconApple}<span>iOS</span></button>
131
+ <button class="glide-btn glide-toggle-btn" data-platform="android">${this.iconAndroid}<span>Android</span></button>
132
+ </div>
133
+
134
+ <div class="glide-qr-area">
135
+ <img
136
+ class="glide-qr-img"
137
+ id="glide-qr-img"
138
+ src="${this.escapeHtml(iosQR)}"
139
+ alt="QR Code"
140
+ class="glide-qr-img"
141
+ data-ios="${this.escapeHtml(iosQR)}"
142
+ data-android="${this.escapeHtml(androidQR)}"
143
+ />
144
+
145
+ <!-- Animation Overlay (Inside QR Area for centering) -->
146
+ <div id="glide-phone-overlay" class="glide-phone-animation-overlay">
147
+ <div class="glide-outlined-phone">
148
+ <div class="glide-scan-line"></div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ `;
154
+ this.createModal(content, '', true);
155
+ this.setupPlatformToggles();
156
+ this.setupHelpInteraction();
157
+ }
158
+ renderDualMode(data, msg) {
159
+ const qrOverlayHtml = `
160
+ <div class="glide-phone-animation-overlay">
161
+ <div class="glide-outlined-phone">
162
+ <div class="glide-scan-line"></div>
163
+ </div>
164
+ </div>
165
+ `;
166
+ this.createModal(`
167
+ <div class="glide-content glide-dual-mode">
168
+ <div class="glide-dual-container">
169
+ <div class="glide-dual-item">
170
+ <div class="glide-os-logo">
171
+ ${this.iconApple}
172
+ <span>iOS</span>
173
+ </div>
174
+ <div class="glide-qr-area">
175
+ <img src="${this.escapeHtml(data.iosQRCode)}" alt="iOS QR" class="glide-qr-img" />
176
+ ${qrOverlayHtml}
177
+ </div>
178
+ </div>
179
+ <div class="glide-dual-separator">
180
+ <div class="glide-separator-line"></div>
181
+ <span class="glide-separator-text">or</span>
182
+ <div class="glide-separator-line"></div>
183
+ </div>
184
+ <div class="glide-dual-item">
185
+ <div class="glide-os-logo">
186
+ ${this.iconAndroid}
187
+ <span>Android</span>
188
+ </div>
189
+ <div class="glide-qr-area">
190
+ <img src="${this.escapeHtml(data.androidQRCode || data.iosQRCode)}" alt="Android QR" class="glide-qr-img" />
191
+ ${qrOverlayHtml}
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ `, 'glide-modal-wide', true);
197
+ this.setupHelpInteraction();
198
+ }
199
+ renderPreStepMode(data, msg) {
200
+ this.currentStep = 'os-choice';
201
+ // First create the initial modal with OS choice buttons
202
+ const contentHtml = `
203
+ <div class="glide-pre-step-container">
204
+ <button class="glide-os-choice-btn" id="glide-btn-ios">
205
+ ${this.iconApple}
206
+ <span>iOS</span>
207
+ </button>
208
+ <button class="glide-os-choice-btn" id="glide-btn-android">
209
+ ${this.iconAndroid}
210
+ <span>Android</span>
211
+ </button>
212
+ </div>
213
+ `;
214
+ this.createModal(contentHtml, '', false);
215
+ this.setupPreStepListeners();
216
+ this.show();
217
+ }
218
+ setupPreStepListeners() {
219
+ if (!this.container)
220
+ return;
221
+ this.container.querySelector('#glide-btn-ios')?.addEventListener('click', () => {
222
+ this.currentStep = 'ios-qr';
223
+ this.updatePreStepUI();
224
+ });
225
+ this.container.querySelector('#glide-btn-android')?.addEventListener('click', () => {
226
+ this.currentStep = 'android-qr';
227
+ this.updatePreStepUI();
228
+ });
229
+ }
230
+ updatePreStepUI() {
231
+ if (!this.container)
232
+ return;
233
+ let contentHtml = '';
234
+ if (this.currentStep === 'os-choice') {
235
+ // Back to OS choice
236
+ contentHtml = `
237
+ <div class="glide-pre-step-container">
238
+ <button class="glide-os-choice-btn" id="glide-btn-ios">
239
+ ${this.iconApple}
240
+ <span>iOS</span>
241
+ </button>
242
+ <button class="glide-os-choice-btn" id="glide-btn-android">
243
+ ${this.iconAndroid}
244
+ <span>Android</span>
245
+ </button>
246
+ </div>
247
+ `;
248
+ this.createModal(contentHtml, '', false);
249
+ this.setupPreStepListeners();
250
+ }
251
+ else if (this.currentStep === 'ios-qr') {
252
+ const qr = (typeof this.qrCodeData === 'object' && this.qrCodeData?.iosQRCode)
253
+ ? this.qrCodeData.iosQRCode
254
+ : this.qrCodeData;
255
+ contentHtml = `
256
+ <div class="glide-content">
257
+ <div class="glide-qr-area">
258
+ <img src="${this.escapeHtml(qr)}" alt="QR Code" class="glide-qr-img" />
259
+
260
+ <!-- Animation Overlay -->
261
+ <div id="glide-phone-overlay" class="glide-phone-animation-overlay">
262
+ <div class="glide-outlined-phone">
263
+ <div class="glide-scan-line"></div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ `;
269
+ // includeHelpButton = true, includeBackButton = true
270
+ this.createModal(contentHtml, '', true, true);
271
+ this.setupBackButton();
272
+ this.setupHelpInteraction();
273
+ }
274
+ else if (this.currentStep === 'android-qr') {
275
+ const qr = (typeof this.qrCodeData === 'object' && this.qrCodeData?.androidQRCode)
276
+ ? this.qrCodeData.androidQRCode
277
+ : (typeof this.qrCodeData === 'object' && this.qrCodeData?.iosQRCode)
278
+ ? this.qrCodeData.iosQRCode // Fallback to iOS QR if no Android QR
279
+ : this.qrCodeData;
280
+ contentHtml = `
281
+ <div class="glide-content">
282
+ <div class="glide-qr-area">
283
+ <img src="${this.escapeHtml(qr)}" alt="QR Code" class="glide-qr-img" />
284
+
285
+ <!-- Animation Overlay -->
286
+ <div id="glide-phone-overlay" class="glide-phone-animation-overlay">
287
+ <div class="glide-outlined-phone">
288
+ <div class="glide-scan-line"></div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ `;
294
+ // includeHelpButton = true, includeBackButton = true
295
+ this.createModal(contentHtml, '', true, true);
296
+ this.setupBackButton();
297
+ this.setupHelpInteraction();
298
+ }
299
+ }
300
+ setupBackButton() {
301
+ if (!this.container)
302
+ return;
303
+ this.container.querySelector('#glide-back-btn')?.addEventListener('click', () => {
304
+ this.currentStep = 'os-choice';
305
+ this.updatePreStepUI();
306
+ });
307
+ }
308
+ // --- Core Methods ---
309
+ createModal(content, extraClass = '', includeHelpButton = false, includeBackButton = false) {
310
+ // If a modal is closing, force cleanup before creating new one
311
+ if (this.isClosing) {
312
+ this.isClosing = false;
313
+ this.cleanup();
314
+ }
315
+ if (!this.container) {
316
+ this.backdrop = document.createElement('div');
317
+ this.backdrop.className = 'glide-backdrop';
318
+ this.backdrop.id = 'glide-backdrop';
319
+ this.container = document.createElement('div');
320
+ this.container.className = `glide-modal ${extraClass}`;
321
+ this.container.id = 'glide-modal';
322
+ if (this.shouldUseDarkMode()) {
323
+ this.container.classList.add('dark');
324
+ }
325
+ if (this.isOpen) {
326
+ document.body.appendChild(this.backdrop);
327
+ document.body.appendChild(this.container);
328
+ }
329
+ }
330
+ else {
331
+ this.container.className = `glide-modal ${extraClass}`;
332
+ if (this.shouldUseDarkMode()) {
333
+ this.container.classList.add('dark');
334
+ }
335
+ }
336
+ this.container.innerHTML = `
337
+ ${includeBackButton ? `
338
+ <button class="glide-btn glide-btn-back" id="glide-back-btn" aria-label="Back">
339
+ ${this.iconBack}
340
+ </button>
341
+ ` : ''}
342
+ ${this.options?.showCloseButton !== false ? `
343
+ <button class="glide-btn glide-btn-close" id="glide-close-btn" aria-label="Close">
344
+ <svg class="glide-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
345
+ <line x1="18" y1="6" x2="6" y2="18"></line>
346
+ <line x1="6" y1="6" x2="18" y2="18"></line>
347
+ </svg>
348
+ </button>
349
+ ` : ''}
350
+ <h2 class="glide-title">${this.options?.title || 'Scan to Verify'}</h2>
351
+ <div id="glide-modal-content">${content}</div>
352
+ <p class="glide-status" id="glide-status">${this.options?.description || ''}</p>
353
+ ${includeHelpButton ? `
354
+ <button class="glide-btn glide-btn-help" id="glide-help-btn">?</button>
355
+ ` : ''}
356
+ `;
357
+ this.injectStyles();
358
+ const closeBtn = this.container.querySelector('#glide-close-btn');
359
+ if (closeBtn) {
360
+ closeBtn.addEventListener('click', () => {
361
+ this.closeCallback?.();
362
+ this.close();
363
+ });
364
+ }
365
+ if (this.backdrop) {
366
+ this.backdrop.onclick = (e) => {
367
+ if (e.target === this.backdrop && this.options?.closeOnBackdropClick !== false) {
368
+ this.closeCallback?.();
369
+ this.close();
370
+ }
371
+ };
372
+ }
373
+ }
374
+ setupHelpInteraction() {
375
+ const trigger = this.container?.querySelector('#glide-help-btn');
376
+ if (trigger) {
377
+ trigger.addEventListener('click', () => {
378
+ // Select all overlays (handles single and dual mode)
379
+ const overlays = this.container?.querySelectorAll('.glide-phone-animation-overlay');
380
+ if (overlays) {
381
+ overlays.forEach(overlay => {
382
+ overlay.classList.remove('playing');
383
+ void overlay.offsetWidth; // Trigger reflow
384
+ overlay.classList.add('playing');
385
+ // Auto-hide after animation (3s)
386
+ setTimeout(() => {
387
+ overlay.classList.remove('playing');
388
+ }, 3000);
389
+ });
390
+ }
391
+ });
392
+ }
393
+ }
394
+ injectStyles() {
395
+ if (document.getElementById('glide-modal-styles'))
396
+ return;
397
+ const styles = document.createElement('style');
398
+ styles.id = 'glide-modal-styles';
399
+ styles.textContent = `
400
+ :root {
401
+ --glide-primary: #007AFF;
402
+ --glide-text: #1d1d1f;
403
+ --glide-bg-light: rgba(255, 255, 255, 0.6);
404
+ --glide-bg-dark: rgba(30, 30, 30, 0.6);
405
+ --glide-phone-border: #000000; /* High Contrast Black */
406
+
407
+ /* Button sizes */
408
+ --glide-btn-size: 28px;
409
+ --glide-help-btn-size: 24px;
410
+ --glide-toggle-icon-size: 14px;
411
+ --glide-dual-icon-size: 18px;
412
+ }
413
+
414
+ #glide-backdrop {
415
+ position: fixed;
416
+ top: 0;
417
+ left: 0;
418
+ right: 0;
419
+ bottom: 0;
420
+ background: rgba(0, 0, 0, 0.2);
421
+ backdrop-filter: blur(8px);
422
+ -webkit-backdrop-filter: blur(8px);
423
+ z-index: 9998;
424
+ opacity: 0;
425
+ transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);
426
+ }
427
+
428
+ #glide-modal {
429
+ position: fixed;
430
+ top: 20px;
431
+ left: 50%;
432
+ transform: translateX(-50%) scale(0.94);
433
+ background: var(--glide-bg-light);
434
+ backdrop-filter: blur(24px) saturate(180%);
435
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
436
+ border-radius: 24px;
437
+ padding: 32px;
438
+ width: 360px;
439
+ max-width: 90%;
440
+ box-shadow:
441
+ 0 20px 40px rgba(0,0,0,0.2),
442
+ 0 0 0 1px rgba(255,255,255,0.6) inset,
443
+ 0 0 0 1px rgba(0,0,0,0.05);
444
+ z-index: 9999;
445
+ opacity: 0;
446
+ transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
447
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
448
+ text-align: center;
449
+ color: var(--glide-text);
450
+ }
451
+
452
+ #glide-modal.glide-modal-wide {
453
+ width: 600px;
454
+ max-width: 95vw;
455
+ }
456
+
457
+ #glide-modal.dark {
458
+ background: var(--glide-bg-dark);
459
+ color: white;
460
+ box-shadow:
461
+ 0 20px 40px rgba(0,0,0,0.4),
462
+ 0 0 0 1px rgba(255,255,255,0.15) inset,
463
+ 0 0 0 1px rgba(0,0,0,0.5);
464
+ --glide-phone-border: #ffffff; /* High Contrast White */
465
+ }
466
+
467
+ #glide-modal .glide-btn-close {
468
+ position: absolute;
469
+ top: 16px;
470
+ right: 16px;
471
+ width: var(--glide-btn-size);
472
+ height: var(--glide-btn-size);
473
+ min-width: var(--glide-btn-size);
474
+ min-height: var(--glide-btn-size);
475
+ max-width: var(--glide-btn-size);
476
+ max-height: var(--glide-btn-size);
477
+ background: rgba(118, 118, 128, 0.12);
478
+ border: none;
479
+ border-radius: 50%;
480
+ display: flex;
481
+ align-items: center;
482
+ justify-content: center;
483
+ cursor: pointer;
484
+ color: rgba(0,0,0,0.5);
485
+ transition: background 0.2s;
486
+ padding: 0;
487
+ z-index: 20;
488
+ box-sizing: border-box;
489
+ flex-shrink: 0;
490
+ }
491
+
492
+ #glide-modal.dark .glide-btn-close {
493
+ background: rgba(255, 255, 255, 0.1);
494
+ color: rgba(255,255,255,0.5);
495
+ }
496
+
497
+ #glide-modal .glide-btn-close:hover {
498
+ background: rgba(118, 118, 128, 0.2);
499
+ }
500
+
501
+ #glide-modal.dark .glide-btn-close:hover {
502
+ background: rgba(255, 255, 255, 0.2);
503
+ }
504
+
505
+ .glide-title {
506
+ margin: 0 0 8px 0;
507
+ font-size: 22px;
508
+ font-weight: 600;
509
+ letter-spacing: -0.01em;
510
+ }
511
+
512
+ .glide-status {
513
+ margin: 12px 0 0 0;
514
+ font-size: 13px;
515
+ color: rgba(0,0,0,0.5);
516
+ text-align: center;
517
+ min-height: 18px;
518
+ }
519
+
520
+ .glide-status:empty {
521
+ display: none;
522
+ }
523
+
524
+ #glide-modal.dark .glide-status {
525
+ color: rgba(255,255,255,0.5);
526
+ }
527
+
528
+ .glide-content {
529
+ display: flex;
530
+ flex-direction: column;
531
+ align-items: center;
532
+ position: relative;
533
+ }
534
+
535
+ /* --- Sliding Toggle Switch --- */
536
+ #glide-toggle {
537
+ background: rgba(118, 118, 128, 0.12);
538
+ padding: 2px;
539
+ border-radius: 8px;
540
+ display: inline-flex;
541
+ margin-bottom: 24px;
542
+ margin-top: 16px;
543
+ position: relative;
544
+ height: 32px;
545
+ width: 200px;
546
+ box-sizing: border-box;
547
+ }
548
+
549
+ #glide-modal.dark #glide-toggle {
550
+ background: rgba(118, 118, 128, 0.24);
551
+ }
552
+
553
+ /* The sliding background */
554
+ .glide-toggle-slider {
555
+ position: absolute;
556
+ top: 2px;
557
+ left: 2px;
558
+ width: calc(50% - 2px);
559
+ height: calc(100% - 4px);
560
+ background: white;
561
+ border-radius: 6px;
562
+ box-shadow: 0 3px 8px rgba(0,0,0,0.12), 0 3px 1px rgba(0,0,0,0.04);
563
+ transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
564
+ z-index: 0;
565
+ }
566
+
567
+ #glide-modal.dark .glide-toggle-slider {
568
+ background: #636366;
569
+ }
570
+
571
+ /* Move slider for Android */
572
+ #glide-toggle[data-active="android"] .glide-toggle-slider {
573
+ transform: translateX(100%);
574
+ }
575
+
576
+ .glide-toggle-btn {
577
+ flex: 1;
578
+ background: none;
579
+ border: none;
580
+ padding: 0;
581
+ margin: 0;
582
+ font-size: 13px;
583
+ font-weight: 500;
584
+ color: inherit;
585
+ cursor: pointer;
586
+ position: relative;
587
+ z-index: 1;
588
+ transition: color 0.2s;
589
+ display: flex;
590
+ align-items: center;
591
+ justify-content: center;
592
+ gap: 6px;
593
+ min-height: auto;
594
+ height: auto;
595
+ min-width: auto;
596
+ border-radius: 0;
597
+ }
598
+
599
+ #glide-modal .glide-toggle-btn .glide-icon-os,
600
+ #glide-modal .glide-toggle-btn svg {
601
+ width: var(--glide-toggle-icon-size);
602
+ height: var(--glide-toggle-icon-size);
603
+ flex-shrink: 0;
604
+ margin: 0;
605
+ padding: 0;
606
+ }
607
+
608
+ .glide-toggle-btn span {
609
+ margin: 0;
610
+ padding: 0;
611
+ line-height: 1;
612
+ }
613
+
614
+ #glide-modal.dark .glide-toggle-btn {
615
+ color: rgba(255,255,255,0.6);
616
+ }
617
+
618
+ #glide-toggle[data-active="ios"] .glide-toggle-btn[data-platform="ios"],
619
+ #glide-toggle[data-active="android"] .glide-toggle-btn[data-platform="android"] {
620
+ color: #1d1d1f;
621
+ }
622
+
623
+ #glide-modal.dark #glide-toggle[data-active="ios"] .glide-toggle-btn[data-platform="ios"],
624
+ #glide-modal.dark #glide-toggle[data-active="android"] .glide-toggle-btn[data-platform="android"] {
625
+ color: white;
626
+ }
627
+
628
+ /* QR Area */
629
+ .glide-qr-area {
630
+ position: relative;
631
+ margin: 0 auto;
632
+ }
633
+
634
+ .glide-qr-img {
635
+ width: 200px;
636
+ height: 200px;
637
+ object-fit: contain;
638
+ display: block;
639
+ border-radius: 16px;
640
+ }
641
+
642
+ /* Dual Mode QR Area - no extra padding, same as single mode */
643
+ .glide-dual-mode .glide-qr-area {
644
+ background: transparent;
645
+ padding: 0;
646
+ }
647
+
648
+ .glide-dual-mode .glide-qr-img {
649
+ width: 180px;
650
+ height: 180px;
651
+ border-radius: 16px;
652
+ }
653
+
654
+ /* --- Help Icon & Interaction --- */
655
+ .glide-btn-help {
656
+ position: absolute;
657
+ bottom: 20px;
658
+ right: 20px;
659
+ width: var(--glide-help-btn-size);
660
+ height: var(--glide-help-btn-size);
661
+ min-width: var(--glide-help-btn-size);
662
+ min-height: var(--glide-help-btn-size);
663
+ max-width: var(--glide-help-btn-size);
664
+ max-height: var(--glide-help-btn-size);
665
+ border-radius: 50%;
666
+ border: 1.5px solid rgba(0,0,0,0.2);
667
+ color: rgba(0,0,0,0.4);
668
+ display: flex;
669
+ align-items: center;
670
+ justify-content: center;
671
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
672
+ font-size: 14px;
673
+ font-weight: 600;
674
+ line-height: 1;
675
+ cursor: pointer;
676
+ transition: all 0.2s;
677
+ background: transparent;
678
+ padding: 0;
679
+ margin: 0;
680
+ box-sizing: border-box;
681
+ z-index: 10;
682
+ }
683
+
684
+ #glide-modal.dark .glide-btn-help {
685
+ border-color: rgba(255,255,255,0.3);
686
+ color: rgba(255,255,255,0.5);
687
+ }
688
+
689
+ .glide-btn-help:hover {
690
+ border-color: var(--glide-primary);
691
+ color: var(--glide-primary);
692
+ background: rgba(0, 122, 255, 0.1);
693
+ }
694
+
695
+ /* Tooltip */
696
+ .glide-btn-help::after {
697
+ content: "Need help?";
698
+ position: absolute;
699
+ bottom: 100%;
700
+ left: 50%;
701
+ transform: translateX(-50%) translateY(-8px);
702
+ background: rgba(0,0,0,0.8);
703
+ color: white;
704
+ padding: 4px 8px;
705
+ border-radius: 4px;
706
+ font-size: 11px;
707
+ white-space: nowrap;
708
+ opacity: 0;
709
+ pointer-events: none;
710
+ transition: opacity 0.2s;
711
+ }
712
+
713
+ .glide-btn-help:hover::after {
714
+ opacity: 1;
715
+ }
716
+
717
+ /* --- Outlined Phone Animation --- */
718
+ .glide-phone-animation-overlay {
719
+ position: absolute;
720
+ top: 0;
721
+ left: 0;
722
+ width: 100%;
723
+ height: 100%;
724
+ display: flex;
725
+ align-items: center;
726
+ justify-content: center;
727
+ pointer-events: none;
728
+ opacity: 0;
729
+ transition: opacity 0.3s;
730
+ border-radius: 16px;
731
+ background: rgba(255,255,255,0.8);
732
+ }
733
+
734
+ #glide-modal.dark .glide-phone-animation-overlay {
735
+ background: rgba(0,0,0,0.6);
736
+ }
737
+
738
+ .glide-phone-animation-overlay.playing {
739
+ opacity: 1;
740
+ }
741
+
742
+ .glide-outlined-phone {
743
+ width: 100px;
744
+ height: 180px;
745
+ border: 4px solid var(--glide-phone-border);
746
+ border-radius: 16px;
747
+ position: relative;
748
+ background: transparent;
749
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
750
+ transform: translateY(20px);
751
+ }
752
+
753
+ /* Notch */
754
+ .glide-outlined-phone::before {
755
+ content: '';
756
+ position: absolute;
757
+ top: -1px;
758
+ left: 50%;
759
+ transform: translateX(-50%);
760
+ width: 40%;
761
+ height: 12px;
762
+ background: var(--glide-phone-border);
763
+ border-bottom-left-radius: 8px;
764
+ border-bottom-right-radius: 8px;
765
+ }
766
+
767
+ /* Scanning Line */
768
+ .glide-scan-line {
769
+ position: absolute;
770
+ top: 10%;
771
+ left: 5%;
772
+ width: 90%;
773
+ height: 2px;
774
+ background: var(--glide-primary);
775
+ box-shadow: 0 0 8px var(--glide-primary);
776
+ opacity: 0;
777
+ }
778
+
779
+ /* Animation Keyframes */
780
+ @keyframes glide-scan-motion {
781
+ 0% { top: 10%; opacity: 0; }
782
+ 10% { opacity: 1; }
783
+ 90% { opacity: 1; }
784
+ 100% { top: 90%; opacity: 0; }
785
+ }
786
+
787
+ .glide-phone-animation-overlay.playing .glide-outlined-phone {
788
+ animation: glide-phone-appear 3s ease-in-out forwards;
789
+ }
790
+
791
+ .glide-phone-animation-overlay.playing .glide-scan-line {
792
+ animation: glide-scan-motion 2s ease-in-out 0.5s infinite;
793
+ }
794
+
795
+ @keyframes glide-phone-appear {
796
+ 0% { transform: translateY(20px); opacity: 0; }
797
+ 10% { transform: translateY(0); opacity: 1; }
798
+ 90% { transform: translateY(0); opacity: 1; }
799
+ 100% { transform: translateY(20px); opacity: 0; }
800
+ }
801
+
802
+ /* Dual Mode */
803
+ .glide-dual-container {
804
+ display: flex;
805
+ justify-content: center;
806
+ align-items: stretch;
807
+ gap: 32px;
808
+ margin-top: 20px;
809
+ }
810
+
811
+ .glide-dual-item {
812
+ display: flex;
813
+ flex-direction: column;
814
+ align-items: center;
815
+ }
816
+
817
+ .glide-os-logo {
818
+ display: flex;
819
+ align-items: center;
820
+ gap: 8px;
821
+ font-weight: 600;
822
+ margin-bottom: 12px;
823
+ font-size: 15px;
824
+ color: inherit;
825
+ }
826
+
827
+ /* Dual mode OS logo - aligned icons with text, spacing to QR */
828
+ .glide-dual-mode .glide-os-logo {
829
+ margin-bottom: 12px; /* Space between label and QR code */
830
+ }
831
+
832
+ #glide-modal .glide-dual-mode .glide-os-logo .glide-icon-os,
833
+ #glide-modal .glide-dual-mode .glide-os-logo svg {
834
+ width: var(--glide-dual-icon-size);
835
+ height: var(--glide-dual-icon-size);
836
+ margin: 0;
837
+ padding: 0;
838
+ flex-shrink: 0;
839
+ }
840
+
841
+ .glide-dual-mode .glide-os-logo span {
842
+ margin: 0;
843
+ padding: 0;
844
+ line-height: 1;
845
+ }
846
+
847
+ .glide-os-logo svg {
848
+ width: 20px;
849
+ height: 20px;
850
+ opacity: 0.8;
851
+ }
852
+
853
+ /* Dual Mode Separator - aligned to QR codes only */
854
+ .glide-dual-separator {
855
+ display: flex;
856
+ flex-direction: column;
857
+ align-items: center;
858
+ justify-content: center;
859
+ align-self: stretch;
860
+ /* Offset for OS label (18px icon + 12px margin = ~30px) */
861
+ margin-top: 30px;
862
+ padding: 20px 0;
863
+ }
864
+
865
+ .glide-separator-line {
866
+ width: 1px;
867
+ flex: 1;
868
+ background: linear-gradient(to bottom, transparent, rgba(128,128,128,0.3), transparent);
869
+ }
870
+
871
+ .glide-separator-text {
872
+ padding: 8px 0;
873
+ font-size: 11px;
874
+ font-weight: 500;
875
+ color: rgba(128,128,128,0.5);
876
+ text-transform: uppercase;
877
+ letter-spacing: 0.5px;
878
+ }
879
+
880
+ #glide-modal.dark .glide-separator-line {
881
+ background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.2), transparent);
882
+ }
883
+
884
+ #glide-modal.dark .glide-separator-text {
885
+ color: rgba(255,255,255,0.4);
886
+ }
887
+
888
+ /* Pre-Step */
889
+ .glide-pre-step-container {
890
+ display: flex;
891
+ gap: 20px;
892
+ justify-content: center;
893
+ margin: 24px 0;
894
+ }
895
+
896
+ .glide-os-choice-btn {
897
+ display: flex;
898
+ flex-direction: column;
899
+ align-items: center;
900
+ justify-content: center;
901
+ width: 140px;
902
+ height: 140px;
903
+ border: 1px solid rgba(0,0,0,0.08);
904
+ border-radius: 20px;
905
+ background: rgba(255,255,255,0.4);
906
+ cursor: pointer;
907
+ transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
908
+ position: relative;
909
+ overflow: hidden;
910
+ font-size: 14px;
911
+ color: inherit;
912
+ }
913
+
914
+ #glide-modal.dark .glide-os-choice-btn {
915
+ background: rgba(255,255,255,0.05);
916
+ border-color: rgba(255,255,255,0.1);
917
+ color: white;
918
+ }
919
+
920
+ .glide-os-choice-btn:hover {
921
+ background: rgba(255,255,255,0.9);
922
+ transform: translateY(-4px);
923
+ box-shadow: 0 12px 24px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.04);
924
+ border-color: var(--glide-primary);
925
+ }
926
+
927
+ #glide-modal.dark .glide-os-choice-btn:hover {
928
+ background: rgba(255,255,255,0.15);
929
+ box-shadow: 0 12px 24px rgba(0,0,0,0.3);
930
+ border-color: var(--glide-primary);
931
+ }
932
+
933
+ .glide-icon-os {
934
+ width: 40px;
935
+ height: 40px;
936
+ margin-bottom: 12px;
937
+ transition: transform 0.3s;
938
+ fill: currentColor;
939
+ }
940
+
941
+ .glide-os-choice-btn:hover .glide-icon-os {
942
+ transform: scale(1.1);
943
+ }
944
+
945
+ #glide-modal .glide-btn-back {
946
+ position: absolute;
947
+ top: 16px;
948
+ left: 16px;
949
+ width: var(--glide-btn-size);
950
+ height: var(--glide-btn-size);
951
+ min-width: var(--glide-btn-size);
952
+ min-height: var(--glide-btn-size);
953
+ max-width: var(--glide-btn-size);
954
+ max-height: var(--glide-btn-size);
955
+ background: rgba(118, 118, 128, 0.12);
956
+ border: none;
957
+ border-radius: 50%;
958
+ display: flex;
959
+ align-items: center;
960
+ justify-content: center;
961
+ cursor: pointer;
962
+ color: rgba(0,0,0,0.5);
963
+ transition: background 0.2s;
964
+ z-index: 20;
965
+ padding: 0;
966
+ margin: 0;
967
+ box-sizing: border-box;
968
+ flex-shrink: 0;
969
+ }
970
+
971
+ #glide-modal.dark .glide-btn-back {
972
+ background: rgba(255, 255, 255, 0.1);
973
+ color: rgba(255,255,255,0.5);
974
+ }
975
+
976
+ .glide-btn-back:hover {
977
+ background: rgba(118, 118, 128, 0.2);
978
+ }
979
+
980
+ #glide-modal.dark .glide-btn-back:hover {
981
+ background: rgba(255, 255, 255, 0.2);
982
+ }
983
+
984
+ .glide-spinner {
985
+ width: 40px;
986
+ height: 40px;
987
+ border: 3px solid rgba(0,0,0,0.1);
988
+ border-top-color: var(--glide-primary);
989
+ border-radius: 50%;
990
+ animation: glide-spin 1s linear infinite;
991
+ margin: 40px auto;
992
+ }
993
+
994
+ #glide-modal.dark .glide-spinner {
995
+ border-color: rgba(255,255,255,0.1);
996
+ border-top-color: var(--glide-primary);
997
+ }
998
+
999
+ @keyframes glide-spin {
1000
+ to { transform: rotate(360deg); }
1001
+ }
1002
+ `;
1003
+ document.head.appendChild(styles);
1004
+ }
1005
+ show() {
1006
+ if (!this.container || !this.backdrop || this.isOpen) {
1007
+ if (this.isOpen && this.container && this.backdrop) {
1008
+ document.removeEventListener('keydown', this.handleEscapeKey);
1009
+ document.addEventListener('keydown', this.handleEscapeKey);
1010
+ }
1011
+ return;
1012
+ }
1013
+ document.body.appendChild(this.backdrop);
1014
+ document.body.appendChild(this.container);
1015
+ // Lock body scroll when modal opens
1016
+ this.lockBodyScroll();
1017
+ document.addEventListener('keydown', this.handleEscapeKey);
1018
+ // Note: setupPlatformToggles and setupHelpInteraction are called in render methods
1019
+ // Don't call them here to avoid duplicate event listeners
1020
+ requestAnimationFrame(() => {
1021
+ if (this.backdrop && this.container) {
1022
+ this.backdrop.style.opacity = '1';
1023
+ this.container.style.opacity = '1';
1024
+ this.container.style.transform = 'translateX(-50%) scale(1)';
1025
+ }
1026
+ });
1027
+ this.isOpen = true;
1028
+ }
1029
+ setupPlatformToggles() {
1030
+ const container = this.container?.querySelector('#glide-toggle');
1031
+ const platformBtns = this.container?.querySelectorAll('.glide-toggle-btn');
1032
+ const qrImg = this.container?.querySelector('#glide-qr-img');
1033
+ const message = this.container?.querySelector('#glide-platform-message');
1034
+ if (!platformBtns || !qrImg)
1035
+ return;
1036
+ platformBtns.forEach((btn) => {
1037
+ btn.addEventListener('click', (e) => {
1038
+ const target = e.currentTarget;
1039
+ const platform = target.getAttribute('data-platform');
1040
+ // Update container state for slider
1041
+ if (container && platform) {
1042
+ container.setAttribute('data-active', platform);
1043
+ }
1044
+ platformBtns.forEach(b => b.classList.remove('active'));
1045
+ target.classList.add('active');
1046
+ if (platform === 'ios') {
1047
+ qrImg.src = qrImg.getAttribute('data-ios') || '';
1048
+ if (message)
1049
+ message.textContent = 'Scan with your iPhone camera';
1050
+ }
1051
+ else if (platform === 'android') {
1052
+ qrImg.src = qrImg.getAttribute('data-android') || '';
1053
+ if (message)
1054
+ message.textContent = 'Scan with your Android camera';
1055
+ }
1056
+ });
1057
+ });
1058
+ }
1059
+ // --- Body Scroll Lock ---
1060
+ lockBodyScroll() {
1061
+ this.originalBodyOverflow = document.body.style.overflow;
1062
+ document.body.style.overflow = 'hidden';
1063
+ }
1064
+ unlockBodyScroll() {
1065
+ document.body.style.overflow = this.originalBodyOverflow;
1066
+ }
1067
+ close() {
1068
+ if (!this.container || !this.backdrop || !this.isOpen)
1069
+ return;
1070
+ this.isClosing = true;
1071
+ document.removeEventListener('keydown', this.handleEscapeKey);
1072
+ this.backdrop.style.opacity = '0';
1073
+ this.container.style.opacity = '0';
1074
+ this.container.style.transform = 'translateX(-50%) scale(0.94)';
1075
+ this.closeCallback = undefined;
1076
+ setTimeout(() => {
1077
+ // Only cleanup if we're still in closing state (not interrupted by new modal)
1078
+ if (this.isClosing) {
1079
+ this.cleanup();
1080
+ this.isOpen = false;
1081
+ this.isClosing = false;
1082
+ }
1083
+ }, 400);
1084
+ }
1085
+ cleanup() {
1086
+ this.container?.remove();
1087
+ this.backdrop?.remove();
1088
+ this.container = null;
1089
+ this.backdrop = null;
1090
+ // Unlock body scroll when modal closes
1091
+ this.unlockBodyScroll();
1092
+ }
1093
+ /**
1094
+ * Check if modal is currently open
1095
+ */
1096
+ isModalOpen() {
1097
+ return this.isOpen;
1098
+ }
1099
+ }
1100
+ /**
1101
+ * Helper to create QRCodeData from DesktopData
1102
+ */
1103
+ export function createQRCodeDataFromDesktop(desktopData) {
1104
+ const { data } = desktopData;
1105
+ // If we have platform-specific QRs
1106
+ if (data.ios_qr_image) {
1107
+ return {
1108
+ iosQRCode: data.ios_qr_image,
1109
+ androidQRCode: data.android_qr_image || data.ios_qr_image,
1110
+ };
1111
+ }
1112
+ // Fallback to single QR
1113
+ if (data.qr_code_image) {
1114
+ return data.qr_code_image;
1115
+ }
1116
+ throw new Error('No QR code data available');
1117
+ }