@glideidentity/web-client-sdk 6.0.0-beta.4 → 6.0.0-beta.5
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.
- package/dist/browser/web-client-sdk.min.js +1 -1
- package/dist/cjs/adapters/react.js +10 -1
- package/dist/cjs/adapters/vue.js +10 -1
- package/dist/cjs/client/http.js +5 -1
- package/dist/cjs/client/logger.js +1 -7
- package/dist/cjs/client/phone-auth-client.js +35 -2
- package/dist/cjs/core/index.js +1 -2
- package/dist/cjs/core/type-guards.js +16 -4
- package/dist/cjs/core/validators.js +0 -29
- package/dist/cjs/index.js +1 -2
- package/dist/cjs/test/fixtures.js +273 -0
- package/dist/cjs/test/setup.js +117 -0
- package/dist/esm/adapters/react.js +10 -1
- package/dist/esm/adapters/vue.js +10 -1
- package/dist/esm/client/http.js +5 -1
- package/dist/esm/client/logger.js +1 -7
- package/dist/esm/client/phone-auth-client.js +36 -3
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/core/type-guards.js +16 -4
- package/dist/esm/core/validators.js +0 -28
- package/dist/esm/index.js +1 -1
- package/dist/esm/test/fixtures.js +270 -0
- package/dist/esm/test/setup.js +110 -0
- package/dist/types/adapters/react.d.ts.map +1 -1
- package/dist/types/adapters/vue.d.ts.map +1 -1
- package/dist/types/client/http.d.ts.map +1 -1
- package/dist/types/client/logger.d.ts.map +1 -1
- package/dist/types/client/phone-auth-client.d.ts.map +1 -1
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/index.d.ts.map +1 -1
- package/dist/types/core/type-guards.d.ts +3 -0
- package/dist/types/core/type-guards.d.ts.map +1 -1
- package/dist/types/core/validators.d.ts +0 -7
- package/dist/types/core/validators.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/test/fixtures.d.ts +178 -0
- package/dist/types/test/fixtures.d.ts.map +1 -0
- package/dist/types/test/setup.d.ts +39 -0
- package/dist/types/test/setup.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(n,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.GlideWebClientSDK=e():n.GlideWebClientSDK=e()}(this,()=>(()=>{"use strict";var n={d:(e,t)=>{for(var i in t)n.o(t,i)&&!n.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},o:(n,e)=>Object.prototype.hasOwnProperty.call(n,e),r:n=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})}},e={};n.r(e),n.d(e,{AUTHENTICATION_STRATEGY:()=>i,AuthModal:()=>x,ERROR_CODES:()=>s,PhoneAuthClient:()=>E,USE_CASE:()=>t,createAuthError:()=>h,createQRCodeDataFromDesktop:()=>w,getUserMessage:()=>p,isAuthCredential:()=>S,isAuthError:()=>d,isCancellable:()=>_,isClientError:()=>c,isDesktopData:()=>T,isDesktopStrategy:()=>I,isErrorResponse:()=>D,isGetPhoneNumberResponse:()=>P,isInvokeResult:()=>C,isLinkData:()=>O,isLinkStrategy:()=>R,isRetryableError:()=>g,isTS43Data:()=>N,isTS43Strategy:()=>A,isVerifyPhoneNumberResponse:()=>L,validatePhoneNumber:()=>r,validatePlmn:()=>a});const t={GET_PHONE_NUMBER:"GetPhoneNumber",VERIFY_PHONE_NUMBER:"VerifyPhoneNumber"},i={TS43:"ts43",LINK:"link",DESKTOP:"desktop"},o=/^\+[1-9]\d{1,14}$/;function r(n){return n?n.startsWith("+")?n.length<8?{valid:!1,error:"Phone number too short for E.164 format (minimum 8 characters including +)"}:n.length>16?{valid:!1,error:"Phone number too long for E.164 format (maximum 15 digits after +)"}:/^\+\d+$/.test(n)?o.test(n)?{valid:!0}:{valid:!1,error:"Invalid E.164 phone number format"}:{valid:!1,error:"Phone number contains invalid characters. E.164 format only allows + followed by digits"}:{valid:!1,error:"Phone number must be in E.164 format (start with +)"}:{valid:!0}}function a(n){if(!n)return{valid:!0};const{mcc:e,mnc:t}=n;return e&&/^\d{3}$/.test(e)?t&&/^\d{2,3}$/.test(t)?{valid:!0}:{valid:!1,error:"MNC must be 2 or 3 digits"}:{valid:!1,error:"MCC must be exactly 3 digits"}}const s={INVALID_PHONE_NUMBER:"INVALID_PHONE_NUMBER",INVALID_PLMN:"INVALID_PLMN",MISSING_PARAMETERS:"MISSING_PARAMETERS",BROWSER_NOT_SUPPORTED:"BROWSER_NOT_SUPPORTED",UNSUPPORTED_STRATEGY:"UNSUPPORTED_STRATEGY",USER_CANCELLED:"USER_CANCELLED",CANCELLED:"CANCELLED",VERIFICATION_FAILED:"VERIFICATION_FAILED",NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT:"TIMEOUT",INVALID_RESPONSE:"INVALID_RESPONSE"},l={[s.INVALID_PHONE_NUMBER]:"Please enter a valid phone number in E.164 format (e.g., +14155551234).",[s.INVALID_PLMN]:"Invalid carrier information provided.",[s.MISSING_PARAMETERS]:"Required information is missing.",[s.BROWSER_NOT_SUPPORTED]:"Your browser does not support this authentication method. Please use Chrome or Edge with the Digital Credentials flag enabled.",[s.UNSUPPORTED_STRATEGY]:"This authentication strategy is not supported.",[s.USER_CANCELLED]:"Authentication was cancelled.",[s.CANCELLED]:"Authentication was cancelled.",[s.VERIFICATION_FAILED]:"Verification failed. Please try again.",[s.NETWORK_ERROR]:"Network connection failed. Please check your connection and try again.",[s.TIMEOUT]:"Request timed out. Please try again.",[s.INVALID_RESPONSE]:"Invalid response received."};function d(n){return null!==n&&"object"==typeof n&&"code"in n&&"message"in n&&"string"==typeof n.code&&"string"==typeof n.message}function c(n){return Object.values(s).includes(n.code)}function g(n){return n.code===s.NETWORK_ERROR||n.code===s.TIMEOUT}function p(n){return c(n)&&l[n.code]||n.message}function h(n,e,t){const i=new Error(e||l[n]||n);return i.code=n,i.details=t,i.timestamp=(new Date).toISOString(),i}const u=/(\+?[1-9]\d{6,14})/g,b=/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,m=/(eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*)/g,f=/session[_-]?key["']?\s*[:=]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi;function y(n){if(null==n)return n;if("string"==typeof n){let e=n;return e=e.replace(u,n=>n.length<8?n:n.slice(0,2)+"***"+n.slice(-4)),e=e.replace(b,n=>{const[e,t]=n.split("@");return e.slice(0,1)+"***@"+t}),e=e.replace(m,"[JWT]"),e=e.replace(f,(n,e)=>n.replace(e,e.slice(0,4)+"***"+e.slice(-4))),e}if(Array.isArray(n))return n.map(y);if("object"==typeof n){const e={};for(const[t,i]of Object.entries(n))["phone_number","phoneNumber","credential","token","password","secret"].some(n=>t.toLowerCase().includes(n.toLowerCase()))?e[t]="string"==typeof i?"[REDACTED]":i:e[t]=y(i);return e}return n}function v(n){const{sessionKey:e,interval:t=2e3,maxAttempts:i=30,pollingEndpoint:o,statusUrl:r,logger:a,headers:l}=n;let d,c,g=0,p=!1,u=!1;function b(){u=!1,d&&(clearInterval(d),d=void 0)}return{start:function(){return new Promise((n,s)=>{c=s,u=!0,g=0;const h=async()=>{if(u&&!p){if(g>=i)return b(),a?.warn("Polling timeout reached",{attempts:g,maxAttempts:i}),void n({status:"expired",message:"Authentication timeout"});try{const t=o?(a?.debug("Using developer config endpoint for polling",{pollingEndpoint:o}),`${o}/${e}`):r?(a?.debug("Using backend status_url for polling",{statusUrl:r}),r):(a?.debug("Using prod fallback for status polling"),`https://api.glideidentity.app/public/status/${e}`);a?.debug("Polling status",{url:t,attempt:g+1,maxAttempts:i});const s=await fetch(t,{method:"GET",headers:{Accept:"application/json",...l}});if(200===s.status){const t=await s.json();if("completed"===t.status){b(),a?.info("Authentication completed");const i=t.credential||e;return void n({status:"completed",credential:i,session:t.session||{session_key:e}})}g++}else if(410===s.status){b();const e=await s.json().catch(()=>({}));n({status:"expired",message:e.message||"Session expired"})}else if(422===s.status){b();const e=await s.json().catch(()=>({}));n({status:"error",message:e.message||"Authentication failed"})}else 404===s.status?(b(),n({status:"error",message:"Session not found"})):g++}catch(n){a?.debug("Polling error, retrying",{error:n,attempt:g+1}),g++}}};h(),d=setInterval(h,t)})},stop:b,cancel:function(){a?.debug("Polling cancelled"),p=!0,b(),c&&(c(h(s.CANCELLED,"Authentication cancelled")),c=void 0)},isPolling:function(){return void 0!==d},cleanup:function(){b(),p=!1,c=void 0}}}class x{constructor(n){this.container=null,this.backdrop=null,this.isOpen=!1,this.currentStep="os-choice",this.qrCodeData=null,this.statusMessage="",this.originalBodyOverflow="",this.isClosing=!1,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>',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>',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>',this.options=n||{},this.theme=this.options.theme||"auto",this.handleEscapeKey=this.handleEscapeKey.bind(this)}shouldUseDarkMode(){return"dark"===this.theme||"light"!==this.theme&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches}handleEscapeKey(n){"Escape"===n.key&&this.isOpen&&!1!==this.options?.closeOnEscape&&(this.closeCallback?.(),this.close())}escapeHtml(n){return n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}showQRCode(n,e="Scan with your phone camera"){this.qrCodeData=n,this.statusMessage=e;const t=this.options.viewMode||"toggle";if("string"==typeof n)this.renderToggleMode(n,e);else switch(t){case"dual":this.renderDualMode(n,e);break;case"pre-step":this.renderPreStepMode(n,e);break;default:this.renderToggleMode(n,e)}this.show()}updateStatus(n,e=!1){if(this.statusMessage=n,this.container){const t=this.container.querySelector("#glide-status");t&&(t.textContent=n,t.style.color=e?"#ff3b30":"")}}setCloseCallback(n){this.closeCallback=n}renderToggleMode(n,e){let t="",i="";"string"==typeof n?(t=n,i=n):(t=n.iosQRCode,i=n.androidQRCode||n.iosQRCode);const o=`\n <div class="glide-content">\n <div class="glide-toggle" id="glide-toggle" data-active="ios">\n <div class="glide-toggle-slider"></div>\n <button class="glide-btn glide-toggle-btn active" data-platform="ios">${this.iconApple}<span>iOS</span></button>\n <button class="glide-btn glide-toggle-btn" data-platform="android">${this.iconAndroid}<span>Android</span></button>\n </div>\n\n <div class="glide-qr-area">\n <img \n class="glide-qr-img"\n id="glide-qr-img" \n src="${this.escapeHtml(t)}" \n alt="QR Code" \n class="glide-qr-img" \n data-ios="${this.escapeHtml(t)}"\n data-android="${this.escapeHtml(i)}"\n />\n \n \x3c!-- Animation Overlay (Inside QR Area for centering) --\x3e\n <div id="glide-phone-overlay" class="glide-phone-animation-overlay">\n <div class="glide-outlined-phone">\n <div class="glide-scan-line"></div>\n </div>\n </div>\n </div>\n </div>\n `;this.createModal(o,"",!0),this.setupPlatformToggles(),this.setupHelpInteraction()}renderDualMode(n,e){const t='\n <div class="glide-phone-animation-overlay">\n <div class="glide-outlined-phone">\n <div class="glide-scan-line"></div>\n </div>\n </div>\n ';this.createModal(`\n <div class="glide-content glide-dual-mode">\n <div class="glide-dual-container">\n <div class="glide-dual-item">\n <div class="glide-os-logo">\n ${this.iconApple}\n <span>iOS</span>\n </div>\n <div class="glide-qr-area">\n <img src="${this.escapeHtml(n.iosQRCode)}" alt="iOS QR" class="glide-qr-img" />\n ${t}\n </div>\n </div>\n <div class="glide-dual-separator">\n <div class="glide-separator-line"></div>\n <span class="glide-separator-text">or</span>\n <div class="glide-separator-line"></div>\n </div>\n <div class="glide-dual-item">\n <div class="glide-os-logo">\n ${this.iconAndroid}\n <span>Android</span>\n </div>\n <div class="glide-qr-area">\n <img src="${this.escapeHtml(n.androidQRCode||n.iosQRCode)}" alt="Android QR" class="glide-qr-img" />\n ${t}\n </div>\n </div>\n </div>\n </div>\n `,"glide-modal-wide",!0),this.setupHelpInteraction()}renderPreStepMode(n,e){this.currentStep="os-choice";const t=`\n <div class="glide-pre-step-container">\n <button class="glide-os-choice-btn" id="glide-btn-ios">\n ${this.iconApple}\n <span>iOS</span>\n </button>\n <button class="glide-os-choice-btn" id="glide-btn-android">\n ${this.iconAndroid}\n <span>Android</span>\n </button>\n </div>\n `;this.createModal(t,"",!1),this.setupPreStepListeners(),this.show()}setupPreStepListeners(){this.container&&(this.container.querySelector("#glide-btn-ios")?.addEventListener("click",()=>{this.currentStep="ios-qr",this.updatePreStepUI()}),this.container.querySelector("#glide-btn-android")?.addEventListener("click",()=>{this.currentStep="android-qr",this.updatePreStepUI()}))}updatePreStepUI(){if(!this.container)return;let n="";if("os-choice"===this.currentStep)n=`\n <div class="glide-pre-step-container">\n <button class="glide-os-choice-btn" id="glide-btn-ios">\n ${this.iconApple}\n <span>iOS</span>\n </button>\n <button class="glide-os-choice-btn" id="glide-btn-android">\n ${this.iconAndroid}\n <span>Android</span>\n </button>\n </div>\n `,this.createModal(n,"",!1),this.setupPreStepListeners();else if("ios-qr"===this.currentStep){const e="object"==typeof this.qrCodeData&&this.qrCodeData?.iosQRCode?this.qrCodeData.iosQRCode:this.qrCodeData;n=`\n <div class="glide-content">\n <div class="glide-qr-area">\n <img src="${this.escapeHtml(e)}" alt="QR Code" class="glide-qr-img" />\n \n \x3c!-- Animation Overlay --\x3e\n <div id="glide-phone-overlay" class="glide-phone-animation-overlay">\n <div class="glide-outlined-phone">\n <div class="glide-scan-line"></div>\n </div>\n </div>\n </div>\n </div>\n `,this.createModal(n,"",!0,!0),this.setupBackButton(),this.setupHelpInteraction()}else if("android-qr"===this.currentStep){const e="object"==typeof this.qrCodeData&&this.qrCodeData?.androidQRCode?this.qrCodeData.androidQRCode:"object"==typeof this.qrCodeData&&this.qrCodeData?.iosQRCode?this.qrCodeData.iosQRCode:this.qrCodeData;n=`\n <div class="glide-content">\n <div class="glide-qr-area">\n <img src="${this.escapeHtml(e)}" alt="QR Code" class="glide-qr-img" />\n \n \x3c!-- Animation Overlay --\x3e\n <div id="glide-phone-overlay" class="glide-phone-animation-overlay">\n <div class="glide-outlined-phone">\n <div class="glide-scan-line"></div>\n </div>\n </div>\n </div>\n </div>\n `,this.createModal(n,"",!0,!0),this.setupBackButton(),this.setupHelpInteraction()}}setupBackButton(){this.container&&this.container.querySelector("#glide-back-btn")?.addEventListener("click",()=>{this.currentStep="os-choice",this.updatePreStepUI()})}createModal(n,e="",t=!1,i=!1){this.isClosing&&(this.isClosing=!1,this.cleanup()),this.container?(this.container.className=`glide-modal ${e}`,this.shouldUseDarkMode()&&this.container.classList.add("dark")):(this.backdrop=document.createElement("div"),this.backdrop.className="glide-backdrop",this.backdrop.id="glide-backdrop",this.container=document.createElement("div"),this.container.className=`glide-modal ${e}`,this.container.id="glide-modal",this.shouldUseDarkMode()&&this.container.classList.add("dark"),this.isOpen&&(document.body.appendChild(this.backdrop),document.body.appendChild(this.container))),this.container.innerHTML=`\n ${i?`\n <button class="glide-btn glide-btn-back" id="glide-back-btn" aria-label="Back">\n ${this.iconBack}\n </button>\n `:""}\n ${!1!==this.options?.showCloseButton?'\n <button class="glide-btn glide-btn-close" id="glide-close-btn" aria-label="Close">\n <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">\n <line x1="18" y1="6" x2="6" y2="18"></line>\n <line x1="6" y1="6" x2="18" y2="18"></line>\n </svg>\n </button>\n ':""}\n <h2 class="glide-title">${this.options?.title||"Scan to Verify"}</h2>\n <div id="glide-modal-content">${n}</div>\n <p class="glide-status" id="glide-status">${this.options?.description||""}</p>\n ${t?'\n <button class="glide-btn glide-btn-help" id="glide-help-btn">?</button>\n ':""}\n `,this.injectStyles();const o=this.container.querySelector("#glide-close-btn");o&&o.addEventListener("click",()=>{this.closeCallback?.(),this.close()}),this.backdrop&&(this.backdrop.onclick=n=>{n.target===this.backdrop&&!1!==this.options?.closeOnBackdropClick&&(this.closeCallback?.(),this.close())})}setupHelpInteraction(){const n=this.container?.querySelector("#glide-help-btn");n&&n.addEventListener("click",()=>{const n=this.container?.querySelectorAll(".glide-phone-animation-overlay");n&&n.forEach(n=>{n.classList.remove("playing"),n.offsetWidth,n.classList.add("playing"),setTimeout(()=>{n.classList.remove("playing")},3e3)})})}injectStyles(){if(document.getElementById("glide-modal-styles"))return;const n=document.createElement("style");n.id="glide-modal-styles",n.textContent='\n :root {\n --glide-primary: #007AFF;\n --glide-text: #1d1d1f;\n --glide-bg-light: rgba(255, 255, 255, 0.6);\n --glide-bg-dark: rgba(30, 30, 30, 0.6);\n --glide-phone-border: #000000; /* High Contrast Black */\n \n /* Button sizes */\n --glide-btn-size: 28px;\n --glide-help-btn-size: 24px;\n --glide-toggle-icon-size: 14px;\n --glide-dual-icon-size: 18px;\n }\n\n #glide-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.2);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n z-index: 9998;\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n #glide-modal {\n position: fixed;\n top: 20px;\n left: 50%;\n transform: translateX(-50%) scale(0.94);\n background: var(--glide-bg-light);\n backdrop-filter: blur(24px) saturate(180%);\n -webkit-backdrop-filter: blur(24px) saturate(180%);\n border-radius: 24px;\n padding: 32px;\n width: 360px;\n max-width: 90%;\n box-shadow: \n 0 20px 40px rgba(0,0,0,0.2),\n 0 0 0 1px rgba(255,255,255,0.6) inset,\n 0 0 0 1px rgba(0,0,0,0.05);\n z-index: 9999;\n opacity: 0;\n transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);\n font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;\n text-align: center;\n color: var(--glide-text);\n }\n \n #glide-modal.glide-modal-wide {\n width: 600px;\n max-width: 95vw;\n }\n\n #glide-modal.dark {\n background: var(--glide-bg-dark);\n color: white;\n box-shadow: \n 0 20px 40px rgba(0,0,0,0.4),\n 0 0 0 1px rgba(255,255,255,0.15) inset,\n 0 0 0 1px rgba(0,0,0,0.5);\n --glide-phone-border: #ffffff; /* High Contrast White */\n }\n\n #glide-modal .glide-btn-close {\n position: absolute;\n top: 16px;\n right: 16px;\n width: var(--glide-btn-size);\n height: var(--glide-btn-size);\n min-width: var(--glide-btn-size);\n min-height: var(--glide-btn-size);\n max-width: var(--glide-btn-size);\n max-height: var(--glide-btn-size);\n background: rgba(118, 118, 128, 0.12);\n border: none;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: rgba(0,0,0,0.5);\n transition: background 0.2s;\n padding: 0;\n z-index: 20;\n box-sizing: border-box;\n flex-shrink: 0;\n }\n\n #glide-modal.dark .glide-btn-close {\n background: rgba(255, 255, 255, 0.1);\n color: rgba(255,255,255,0.5);\n }\n\n #glide-modal .glide-btn-close:hover {\n background: rgba(118, 118, 128, 0.2);\n }\n \n #glide-modal.dark .glide-btn-close:hover {\n background: rgba(255, 255, 255, 0.2);\n }\n\n .glide-title {\n margin: 0 0 8px 0;\n font-size: 22px;\n font-weight: 600;\n letter-spacing: -0.01em;\n }\n \n .glide-status {\n margin: 12px 0 0 0;\n font-size: 13px;\n color: rgba(0,0,0,0.5);\n text-align: center;\n min-height: 18px;\n }\n \n .glide-status:empty {\n display: none;\n }\n \n #glide-modal.dark .glide-status {\n color: rgba(255,255,255,0.5);\n }\n \n .glide-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n position: relative;\n }\n \n /* --- Sliding Toggle Switch --- */\n #glide-toggle {\n background: rgba(118, 118, 128, 0.12);\n padding: 2px;\n border-radius: 8px;\n display: inline-flex;\n margin-bottom: 24px;\n margin-top: 16px;\n position: relative;\n height: 32px;\n width: 200px;\n box-sizing: border-box;\n }\n\n #glide-modal.dark #glide-toggle {\n background: rgba(118, 118, 128, 0.24);\n }\n \n /* The sliding background */\n .glide-toggle-slider {\n position: absolute;\n top: 2px;\n left: 2px;\n width: calc(50% - 2px);\n height: calc(100% - 4px);\n background: white;\n border-radius: 6px;\n box-shadow: 0 3px 8px rgba(0,0,0,0.12), 0 3px 1px rgba(0,0,0,0.04);\n transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n z-index: 0;\n }\n \n #glide-modal.dark .glide-toggle-slider {\n background: #636366;\n }\n \n /* Move slider for Android */\n #glide-toggle[data-active="android"] .glide-toggle-slider {\n transform: translateX(100%);\n }\n\n .glide-toggle-btn {\n flex: 1;\n background: none;\n border: none;\n padding: 0;\n margin: 0;\n font-size: 13px;\n font-weight: 500;\n color: inherit;\n cursor: pointer;\n position: relative;\n z-index: 1;\n transition: color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n min-height: auto;\n height: auto;\n min-width: auto;\n border-radius: 0;\n }\n \n #glide-modal .glide-toggle-btn .glide-icon-os,\n #glide-modal .glide-toggle-btn svg {\n width: var(--glide-toggle-icon-size);\n height: var(--glide-toggle-icon-size);\n flex-shrink: 0;\n margin: 0;\n padding: 0;\n }\n \n .glide-toggle-btn span {\n margin: 0;\n padding: 0;\n line-height: 1;\n }\n \n #glide-modal.dark .glide-toggle-btn {\n color: rgba(255,255,255,0.6);\n }\n \n #glide-toggle[data-active="ios"] .glide-toggle-btn[data-platform="ios"],\n #glide-toggle[data-active="android"] .glide-toggle-btn[data-platform="android"] {\n color: #1d1d1f;\n }\n \n #glide-modal.dark #glide-toggle[data-active="ios"] .glide-toggle-btn[data-platform="ios"],\n #glide-modal.dark #glide-toggle[data-active="android"] .glide-toggle-btn[data-platform="android"] {\n color: white;\n }\n\n /* QR Area */\n .glide-qr-area {\n position: relative;\n margin: 0 auto;\n }\n\n .glide-qr-img {\n width: 200px;\n height: 200px;\n object-fit: contain;\n display: block;\n border-radius: 16px;\n }\n \n /* Dual Mode QR Area - no extra padding, same as single mode */\n .glide-dual-mode .glide-qr-area {\n background: transparent;\n padding: 0;\n }\n \n .glide-dual-mode .glide-qr-img {\n width: 180px;\n height: 180px;\n border-radius: 16px;\n }\n \n /* --- Help Icon & Interaction --- */\n .glide-btn-help {\n position: absolute;\n bottom: 20px;\n right: 20px;\n width: var(--glide-help-btn-size);\n height: var(--glide-help-btn-size);\n min-width: var(--glide-help-btn-size);\n min-height: var(--glide-help-btn-size);\n max-width: var(--glide-help-btn-size);\n max-height: var(--glide-help-btn-size);\n border-radius: 50%;\n border: 1.5px solid rgba(0,0,0,0.2);\n color: rgba(0,0,0,0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;\n font-size: 14px;\n font-weight: 600;\n line-height: 1;\n cursor: pointer;\n transition: all 0.2s;\n background: transparent;\n padding: 0;\n margin: 0;\n box-sizing: border-box;\n z-index: 10;\n }\n \n #glide-modal.dark .glide-btn-help {\n border-color: rgba(255,255,255,0.3);\n color: rgba(255,255,255,0.5);\n }\n\n .glide-btn-help:hover {\n border-color: var(--glide-primary);\n color: var(--glide-primary);\n background: rgba(0, 122, 255, 0.1);\n }\n \n /* Tooltip */\n .glide-btn-help::after {\n content: "Need help?";\n position: absolute;\n bottom: 100%;\n left: 50%;\n transform: translateX(-50%) translateY(-8px);\n background: rgba(0,0,0,0.8);\n color: white;\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 11px;\n white-space: nowrap;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.2s;\n }\n \n .glide-btn-help:hover::after {\n opacity: 1;\n }\n\n /* --- Outlined Phone Animation --- */\n .glide-phone-animation-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.3s;\n border-radius: 16px;\n background: rgba(255,255,255,0.8);\n }\n \n #glide-modal.dark .glide-phone-animation-overlay {\n background: rgba(0,0,0,0.6);\n }\n \n .glide-phone-animation-overlay.playing {\n opacity: 1;\n }\n\n .glide-outlined-phone {\n width: 100px;\n height: 180px;\n border: 4px solid var(--glide-phone-border);\n border-radius: 16px;\n position: relative;\n background: transparent;\n box-shadow: 0 10px 30px rgba(0,0,0,0.2);\n transform: translateY(20px);\n }\n \n /* Notch */\n .glide-outlined-phone::before {\n content: \'\';\n position: absolute;\n top: -1px;\n left: 50%;\n transform: translateX(-50%);\n width: 40%;\n height: 12px;\n background: var(--glide-phone-border);\n border-bottom-left-radius: 8px;\n border-bottom-right-radius: 8px;\n }\n \n /* Scanning Line */\n .glide-scan-line {\n position: absolute;\n top: 10%;\n left: 5%;\n width: 90%;\n height: 2px;\n background: var(--glide-primary);\n box-shadow: 0 0 8px var(--glide-primary);\n opacity: 0;\n }\n \n /* Animation Keyframes */\n @keyframes glide-scan-motion {\n 0% { top: 10%; opacity: 0; }\n 10% { opacity: 1; }\n 90% { opacity: 1; }\n 100% { top: 90%; opacity: 0; }\n }\n \n .glide-phone-animation-overlay.playing .glide-outlined-phone {\n animation: glide-phone-appear 3s ease-in-out forwards;\n }\n \n .glide-phone-animation-overlay.playing .glide-scan-line {\n animation: glide-scan-motion 2s ease-in-out 0.5s infinite;\n }\n \n @keyframes glide-phone-appear {\n 0% { transform: translateY(20px); opacity: 0; }\n 10% { transform: translateY(0); opacity: 1; }\n 90% { transform: translateY(0); opacity: 1; }\n 100% { transform: translateY(20px); opacity: 0; }\n }\n \n /* Dual Mode */\n .glide-dual-container {\n display: flex;\n justify-content: center;\n align-items: stretch;\n gap: 32px;\n margin-top: 20px;\n }\n \n .glide-dual-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n \n .glide-os-logo {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n margin-bottom: 12px;\n font-size: 15px;\n color: inherit;\n }\n \n /* Dual mode OS logo - aligned icons with text, spacing to QR */\n .glide-dual-mode .glide-os-logo {\n margin-bottom: 12px; /* Space between label and QR code */\n }\n \n #glide-modal .glide-dual-mode .glide-os-logo .glide-icon-os,\n #glide-modal .glide-dual-mode .glide-os-logo svg {\n width: var(--glide-dual-icon-size);\n height: var(--glide-dual-icon-size);\n margin: 0;\n padding: 0;\n flex-shrink: 0;\n }\n \n .glide-dual-mode .glide-os-logo span {\n margin: 0;\n padding: 0;\n line-height: 1;\n }\n \n .glide-os-logo svg {\n width: 20px;\n height: 20px;\n opacity: 0.8;\n }\n \n /* Dual Mode Separator - aligned to QR codes only */\n .glide-dual-separator {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n align-self: stretch;\n /* Offset for OS label (18px icon + 12px margin = ~30px) */\n margin-top: 30px;\n padding: 20px 0;\n }\n \n .glide-separator-line {\n width: 1px;\n flex: 1;\n background: linear-gradient(to bottom, transparent, rgba(128,128,128,0.3), transparent);\n }\n \n .glide-separator-text {\n padding: 8px 0;\n font-size: 11px;\n font-weight: 500;\n color: rgba(128,128,128,0.5);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n #glide-modal.dark .glide-separator-line {\n background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.2), transparent);\n }\n \n #glide-modal.dark .glide-separator-text {\n color: rgba(255,255,255,0.4);\n }\n \n /* Pre-Step */\n .glide-pre-step-container {\n display: flex;\n gap: 20px;\n justify-content: center;\n margin: 24px 0;\n }\n \n .glide-os-choice-btn {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n width: 140px;\n height: 140px;\n border: 1px solid rgba(0,0,0,0.08);\n border-radius: 20px;\n background: rgba(255,255,255,0.4);\n cursor: pointer;\n transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n position: relative;\n overflow: hidden;\n font-size: 14px;\n color: inherit;\n }\n \n #glide-modal.dark .glide-os-choice-btn {\n background: rgba(255,255,255,0.05);\n border-color: rgba(255,255,255,0.1);\n color: white;\n }\n \n .glide-os-choice-btn:hover {\n background: rgba(255,255,255,0.9);\n transform: translateY(-4px);\n box-shadow: 0 12px 24px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.04);\n border-color: var(--glide-primary);\n }\n \n #glide-modal.dark .glide-os-choice-btn:hover {\n background: rgba(255,255,255,0.15);\n box-shadow: 0 12px 24px rgba(0,0,0,0.3);\n border-color: var(--glide-primary);\n }\n \n .glide-icon-os {\n width: 40px;\n height: 40px;\n margin-bottom: 12px;\n transition: transform 0.3s;\n fill: currentColor;\n }\n \n .glide-os-choice-btn:hover .glide-icon-os {\n transform: scale(1.1);\n }\n \n #glide-modal .glide-btn-back {\n position: absolute;\n top: 16px;\n left: 16px;\n width: var(--glide-btn-size);\n height: var(--glide-btn-size);\n min-width: var(--glide-btn-size);\n min-height: var(--glide-btn-size);\n max-width: var(--glide-btn-size);\n max-height: var(--glide-btn-size);\n background: rgba(118, 118, 128, 0.12);\n border: none;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: rgba(0,0,0,0.5);\n transition: background 0.2s;\n z-index: 20;\n padding: 0;\n margin: 0;\n box-sizing: border-box;\n flex-shrink: 0;\n }\n \n #glide-modal.dark .glide-btn-back {\n background: rgba(255, 255, 255, 0.1);\n color: rgba(255,255,255,0.5);\n }\n\n .glide-btn-back:hover {\n background: rgba(118, 118, 128, 0.2);\n }\n \n #glide-modal.dark .glide-btn-back:hover {\n background: rgba(255, 255, 255, 0.2);\n }\n \n .glide-spinner {\n width: 40px;\n height: 40px;\n border: 3px solid rgba(0,0,0,0.1);\n border-top-color: var(--glide-primary);\n border-radius: 50%;\n animation: glide-spin 1s linear infinite;\n margin: 40px auto;\n }\n \n #glide-modal.dark .glide-spinner {\n border-color: rgba(255,255,255,0.1);\n border-top-color: var(--glide-primary);\n }\n \n @keyframes glide-spin {\n to { transform: rotate(360deg); }\n }\n ',document.head.appendChild(n)}show(){this.container&&this.backdrop&&!this.isOpen?(document.body.appendChild(this.backdrop),document.body.appendChild(this.container),this.lockBodyScroll(),document.addEventListener("keydown",this.handleEscapeKey),requestAnimationFrame(()=>{this.backdrop&&this.container&&(this.backdrop.style.opacity="1",this.container.style.opacity="1",this.container.style.transform="translateX(-50%) scale(1)")}),this.isOpen=!0):this.isOpen&&this.container&&this.backdrop&&(document.removeEventListener("keydown",this.handleEscapeKey),document.addEventListener("keydown",this.handleEscapeKey))}setupPlatformToggles(){const n=this.container?.querySelector("#glide-toggle"),e=this.container?.querySelectorAll(".glide-toggle-btn"),t=this.container?.querySelector("#glide-qr-img"),i=this.container?.querySelector("#glide-platform-message");e&&t&&e.forEach(o=>{o.addEventListener("click",o=>{const r=o.currentTarget,a=r.getAttribute("data-platform");n&&a&&n.setAttribute("data-active",a),e.forEach(n=>n.classList.remove("active")),r.classList.add("active"),"ios"===a?(t.src=t.getAttribute("data-ios")||"",i&&(i.textContent="Scan with your iPhone camera")):"android"===a&&(t.src=t.getAttribute("data-android")||"",i&&(i.textContent="Scan with your Android camera"))})})}lockBodyScroll(){this.originalBodyOverflow=document.body.style.overflow,document.body.style.overflow="hidden"}unlockBodyScroll(){document.body.style.overflow=this.originalBodyOverflow}close(){this.container&&this.backdrop&&this.isOpen&&(this.isClosing=!0,document.removeEventListener("keydown",this.handleEscapeKey),this.backdrop.style.opacity="0",this.container.style.opacity="0",this.container.style.transform="translateX(-50%) scale(0.94)",this.closeCallback=void 0,setTimeout(()=>{this.isClosing&&(this.cleanup(),this.isOpen=!1,this.isClosing=!1)},400))}cleanup(){this.container?.remove(),this.backdrop?.remove(),this.container=null,this.backdrop=null,this.unlockBodyScroll()}isModalOpen(){return this.isOpen}}function w(n){const{data:e}=n;if(e.ios_qr_image)return{iosQRCode:e.ios_qr_image,androidQRCode:e.android_qr_image||e.ios_qr_image};if(e.qr_code_image)return e.qr_code_image;throw new Error("No QR code data available")}class k{constructor(){this.logs=[],this.container=null,this.logsContainer=null,this.floatingToggle=null,this.isAtBottom=!0,this.isVisible=!0,this.originalConsole={log:console.log,error:console.error,warn:console.warn,debug:console.debug,info:console.info},this.interceptConsole(),this.createUI()}static init(){return k.instance||(k.instance=new k),k.instance}static destroy(){k.instance&&(k.instance.cleanup(),k.instance=null)}interceptConsole(){["log","error","warn","debug","info"].forEach(n=>{const e=this.originalConsole[n];console[n]=(...t)=>{e.apply(console,t),this.addLog(n,t)}})}addLog(n,e){const t=(new Date).toTimeString().split(" ")[0],i=e.map(n=>{if("object"==typeof n)try{return JSON.stringify(n,null,2)}catch{return"[Object]"}return String(n)}).join(" "),o={log:"#abb2bf",error:"#e06c75",warn:"#e5c07b",info:"#61afef",debug:"#5c6370"},r=`\n <div style="margin: 3px 0; font-family: 'SF Mono', Menlo, Monaco, monospace; font-size: 11px; color: ${o[n]||"#abb2bf"}; line-height: 1.5;">\n <span style="color: #5c6370; font-size: 10px;">${t}</span>\n <span style="background: ${{log:"#3c3c3c",error:"rgba(224, 108, 117, 0.2)",warn:"rgba(229, 192, 123, 0.2)",info:"rgba(97, 175, 239, 0.2)",debug:"#2d2d2d"}[n]}; color: ${o[n]}; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 500; text-transform: uppercase; margin: 0 6px;">${n}</span>\n <span style="white-space: pre-wrap; word-break: break-all;">${this.escapeHtml(i)}</span>\n </div>\n `;this.logs.push(r),this.logs.length>500&&this.logs.shift(),this.updateDisplay()}updateDisplay(){this.logsContainer&&this.isVisible&&(this.isAtBottom=this.logsContainer.scrollHeight-this.logsContainer.scrollTop<=this.logsContainer.clientHeight+50,this.logsContainer.innerHTML=this.logs.join(""),this.isAtBottom&&(this.logsContainer.scrollTop=this.logsContainer.scrollHeight))}createUI(){const n=document.createElement("style");n.textContent="\n #mobile-debug-console {\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n height: 45vh;\n background: #1e1e1e;\n z-index: 999999;\n display: flex;\n flex-direction: column;\n font-family: 'SF Mono', Menlo, Monaco, 'Courier New', monospace;\n transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n border-top: 1px solid #3c3c3c;\n box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);\n }\n \n #mobile-debug-console.hidden {\n transform: translateY(100%);\n }\n \n #debug-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 12px;\n background: #2d2d2d;\n border-bottom: 1px solid #3c3c3c;\n min-height: 36px;\n }\n \n #debug-title {\n display: flex;\n align-items: center;\n gap: 8px;\n color: #9da5b4;\n font-size: 12px;\n font-weight: 500;\n letter-spacing: 0.3px;\n }\n \n #debug-title svg {\n width: 14px;\n height: 14px;\n opacity: 0.8;\n }\n \n /* Traffic light buttons - using ID for specificity */\n #mobile-debug-console .debug-traffic-lights {\n display: flex;\n gap: 8px;\n align-items: center;\n }\n \n #mobile-debug-console button.debug-traffic-btn {\n width: 16px;\n height: 16px;\n min-width: 16px;\n min-height: 16px;\n border-radius: 50%;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n font-size: 0;\n line-height: 1;\n }\n \n #mobile-debug-console .debug-traffic-btn svg {\n width: 8px;\n height: 8px;\n }\n \n #mobile-debug-console .debug-traffic-btn.close {\n background: #ff5f57;\n }\n \n #mobile-debug-console .debug-traffic-btn.close svg {\n stroke: #820005;\n stroke-width: 2;\n }\n \n #mobile-debug-console .debug-traffic-btn.clear {\n background: #febc2e;\n }\n \n #mobile-debug-console .debug-traffic-btn.minimize {\n background: #28c840;\n }\n \n #mobile-debug-console .debug-traffic-btn.minimize svg {\n stroke: #006500;\n stroke-width: 2;\n }\n \n #mobile-debug-console .debug-traffic-btn:active {\n filter: brightness(0.85);\n }\n \n #debug-logs {\n flex: 1;\n overflow-y: auto;\n padding: 12px;\n background: #1e1e1e;\n -webkit-overflow-scrolling: touch;\n }\n \n button#debug-floating-toggle {\n position: fixed !important;\n bottom: 20px !important;\n right: 20px !important;\n width: 42px !important;\n height: 42px !important;\n min-width: 42px !important;\n min-height: 42px !important;\n border-radius: 8px !important;\n background: #2d2d2d !important;\n border: 1px solid #3c3c3c !important;\n color: #9da5b4 !important;\n cursor: pointer !important;\n z-index: 999998 !important;\n display: none;\n align-items: center !important;\n justify-content: center !important;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;\n transition: all 0.2s ease !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n \n button#debug-floating-toggle:hover {\n background: #3c3c3c !important;\n color: #fff !important;\n transform: translateY(-2px) !important;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;\n }\n \n button#debug-floating-toggle:active {\n transform: translateY(0) !important;\n }\n \n button#debug-floating-toggle.visible {\n display: flex !important;\n }\n \n button#debug-floating-toggle svg {\n width: 20px !important;\n height: 20px !important;\n }\n ",document.head.appendChild(n);const e='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>';this.container=document.createElement("div"),this.container.id="mobile-debug-console",this.isVisible||(this.container.className="hidden");const t=document.createElement("div");t.id="debug-header";const i=document.createElement("div");i.id="debug-title",i.innerHTML=`${e}<span>Mobile Console</span>`;const o=document.createElement("div");o.className="debug-traffic-lights";const r=document.createElement("button");r.className="debug-traffic-btn close",r.title="Close",r.innerHTML='<svg viewBox="0 0 10 10" fill="none"><line x1="2.5" y1="2.5" x2="7.5" y2="7.5" stroke="currentColor"/><line x1="7.5" y1="2.5" x2="2.5" y2="7.5" stroke="currentColor"/></svg>',r.onclick=()=>this.cleanup();const a=document.createElement("button");a.className="debug-traffic-btn clear",a.title="Clear",a.innerHTML='<svg viewBox="0 0 16 16" fill="currentColor"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/></svg>',a.onclick=()=>this.clear();const s=document.createElement("button");s.className="debug-traffic-btn minimize",s.title="Minimize",s.innerHTML='<svg viewBox="0 0 10 10" fill="none"><path d="M2 3L5 6L8 3" stroke="currentColor" fill="none"/></svg>',s.onclick=()=>this.toggle(),o.appendChild(r),o.appendChild(a),o.appendChild(s),t.appendChild(o),t.appendChild(i),this.logsContainer=document.createElement("div"),this.logsContainer.id="debug-logs",this.logsContainer.addEventListener("scroll",()=>{this.isAtBottom=this.logsContainer.scrollHeight-this.logsContainer.scrollTop<=this.logsContainer.clientHeight+50}),this.container.appendChild(t),this.container.appendChild(this.logsContainer),document.body.appendChild(this.container),this.floatingToggle=document.createElement("button"),this.floatingToggle.id="debug-floating-toggle",this.floatingToggle.innerHTML=e,this.floatingToggle.title="Show Console",this.floatingToggle.onclick=()=>this.toggle(),document.body.appendChild(this.floatingToggle)}escapeHtml(n){const e=document.createElement("div");return e.textContent=n,e.innerHTML}toggle(){this.isVisible=!this.isVisible,this.container&&this.floatingToggle&&(this.isVisible?(this.container.classList.remove("hidden"),this.floatingToggle.classList.remove("visible"),this.updateDisplay()):(this.container.classList.add("hidden"),this.floatingToggle.classList.add("visible")))}clear(){this.logs=[],this.logsContainer&&(this.logsContainer.innerHTML="")}cleanup(){Object.keys(this.originalConsole).forEach(n=>{console[n]=this.originalConsole[n]}),this.container&&this.container.remove(),this.floatingToggle&&this.floatingToggle.remove()}}k.instance=null;class E{constructor(n={}){this.config={...n,endpoints:{prepare:n.endpoints?.prepare||"/api/magic-auth/prepare",process:n.endpoints?.process||"/api/magic-auth/process",polling:n.endpoints?.polling},headers:n.headers,timeout:n.timeout||3e4,pollingInterval:n.pollingInterval||2e3,maxPollingAttempts:n.maxPollingAttempts||30,debug:n.debug||!1},this.http=n.httpClient||function(n={}){const{timeout:e=3e4,headers:t={},dynamicHeaders:i}=n;async function o(n,e){const o={"Content-Type":"application/json"};t.common&&Object.assign(o,t.common);const r=t[n.endpoint];if(r&&"object"==typeof r&&Object.assign(o,r),i){const e=await i(n);Object.assign(o,e)}return e&&Object.assign(o,e),o}async function r(n,e,t,i){const o=new AbortController,r=setTimeout(()=>o.abort(),t),a=i?()=>o.abort():null;i&&a&&i.addEventListener("abort",a);try{return await fetch(n,{...e,signal:o.signal})}catch(n){if(n instanceof Error&&"AbortError"===n.name){if(i?.aborted)throw h(s.CANCELLED,"Request was cancelled");throw h(s.TIMEOUT,"Request timed out")}throw h(s.NETWORK_ERROR,n instanceof Error?n.message:"Network request failed")}finally{clearTimeout(r),i&&a&&i.removeEventListener("abort",a)}}async function a(n){let e;try{e=await n.json()}catch{if(!n.ok)throw h(s.INVALID_RESPONSE,`Request failed with status ${n.status}`);throw h(s.INVALID_RESPONSE,"Failed to parse response")}if(!n.ok)throw function(n){if(n&&("code"in n||"error"in n)){const e=n,t=e.code||e.error,i=e.message,o=new Error(i||"An error occurred");return o.code=t,o.status=e.status,o.requestId=e.requestId||e.request_id,o.timestamp=e.timestamp,o.details=e.details,o.traceId=e.trace_id||e.traceId,o.spanId=e.span_id||e.spanId,o.service=e.service,e.details&&"object"==typeof e.details&&"retryAfter"in e.details&&(o.retryAfter=e.details.retryAfter),o}if(n&&"status"in n){const e=n,t=e.status,i=new Error(e.message||`Request failed with status ${t}`);return i.code=`HTTP_${t}`,i.status=t,i}const e=new Error("An unexpected error occurred");return e.code="UNKNOWN_ERROR",e}({...e,status:n.status});return e}function l(n){return n.includes("/prepare")?"prepare":n.includes("/process")||n.includes("/get-phone")||n.includes("/verify-phone")?"process":n.includes("/status")?"polling":"prepare"}return{async post(n,t,i){const s=l(n),d=await o({endpoint:s,method:"POST"},i?.headers);return a(await r(n,{method:"POST",headers:d,body:JSON.stringify(t)},i?.timeout||e,i?.signal))},async get(n,t){const i=l(n),s=await o({endpoint:i,method:"GET"},t?.headers);return delete s["Content-Type"],a(await r(n,{method:"GET",headers:s},t?.timeout||e,t?.signal))}}}({timeout:this.config.timeout,headers:n.headers,dynamicHeaders:n.dynamicHeaders}),this.logger=n.logger||(this.config.debug?function(n={}){const{debug:e=!1,prefix:t="[PhoneAuth]"}=n;function i(n,e){const i=`${t} ${n}`;return void 0!==e?[i,y(e)]:[i]}return{debug(n,t){if(e){const e=i(n,t);console.debug(...e)}},info(n,e){const t=i(n,e);console.info(...t)},warn(n,e){const t=i(n,e);console.warn(...t)},error(n,e){const t=i(n,e);console.error(...t)}}}({debug:!0}):{debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}}),this.logger.debug("PhoneAuthClient initialized",{endpoints:this.config.endpoints,timeout:this.config.timeout}),n.devtools?.showMobileConsole&&"undefined"!=typeof window&&(k.init(),this.logger.info("Mobile debug console enabled"))}async authenticate(n,e){const i=await this.prepare(n),o=await this.invokeSecurePrompt(i,e),r=await o.credential,a=i.session.use_case||i.session.metadata?.use_case||n.use_case;if(!a)throw h(s.INVALID_RESPONSE,"Could not determine use_case from session or request");return a===t.VERIFY_PHONE_NUMBER?this.verifyPhoneNumber(r,i.session):this.getPhoneNumber(r,i.session)}async prepare(n){if(this.logger.debug("Preparing authentication",{use_case:n.use_case}),n.phone_number){const e=r(n.phone_number);if(!e.valid)throw h(s.INVALID_PHONE_NUMBER,e.error)}if(n.plmn){const e=a(n.plmn);if(!e.valid)throw h(s.INVALID_PLMN,e.error)}if(!n.use_case&&!n.options?.parent_session_id)throw h(s.MISSING_PARAMETERS,"use_case is required");const e={...n,id:n.id||this.generateRequestId(),client_info:n.client_info||{user_agent:navigator.userAgent,platform:navigator.platform}};try{const n=await this.http.post(this.config.endpoints.prepare,e);return this.logger.debug("Prepare successful",{strategy:n.authentication_strategy,sessionKey:n.session.session_key}),n}catch(n){throw this.logger.error("Prepare failed",n),n}}async invokeSecurePrompt(n,e){const{authentication_strategy:t,session:o,data:r}=n;switch(this.logger.debug("Invoking secure prompt",{strategy:t}),t){case i.TS43:return this.handleTS43(r,o);case i.LINK:return this.handleLink(r,o,e);case i.DESKTOP:return this.handleDesktop(r,o,e);default:throw h(s.UNSUPPORTED_STRATEGY,`Unknown strategy: ${t}`)}}async getPhoneNumber(n,e){this.logger.debug("Getting phone number");const i={session:e,credential:n,use_case:t.GET_PHONE_NUMBER};try{const n=await this.http.post(this.config.endpoints.process,i);return this.logger.info("Phone number retrieved"),n}catch(n){throw this.logger.error("Get phone number failed",n),n}}async verifyPhoneNumber(n,e){this.logger.debug("Verifying phone number");const i={session:e,credential:n,use_case:t.VERIFY_PHONE_NUMBER};try{const n=await this.http.post(this.config.endpoints.process,i);return this.logger.info("Phone number verified",{verified:n.verified}),n}catch(n){throw this.logger.error("Verify phone number failed",n),n}}isSupported(){return"undefined"!=typeof window&&"DigitalCredential"in window}getBrowserSupportInfo(){if("undefined"==typeof window)return{supported:!1,browser:"unknown",message:"Not in browser"};const n=navigator.userAgent,e=/Chrome/.test(n)&&/Google Inc/.test(navigator.vendor),t=/Edg\//.test(n);return this.isSupported()?{supported:!0,browser:e?"Chrome":t?"Edge":"Other"}:{supported:!1,browser:e?"Chrome":t?"Edge":"Other",message:"Enable chrome://flags/#web-identity-digital-credentials"}}async handleTS43(n,e){if(!this.isSupported())throw h(s.BROWSER_NOT_SUPPORTED,"Digital Credentials API not available");const t={digital:{requests:[{protocol:n.protocol,data:n.data}]}};return this.logger.debug("Invoking Digital Credentials API"),{credential:(async()=>{try{const n=await navigator.credentials.get(t);if(!n?.data?.vp_token)throw h(s.INVALID_RESPONSE,"No credential returned from Digital Credentials API");const e=n.data.vp_token,i="string"==typeof e?e:Object.values(e)[0],o=Array.isArray(i)?i[0]:i;if(!o||"string"==typeof o&&""===o.trim())throw h(s.INVALID_RESPONSE,"Empty credential returned from Digital Credentials API");return o}catch(n){if(d(n))throw n;const e=n;if("NotAllowedError"===e.name||"NetworkError"===e.name&&19===e.code)throw h(s.USER_CANCELLED,"User cancelled authentication");if("NotSupportedError"===e.name)throw h(s.BROWSER_NOT_SUPPORTED,"Browser not supported");throw h(s.NETWORK_ERROR,e.message||"Credential request failed")}})(),cancel:void 0,strategy:i.TS43,session:e}}handleLink(n,e,t){if(!n.url)throw h(s.INVALID_RESPONSE,"Missing link URL");this.logger.debug("Opening App Clip",{url:n.url}),window.location.href=n.url;const o={...this.config.headers?.common,...this.config.headers?.polling},r=v({sessionKey:e.session_key,interval:t?.pollingInterval||this.config.pollingInterval,maxAttempts:t?.maxPollingAttempts||this.config.maxPollingAttempts,pollingEndpoint:t?.pollingEndpoint||this.config.endpoints.polling,statusUrl:n.status_url,logger:this.logger,headers:Object.keys(o).length>0?o:void 0});return{credential:r.start().then(n=>{if("completed"===n.status&&n.credential)return n.credential;throw h(s.VERIFICATION_FAILED,n.message||"Link authentication failed")}),cancel:()=>r.cancel(),strategy:i.LINK,session:e}}handleDesktop(n,e,t){const o=n.data?.session_id||e.session_key;this.logger.debug("Starting desktop authentication",{sessionId:o});const r={...this.config.headers?.common,...this.config.headers?.polling},a=v({sessionKey:o,interval:t?.pollingInterval||this.config.pollingInterval,maxAttempts:t?.maxPollingAttempts||this.config.maxPollingAttempts,pollingEndpoint:t?.pollingEndpoint||this.config.endpoints.polling,statusUrl:n.data?.status_url,logger:this.logger,headers:Object.keys(r).length>0?r:void 0});let l=null;if(!t?.preventDefaultUI)try{const e=w(n);l=new x(t?.modalOptions),l.showQRCode(e,t?.modalOptions?.description||"Scan with your phone camera"),l.setCloseCallback(()=>{this.logger.debug("Modal closed by user, cancelling authentication"),a.cancel()}),this.logger.debug("Desktop modal displayed")}catch(n){this.logger.warn("Failed to show modal, continuing with polling only",n)}return{credential:a.start().then(n=>{if(l&&l.close(),"completed"===n.status&&n.credential)return n.credential;throw h(s.VERIFICATION_FAILED,n.message||"Desktop authentication failed")}).catch(n=>{throw l&&l.close(),n}),cancel:()=>{a.cancel(),l&&l.close()},strategy:i.DESKTOP,session:e}}generateRequestId(){return`web-${Date.now()}-${Math.random().toString(36).substring(2,11)}`}}function C(n){return null!==n&&"object"==typeof n&&"credential"in n&&"strategy"in n&&"session"in n}function S(n){return null!==n&&"object"==typeof n&&"credential"in n&&"authenticated"in n&&"session"in n}function A(n){return n.strategy===i.TS43}function R(n){return n.strategy===i.LINK}function I(n){return n.strategy===i.DESKTOP}function _(n){return void 0!==n.cancel}function N(n){return null!==n&&"object"==typeof n&&"protocol"in n&&"data"in n&&"object"==typeof n.data&&"dcql_query"in n.data}function O(n){return null!==n&&"object"==typeof n&&"url"in n&&"string"==typeof n.url}function T(n){return null!==n&&"object"==typeof n&&"data"in n&&"object"==typeof n.data}function P(n){return!("verified"in n)}function L(n){return"verified"in n}function D(n){return null!==n&&"object"==typeof n&&"code"in n&&"message"in n}return e})());
|
|
1
|
+
!function(e,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.GlideWebClientSDK=n():e.GlideWebClientSDK=n()}(this,()=>(()=>{"use strict";var e={d:(n,t)=>{for(var i in t)e.o(t,i)&&!e.o(n,i)&&Object.defineProperty(n,i,{enumerable:!0,get:t[i]})},o:(e,n)=>Object.prototype.hasOwnProperty.call(e,n),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},n={};e.r(n),e.d(n,{AUTHENTICATION_STRATEGY:()=>i,AuthModal:()=>y,ERROR_CODES:()=>s,PhoneAuthClient:()=>E,USE_CASE:()=>t,createAuthError:()=>h,createQRCodeDataFromDesktop:()=>x,getUserMessage:()=>p,isAuthCredential:()=>C,isAuthError:()=>d,isCancellable:()=>_,isClientError:()=>c,isDesktopData:()=>O,isDesktopStrategy:()=>R,isErrorResponse:()=>L,isGetPhoneNumberResponse:()=>T,isInvokeResult:()=>k,isLinkData:()=>N,isLinkStrategy:()=>A,isRetryableError:()=>g,isTS43Data:()=>I,isTS43Strategy:()=>S,isVerifyPhoneNumberResponse:()=>P,validatePhoneNumber:()=>r,validatePlmn:()=>a});const t={GET_PHONE_NUMBER:"GetPhoneNumber",VERIFY_PHONE_NUMBER:"VerifyPhoneNumber"},i={TS43:"ts43",LINK:"link",DESKTOP:"desktop"},o=/^\+[1-9]\d{1,14}$/;function r(e){return e?e.startsWith("+")?e.length<8?{valid:!1,error:"Phone number too short for E.164 format (minimum 8 characters including +)"}:e.length>16?{valid:!1,error:"Phone number too long for E.164 format (maximum 15 digits after +)"}:/^\+\d+$/.test(e)?o.test(e)?{valid:!0}:{valid:!1,error:"Invalid E.164 phone number format"}:{valid:!1,error:"Phone number contains invalid characters. E.164 format only allows + followed by digits"}:{valid:!1,error:"Phone number must be in E.164 format (start with +)"}:{valid:!0}}function a(e){if(!e)return{valid:!0};const{mcc:n,mnc:t}=e;return n&&/^\d{3}$/.test(n)?t&&/^\d{2,3}$/.test(t)?{valid:!0}:{valid:!1,error:"MNC must be 2 or 3 digits"}:{valid:!1,error:"MCC must be exactly 3 digits"}}const s={INVALID_PHONE_NUMBER:"INVALID_PHONE_NUMBER",INVALID_PLMN:"INVALID_PLMN",MISSING_PARAMETERS:"MISSING_PARAMETERS",BROWSER_NOT_SUPPORTED:"BROWSER_NOT_SUPPORTED",UNSUPPORTED_STRATEGY:"UNSUPPORTED_STRATEGY",USER_CANCELLED:"USER_CANCELLED",CANCELLED:"CANCELLED",VERIFICATION_FAILED:"VERIFICATION_FAILED",NETWORK_ERROR:"NETWORK_ERROR",TIMEOUT:"TIMEOUT",INVALID_RESPONSE:"INVALID_RESPONSE"},l={[s.INVALID_PHONE_NUMBER]:"Please enter a valid phone number in E.164 format (e.g., +14155551234).",[s.INVALID_PLMN]:"Invalid carrier information provided.",[s.MISSING_PARAMETERS]:"Required information is missing.",[s.BROWSER_NOT_SUPPORTED]:"Your browser does not support this authentication method. Please use Chrome or Edge with the Digital Credentials flag enabled.",[s.UNSUPPORTED_STRATEGY]:"This authentication strategy is not supported.",[s.USER_CANCELLED]:"Authentication was cancelled.",[s.CANCELLED]:"Authentication was cancelled.",[s.VERIFICATION_FAILED]:"Verification failed. Please try again.",[s.NETWORK_ERROR]:"Network connection failed. Please check your connection and try again.",[s.TIMEOUT]:"Request timed out. Please try again.",[s.INVALID_RESPONSE]:"Invalid response received."};function d(e){return null!==e&&"object"==typeof e&&"code"in e&&"message"in e&&"string"==typeof e.code&&"string"==typeof e.message}function c(e){return Object.values(s).includes(e.code)}function g(e){return e.code===s.NETWORK_ERROR||e.code===s.TIMEOUT}function p(e){return c(e)&&l[e.code]||e.message}function h(e,n,t){const i=new Error(n||l[e]||e);return i.code=e,i.details=t,i.timestamp=(new Date).toISOString(),i}const u=/(\+?[1-9]\d{6,14})/g,b=/(eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*)/g,m=/session[_-]?key["']?\s*[:=]\s*["']?([a-zA-Z0-9_-]{20,})["']?/gi;function f(e){if(null==e)return e;if("string"==typeof e){let n=e;return n=n.replace(u,e=>e.length<8?e:e.slice(0,2)+"***"+e.slice(-4)),n=n.replace(b,"[JWT]"),n=n.replace(m,(e,n)=>e.replace(n,n.slice(0,4)+"***"+n.slice(-4))),n}if(Array.isArray(e))return e.map(f);if("object"==typeof e){const n={};for(const[t,i]of Object.entries(e))["phone_number","phoneNumber","credential","token","password","secret"].some(e=>t.toLowerCase().includes(e.toLowerCase()))?n[t]="string"==typeof i?"[REDACTED]":i:n[t]=f(i);return n}return e}function v(e){const{sessionKey:n,interval:t=2e3,maxAttempts:i=30,pollingEndpoint:o,statusUrl:r,logger:a,headers:l}=e;let d,c,g=0,p=!1,u=!1;function b(){u=!1,d&&(clearInterval(d),d=void 0)}return{start:function(){return new Promise((e,s)=>{c=s,u=!0,g=0;const h=async()=>{if(u&&!p){if(g>=i)return b(),a?.warn("Polling timeout reached",{attempts:g,maxAttempts:i}),void e({status:"expired",message:"Authentication timeout"});try{const t=o?(a?.debug("Using developer config endpoint for polling",{pollingEndpoint:o}),`${o}/${n}`):r?(a?.debug("Using backend status_url for polling",{statusUrl:r}),r):(a?.debug("Using prod fallback for status polling"),`https://api.glideidentity.app/public/status/${n}`);a?.debug("Polling status",{url:t,attempt:g+1,maxAttempts:i});const s=await fetch(t,{method:"GET",headers:{Accept:"application/json",...l}});if(200===s.status){const t=await s.json();if("completed"===t.status){b(),a?.info("Authentication completed");const i=t.credential||n;return void e({status:"completed",credential:i,session:t.session||{session_key:n}})}g++}else if(410===s.status){b();const n=await s.json().catch(()=>({}));e({status:"expired",message:n.message||"Session expired"})}else if(422===s.status){b();const n=await s.json().catch(()=>({}));e({status:"error",message:n.message||"Authentication failed"})}else 404===s.status?(b(),e({status:"error",message:"Session not found"})):g++}catch(e){a?.debug("Polling error, retrying",{error:e,attempt:g+1}),g++}}};h(),d=setInterval(h,t)})},stop:b,cancel:function(){a?.debug("Polling cancelled"),p=!0,b(),c&&(c(h(s.CANCELLED,"Authentication cancelled")),c=void 0)},isPolling:function(){return void 0!==d},cleanup:function(){b(),p=!1,c=void 0}}}class y{constructor(e){this.container=null,this.backdrop=null,this.isOpen=!1,this.currentStep="os-choice",this.qrCodeData=null,this.statusMessage="",this.originalBodyOverflow="",this.isClosing=!1,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>',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>',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>',this.options=e||{},this.theme=this.options.theme||"auto",this.handleEscapeKey=this.handleEscapeKey.bind(this)}shouldUseDarkMode(){return"dark"===this.theme||"light"!==this.theme&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches}handleEscapeKey(e){"Escape"===e.key&&this.isOpen&&!1!==this.options?.closeOnEscape&&(this.closeCallback?.(),this.close())}escapeHtml(e){return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}showQRCode(e,n="Scan with your phone camera"){this.qrCodeData=e,this.statusMessage=n;const t=this.options.viewMode||"toggle";if("string"==typeof e)this.renderToggleMode(e,n);else switch(t){case"dual":this.renderDualMode(e,n);break;case"pre-step":this.renderPreStepMode(e,n);break;default:this.renderToggleMode(e,n)}this.show()}updateStatus(e,n=!1){if(this.statusMessage=e,this.container){const t=this.container.querySelector("#glide-status");t&&(t.textContent=e,t.style.color=n?"#ff3b30":"")}}setCloseCallback(e){this.closeCallback=e}renderToggleMode(e,n){let t="",i="";"string"==typeof e?(t=e,i=e):(t=e.iosQRCode,i=e.androidQRCode||e.iosQRCode);const o=`\n <div class="glide-content">\n <div class="glide-toggle" id="glide-toggle" data-active="ios">\n <div class="glide-toggle-slider"></div>\n <button class="glide-btn glide-toggle-btn active" data-platform="ios">${this.iconApple}<span>iOS</span></button>\n <button class="glide-btn glide-toggle-btn" data-platform="android">${this.iconAndroid}<span>Android</span></button>\n </div>\n\n <div class="glide-qr-area">\n <img \n class="glide-qr-img"\n id="glide-qr-img" \n src="${this.escapeHtml(t)}" \n alt="QR Code" \n class="glide-qr-img" \n data-ios="${this.escapeHtml(t)}"\n data-android="${this.escapeHtml(i)}"\n />\n \n \x3c!-- Animation Overlay (Inside QR Area for centering) --\x3e\n <div id="glide-phone-overlay" class="glide-phone-animation-overlay">\n <div class="glide-outlined-phone">\n <div class="glide-scan-line"></div>\n </div>\n </div>\n </div>\n </div>\n `;this.createModal(o,"",!0),this.setupPlatformToggles(),this.setupHelpInteraction()}renderDualMode(e,n){const t='\n <div class="glide-phone-animation-overlay">\n <div class="glide-outlined-phone">\n <div class="glide-scan-line"></div>\n </div>\n </div>\n ';this.createModal(`\n <div class="glide-content glide-dual-mode">\n <div class="glide-dual-container">\n <div class="glide-dual-item">\n <div class="glide-os-logo">\n ${this.iconApple}\n <span>iOS</span>\n </div>\n <div class="glide-qr-area">\n <img src="${this.escapeHtml(e.iosQRCode)}" alt="iOS QR" class="glide-qr-img" />\n ${t}\n </div>\n </div>\n <div class="glide-dual-separator">\n <div class="glide-separator-line"></div>\n <span class="glide-separator-text">or</span>\n <div class="glide-separator-line"></div>\n </div>\n <div class="glide-dual-item">\n <div class="glide-os-logo">\n ${this.iconAndroid}\n <span>Android</span>\n </div>\n <div class="glide-qr-area">\n <img src="${this.escapeHtml(e.androidQRCode||e.iosQRCode)}" alt="Android QR" class="glide-qr-img" />\n ${t}\n </div>\n </div>\n </div>\n </div>\n `,"glide-modal-wide",!0),this.setupHelpInteraction()}renderPreStepMode(e,n){this.currentStep="os-choice";const t=`\n <div class="glide-pre-step-container">\n <button class="glide-os-choice-btn" id="glide-btn-ios">\n ${this.iconApple}\n <span>iOS</span>\n </button>\n <button class="glide-os-choice-btn" id="glide-btn-android">\n ${this.iconAndroid}\n <span>Android</span>\n </button>\n </div>\n `;this.createModal(t,"",!1),this.setupPreStepListeners(),this.show()}setupPreStepListeners(){this.container&&(this.container.querySelector("#glide-btn-ios")?.addEventListener("click",()=>{this.currentStep="ios-qr",this.updatePreStepUI()}),this.container.querySelector("#glide-btn-android")?.addEventListener("click",()=>{this.currentStep="android-qr",this.updatePreStepUI()}))}updatePreStepUI(){if(!this.container)return;let e="";if("os-choice"===this.currentStep)e=`\n <div class="glide-pre-step-container">\n <button class="glide-os-choice-btn" id="glide-btn-ios">\n ${this.iconApple}\n <span>iOS</span>\n </button>\n <button class="glide-os-choice-btn" id="glide-btn-android">\n ${this.iconAndroid}\n <span>Android</span>\n </button>\n </div>\n `,this.createModal(e,"",!1),this.setupPreStepListeners();else if("ios-qr"===this.currentStep){const n="object"==typeof this.qrCodeData&&this.qrCodeData?.iosQRCode?this.qrCodeData.iosQRCode:this.qrCodeData;e=`\n <div class="glide-content">\n <div class="glide-qr-area">\n <img src="${this.escapeHtml(n)}" alt="QR Code" class="glide-qr-img" />\n \n \x3c!-- Animation Overlay --\x3e\n <div id="glide-phone-overlay" class="glide-phone-animation-overlay">\n <div class="glide-outlined-phone">\n <div class="glide-scan-line"></div>\n </div>\n </div>\n </div>\n </div>\n `,this.createModal(e,"",!0,!0),this.setupBackButton(),this.setupHelpInteraction()}else if("android-qr"===this.currentStep){const n="object"==typeof this.qrCodeData&&this.qrCodeData?.androidQRCode?this.qrCodeData.androidQRCode:"object"==typeof this.qrCodeData&&this.qrCodeData?.iosQRCode?this.qrCodeData.iosQRCode:this.qrCodeData;e=`\n <div class="glide-content">\n <div class="glide-qr-area">\n <img src="${this.escapeHtml(n)}" alt="QR Code" class="glide-qr-img" />\n \n \x3c!-- Animation Overlay --\x3e\n <div id="glide-phone-overlay" class="glide-phone-animation-overlay">\n <div class="glide-outlined-phone">\n <div class="glide-scan-line"></div>\n </div>\n </div>\n </div>\n </div>\n `,this.createModal(e,"",!0,!0),this.setupBackButton(),this.setupHelpInteraction()}}setupBackButton(){this.container&&this.container.querySelector("#glide-back-btn")?.addEventListener("click",()=>{this.currentStep="os-choice",this.updatePreStepUI()})}createModal(e,n="",t=!1,i=!1){this.isClosing&&(this.isClosing=!1,this.cleanup()),this.container?(this.container.className=`glide-modal ${n}`,this.shouldUseDarkMode()&&this.container.classList.add("dark")):(this.backdrop=document.createElement("div"),this.backdrop.className="glide-backdrop",this.backdrop.id="glide-backdrop",this.container=document.createElement("div"),this.container.className=`glide-modal ${n}`,this.container.id="glide-modal",this.shouldUseDarkMode()&&this.container.classList.add("dark"),this.isOpen&&(document.body.appendChild(this.backdrop),document.body.appendChild(this.container))),this.container.innerHTML=`\n ${i?`\n <button class="glide-btn glide-btn-back" id="glide-back-btn" aria-label="Back">\n ${this.iconBack}\n </button>\n `:""}\n ${!1!==this.options?.showCloseButton?'\n <button class="glide-btn glide-btn-close" id="glide-close-btn" aria-label="Close">\n <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">\n <line x1="18" y1="6" x2="6" y2="18"></line>\n <line x1="6" y1="6" x2="18" y2="18"></line>\n </svg>\n </button>\n ':""}\n <h2 class="glide-title">${this.options?.title||"Scan to Verify"}</h2>\n <div id="glide-modal-content">${e}</div>\n <p class="glide-status" id="glide-status">${this.options?.description||""}</p>\n ${t?'\n <button class="glide-btn glide-btn-help" id="glide-help-btn">?</button>\n ':""}\n `,this.injectStyles();const o=this.container.querySelector("#glide-close-btn");o&&o.addEventListener("click",()=>{this.closeCallback?.(),this.close()}),this.backdrop&&(this.backdrop.onclick=e=>{e.target===this.backdrop&&!1!==this.options?.closeOnBackdropClick&&(this.closeCallback?.(),this.close())})}setupHelpInteraction(){const e=this.container?.querySelector("#glide-help-btn");e&&e.addEventListener("click",()=>{const e=this.container?.querySelectorAll(".glide-phone-animation-overlay");e&&e.forEach(e=>{e.classList.remove("playing"),e.offsetWidth,e.classList.add("playing"),setTimeout(()=>{e.classList.remove("playing")},3e3)})})}injectStyles(){if(document.getElementById("glide-modal-styles"))return;const e=document.createElement("style");e.id="glide-modal-styles",e.textContent='\n :root {\n --glide-primary: #007AFF;\n --glide-text: #1d1d1f;\n --glide-bg-light: rgba(255, 255, 255, 0.6);\n --glide-bg-dark: rgba(30, 30, 30, 0.6);\n --glide-phone-border: #000000; /* High Contrast Black */\n \n /* Button sizes */\n --glide-btn-size: 28px;\n --glide-help-btn-size: 24px;\n --glide-toggle-icon-size: 14px;\n --glide-dual-icon-size: 18px;\n }\n\n #glide-backdrop {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.2);\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n z-index: 9998;\n opacity: 0;\n transition: opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1);\n }\n\n #glide-modal {\n position: fixed;\n top: 20px;\n left: 50%;\n transform: translateX(-50%) scale(0.94);\n background: var(--glide-bg-light);\n backdrop-filter: blur(24px) saturate(180%);\n -webkit-backdrop-filter: blur(24px) saturate(180%);\n border-radius: 24px;\n padding: 32px;\n width: 360px;\n max-width: 90%;\n box-shadow: \n 0 20px 40px rgba(0,0,0,0.2),\n 0 0 0 1px rgba(255,255,255,0.6) inset,\n 0 0 0 1px rgba(0,0,0,0.05);\n z-index: 9999;\n opacity: 0;\n transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);\n font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;\n text-align: center;\n color: var(--glide-text);\n }\n \n #glide-modal.glide-modal-wide {\n width: 600px;\n max-width: 95vw;\n }\n\n #glide-modal.dark {\n background: var(--glide-bg-dark);\n color: white;\n box-shadow: \n 0 20px 40px rgba(0,0,0,0.4),\n 0 0 0 1px rgba(255,255,255,0.15) inset,\n 0 0 0 1px rgba(0,0,0,0.5);\n --glide-phone-border: #ffffff; /* High Contrast White */\n }\n\n #glide-modal .glide-btn-close {\n position: absolute;\n top: 16px;\n right: 16px;\n width: var(--glide-btn-size);\n height: var(--glide-btn-size);\n min-width: var(--glide-btn-size);\n min-height: var(--glide-btn-size);\n max-width: var(--glide-btn-size);\n max-height: var(--glide-btn-size);\n background: rgba(118, 118, 128, 0.12);\n border: none;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: rgba(0,0,0,0.5);\n transition: background 0.2s;\n padding: 0;\n z-index: 20;\n box-sizing: border-box;\n flex-shrink: 0;\n }\n\n #glide-modal.dark .glide-btn-close {\n background: rgba(255, 255, 255, 0.1);\n color: rgba(255,255,255,0.5);\n }\n\n #glide-modal .glide-btn-close:hover {\n background: rgba(118, 118, 128, 0.2);\n }\n \n #glide-modal.dark .glide-btn-close:hover {\n background: rgba(255, 255, 255, 0.2);\n }\n\n .glide-title {\n margin: 0 0 8px 0;\n font-size: 22px;\n font-weight: 600;\n letter-spacing: -0.01em;\n }\n \n .glide-status {\n margin: 12px 0 0 0;\n font-size: 13px;\n color: rgba(0,0,0,0.5);\n text-align: center;\n min-height: 18px;\n }\n \n .glide-status:empty {\n display: none;\n }\n \n #glide-modal.dark .glide-status {\n color: rgba(255,255,255,0.5);\n }\n \n .glide-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n position: relative;\n }\n \n /* --- Sliding Toggle Switch --- */\n #glide-toggle {\n background: rgba(118, 118, 128, 0.12);\n padding: 2px;\n border-radius: 8px;\n display: inline-flex;\n margin-bottom: 24px;\n margin-top: 16px;\n position: relative;\n height: 32px;\n width: 200px;\n box-sizing: border-box;\n }\n\n #glide-modal.dark #glide-toggle {\n background: rgba(118, 118, 128, 0.24);\n }\n \n /* The sliding background */\n .glide-toggle-slider {\n position: absolute;\n top: 2px;\n left: 2px;\n width: calc(50% - 2px);\n height: calc(100% - 4px);\n background: white;\n border-radius: 6px;\n box-shadow: 0 3px 8px rgba(0,0,0,0.12), 0 3px 1px rgba(0,0,0,0.04);\n transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n z-index: 0;\n }\n \n #glide-modal.dark .glide-toggle-slider {\n background: #636366;\n }\n \n /* Move slider for Android */\n #glide-toggle[data-active="android"] .glide-toggle-slider {\n transform: translateX(100%);\n }\n\n .glide-toggle-btn {\n flex: 1;\n background: none;\n border: none;\n padding: 0;\n margin: 0;\n font-size: 13px;\n font-weight: 500;\n color: inherit;\n cursor: pointer;\n position: relative;\n z-index: 1;\n transition: color 0.2s;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n min-height: auto;\n height: auto;\n min-width: auto;\n border-radius: 0;\n }\n \n #glide-modal .glide-toggle-btn .glide-icon-os,\n #glide-modal .glide-toggle-btn svg {\n width: var(--glide-toggle-icon-size);\n height: var(--glide-toggle-icon-size);\n flex-shrink: 0;\n margin: 0;\n padding: 0;\n }\n \n .glide-toggle-btn span {\n margin: 0;\n padding: 0;\n line-height: 1;\n }\n \n #glide-modal.dark .glide-toggle-btn {\n color: rgba(255,255,255,0.6);\n }\n \n #glide-toggle[data-active="ios"] .glide-toggle-btn[data-platform="ios"],\n #glide-toggle[data-active="android"] .glide-toggle-btn[data-platform="android"] {\n color: #1d1d1f;\n }\n \n #glide-modal.dark #glide-toggle[data-active="ios"] .glide-toggle-btn[data-platform="ios"],\n #glide-modal.dark #glide-toggle[data-active="android"] .glide-toggle-btn[data-platform="android"] {\n color: white;\n }\n\n /* QR Area */\n .glide-qr-area {\n position: relative;\n margin: 0 auto;\n }\n\n .glide-qr-img {\n width: 200px;\n height: 200px;\n object-fit: contain;\n display: block;\n border-radius: 16px;\n }\n \n /* Dual Mode QR Area - no extra padding, same as single mode */\n .glide-dual-mode .glide-qr-area {\n background: transparent;\n padding: 0;\n }\n \n .glide-dual-mode .glide-qr-img {\n width: 180px;\n height: 180px;\n border-radius: 16px;\n }\n \n /* --- Help Icon & Interaction --- */\n .glide-btn-help {\n position: absolute;\n bottom: 20px;\n right: 20px;\n width: var(--glide-help-btn-size);\n height: var(--glide-help-btn-size);\n min-width: var(--glide-help-btn-size);\n min-height: var(--glide-help-btn-size);\n max-width: var(--glide-help-btn-size);\n max-height: var(--glide-help-btn-size);\n border-radius: 50%;\n border: 1.5px solid rgba(0,0,0,0.2);\n color: rgba(0,0,0,0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, sans-serif;\n font-size: 14px;\n font-weight: 600;\n line-height: 1;\n cursor: pointer;\n transition: all 0.2s;\n background: transparent;\n padding: 0;\n margin: 0;\n box-sizing: border-box;\n z-index: 10;\n }\n \n #glide-modal.dark .glide-btn-help {\n border-color: rgba(255,255,255,0.3);\n color: rgba(255,255,255,0.5);\n }\n\n .glide-btn-help:hover {\n border-color: var(--glide-primary);\n color: var(--glide-primary);\n background: rgba(0, 122, 255, 0.1);\n }\n \n /* Tooltip */\n .glide-btn-help::after {\n content: "Need help?";\n position: absolute;\n bottom: 100%;\n left: 50%;\n transform: translateX(-50%) translateY(-8px);\n background: rgba(0,0,0,0.8);\n color: white;\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 11px;\n white-space: nowrap;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.2s;\n }\n \n .glide-btn-help:hover::after {\n opacity: 1;\n }\n\n /* --- Outlined Phone Animation --- */\n .glide-phone-animation-overlay {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n opacity: 0;\n transition: opacity 0.3s;\n border-radius: 16px;\n background: rgba(255,255,255,0.8);\n }\n \n #glide-modal.dark .glide-phone-animation-overlay {\n background: rgba(0,0,0,0.6);\n }\n \n .glide-phone-animation-overlay.playing {\n opacity: 1;\n }\n\n .glide-outlined-phone {\n width: 100px;\n height: 180px;\n border: 4px solid var(--glide-phone-border);\n border-radius: 16px;\n position: relative;\n background: transparent;\n box-shadow: 0 10px 30px rgba(0,0,0,0.2);\n transform: translateY(20px);\n }\n \n /* Notch */\n .glide-outlined-phone::before {\n content: \'\';\n position: absolute;\n top: -1px;\n left: 50%;\n transform: translateX(-50%);\n width: 40%;\n height: 12px;\n background: var(--glide-phone-border);\n border-bottom-left-radius: 8px;\n border-bottom-right-radius: 8px;\n }\n \n /* Scanning Line */\n .glide-scan-line {\n position: absolute;\n top: 10%;\n left: 5%;\n width: 90%;\n height: 2px;\n background: var(--glide-primary);\n box-shadow: 0 0 8px var(--glide-primary);\n opacity: 0;\n }\n \n /* Animation Keyframes */\n @keyframes glide-scan-motion {\n 0% { top: 10%; opacity: 0; }\n 10% { opacity: 1; }\n 90% { opacity: 1; }\n 100% { top: 90%; opacity: 0; }\n }\n \n .glide-phone-animation-overlay.playing .glide-outlined-phone {\n animation: glide-phone-appear 3s ease-in-out forwards;\n }\n \n .glide-phone-animation-overlay.playing .glide-scan-line {\n animation: glide-scan-motion 2s ease-in-out 0.5s infinite;\n }\n \n @keyframes glide-phone-appear {\n 0% { transform: translateY(20px); opacity: 0; }\n 10% { transform: translateY(0); opacity: 1; }\n 90% { transform: translateY(0); opacity: 1; }\n 100% { transform: translateY(20px); opacity: 0; }\n }\n \n /* Dual Mode */\n .glide-dual-container {\n display: flex;\n justify-content: center;\n align-items: stretch;\n gap: 32px;\n margin-top: 20px;\n }\n \n .glide-dual-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n \n .glide-os-logo {\n display: flex;\n align-items: center;\n gap: 8px;\n font-weight: 600;\n margin-bottom: 12px;\n font-size: 15px;\n color: inherit;\n }\n \n /* Dual mode OS logo - aligned icons with text, spacing to QR */\n .glide-dual-mode .glide-os-logo {\n margin-bottom: 12px; /* Space between label and QR code */\n }\n \n #glide-modal .glide-dual-mode .glide-os-logo .glide-icon-os,\n #glide-modal .glide-dual-mode .glide-os-logo svg {\n width: var(--glide-dual-icon-size);\n height: var(--glide-dual-icon-size);\n margin: 0;\n padding: 0;\n flex-shrink: 0;\n }\n \n .glide-dual-mode .glide-os-logo span {\n margin: 0;\n padding: 0;\n line-height: 1;\n }\n \n .glide-os-logo svg {\n width: 20px;\n height: 20px;\n opacity: 0.8;\n }\n \n /* Dual Mode Separator - aligned to QR codes only */\n .glide-dual-separator {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n align-self: stretch;\n /* Offset for OS label (18px icon + 12px margin = ~30px) */\n margin-top: 30px;\n padding: 20px 0;\n }\n \n .glide-separator-line {\n width: 1px;\n flex: 1;\n background: linear-gradient(to bottom, transparent, rgba(128,128,128,0.3), transparent);\n }\n \n .glide-separator-text {\n padding: 8px 0;\n font-size: 11px;\n font-weight: 500;\n color: rgba(128,128,128,0.5);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n #glide-modal.dark .glide-separator-line {\n background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.2), transparent);\n }\n \n #glide-modal.dark .glide-separator-text {\n color: rgba(255,255,255,0.4);\n }\n \n /* Pre-Step */\n .glide-pre-step-container {\n display: flex;\n gap: 20px;\n justify-content: center;\n margin: 24px 0;\n }\n \n .glide-os-choice-btn {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n width: 140px;\n height: 140px;\n border: 1px solid rgba(0,0,0,0.08);\n border-radius: 20px;\n background: rgba(255,255,255,0.4);\n cursor: pointer;\n transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n position: relative;\n overflow: hidden;\n font-size: 14px;\n color: inherit;\n }\n \n #glide-modal.dark .glide-os-choice-btn {\n background: rgba(255,255,255,0.05);\n border-color: rgba(255,255,255,0.1);\n color: white;\n }\n \n .glide-os-choice-btn:hover {\n background: rgba(255,255,255,0.9);\n transform: translateY(-4px);\n box-shadow: 0 12px 24px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.04);\n border-color: var(--glide-primary);\n }\n \n #glide-modal.dark .glide-os-choice-btn:hover {\n background: rgba(255,255,255,0.15);\n box-shadow: 0 12px 24px rgba(0,0,0,0.3);\n border-color: var(--glide-primary);\n }\n \n .glide-icon-os {\n width: 40px;\n height: 40px;\n margin-bottom: 12px;\n transition: transform 0.3s;\n fill: currentColor;\n }\n \n .glide-os-choice-btn:hover .glide-icon-os {\n transform: scale(1.1);\n }\n \n #glide-modal .glide-btn-back {\n position: absolute;\n top: 16px;\n left: 16px;\n width: var(--glide-btn-size);\n height: var(--glide-btn-size);\n min-width: var(--glide-btn-size);\n min-height: var(--glide-btn-size);\n max-width: var(--glide-btn-size);\n max-height: var(--glide-btn-size);\n background: rgba(118, 118, 128, 0.12);\n border: none;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n color: rgba(0,0,0,0.5);\n transition: background 0.2s;\n z-index: 20;\n padding: 0;\n margin: 0;\n box-sizing: border-box;\n flex-shrink: 0;\n }\n \n #glide-modal.dark .glide-btn-back {\n background: rgba(255, 255, 255, 0.1);\n color: rgba(255,255,255,0.5);\n }\n\n .glide-btn-back:hover {\n background: rgba(118, 118, 128, 0.2);\n }\n \n #glide-modal.dark .glide-btn-back:hover {\n background: rgba(255, 255, 255, 0.2);\n }\n \n .glide-spinner {\n width: 40px;\n height: 40px;\n border: 3px solid rgba(0,0,0,0.1);\n border-top-color: var(--glide-primary);\n border-radius: 50%;\n animation: glide-spin 1s linear infinite;\n margin: 40px auto;\n }\n \n #glide-modal.dark .glide-spinner {\n border-color: rgba(255,255,255,0.1);\n border-top-color: var(--glide-primary);\n }\n \n @keyframes glide-spin {\n to { transform: rotate(360deg); }\n }\n ',document.head.appendChild(e)}show(){this.container&&this.backdrop&&!this.isOpen?(document.body.appendChild(this.backdrop),document.body.appendChild(this.container),this.lockBodyScroll(),document.addEventListener("keydown",this.handleEscapeKey),requestAnimationFrame(()=>{this.backdrop&&this.container&&(this.backdrop.style.opacity="1",this.container.style.opacity="1",this.container.style.transform="translateX(-50%) scale(1)")}),this.isOpen=!0):this.isOpen&&this.container&&this.backdrop&&(document.removeEventListener("keydown",this.handleEscapeKey),document.addEventListener("keydown",this.handleEscapeKey))}setupPlatformToggles(){const e=this.container?.querySelector("#glide-toggle"),n=this.container?.querySelectorAll(".glide-toggle-btn"),t=this.container?.querySelector("#glide-qr-img"),i=this.container?.querySelector("#glide-platform-message");n&&t&&n.forEach(o=>{o.addEventListener("click",o=>{const r=o.currentTarget,a=r.getAttribute("data-platform");e&&a&&e.setAttribute("data-active",a),n.forEach(e=>e.classList.remove("active")),r.classList.add("active"),"ios"===a?(t.src=t.getAttribute("data-ios")||"",i&&(i.textContent="Scan with your iPhone camera")):"android"===a&&(t.src=t.getAttribute("data-android")||"",i&&(i.textContent="Scan with your Android camera"))})})}lockBodyScroll(){this.originalBodyOverflow=document.body.style.overflow,document.body.style.overflow="hidden"}unlockBodyScroll(){document.body.style.overflow=this.originalBodyOverflow}close(){this.container&&this.backdrop&&this.isOpen&&(this.isClosing=!0,document.removeEventListener("keydown",this.handleEscapeKey),this.backdrop.style.opacity="0",this.container.style.opacity="0",this.container.style.transform="translateX(-50%) scale(0.94)",this.closeCallback=void 0,setTimeout(()=>{this.isClosing&&(this.cleanup(),this.isOpen=!1,this.isClosing=!1)},400))}cleanup(){this.container?.remove(),this.backdrop?.remove(),this.container=null,this.backdrop=null,this.unlockBodyScroll()}isModalOpen(){return this.isOpen}}function x(e){const{data:n}=e;if(n.ios_qr_image)return{iosQRCode:n.ios_qr_image,androidQRCode:n.android_qr_image||n.ios_qr_image};if(n.qr_code_image)return n.qr_code_image;throw new Error("No QR code data available")}class w{constructor(){this.logs=[],this.container=null,this.logsContainer=null,this.floatingToggle=null,this.isAtBottom=!0,this.isVisible=!0,this.originalConsole={log:console.log,error:console.error,warn:console.warn,debug:console.debug,info:console.info},this.interceptConsole(),this.createUI()}static init(){return w.instance||(w.instance=new w),w.instance}static destroy(){w.instance&&(w.instance.cleanup(),w.instance=null)}interceptConsole(){["log","error","warn","debug","info"].forEach(e=>{const n=this.originalConsole[e];console[e]=(...t)=>{n.apply(console,t),this.addLog(e,t)}})}addLog(e,n){const t=(new Date).toTimeString().split(" ")[0],i=n.map(e=>{if("object"==typeof e)try{return JSON.stringify(e,null,2)}catch{return"[Object]"}return String(e)}).join(" "),o={log:"#abb2bf",error:"#e06c75",warn:"#e5c07b",info:"#61afef",debug:"#5c6370"},r=`\n <div style="margin: 3px 0; font-family: 'SF Mono', Menlo, Monaco, monospace; font-size: 11px; color: ${o[e]||"#abb2bf"}; line-height: 1.5;">\n <span style="color: #5c6370; font-size: 10px;">${t}</span>\n <span style="background: ${{log:"#3c3c3c",error:"rgba(224, 108, 117, 0.2)",warn:"rgba(229, 192, 123, 0.2)",info:"rgba(97, 175, 239, 0.2)",debug:"#2d2d2d"}[e]}; color: ${o[e]}; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 500; text-transform: uppercase; margin: 0 6px;">${e}</span>\n <span style="white-space: pre-wrap; word-break: break-all;">${this.escapeHtml(i)}</span>\n </div>\n `;this.logs.push(r),this.logs.length>500&&this.logs.shift(),this.updateDisplay()}updateDisplay(){this.logsContainer&&this.isVisible&&(this.isAtBottom=this.logsContainer.scrollHeight-this.logsContainer.scrollTop<=this.logsContainer.clientHeight+50,this.logsContainer.innerHTML=this.logs.join(""),this.isAtBottom&&(this.logsContainer.scrollTop=this.logsContainer.scrollHeight))}createUI(){const e=document.createElement("style");e.textContent="\n #mobile-debug-console {\n position: fixed;\n bottom: 0;\n left: 0;\n right: 0;\n height: 45vh;\n background: #1e1e1e;\n z-index: 999999;\n display: flex;\n flex-direction: column;\n font-family: 'SF Mono', Menlo, Monaco, 'Courier New', monospace;\n transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n border-top: 1px solid #3c3c3c;\n box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);\n }\n \n #mobile-debug-console.hidden {\n transform: translateY(100%);\n }\n \n #debug-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 12px;\n background: #2d2d2d;\n border-bottom: 1px solid #3c3c3c;\n min-height: 36px;\n }\n \n #debug-title {\n display: flex;\n align-items: center;\n gap: 8px;\n color: #9da5b4;\n font-size: 12px;\n font-weight: 500;\n letter-spacing: 0.3px;\n }\n \n #debug-title svg {\n width: 14px;\n height: 14px;\n opacity: 0.8;\n }\n \n /* Traffic light buttons - using ID for specificity */\n #mobile-debug-console .debug-traffic-lights {\n display: flex;\n gap: 8px;\n align-items: center;\n }\n \n #mobile-debug-console button.debug-traffic-btn {\n width: 16px;\n height: 16px;\n min-width: 16px;\n min-height: 16px;\n border-radius: 50%;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n font-size: 0;\n line-height: 1;\n }\n \n #mobile-debug-console .debug-traffic-btn svg {\n width: 8px;\n height: 8px;\n }\n \n #mobile-debug-console .debug-traffic-btn.close {\n background: #ff5f57;\n }\n \n #mobile-debug-console .debug-traffic-btn.close svg {\n stroke: #820005;\n stroke-width: 2;\n }\n \n #mobile-debug-console .debug-traffic-btn.clear {\n background: #febc2e;\n }\n \n #mobile-debug-console .debug-traffic-btn.minimize {\n background: #28c840;\n }\n \n #mobile-debug-console .debug-traffic-btn.minimize svg {\n stroke: #006500;\n stroke-width: 2;\n }\n \n #mobile-debug-console .debug-traffic-btn:active {\n filter: brightness(0.85);\n }\n \n #debug-logs {\n flex: 1;\n overflow-y: auto;\n padding: 12px;\n background: #1e1e1e;\n -webkit-overflow-scrolling: touch;\n }\n \n button#debug-floating-toggle {\n position: fixed !important;\n bottom: 20px !important;\n right: 20px !important;\n width: 42px !important;\n height: 42px !important;\n min-width: 42px !important;\n min-height: 42px !important;\n border-radius: 8px !important;\n background: #2d2d2d !important;\n border: 1px solid #3c3c3c !important;\n color: #9da5b4 !important;\n cursor: pointer !important;\n z-index: 999998 !important;\n display: none;\n align-items: center !important;\n justify-content: center !important;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;\n transition: all 0.2s ease !important;\n padding: 0 !important;\n margin: 0 !important;\n }\n \n button#debug-floating-toggle:hover {\n background: #3c3c3c !important;\n color: #fff !important;\n transform: translateY(-2px) !important;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4) !important;\n }\n \n button#debug-floating-toggle:active {\n transform: translateY(0) !important;\n }\n \n button#debug-floating-toggle.visible {\n display: flex !important;\n }\n \n button#debug-floating-toggle svg {\n width: 20px !important;\n height: 20px !important;\n }\n ",document.head.appendChild(e);const n='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>';this.container=document.createElement("div"),this.container.id="mobile-debug-console",this.isVisible||(this.container.className="hidden");const t=document.createElement("div");t.id="debug-header";const i=document.createElement("div");i.id="debug-title",i.innerHTML=`${n}<span>Mobile Console</span>`;const o=document.createElement("div");o.className="debug-traffic-lights";const r=document.createElement("button");r.className="debug-traffic-btn close",r.title="Close",r.innerHTML='<svg viewBox="0 0 10 10" fill="none"><line x1="2.5" y1="2.5" x2="7.5" y2="7.5" stroke="currentColor"/><line x1="7.5" y1="2.5" x2="2.5" y2="7.5" stroke="currentColor"/></svg>',r.onclick=()=>this.cleanup();const a=document.createElement("button");a.className="debug-traffic-btn clear",a.title="Clear",a.innerHTML='<svg viewBox="0 0 16 16" fill="currentColor"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z"/></svg>',a.onclick=()=>this.clear();const s=document.createElement("button");s.className="debug-traffic-btn minimize",s.title="Minimize",s.innerHTML='<svg viewBox="0 0 10 10" fill="none"><path d="M2 3L5 6L8 3" stroke="currentColor" fill="none"/></svg>',s.onclick=()=>this.toggle(),o.appendChild(r),o.appendChild(a),o.appendChild(s),t.appendChild(o),t.appendChild(i),this.logsContainer=document.createElement("div"),this.logsContainer.id="debug-logs",this.logsContainer.addEventListener("scroll",()=>{this.isAtBottom=this.logsContainer.scrollHeight-this.logsContainer.scrollTop<=this.logsContainer.clientHeight+50}),this.container.appendChild(t),this.container.appendChild(this.logsContainer),document.body.appendChild(this.container),this.floatingToggle=document.createElement("button"),this.floatingToggle.id="debug-floating-toggle",this.floatingToggle.innerHTML=n,this.floatingToggle.title="Show Console",this.floatingToggle.onclick=()=>this.toggle(),document.body.appendChild(this.floatingToggle)}escapeHtml(e){const n=document.createElement("div");return n.textContent=e,n.innerHTML}toggle(){this.isVisible=!this.isVisible,this.container&&this.floatingToggle&&(this.isVisible?(this.container.classList.remove("hidden"),this.floatingToggle.classList.remove("visible"),this.updateDisplay()):(this.container.classList.add("hidden"),this.floatingToggle.classList.add("visible")))}clear(){this.logs=[],this.logsContainer&&(this.logsContainer.innerHTML="")}cleanup(){Object.keys(this.originalConsole).forEach(e=>{console[e]=this.originalConsole[e]}),this.container&&this.container.remove(),this.floatingToggle&&this.floatingToggle.remove()}}w.instance=null;class E{constructor(e={}){this.config={...e,endpoints:{prepare:e.endpoints?.prepare||"/api/magic-auth/prepare",process:e.endpoints?.process||"/api/magic-auth/process",polling:e.endpoints?.polling},headers:e.headers,timeout:e.timeout||3e4,pollingInterval:e.pollingInterval||2e3,maxPollingAttempts:e.maxPollingAttempts||30,debug:e.debug||!1,devEnv:e.devEnv},this.http=e.httpClient||function(e={}){const{timeout:n=3e4,headers:t={},dynamicHeaders:i}=e;async function o(e,n){const o={"Content-Type":"application/json"};t.common&&Object.assign(o,t.common);const r=t[e.endpoint];if(r&&"object"==typeof r&&Object.assign(o,r),i){const n=await i(e);Object.assign(o,n)}return n&&Object.assign(o,n),o}async function r(e,n,t,i){const o=new AbortController,r=setTimeout(()=>o.abort(),t),a=i?()=>o.abort():null;i&&a&&i.addEventListener("abort",a);try{return await fetch(e,{...n,signal:o.signal})}catch(e){if(e instanceof Error&&"AbortError"===e.name){if(i?.aborted)throw h(s.CANCELLED,"Request was cancelled");throw h(s.TIMEOUT,"Request timed out")}throw h(s.NETWORK_ERROR,e instanceof Error?e.message:"Network request failed")}finally{clearTimeout(r),i&&a&&i.removeEventListener("abort",a)}}async function a(e){let n;try{n=await e.json()}catch{if(!e.ok)throw h(s.INVALID_RESPONSE,`Request failed with status ${e.status}`);throw h(s.INVALID_RESPONSE,"Failed to parse response")}if(!e.ok)throw function(e){if(e&&"object"==typeof e&&("code"in e||"error"in e)){const n=e,t=n.code||n.error,i=n.message,o=new Error(i||"An error occurred");return o.code=t,o.status=n.status,o.requestId=n.requestId||n.request_id,o.timestamp=n.timestamp,o.details=n.details,o.traceId=n.trace_id||n.traceId,o.spanId=n.span_id||n.spanId,o.service=n.service,n.details&&"object"==typeof n.details&&"retryAfter"in n.details&&(o.retryAfter=n.details.retryAfter),o}if(e&&"object"==typeof e&&"status"in e){const n=e,t=n.status,i=new Error(n.message||`Request failed with status ${t}`);return i.code=`HTTP_${t}`,i.status=t,i}const n=new Error("An unexpected error occurred");return n.code="UNKNOWN_ERROR",n}("object"!=typeof n||null===n||Array.isArray(n)?{status:e.status,rawResponse:n}:{...n,status:e.status});return n}function l(e){return e.includes("/prepare")?"prepare":e.includes("/process")||e.includes("/get-phone")||e.includes("/verify-phone")?"process":e.includes("/status")?"polling":"prepare"}return{async post(e,t,i){const s=l(e),d=await o({endpoint:s,method:"POST"},i?.headers);return a(await r(e,{method:"POST",headers:d,body:JSON.stringify(t)},i?.timeout||n,i?.signal))},async get(e,t){const i=l(e),s=await o({endpoint:i,method:"GET"},t?.headers);return delete s["Content-Type"],a(await r(e,{method:"GET",headers:s},t?.timeout||n,t?.signal))}}}({timeout:this.config.timeout,headers:e.headers,dynamicHeaders:e.dynamicHeaders}),this.logger=e.logger||(this.config.debug?function(e={}){const{debug:n=!1,prefix:t="[PhoneAuth]"}=e;function i(e,n){const i=`${t} ${e}`;return void 0!==n?[i,f(n)]:[i]}return{debug(e,t){if(n){const n=i(e,t);console.debug(...n)}},info(e,n){const t=i(e,n);console.info(...t)},warn(e,n){const t=i(e,n);console.warn(...t)},error(e,n){const t=i(e,n);console.error(...t)}}}({debug:!0}):{debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}}),this.logger.debug("PhoneAuthClient initialized",{endpoints:this.config.endpoints,timeout:this.config.timeout,...e.devEnv&&{devEnv:e.devEnv}}),e.devtools?.showMobileConsole&&"undefined"!=typeof window&&(w.init(),this.logger.info("Mobile debug console enabled"))}async authenticate(e,n){const i=await this.prepare(e),o=await this.invokeSecurePrompt(i,n),r=await o.credential,a=i.session.use_case||i.session.metadata?.use_case||e.use_case;if(!a)throw h(s.INVALID_RESPONSE,"Could not determine use_case from session or request");return a===t.VERIFY_PHONE_NUMBER?this.verifyPhoneNumber(r,i.session):this.getPhoneNumber(r,i.session)}async prepare(e){if(this.logger.debug("Preparing authentication",{use_case:e.use_case}),e.phone_number){const n=r(e.phone_number);if(!n.valid)throw h(s.INVALID_PHONE_NUMBER,n.error)}if(e.plmn){const n=a(e.plmn);if(!n.valid)throw h(s.INVALID_PLMN,n.error)}if(!e.use_case&&!e.options?.parent_session_id)throw h(s.MISSING_PARAMETERS,"use_case is required");if(e.use_case){const r=(n=e.use_case,i=e.phone_number,o=!!e.options?.parent_session_id,n!==t.VERIFY_PHONE_NUMBER||i||o?{valid:!0}:{valid:!1,error:"Phone number is required for VerifyPhoneNumber use case"});if(!r.valid)throw h(s.MISSING_PARAMETERS,r.error)}var n,i,o;const l={...e,id:e.id||this.generateRequestId(),client_info:e.client_info||{user_agent:navigator.userAgent,platform:navigator.platform}};try{const e=await this.http.post(this.config.endpoints.prepare,l);return this.logger.debug("Prepare successful",{strategy:e.authentication_strategy,sessionKey:e.session.session_key}),e}catch(e){throw this.logger.error("Prepare failed",e),e}}async invokeSecurePrompt(e,n){const{authentication_strategy:t,session:o,data:r}=e;switch(this.logger.debug("Invoking secure prompt",{strategy:t}),t){case i.TS43:return this.handleTS43(r,o);case i.LINK:return this.handleLink(r,o,n);case i.DESKTOP:return this.handleDesktop(r,o,n);default:throw h(s.UNSUPPORTED_STRATEGY,`Unknown strategy: ${t}`)}}async getPhoneNumber(e,n){this.logger.debug("Getting phone number");const i={session:n,credential:e,use_case:t.GET_PHONE_NUMBER};try{const e=await this.http.post(this.config.endpoints.process,i);return this.logger.info("Phone number retrieved"),e}catch(e){throw this.logger.error("Get phone number failed",e),e}}async verifyPhoneNumber(e,n){this.logger.debug("Verifying phone number");const i={session:n,credential:e,use_case:t.VERIFY_PHONE_NUMBER};try{const e=await this.http.post(this.config.endpoints.process,i);return this.logger.info("Phone number verified",{verified:e.verified}),e}catch(e){throw this.logger.error("Verify phone number failed",e),e}}isSupported(){return"undefined"!=typeof window&&"DigitalCredential"in window}getBrowserSupportInfo(){if("undefined"==typeof window)return{supported:!1,browser:"unknown",message:"Not in browser"};const e=navigator.userAgent,n=/Chrome/.test(e)&&/Google Inc/.test(navigator.vendor),t=/Edg\//.test(e);return this.isSupported()?{supported:!0,browser:n?"Chrome":t?"Edge":"Other"}:{supported:!1,browser:n?"Chrome":t?"Edge":"Other",message:"Enable chrome://flags/#web-identity-digital-credentials"}}async handleTS43(e,n){if(!this.isSupported())throw h(s.BROWSER_NOT_SUPPORTED,"Digital Credentials API not available");const t={digital:{requests:[{protocol:e.protocol,data:e.data}]}};return this.logger.debug("Invoking Digital Credentials API"),{credential:(async()=>{try{const e=await navigator.credentials.get(t);if(!e?.data?.vp_token)throw h(s.INVALID_RESPONSE,"No credential returned from Digital Credentials API");const n=e.data.vp_token,i="string"==typeof n?n:Object.values(n)[0],o=Array.isArray(i)?i[0]:i;if(!o||"string"==typeof o&&""===o.trim())throw h(s.INVALID_RESPONSE,"Empty credential returned from Digital Credentials API");return"undefined"!=typeof navigator&&navigator.vibrate&&setTimeout(()=>{try{navigator.vibrate([80,50,80])}catch(e){}},200),o}catch(e){if(d(e))throw e;const n=e;if("NotAllowedError"===n.name||"NetworkError"===n.name&&19===n.code)throw h(s.USER_CANCELLED,"User cancelled authentication");if("NotSupportedError"===n.name)throw h(s.BROWSER_NOT_SUPPORTED,"Browser not supported");throw h(s.NETWORK_ERROR,n.message||"Credential request failed")}})(),cancel:void 0,strategy:i.TS43,session:n}}handleLink(e,n,t){if(!e.url)throw h(s.INVALID_RESPONSE,"Missing link URL");this.logger.debug("Opening App Clip",{url:e.url}),window.location.href=e.url;const o={...this.config.headers?.common,...this.config.headers?.polling};this.config.devEnv&&(o.developer=this.config.devEnv,this.logger.debug("Adding developer header for polling",{devEnv:this.config.devEnv}));const r=v({sessionKey:n.session_key,interval:t?.pollingInterval||this.config.pollingInterval,maxAttempts:t?.maxPollingAttempts||this.config.maxPollingAttempts,pollingEndpoint:t?.pollingEndpoint||this.config.endpoints.polling,statusUrl:e.status_url,logger:this.logger,headers:Object.keys(o).length>0?o:void 0});return{credential:r.start().then(e=>{if("completed"===e.status&&e.credential)return e.credential;throw h(s.VERIFICATION_FAILED,e.message||"Link authentication failed")}),cancel:()=>r.cancel(),strategy:i.LINK,session:n}}handleDesktop(e,n,t){const o=e.data?.session_id||n.session_key;this.logger.debug("Starting desktop authentication",{sessionId:o});const r={...this.config.headers?.common,...this.config.headers?.polling};this.config.devEnv&&(r.developer=this.config.devEnv,this.logger.debug("Adding developer header for polling",{devEnv:this.config.devEnv}));const a=v({sessionKey:o,interval:t?.pollingInterval||this.config.pollingInterval,maxAttempts:t?.maxPollingAttempts||this.config.maxPollingAttempts,pollingEndpoint:t?.pollingEndpoint||this.config.endpoints.polling,statusUrl:e.data?.status_url,logger:this.logger,headers:Object.keys(r).length>0?r:void 0});let l=null;if(!t?.preventDefaultUI)try{const n=x(e);l=new y(t?.modalOptions),l.showQRCode(n,t?.modalOptions?.description||"Scan with your phone camera"),l.setCloseCallback(()=>{this.logger.debug("Modal closed by user, cancelling authentication"),a.cancel()}),this.logger.debug("Desktop modal displayed")}catch(e){this.logger.warn("Failed to show modal, continuing with polling only",e)}return{credential:a.start().then(e=>{if(l&&l.close(),"completed"===e.status&&e.credential)return e.credential;throw h(s.VERIFICATION_FAILED,e.message||"Desktop authentication failed")}).catch(e=>{throw l&&l.close(),e}),cancel:()=>{a.cancel(),l&&l.close()},strategy:i.DESKTOP,session:n}}generateRequestId(){return`web-${Date.now()}-${Math.random().toString(36).substring(2,11)}`}}function k(e){return null!==e&&"object"==typeof e&&"credential"in e&&"strategy"in e&&"session"in e}function C(e){return null!==e&&"object"==typeof e&&"credential"in e&&"authenticated"in e&&"session"in e}function S(e){return e.strategy===i.TS43}function A(e){return e.strategy===i.LINK}function R(e){return e.strategy===i.DESKTOP}function _(e){return void 0!==e.cancel}function I(e){return null!==e&&"object"==typeof e&&"protocol"in e&&"data"in e&&"object"==typeof e.data&&"dcql_query"in e.data}function N(e){return null!==e&&"object"==typeof e&&"url"in e&&"string"==typeof e.url}function O(e){return null!==e&&"object"==typeof e&&"data"in e&&"object"==typeof e.data&&null!==e.data&&!("dcql_query"in e.data)}function T(e){return!("verified"in e)}function P(e){return"verified"in e}function L(e){return null!==e&&"object"==typeof e&&"code"in e&&"message"in e}return n})());
|
|
@@ -118,9 +118,13 @@ function usePhoneAuth(config) {
|
|
|
118
118
|
* Granular: Invoke.
|
|
119
119
|
*/
|
|
120
120
|
const invokeSecurePrompt = (0, react_1.useCallback)(async (prepared, options) => {
|
|
121
|
+
setIsLoading(true);
|
|
122
|
+
setError(null);
|
|
121
123
|
setStep('invoking');
|
|
122
124
|
try {
|
|
123
|
-
|
|
125
|
+
const result = await client.invokeSecurePrompt(prepared, options);
|
|
126
|
+
setStep('idle'); // Reset step on success (invoke is intermediate step)
|
|
127
|
+
return result;
|
|
124
128
|
}
|
|
125
129
|
catch (err) {
|
|
126
130
|
const authError = err;
|
|
@@ -128,12 +132,16 @@ function usePhoneAuth(config) {
|
|
|
128
132
|
setStep('error');
|
|
129
133
|
throw err;
|
|
130
134
|
}
|
|
135
|
+
finally {
|
|
136
|
+
setIsLoading(false);
|
|
137
|
+
}
|
|
131
138
|
}, [client]);
|
|
132
139
|
/**
|
|
133
140
|
* Granular: Get phone number.
|
|
134
141
|
*/
|
|
135
142
|
const getPhoneNumber = (0, react_1.useCallback)(async (credential, session) => {
|
|
136
143
|
setIsLoading(true);
|
|
144
|
+
setError(null);
|
|
137
145
|
setStep('processing');
|
|
138
146
|
try {
|
|
139
147
|
const authResult = await client.getPhoneNumber(credential, session);
|
|
@@ -156,6 +164,7 @@ function usePhoneAuth(config) {
|
|
|
156
164
|
*/
|
|
157
165
|
const verifyPhoneNumber = (0, react_1.useCallback)(async (credential, session) => {
|
|
158
166
|
setIsLoading(true);
|
|
167
|
+
setError(null);
|
|
159
168
|
setStep('processing');
|
|
160
169
|
try {
|
|
161
170
|
const authResult = await client.verifyPhoneNumber(credential, session);
|
package/dist/cjs/adapters/vue.js
CHANGED
|
@@ -109,21 +109,29 @@ function usePhoneAuth(config) {
|
|
|
109
109
|
* Granular: Invoke.
|
|
110
110
|
*/
|
|
111
111
|
async function invokeSecurePrompt(prepared, options) {
|
|
112
|
+
isLoading.value = true;
|
|
113
|
+
error.value = null;
|
|
112
114
|
step.value = 'invoking';
|
|
113
115
|
try {
|
|
114
|
-
|
|
116
|
+
const invokeResult = await client.invokeSecurePrompt(prepared, options);
|
|
117
|
+
step.value = 'idle'; // Reset step on success (invoke is intermediate step)
|
|
118
|
+
return invokeResult;
|
|
115
119
|
}
|
|
116
120
|
catch (err) {
|
|
117
121
|
error.value = err;
|
|
118
122
|
step.value = 'error';
|
|
119
123
|
throw err;
|
|
120
124
|
}
|
|
125
|
+
finally {
|
|
126
|
+
isLoading.value = false;
|
|
127
|
+
}
|
|
121
128
|
}
|
|
122
129
|
/**
|
|
123
130
|
* Granular: Get phone number.
|
|
124
131
|
*/
|
|
125
132
|
async function getPhoneNumber(credential, session) {
|
|
126
133
|
isLoading.value = true;
|
|
134
|
+
error.value = null;
|
|
127
135
|
step.value = 'processing';
|
|
128
136
|
try {
|
|
129
137
|
const authResult = await client.getPhoneNumber(credential, session);
|
|
@@ -145,6 +153,7 @@ function usePhoneAuth(config) {
|
|
|
145
153
|
*/
|
|
146
154
|
async function verifyPhoneNumber(credential, session) {
|
|
147
155
|
isLoading.value = true;
|
|
156
|
+
error.value = null;
|
|
148
157
|
step.value = 'processing';
|
|
149
158
|
try {
|
|
150
159
|
const authResult = await client.verifyPhoneNumber(credential, session);
|
package/dist/cjs/client/http.js
CHANGED
|
@@ -117,7 +117,11 @@ function createHttpClient(config = {}) {
|
|
|
117
117
|
}
|
|
118
118
|
// If response is not ok, parse as error
|
|
119
119
|
if (!response.ok) {
|
|
120
|
-
|
|
120
|
+
// Safely build error object - handle non-object responses (strings, null, arrays)
|
|
121
|
+
const errorData = typeof data === 'object' && data !== null && !Array.isArray(data)
|
|
122
|
+
? { ...data, status: response.status }
|
|
123
|
+
: { status: response.status, rawResponse: data };
|
|
124
|
+
throw (0, errors_1.parseBackendError)(errorData);
|
|
121
125
|
}
|
|
122
126
|
return data;
|
|
123
127
|
}
|
|
@@ -15,12 +15,11 @@ exports.createNoopLogger = createNoopLogger;
|
|
|
15
15
|
// ============================================================================
|
|
16
16
|
/**
|
|
17
17
|
* Patterns to detect and sanitize PII.
|
|
18
|
+
* Note: Email pattern removed - this SDK only handles phone authentication.
|
|
18
19
|
*/
|
|
19
20
|
const PII_PATTERNS = {
|
|
20
21
|
// Phone numbers: +1234567890 or variations
|
|
21
22
|
phone: /(\+?[1-9]\d{6,14})/g,
|
|
22
|
-
// Email addresses
|
|
23
|
-
email: /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
|
|
24
23
|
// JWT tokens (three base64 segments separated by dots)
|
|
25
24
|
jwt: /(eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*)/g,
|
|
26
25
|
// Session keys (long alphanumeric strings)
|
|
@@ -42,11 +41,6 @@ function sanitize(data) {
|
|
|
42
41
|
const visible = 4;
|
|
43
42
|
return match.slice(0, 2) + '***' + match.slice(-visible);
|
|
44
43
|
});
|
|
45
|
-
// Mask emails: user@example.com -> u***@example.com
|
|
46
|
-
sanitized = sanitized.replace(PII_PATTERNS.email, (match) => {
|
|
47
|
-
const [local, domain] = match.split('@');
|
|
48
|
-
return local.slice(0, 1) + '***@' + domain;
|
|
49
|
-
});
|
|
50
44
|
// Mask JWTs: eyJ... -> [JWT]
|
|
51
45
|
sanitized = sanitized.replace(PII_PATTERNS.jwt, '[JWT]');
|
|
52
46
|
// Mask session keys
|
|
@@ -33,6 +33,7 @@ class PhoneAuthClient {
|
|
|
33
33
|
pollingInterval: config.pollingInterval || 2000,
|
|
34
34
|
maxPollingAttempts: config.maxPollingAttempts || 30,
|
|
35
35
|
debug: config.debug || false,
|
|
36
|
+
devEnv: config.devEnv,
|
|
36
37
|
};
|
|
37
38
|
// Use custom or default HTTP client
|
|
38
39
|
this.http = config.httpClient || (0, http_1.createHttpClient)({
|
|
@@ -47,6 +48,7 @@ class PhoneAuthClient {
|
|
|
47
48
|
this.logger.debug('PhoneAuthClient initialized', {
|
|
48
49
|
endpoints: this.config.endpoints,
|
|
49
50
|
timeout: this.config.timeout,
|
|
51
|
+
...(config.devEnv && { devEnv: config.devEnv }),
|
|
50
52
|
});
|
|
51
53
|
// Initialize mobile debug console if configured
|
|
52
54
|
if (config.devtools?.showMobileConsole && typeof window !== 'undefined') {
|
|
@@ -126,6 +128,13 @@ class PhoneAuthClient {
|
|
|
126
128
|
if (!request.use_case && !request.options?.parent_session_id) {
|
|
127
129
|
throw (0, errors_1.createAuthError)(errors_1.ERROR_CODES.MISSING_PARAMETERS, 'use_case is required');
|
|
128
130
|
}
|
|
131
|
+
// Validate use_case requirements (e.g., VerifyPhoneNumber requires phone_number)
|
|
132
|
+
if (request.use_case) {
|
|
133
|
+
const useCaseValidation = (0, core_1.validateUseCaseRequirements)(request.use_case, request.phone_number, !!request.options?.parent_session_id);
|
|
134
|
+
if (!useCaseValidation.valid) {
|
|
135
|
+
throw (0, errors_1.createAuthError)(errors_1.ERROR_CODES.MISSING_PARAMETERS, useCaseValidation.error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
129
138
|
// Build request with client info
|
|
130
139
|
const requestBody = {
|
|
131
140
|
...request,
|
|
@@ -288,6 +297,20 @@ class PhoneAuthClient {
|
|
|
288
297
|
if (!credential || (typeof credential === 'string' && credential.trim() === '')) {
|
|
289
298
|
throw (0, errors_1.createAuthError)(errors_1.ERROR_CODES.INVALID_RESPONSE, 'Empty credential returned from Digital Credentials API');
|
|
290
299
|
}
|
|
300
|
+
// Success haptic feedback
|
|
301
|
+
if (typeof navigator !== 'undefined' && navigator.vibrate) {
|
|
302
|
+
// Add a small delay to ensure the browser has regained focus/visibility
|
|
303
|
+
// after the native bottom sheet closes
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
try {
|
|
306
|
+
// Double tap pattern: vibrate 80ms, pause 50ms, vibrate 80ms
|
|
307
|
+
navigator.vibrate([80, 50, 80]);
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
// Ignore vibration errors
|
|
311
|
+
}
|
|
312
|
+
}, 200);
|
|
313
|
+
}
|
|
291
314
|
// Return just the credential string
|
|
292
315
|
return credential;
|
|
293
316
|
}
|
|
@@ -328,11 +351,16 @@ class PhoneAuthClient {
|
|
|
328
351
|
// Navigate to App Clip URL (must be from user gesture)
|
|
329
352
|
// Using window.location.href for better iOS compatibility
|
|
330
353
|
window.location.href = data.url;
|
|
331
|
-
// Build polling headers (merge common + polling-specific)
|
|
354
|
+
// Build polling headers (merge common + polling-specific + devEnv)
|
|
332
355
|
const pollingHeaders = {
|
|
333
356
|
...this.config.headers?.common,
|
|
334
357
|
...this.config.headers?.polling,
|
|
335
358
|
};
|
|
359
|
+
// Add developer header if devEnv is set
|
|
360
|
+
if (this.config.devEnv) {
|
|
361
|
+
pollingHeaders['developer'] = this.config.devEnv;
|
|
362
|
+
this.logger.debug('Adding developer header for polling', { devEnv: this.config.devEnv });
|
|
363
|
+
}
|
|
336
364
|
// Start polling for authentication status
|
|
337
365
|
const polling = (0, polling_1.createPollingHandler)({
|
|
338
366
|
sessionKey: session.session_key,
|
|
@@ -364,11 +392,16 @@ class PhoneAuthClient {
|
|
|
364
392
|
// Extract session ID for polling
|
|
365
393
|
const sessionId = data.data?.session_id || session.session_key;
|
|
366
394
|
this.logger.debug('Starting desktop authentication', { sessionId });
|
|
367
|
-
// Build polling headers (merge common + polling-specific)
|
|
395
|
+
// Build polling headers (merge common + polling-specific + devEnv)
|
|
368
396
|
const pollingHeaders = {
|
|
369
397
|
...this.config.headers?.common,
|
|
370
398
|
...this.config.headers?.polling,
|
|
371
399
|
};
|
|
400
|
+
// Add developer header if devEnv is set
|
|
401
|
+
if (this.config.devEnv) {
|
|
402
|
+
pollingHeaders['developer'] = this.config.devEnv;
|
|
403
|
+
this.logger.debug('Adding developer header for polling', { devEnv: this.config.devEnv });
|
|
404
|
+
}
|
|
372
405
|
// Start polling for authentication status
|
|
373
406
|
const polling = (0, polling_1.createPollingHandler)({
|
|
374
407
|
sessionKey: sessionId,
|
package/dist/cjs/core/index.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.serializeError = exports.parseBackendError = exports.createAuthErrorWithContext = exports.createAuthError = exports.getUserMessage = exports.isRetryableError = exports.isClientError = exports.isAuthError = exports.isErrorResponse = exports.isVerifyPhoneNumberResponse = exports.isGetPhoneNumberResponse = exports.getStrategyData = exports.isDesktopData = exports.isLinkData = exports.isTS43Data = exports.isCancellable = exports.isDesktopStrategy = exports.isLinkStrategy = exports.isTS43Strategy = exports.isAuthCredential = exports.isInvokeResult = exports.E164_REGEX = exports.validateSessionKey = exports.
|
|
26
|
+
exports.serializeError = exports.parseBackendError = exports.createAuthErrorWithContext = exports.createAuthError = exports.getUserMessage = exports.isRetryableError = exports.isClientError = exports.isAuthError = exports.isErrorResponse = exports.isVerifyPhoneNumberResponse = exports.isGetPhoneNumberResponse = exports.getStrategyData = exports.isDesktopData = exports.isLinkData = exports.isTS43Data = exports.isCancellable = exports.isDesktopStrategy = exports.isLinkStrategy = exports.isTS43Strategy = exports.isAuthCredential = exports.isInvokeResult = exports.E164_REGEX = exports.validateSessionKey = exports.validateUseCaseRequirements = exports.validatePlmn = exports.validatePhoneNumber = exports.ERROR_CODES = exports.AUTHENTICATION_STRATEGY = exports.USE_CASE = void 0;
|
|
27
27
|
// ============================================================================
|
|
28
28
|
// CONSTANTS
|
|
29
29
|
// ============================================================================
|
|
@@ -39,7 +39,6 @@ var validators_1 = require("./validators");
|
|
|
39
39
|
Object.defineProperty(exports, "validatePhoneNumber", { enumerable: true, get: function () { return validators_1.validatePhoneNumber; } });
|
|
40
40
|
Object.defineProperty(exports, "validatePlmn", { enumerable: true, get: function () { return validators_1.validatePlmn; } });
|
|
41
41
|
Object.defineProperty(exports, "validateUseCaseRequirements", { enumerable: true, get: function () { return validators_1.validateUseCaseRequirements; } });
|
|
42
|
-
Object.defineProperty(exports, "validateNonce", { enumerable: true, get: function () { return validators_1.validateNonce; } });
|
|
43
42
|
Object.defineProperty(exports, "validateSessionKey", { enumerable: true, get: function () { return validators_1.validateSessionKey; } });
|
|
44
43
|
Object.defineProperty(exports, "E164_REGEX", { enumerable: true, get: function () { return validators_1.E164_REGEX; } });
|
|
45
44
|
// ============================================================================
|
|
@@ -143,12 +143,24 @@ function isLinkData(data) {
|
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
145
|
* Check if prepare response data is DesktopData.
|
|
146
|
+
*
|
|
147
|
+
* Note: Both TS43Data and DesktopData have nested 'data' objects,
|
|
148
|
+
* so we must explicitly exclude TS43Data by checking for absence of 'dcql_query'.
|
|
146
149
|
*/
|
|
147
150
|
function isDesktopData(data) {
|
|
148
|
-
|
|
149
|
-
typeof data
|
|
150
|
-
'data' in data
|
|
151
|
-
typeof data.data
|
|
151
|
+
if (data === null ||
|
|
152
|
+
typeof data !== 'object' ||
|
|
153
|
+
!('data' in data) ||
|
|
154
|
+
typeof data.data !== 'object' ||
|
|
155
|
+
data.data === null) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
// Exclude TS43Data which also has a nested 'data' object but contains 'dcql_query'
|
|
159
|
+
const nestedData = data.data;
|
|
160
|
+
if ('dcql_query' in nestedData) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
152
164
|
}
|
|
153
165
|
/**
|
|
154
166
|
* Get strategy-specific data from PrepareResponse with proper typing.
|
|
@@ -10,7 +10,6 @@ exports.E164_REGEX = void 0;
|
|
|
10
10
|
exports.validatePhoneNumber = validatePhoneNumber;
|
|
11
11
|
exports.validatePlmn = validatePlmn;
|
|
12
12
|
exports.validateUseCaseRequirements = validateUseCaseRequirements;
|
|
13
|
-
exports.validateNonce = validateNonce;
|
|
14
13
|
exports.validateSessionKey = validateSessionKey;
|
|
15
14
|
const types_1 = require("./types");
|
|
16
15
|
/** E.164 phone number regex */
|
|
@@ -114,34 +113,6 @@ function validateUseCaseRequirements(useCase, phoneNumber, hasParentSessionId) {
|
|
|
114
113
|
}
|
|
115
114
|
return { valid: true };
|
|
116
115
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Validates nonce format.
|
|
119
|
-
*
|
|
120
|
-
* @param nonce - Nonce string to validate
|
|
121
|
-
* @returns Validation result
|
|
122
|
-
*/
|
|
123
|
-
function validateNonce(nonce) {
|
|
124
|
-
const base64urlRegex = /^[A-Za-z0-9_-]+$/;
|
|
125
|
-
if (!nonce || nonce.length === 0) {
|
|
126
|
-
return {
|
|
127
|
-
valid: false,
|
|
128
|
-
error: 'Nonce is required'
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (!base64urlRegex.test(nonce)) {
|
|
132
|
-
return {
|
|
133
|
-
valid: false,
|
|
134
|
-
error: 'Nonce must be base64url encoded'
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (nonce.length < 32 || nonce.length > 128) {
|
|
138
|
-
return {
|
|
139
|
-
valid: false,
|
|
140
|
-
error: 'Nonce must be between 32 and 128 characters'
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
return { valid: true };
|
|
144
|
-
}
|
|
145
116
|
/**
|
|
146
117
|
* Validates session key format.
|
|
147
118
|
*
|
package/dist/cjs/index.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.createQRCodeDataFromDesktop = exports.AuthModal = exports.createNoopLogger = exports.createLogger = exports.createHttpClient = exports.E164_REGEX = exports.validateSessionKey = exports.
|
|
32
|
+
exports.createQRCodeDataFromDesktop = exports.AuthModal = exports.createNoopLogger = exports.createLogger = exports.createHttpClient = exports.E164_REGEX = exports.validateSessionKey = exports.validateUseCaseRequirements = exports.validatePlmn = exports.validatePhoneNumber = exports.isErrorResponse = exports.isVerifyPhoneNumberResponse = exports.isGetPhoneNumberResponse = exports.isDesktopData = exports.isLinkData = exports.isTS43Data = exports.isCancellable = exports.isDesktopStrategy = exports.isLinkStrategy = exports.isTS43Strategy = exports.isAuthCredential = exports.isInvokeResult = exports.parseBackendError = exports.createAuthError = exports.getUserMessage = exports.isRetryableError = exports.isClientError = exports.isAuthError = exports.ERROR_CODES = exports.AUTHENTICATION_STRATEGY = exports.USE_CASE = exports.PhoneAuthClient = void 0;
|
|
33
33
|
// Main client
|
|
34
34
|
var phone_auth_client_1 = require("./client/phone-auth-client");
|
|
35
35
|
Object.defineProperty(exports, "PhoneAuthClient", { enumerable: true, get: function () { return phone_auth_client_1.PhoneAuthClient; } });
|
|
@@ -66,7 +66,6 @@ var validators_1 = require("./core/validators");
|
|
|
66
66
|
Object.defineProperty(exports, "validatePhoneNumber", { enumerable: true, get: function () { return validators_1.validatePhoneNumber; } });
|
|
67
67
|
Object.defineProperty(exports, "validatePlmn", { enumerable: true, get: function () { return validators_1.validatePlmn; } });
|
|
68
68
|
Object.defineProperty(exports, "validateUseCaseRequirements", { enumerable: true, get: function () { return validators_1.validateUseCaseRequirements; } });
|
|
69
|
-
Object.defineProperty(exports, "validateNonce", { enumerable: true, get: function () { return validators_1.validateNonce; } });
|
|
70
69
|
Object.defineProperty(exports, "validateSessionKey", { enumerable: true, get: function () { return validators_1.validateSessionKey; } });
|
|
71
70
|
Object.defineProperty(exports, "E164_REGEX", { enumerable: true, get: function () { return validators_1.E164_REGEX; } });
|
|
72
71
|
// Client utilities (for advanced use)
|