@glideidentity/web-client-sdk 4.4.8-beta.2 → 4.4.8

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.
@@ -170,6 +170,10 @@ export class PhoneAuthClient {
170
170
  }
171
171
  // Step 3: Process the response through appropriate endpoint
172
172
  const credential = credentialResponse;
173
+ // Validate use_case is provided for endpoint selection
174
+ if (!options.use_case) {
175
+ throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
176
+ }
173
177
  const result = options.use_case === API.USE_CASE.GET_PHONE_NUMBER
174
178
  ? yield this.getPhoneNumber(credential, preparedRequest.session)
175
179
  : yield this.verifyPhoneNumber(credential, preparedRequest.session);
@@ -278,16 +282,7 @@ export class PhoneAuthClient {
278
282
  */
279
283
  preparePhoneRequest(options) {
280
284
  return __awaiter(this, void 0, void 0, function* () {
281
- // Validate use case requirements first
282
- // const useCaseValidation = validateUseCaseRequirements(options.use_case, options.phone_number);
283
- // if (!useCaseValidation.valid) {
284
- // throw this.createError(
285
- // PhoneAuthErrorCode.BAD_REQUEST,
286
- // useCaseValidation.error!,
287
- // { field: 'use_case' }
288
- // );
289
- // }
290
- var _a, _b;
285
+ var _a, _b, _c;
291
286
  // Validate phone number if provided
292
287
  if (options.phone_number) {
293
288
  const phoneValidation = validatePhoneNumber(options.phone_number);
@@ -309,8 +304,12 @@ export class PhoneAuthClient {
309
304
  throw this.createError(PhoneAuthErrorCode.BAD_REQUEST, consentValidation.error, { field: 'consent_data' });
310
305
  }
311
306
  }
307
+ // Validate use_case is provided (unless only parent_session_id is given)
308
+ if (!options.use_case && !(((_a = options.options) === null || _a === void 0 ? void 0 : _a.parent_session_id) && !options.phone_number && !options.plmn)) {
309
+ throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
310
+ }
312
311
  // Validate required parameters based on use case
313
- if (!options.phone_number && !options.plmn && !((_a = options.options) === null || _a === void 0 ? void 0 : _a.parent_session_id)) {
312
+ if (!options.phone_number && !options.plmn && !((_b = options.options) === null || _b === void 0 ? void 0 : _b.parent_session_id)) {
314
313
  // Provide specific error message based on use case
315
314
  if (options.use_case === API.USE_CASE.GET_PHONE_NUMBER) {
316
315
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'PLMN (MCC/MNC) is required for GetPhoneNumber. Please provide carrier network information.', { field: 'plmn', useCase: 'GetPhoneNumber' });
@@ -318,19 +317,15 @@ export class PhoneAuthClient {
318
317
  else if (options.use_case === API.USE_CASE.VERIFY_PHONE_NUMBER) {
319
318
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'Phone number is required for VerifyPhoneNumber', { field: 'phoneNumber', useCase: 'VerifyPhoneNumber' });
320
319
  }
321
- else if (!options.use_case) {
322
- // If no use case, that's an error
323
- throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'use_case is required', { field: 'use_case' });
324
- }
325
320
  else {
326
321
  // Fallback for other use cases
327
322
  throw this.createError(PhoneAuthErrorCode.MISSING_PARAMETERS, 'Either phone number or PLMN (MCC/MNC) must be provided', { field: 'phoneNumber,plmn' });
328
323
  }
329
324
  }
330
- // If only parent_session_id is provided, use_case is optional (will be inherited)
331
- if (((_b = options.options) === null || _b === void 0 ? void 0 : _b.parent_session_id) && !options.phone_number && !options.plmn && !options.use_case) {
325
+ // Log parent session usage
326
+ if (((_c = options.options) === null || _c === void 0 ? void 0 : _c.parent_session_id) && !options.phone_number && !options.plmn) {
332
327
  if (this.debug) {
333
- console.log('[PhoneAuth] Using parent_session_id only - all fields will be inherited from parent session');
328
+ console.log('[PhoneAuth] Using parent_session_id: %s, use_case: %s', options.options.parent_session_id, options.use_case || 'not provided');
334
329
  }
335
330
  }
336
331
  const requestId = `web-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
@@ -345,11 +340,13 @@ export class PhoneAuthClient {
345
340
  throw this.createError(PhoneAuthErrorCode.INTERNAL_SERVER_ERROR, 'Failed to generate valid nonce', { field: 'nonce' });
346
341
  }
347
342
  // Build properly typed request body according to API specification
343
+ // Be permissive - backend will ignore extra fields if not needed
348
344
  const requestBody = {
349
- // Required fields
345
+ // Include use_case if provided (optional when parent_session_id is given)
350
346
  use_case: options.use_case,
351
- // One of these is required
347
+ // Include phone_number if provided (backend ignores if not needed)
352
348
  phone_number: options.phone_number,
349
+ // Include PLMN if provided
353
350
  plmn: options.plmn ? {
354
351
  mcc: options.plmn.mcc,
355
352
  mnc: options.plmn.mnc
@@ -381,7 +378,7 @@ export class PhoneAuthClient {
381
378
  // Always include the HTTP status from the response
382
379
  errorDetails = Object.assign(Object.assign({}, errorDetails), { status: response.status });
383
380
  }
384
- catch (_c) {
381
+ catch (_d) {
385
382
  // If JSON parsing fails, use status text
386
383
  errorDetails = { status: response.status, statusText: response.statusText };
387
384
  }
@@ -478,7 +475,7 @@ export class PhoneAuthClient {
478
475
  // This ensures we work with plain objects for browser APIs
479
476
  // Vue's reactivity system wraps objects in Proxies which can interfere
480
477
  // with browser APIs like Digital Credentials API that expect plain objects
481
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
478
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
482
479
  // Try structuredClone first (modern browsers), but catch errors and fallback to JSON method
483
480
  let plainResponse;
484
481
  try {
@@ -649,12 +646,15 @@ export class PhoneAuthClient {
649
646
  });
650
647
  // TS43 always auto-triggers (no SDK UI ever)
651
648
  // The Digital Credentials API provides its own OS-level UI
652
- let credentialPromise;
649
+ // Use a wrapper object to allow updating the promise reference
650
+ const credentialWrapper = {
651
+ promise: null
652
+ };
653
653
  try {
654
654
  // Always try to trigger immediately
655
655
  const vpToken = yield enhancedTriggerTS43();
656
656
  // Convert to AuthCredential format
657
- credentialPromise = Promise.resolve({
657
+ credentialWrapper.promise = Promise.resolve({
658
658
  credential: typeof vpToken === 'string' ? vpToken : Object.values(vpToken)[0],
659
659
  session: plainResponse.session,
660
660
  authenticated: true
@@ -662,36 +662,46 @@ export class PhoneAuthClient {
662
662
  }
663
663
  catch (error) {
664
664
  // If auto-trigger fails, create a rejected promise
665
- credentialPromise = Promise.reject(error);
665
+ credentialWrapper.promise = Promise.reject(error);
666
666
  }
667
667
  // Handle based on execution mode
668
668
  if (executionMode === 'extended') {
669
669
  // Extended mode - return control methods
670
- return {
670
+ const response = {
671
671
  strategy: 'ts43',
672
672
  session: plainResponse.session,
673
- credential: credentialPromise,
673
+ credential: credentialWrapper.promise, // Initial value
674
674
  // Re-trigger credential request
675
675
  trigger: () => __awaiter(this, void 0, void 0, function* () {
676
676
  const vpToken = yield enhancedTriggerTS43();
677
- // Update the credential promise
678
- credentialPromise = Promise.resolve({
677
+ // Update the credential promise in the wrapper
678
+ credentialWrapper.promise = Promise.resolve({
679
679
  credential: typeof vpToken === 'string' ? vpToken : Object.values(vpToken)[0],
680
680
  session: plainResponse.session,
681
681
  authenticated: true
682
682
  });
683
+ // Return void as per interface
683
684
  }),
684
685
  cancel: () => {
685
686
  // TS43 doesn't have a way to cancel once triggered
686
687
  // but we can reject the promise
687
- credentialPromise = Promise.reject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Authentication cancelled'));
688
+ credentialWrapper.promise = Promise.reject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Authentication cancelled'));
688
689
  }
689
690
  };
691
+ // Define credential as a getter that always returns the current promise
692
+ Object.defineProperty(response, 'credential', {
693
+ get() {
694
+ return credentialWrapper.promise;
695
+ },
696
+ enumerable: true,
697
+ configurable: true
698
+ });
699
+ return response;
690
700
  }
691
701
  else {
692
702
  // Standard mode - just return credential
693
703
  // Wait for and return the credential
694
- const credential = yield credentialPromise;
704
+ const credential = yield credentialWrapper.promise;
695
705
  // Return in standard format
696
706
  return {
697
707
  [plainResponse.session.session_key]: credential.credential
@@ -721,7 +731,7 @@ export class PhoneAuthClient {
721
731
  const pollingEndpointToUse = (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingEndpoint) ||
722
732
  (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.pollingEndpoint) ||
723
733
  ((_m = this.config.endpoints) === null || _m === void 0 ? void 0 : _m.polling);
724
- const pollingOptions = Object.assign(Object.assign({}, desktopOptions), { pollingEndpoint: pollingEndpointToUse, pollingInterval: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingInterval) || (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.pollingInterval) || this.config.pollingInterval || 2000, maxPollingAttempts: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.maxPollingAttempts) || (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.maxPollingAttempts) || this.config.maxPollingAttempts || 150, onQRCodeReady: undefined, onStatusUpdate: undefined });
734
+ const pollingOptions = Object.assign(Object.assign({}, desktopOptions), { pollingEndpoint: pollingEndpointToUse, pollingInterval: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingInterval) || (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.pollingInterval) || this.config.pollingInterval || 2000, maxPollingAttempts: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.maxPollingAttempts) || (desktopOptions === null || desktopOptions === void 0 ? void 0 : desktopOptions.maxPollingAttempts) || this.config.maxPollingAttempts || 30, onQRCodeReady: undefined, onStatusUpdate: undefined });
725
735
  // Decide whether to show modal based on preventDefaultUI
726
736
  const showModal = !preventDefaultUI;
727
737
  let modal;
@@ -784,31 +794,79 @@ export class PhoneAuthClient {
784
794
  // Handle based on execution mode
785
795
  if (executionMode === 'extended') {
786
796
  // Extended mode - return control methods
787
- const credentialPromise = showModal ? startPolling() : Promise.resolve({
788
- credential: '',
789
- session: plainResponse.session,
790
- authenticated: false
797
+ let credentialResolve = null;
798
+ let credentialReject = null;
799
+ let pollingStarted = false;
800
+ // Create a promise that will remain pending until polling starts
801
+ const credentialPromise = new Promise((resolve, reject) => {
802
+ credentialResolve = resolve;
803
+ credentialReject = reject;
804
+ // If modal is shown, start polling immediately
805
+ if (showModal) {
806
+ pollingStarted = true;
807
+ startPolling()
808
+ .then(resolve)
809
+ .catch(reject);
810
+ }
811
+ // Otherwise, promise remains pending until start_polling() is called
791
812
  });
792
- return {
813
+ // Create wrapped functions
814
+ const wrappedStartPolling = () => __awaiter(this, void 0, void 0, function* () {
815
+ if (pollingStarted) {
816
+ throw this.createError(PhoneAuthErrorCode.INVALID_SESSION_STATE, 'Polling has already been started');
817
+ }
818
+ pollingStarted = true;
819
+ try {
820
+ const result = yield startPolling();
821
+ if (credentialResolve) {
822
+ credentialResolve(result);
823
+ }
824
+ return result;
825
+ }
826
+ catch (err) {
827
+ if (credentialReject) {
828
+ credentialReject(err);
829
+ }
830
+ throw err;
831
+ }
832
+ });
833
+ const wrappedStopPolling = () => {
834
+ handler.cleanup();
835
+ if (modal)
836
+ modal.close();
837
+ };
838
+ const wrappedCancel = () => {
839
+ handler.cancel();
840
+ handler.cleanup();
841
+ if (modal)
842
+ modal.close();
843
+ // Reject the credential promise if it's still pending
844
+ if (!pollingStarted && credentialReject) {
845
+ credentialReject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Desktop authentication cancelled by user'));
846
+ }
847
+ };
848
+ // Create the response object with a getter for is_polling
849
+ const response = {
793
850
  strategy: 'desktop',
794
851
  session: plainResponse.session,
795
852
  credential: credentialPromise,
796
853
  qr_code_data: qrCodeDataSnakeCase,
797
854
  modal_ref: modalRef,
798
- start_polling: startPolling,
799
- stop_polling: () => {
800
- handler.cleanup();
801
- if (modal)
802
- modal.close();
803
- },
804
- cancel: () => {
805
- handler.cancel();
806
- handler.cleanup();
807
- if (modal)
808
- modal.close();
809
- },
810
- is_polling: showModal // If modal shown, polling already started
855
+ start_polling: wrappedStartPolling,
856
+ stop_polling: wrappedStopPolling,
857
+ cancel: wrappedCancel,
858
+ // This will be replaced with a getter
859
+ is_polling: false // Initial value, will be overridden by getter
811
860
  };
861
+ // Define is_polling as a getter that returns current state
862
+ Object.defineProperty(response, 'is_polling', {
863
+ get() {
864
+ return handler.isPolling();
865
+ },
866
+ enumerable: true,
867
+ configurable: true
868
+ });
869
+ return response;
812
870
  }
813
871
  else {
814
872
  // Standard mode - return credential when complete
@@ -864,30 +922,70 @@ export class PhoneAuthClient {
864
922
  triggerLink();
865
923
  }
866
924
  // Start polling in the background
925
+ console.log('[PhoneAuth Client] Link polling config:', {
926
+ fromInvokeOptions: invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingEndpoint,
927
+ fromClientConfig: (_o = this.config.endpoints) === null || _o === void 0 ? void 0 : _o.polling,
928
+ finalPollingEndpoint: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingEndpoint) || ((_p = this.config.endpoints) === null || _p === void 0 ? void 0 : _p.polling)
929
+ });
867
930
  const pollingOptions = {
868
- pollingEndpoint: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingEndpoint) || ((_o = this.config.endpoints) === null || _o === void 0 ? void 0 : _o.polling),
931
+ pollingEndpoint: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingEndpoint) || ((_q = this.config.endpoints) === null || _q === void 0 ? void 0 : _q.polling),
869
932
  pollingInterval: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.pollingInterval) || this.config.pollingInterval || 2000,
870
- maxPollingAttempts: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.maxPollingAttempts) || this.config.maxPollingAttempts || 150,
933
+ maxPollingAttempts: (invokeOptions === null || invokeOptions === void 0 ? void 0 : invokeOptions.maxPollingAttempts) || this.config.maxPollingAttempts || 30,
871
934
  onLinkOpened: undefined,
872
935
  onStatusUpdate: undefined
873
936
  };
874
- // Create credential promise
875
- const credentialPromise = handler.invoke(plainResponse, pollingOptions).then(result => {
876
- if (result.authenticated && result.credential) {
877
- return {
878
- credential: result.credential,
879
- session: plainResponse.session,
880
- authenticated: true
881
- };
882
- }
883
- else {
884
- throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
885
- }
886
- });
937
+ console.log('[PhoneAuth Client] Final Link polling options:', pollingOptions);
887
938
  // Handle based on execution mode
888
939
  if (executionMode === 'extended') {
889
- // Extended mode - return control methods
890
- return {
940
+ // Extended mode - return control methods without starting polling immediately
941
+ let credentialResolve = null;
942
+ let credentialReject = null;
943
+ let pollingStarted = false;
944
+ let pollingPromise = null;
945
+ // Create a pending promise that will be resolved when polling completes
946
+ const credentialPromise = new Promise((resolve, reject) => {
947
+ credentialResolve = resolve;
948
+ credentialReject = reject;
949
+ });
950
+ // Function to start polling (only once)
951
+ const startPolling = () => __awaiter(this, void 0, void 0, function* () {
952
+ if (pollingStarted) {
953
+ throw this.createError(PhoneAuthErrorCode.INVALID_SESSION_STATE, 'Polling has already been started');
954
+ }
955
+ pollingStarted = true;
956
+ try {
957
+ // Store the polling promise to ensure we use the same one
958
+ if (!pollingPromise) {
959
+ pollingPromise = handler.invoke(plainResponse, pollingOptions);
960
+ }
961
+ const result = yield pollingPromise;
962
+ if (result.authenticated && result.credential) {
963
+ const authCredential = {
964
+ credential: result.credential,
965
+ session: plainResponse.session,
966
+ authenticated: true
967
+ };
968
+ if (credentialResolve) {
969
+ credentialResolve(authCredential);
970
+ }
971
+ return authCredential;
972
+ }
973
+ else {
974
+ const error = this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
975
+ if (credentialReject) {
976
+ credentialReject(error);
977
+ }
978
+ throw error;
979
+ }
980
+ }
981
+ catch (err) {
982
+ if (credentialReject) {
983
+ credentialReject(err);
984
+ }
985
+ throw err;
986
+ }
987
+ });
988
+ const response = {
891
989
  strategy: 'link',
892
990
  session: plainResponse.session,
893
991
  credential: credentialPromise,
@@ -896,22 +994,44 @@ export class PhoneAuthClient {
896
994
  },
897
995
  // Re-open the app link
898
996
  trigger: triggerLink,
899
- // Polling control
900
- start_polling: () => handler.invoke(plainResponse, pollingOptions).then(result => ({
901
- credential: result.credential || '',
902
- session: plainResponse.session,
903
- authenticated: result.authenticated
904
- })),
997
+ // Polling control - now prevents double polling
998
+ start_polling: startPolling,
905
999
  stop_polling: () => handler.cleanup(),
906
1000
  cancel: () => {
907
1001
  handler.cancel();
908
1002
  handler.cleanup();
1003
+ // Reject the credential promise if polling hasn't started
1004
+ if (!pollingStarted && credentialReject) {
1005
+ credentialReject(this.createError(PhoneAuthErrorCode.USER_DENIED, 'Link authentication cancelled by user'));
1006
+ }
909
1007
  },
910
- is_polling: true
1008
+ // This will be replaced with a getter
1009
+ is_polling: false // Initial value, will be overridden by getter
911
1010
  };
1011
+ // Define is_polling as a getter that returns current state
1012
+ Object.defineProperty(response, 'is_polling', {
1013
+ get() {
1014
+ return handler.isPolling();
1015
+ },
1016
+ enumerable: true,
1017
+ configurable: true
1018
+ });
1019
+ return response;
912
1020
  }
913
1021
  else {
914
- // Standard mode - return credential when complete
1022
+ // Standard mode - start polling immediately and wait for completion
1023
+ const credentialPromise = handler.invoke(plainResponse, pollingOptions).then(result => {
1024
+ if (result.authenticated && result.credential) {
1025
+ return {
1026
+ credential: result.credential,
1027
+ session: plainResponse.session,
1028
+ authenticated: true
1029
+ };
1030
+ }
1031
+ else {
1032
+ throw this.createError(PhoneAuthErrorCode.USER_DENIED, result.error || 'Link authentication failed');
1033
+ }
1034
+ });
915
1035
  // Wait for credential and return in standard format
916
1036
  const credential = yield credentialPromise;
917
1037
  const aggregatorId = this.config.aggregatorId || 'default';
@@ -17,7 +17,7 @@ export interface QRCodeData {
17
17
  export interface DesktopAuthOptions {
18
18
  /** Custom polling interval in milliseconds (overrides server-provided value) */
19
19
  pollingInterval?: number;
20
- /** Maximum polling attempts before timeout (default: 150 = 5 minutes with 2s interval) */
20
+ /** Maximum polling attempts before timeout (default: 30 = 1 minute with 2s interval) */
21
21
  maxPollingAttempts?: number;
22
22
  /** Custom polling endpoint (overrides backend-provided or uses configured endpoint) */
23
23
  pollingEndpoint?: string;
@@ -56,6 +56,7 @@ export declare class DesktopHandler implements StrategyHandler {
56
56
  private isCancelled;
57
57
  private onCancel?;
58
58
  private pollingReject?;
59
+ private isPollingInProgress;
59
60
  /**
60
61
  * Maps backend HTTP status codes to client status
61
62
  * @param httpStatus HTTP status code from backend
@@ -76,6 +77,10 @@ export declare class DesktopHandler implements StrategyHandler {
76
77
  * Stop polling
77
78
  */
78
79
  private stopPolling;
80
+ /**
81
+ * Check if polling is currently active
82
+ */
83
+ isPolling(): boolean;
79
84
  /**
80
85
  * Format response for backend processing
81
86
  * Desktop strategy typically returns the credential from mobile authentication
@@ -98,7 +103,7 @@ export declare class DesktopHandler implements StrategyHandler {
98
103
  /**
99
104
  * Helper function to display QR code in a modal or inline
100
105
  */
101
- export declare function createQRCodeDisplay(qrCodeData: string, options?: {
106
+ export declare function createQRCodeDisplay(qrCodeData: string | QRCodeData, options?: {
102
107
  container?: HTMLElement;
103
108
  size?: number;
104
109
  title?: string;
@@ -106,8 +111,9 @@ export declare function createQRCodeDisplay(qrCodeData: string, options?: {
106
111
  }): HTMLElement;
107
112
  /**
108
113
  * Helper function to create a modal for QR code display
114
+ * Supports both string QR codes and QRCodeData objects for dual-platform support
109
115
  */
110
- export declare function showQRCodeModal(qrCodeData: string, options?: {
116
+ export declare function showQRCodeModal(qrCodeData: string | QRCodeData, options?: {
111
117
  title?: string;
112
118
  description?: string;
113
119
  onClose?: () => void;
@@ -16,6 +16,7 @@ export class DesktopHandler {
16
16
  constructor() {
17
17
  this.pollingAttempts = 0;
18
18
  this.isCancelled = false;
19
+ this.isPollingInProgress = false;
19
20
  }
20
21
  /**
21
22
  * Maps backend HTTP status codes to client status
@@ -58,8 +59,8 @@ export class DesktopHandler {
58
59
  const innerData = desktopData.data;
59
60
  // Try to extract from inner data
60
61
  // Support both single QR (qr_code_image) and dual-platform QR (ios/android)
61
- if (innerData.ios_qr_image || innerData.android_qr_image) {
62
- // Dual-platform QR format
62
+ if (innerData.ios_qr_image && innerData.android_qr_image) {
63
+ // Dual-platform QR format - both must be present
63
64
  qrCode = {
64
65
  iosQRCode: innerData.ios_qr_image,
65
66
  androidQRCode: innerData.android_qr_image,
@@ -67,6 +68,10 @@ export class DesktopHandler {
67
68
  androidUrl: innerData.android_url
68
69
  };
69
70
  }
71
+ else if (innerData.ios_qr_image || innerData.android_qr_image) {
72
+ // Only one platform QR - use as single QR format
73
+ qrCode = innerData.ios_qr_image || innerData.android_qr_image;
74
+ }
70
75
  else {
71
76
  // Single QR format (legacy)
72
77
  qrCode = innerData.qr_code_image || innerData.qr_code;
@@ -117,7 +122,7 @@ export class DesktopHandler {
117
122
  console.log('[Desktop QR] Selected endpoint:', finalPollingEndpoint, 'from source:', endpointSource);
118
123
  // Start polling for authentication status
119
124
  const finalPollingInterval = (options === null || options === void 0 ? void 0 : options.pollingInterval) || pollingInterval || 2000;
120
- const maxAttempts = (options === null || options === void 0 ? void 0 : options.maxPollingAttempts) || 150; // Default to 5 minutes
125
+ const maxAttempts = (options === null || options === void 0 ? void 0 : options.maxPollingAttempts) || 30; // Default to 1 minute
121
126
  console.log(`[Desktop QR] Starting polling - endpoint source: ${endpointSource}, interval: ${finalPollingInterval}ms, max attempts: ${maxAttempts}`);
122
127
  return this.startPolling(finalPollingEndpoint || '', // Pass empty string if undefined, will use fallback
123
128
  sessionId, finalPollingInterval, maxAttempts, expiresIn || 300, // Default 5 minutes expiry
@@ -136,7 +141,12 @@ export class DesktopHandler {
136
141
  // Store the reject function so we can call it from cancel()
137
142
  this.pollingReject = reject;
138
143
  const poll = () => __awaiter(this, void 0, void 0, function* () {
144
+ // Skip if another poll is already in progress
145
+ if (this.isPollingInProgress) {
146
+ return;
147
+ }
139
148
  try {
149
+ this.isPollingInProgress = true;
140
150
  // Check if cancelled
141
151
  if (this.isCancelled) {
142
152
  this.stopPolling();
@@ -360,6 +370,10 @@ export class DesktopHandler {
360
370
  });
361
371
  }
362
372
  }
373
+ finally {
374
+ // Always clear the polling flag when done
375
+ this.isPollingInProgress = false;
376
+ }
363
377
  });
364
378
  // Start initial poll
365
379
  poll();
@@ -378,8 +392,15 @@ export class DesktopHandler {
378
392
  this.pollingIntervalId = undefined;
379
393
  }
380
394
  this.pollingAttempts = 0;
395
+ this.isPollingInProgress = false;
381
396
  // Don't clear pollingReject here - it's needed by cancel()
382
397
  }
398
+ /**
399
+ * Check if polling is currently active
400
+ */
401
+ isPolling() {
402
+ return this.pollingIntervalId !== undefined;
403
+ }
383
404
  /**
384
405
  * Format response for backend processing
385
406
  * Desktop strategy typically returns the credential from mobile authentication
@@ -437,9 +458,30 @@ export class DesktopHandler {
437
458
  export function createQRCodeDisplay(qrCodeData, options) {
438
459
  const container = (options === null || options === void 0 ? void 0 : options.container) || document.createElement('div');
439
460
  container.className = 'phone-auth-qr-container';
461
+ // Determine QR code to display
462
+ let qrCodeSrc;
463
+ if (typeof qrCodeData === 'string') {
464
+ // Simple string QR code
465
+ qrCodeSrc = qrCodeData;
466
+ }
467
+ else if (qrCodeData && typeof qrCodeData === 'object') {
468
+ // QRCodeData object - prefer iOS QR if available
469
+ if (qrCodeData.iosQRCode) {
470
+ qrCodeSrc = qrCodeData.iosQRCode;
471
+ }
472
+ else if (qrCodeData.androidQRCode) {
473
+ qrCodeSrc = qrCodeData.androidQRCode;
474
+ }
475
+ else {
476
+ throw new Error('Invalid QRCodeData: missing QR code images');
477
+ }
478
+ }
479
+ else {
480
+ throw new Error('Invalid qrCodeData: must be string or QRCodeData object');
481
+ }
440
482
  // Create QR code image
441
483
  const img = document.createElement('img');
442
- img.src = qrCodeData;
484
+ img.src = qrCodeSrc;
443
485
  img.alt = 'QR Code for authentication';
444
486
  img.style.width = `${(options === null || options === void 0 ? void 0 : options.size) || 256}px`;
445
487
  img.style.height = `${(options === null || options === void 0 ? void 0 : options.size) || 256}px`;
@@ -467,6 +509,7 @@ export function createQRCodeDisplay(qrCodeData, options) {
467
509
  }
468
510
  /**
469
511
  * Helper function to create a modal for QR code display
512
+ * Supports both string QR codes and QRCodeData objects for dual-platform support
470
513
  */
471
514
  export function showQRCodeModal(qrCodeData, options) {
472
515
  // Create modal overlay
@@ -8,7 +8,7 @@ import type { PrepareResponse } from '../types';
8
8
  export interface LinkAuthOptions {
9
9
  /** Fixed polling interval in milliseconds (default: 2000) */
10
10
  pollingInterval?: number;
11
- /** Maximum polling attempts before timeout (default: 150 = 5 minutes with 2s interval) */
11
+ /** Maximum polling attempts before timeout (default: 30 = 1 minute with 2s interval) */
12
12
  maxPollingAttempts?: number;
13
13
  /** Custom polling endpoint (overrides backend-provided or configured endpoint) */
14
14
  pollingEndpoint?: string;
@@ -41,9 +41,11 @@ export interface LinkAuthResult {
41
41
  }
42
42
  export declare class LinkHandler implements StrategyHandler {
43
43
  private pollingInterval?;
44
- private isPolling;
44
+ private isPollingActive;
45
45
  private isCancelled;
46
46
  private onCancel?;
47
+ private pollingReject?;
48
+ private isPollingInProgress;
47
49
  /**
48
50
  * Invoke link-based authentication
49
51
  * Opens authentication app while keeping user on current page
@@ -74,6 +76,10 @@ export declare class LinkHandler implements StrategyHandler {
74
76
  * Clean up resources (stop polling if active)
75
77
  */
76
78
  cleanup(): void;
79
+ /**
80
+ * Check if polling is currently active
81
+ */
82
+ isPolling(): boolean;
77
83
  /**
78
84
  * Cancel the ongoing authentication
79
85
  */