@poly-x/react 0.1.0-alpha.16 → 0.1.0-alpha.17

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/index.cjs CHANGED
@@ -515,43 +515,68 @@ function Protected({ children, fallback = null, loading = null }) {
515
515
  }
516
516
  //#endregion
517
517
  //#region src/geolocation.ts
518
- /**
519
- * Capture the browser's precise location IF the user consented. Resolves to a consent-labelled
520
- * result and never throws/rejects.
521
- */
522
- async function capturePreciseLocation(consented, opts = {}) {
523
- if (!consented) return { consent: "denied" };
524
- const geo = opts.geolocation ?? (typeof navigator !== "undefined" ? navigator.geolocation : void 0);
525
- if (!geo) return { consent: "denied" };
526
- const timeoutMs = opts.timeoutMs ?? 8e3;
518
+ /** W3C Geolocation `PositionError.PERMISSION_DENIED`. The only code that means a real "no". */
519
+ const PERMISSION_DENIED = 1;
520
+ /** One capture attempt at a given accuracy. Never rejects. */
521
+ function attemptCapture(geo, highAccuracy, timeout) {
527
522
  return new Promise((resolve) => {
528
523
  let settled = false;
529
- const done = (result) => {
524
+ const done = (outcome) => {
530
525
  if (settled) return;
531
526
  settled = true;
532
- resolve(result);
527
+ resolve(outcome);
533
528
  };
534
- const timer = setTimeout(() => done({ consent: "denied" }), timeoutMs + 500);
535
- const finish = (result) => {
529
+ const timer = setTimeout(() => done({
530
+ result: { consent: "granted" },
531
+ retriable: true
532
+ }), timeout + 1500);
533
+ const finish = (outcome) => {
536
534
  clearTimeout(timer);
537
- done(result);
535
+ done(outcome);
538
536
  };
539
537
  try {
540
538
  geo.getCurrentPosition((position) => finish({
541
- latitude: String(position.coords.latitude),
542
- longitude: String(position.coords.longitude),
543
- accuracyMeters: typeof position.coords.accuracy === "number" ? position.coords.accuracy : void 0,
544
- consent: "granted"
545
- }), () => finish({ consent: "denied" }), {
546
- enableHighAccuracy: true,
547
- timeout: timeoutMs,
539
+ result: {
540
+ latitude: String(position.coords.latitude),
541
+ longitude: String(position.coords.longitude),
542
+ accuracyMeters: typeof position.coords.accuracy === "number" ? position.coords.accuracy : void 0,
543
+ consent: "granted"
544
+ },
545
+ retriable: false
546
+ }), (err) => {
547
+ const code = err?.code;
548
+ const denied = typeof code === "number" && code === PERMISSION_DENIED;
549
+ finish({
550
+ result: { consent: denied ? "denied" : "granted" },
551
+ retriable: !denied
552
+ });
553
+ }, {
554
+ enableHighAccuracy: highAccuracy,
555
+ timeout,
548
556
  maximumAge: 0
549
557
  });
550
558
  } catch {
551
- finish({ consent: "denied" });
559
+ finish({
560
+ result: { consent: "granted" },
561
+ retriable: true
562
+ });
552
563
  }
553
564
  });
554
565
  }
566
+ /**
567
+ * Capture the browser's precise location. Resolves to a consent-labelled result and never
568
+ * throws/rejects. High-accuracy first; on a no-fix (not a denial) retries once at low accuracy.
569
+ */
570
+ async function capturePreciseLocation(consented, opts = {}) {
571
+ if (!consented) return { consent: "denied" };
572
+ const geo = opts.geolocation ?? (typeof navigator !== "undefined" ? navigator.geolocation : void 0);
573
+ if (!geo) return { consent: "denied" };
574
+ const highAccuracyMs = opts.timeoutMs ?? 8e3;
575
+ const lowAccuracyMs = opts.fallbackTimeoutMs ?? 12e3;
576
+ const first = await attemptCapture(geo, true, highAccuracyMs);
577
+ if (first.result.latitude !== void 0 || !first.retriable) return first.result;
578
+ return (await attemptCapture(geo, false, lowAccuracyMs)).result;
579
+ }
555
580
  //#endregion
556
581
  //#region src/components/sign-in.tsx
557
582
  const DEFAULT_LABELS$3 = {
package/dist/index.d.cts CHANGED
@@ -410,11 +410,17 @@ declare function AuthCallback({
410
410
  /**
411
411
  * @poly-x/react/geolocation — browser precise-location capture for sign-in (v6, FR-CNST/FR-LOC).
412
412
  *
413
- * The consent-gated bridge to `navigator.geolocation`. It NEVER rejects: a decline, a browser
414
- * permission denial, a missing API, or a timeout all resolve to `{ consent: "denied" }` with no
415
- * coordinates, so the caller can always proceed — sign-in must never block on location (D52).
416
- * The captured value is posted to the BFF, which forwards it to the platform on the token
417
- * exchange; the platform stores precise only when consent is `granted`, else coarse (IP).
413
+ * The consent-gated bridge to `navigator.geolocation`. It NEVER rejects: a missing API resolves
414
+ * to `{ consent: "denied" }` with no coordinates so the caller can always proceed — sign-in must
415
+ * never block on location (D52).
416
+ *
417
+ * Consent vs. fix are kept SEPARATE. `consent` reflects the browser PERMISSION decision only:
418
+ * `denied` means the user actually said no (`PERMISSION_DENIED`). A position that is merely
419
+ * unavailable or times out (common on desktops with no GPS) is NOT a denial — the user consented,
420
+ * we just couldn't get coordinates, so consent stays `granted` with no coords and the platform
421
+ * uses the coarse (IP) fallback. Conflating the two is why an allowed login could be stored as
422
+ * `denied` + coarse. On a no-fix (not a denial) we retry once at low accuracy (network-based),
423
+ * which is far more reliable than high-accuracy GPS on desktop browsers.
418
424
  */
419
425
  /** Minimal shape of the browser Geolocation API we depend on (injectable for testing). */
420
426
  interface GeolocationLike {
@@ -424,28 +430,33 @@ interface GeolocationLike {
424
430
  longitude: number;
425
431
  accuracy?: number;
426
432
  };
427
- }) => void, error?: (err: unknown) => void, options?: {
433
+ }) => void, error?: (err: {
434
+ code?: number;
435
+ } | unknown) => void, options?: {
428
436
  enableHighAccuracy?: boolean;
429
437
  timeout?: number;
430
438
  maximumAge?: number;
431
439
  }): void;
432
440
  }
433
- /** The consent-labelled result of a capture attempt. Coordinates present only when granted. */
441
+ /** The consent-labelled result of a capture attempt. Coordinates present only on a successful fix. */
434
442
  interface CapturedGeo {
435
443
  latitude?: string;
436
444
  longitude?: string;
437
445
  accuracyMeters?: number;
446
+ /** The browser PERMISSION decision only — never downgraded by a failed/timed-out fix. */
438
447
  consent: "granted" | "denied";
439
448
  }
440
449
  interface CaptureOptions {
441
- /** Max time to wait for a fix before giving up (coarse fallback). Default 8000ms. */
450
+ /** High-accuracy (GPS) attempt budget before the network-based retry. Default 8000ms. */
442
451
  timeoutMs?: number;
452
+ /** Low-accuracy (network) retry budget when the first attempt gets no fix. Default 12000ms. */
453
+ fallbackTimeoutMs?: number;
443
454
  /** Override the geolocation source (tests). Defaults to `navigator.geolocation`. */
444
455
  geolocation?: GeolocationLike;
445
456
  }
446
457
  /**
447
- * Capture the browser's precise location IF the user consented. Resolves to a consent-labelled
448
- * result and never throws/rejects.
458
+ * Capture the browser's precise location. Resolves to a consent-labelled result and never
459
+ * throws/rejects. High-accuracy first; on a no-fix (not a denial) retries once at low accuracy.
449
460
  */
450
461
  declare function capturePreciseLocation(consented: boolean, opts?: CaptureOptions): Promise<CapturedGeo>;
451
462
  //#endregion
package/dist/index.d.mts CHANGED
@@ -410,11 +410,17 @@ declare function AuthCallback({
410
410
  /**
411
411
  * @poly-x/react/geolocation — browser precise-location capture for sign-in (v6, FR-CNST/FR-LOC).
412
412
  *
413
- * The consent-gated bridge to `navigator.geolocation`. It NEVER rejects: a decline, a browser
414
- * permission denial, a missing API, or a timeout all resolve to `{ consent: "denied" }` with no
415
- * coordinates, so the caller can always proceed — sign-in must never block on location (D52).
416
- * The captured value is posted to the BFF, which forwards it to the platform on the token
417
- * exchange; the platform stores precise only when consent is `granted`, else coarse (IP).
413
+ * The consent-gated bridge to `navigator.geolocation`. It NEVER rejects: a missing API resolves
414
+ * to `{ consent: "denied" }` with no coordinates so the caller can always proceed — sign-in must
415
+ * never block on location (D52).
416
+ *
417
+ * Consent vs. fix are kept SEPARATE. `consent` reflects the browser PERMISSION decision only:
418
+ * `denied` means the user actually said no (`PERMISSION_DENIED`). A position that is merely
419
+ * unavailable or times out (common on desktops with no GPS) is NOT a denial — the user consented,
420
+ * we just couldn't get coordinates, so consent stays `granted` with no coords and the platform
421
+ * uses the coarse (IP) fallback. Conflating the two is why an allowed login could be stored as
422
+ * `denied` + coarse. On a no-fix (not a denial) we retry once at low accuracy (network-based),
423
+ * which is far more reliable than high-accuracy GPS on desktop browsers.
418
424
  */
419
425
  /** Minimal shape of the browser Geolocation API we depend on (injectable for testing). */
420
426
  interface GeolocationLike {
@@ -424,28 +430,33 @@ interface GeolocationLike {
424
430
  longitude: number;
425
431
  accuracy?: number;
426
432
  };
427
- }) => void, error?: (err: unknown) => void, options?: {
433
+ }) => void, error?: (err: {
434
+ code?: number;
435
+ } | unknown) => void, options?: {
428
436
  enableHighAccuracy?: boolean;
429
437
  timeout?: number;
430
438
  maximumAge?: number;
431
439
  }): void;
432
440
  }
433
- /** The consent-labelled result of a capture attempt. Coordinates present only when granted. */
441
+ /** The consent-labelled result of a capture attempt. Coordinates present only on a successful fix. */
434
442
  interface CapturedGeo {
435
443
  latitude?: string;
436
444
  longitude?: string;
437
445
  accuracyMeters?: number;
446
+ /** The browser PERMISSION decision only — never downgraded by a failed/timed-out fix. */
438
447
  consent: "granted" | "denied";
439
448
  }
440
449
  interface CaptureOptions {
441
- /** Max time to wait for a fix before giving up (coarse fallback). Default 8000ms. */
450
+ /** High-accuracy (GPS) attempt budget before the network-based retry. Default 8000ms. */
442
451
  timeoutMs?: number;
452
+ /** Low-accuracy (network) retry budget when the first attempt gets no fix. Default 12000ms. */
453
+ fallbackTimeoutMs?: number;
443
454
  /** Override the geolocation source (tests). Defaults to `navigator.geolocation`. */
444
455
  geolocation?: GeolocationLike;
445
456
  }
446
457
  /**
447
- * Capture the browser's precise location IF the user consented. Resolves to a consent-labelled
448
- * result and never throws/rejects.
458
+ * Capture the browser's precise location. Resolves to a consent-labelled result and never
459
+ * throws/rejects. High-accuracy first; on a no-fix (not a denial) retries once at low accuracy.
449
460
  */
450
461
  declare function capturePreciseLocation(consented: boolean, opts?: CaptureOptions): Promise<CapturedGeo>;
451
462
  //#endregion
package/dist/index.mjs CHANGED
@@ -514,43 +514,68 @@ function Protected({ children, fallback = null, loading = null }) {
514
514
  }
515
515
  //#endregion
516
516
  //#region src/geolocation.ts
517
- /**
518
- * Capture the browser's precise location IF the user consented. Resolves to a consent-labelled
519
- * result and never throws/rejects.
520
- */
521
- async function capturePreciseLocation(consented, opts = {}) {
522
- if (!consented) return { consent: "denied" };
523
- const geo = opts.geolocation ?? (typeof navigator !== "undefined" ? navigator.geolocation : void 0);
524
- if (!geo) return { consent: "denied" };
525
- const timeoutMs = opts.timeoutMs ?? 8e3;
517
+ /** W3C Geolocation `PositionError.PERMISSION_DENIED`. The only code that means a real "no". */
518
+ const PERMISSION_DENIED = 1;
519
+ /** One capture attempt at a given accuracy. Never rejects. */
520
+ function attemptCapture(geo, highAccuracy, timeout) {
526
521
  return new Promise((resolve) => {
527
522
  let settled = false;
528
- const done = (result) => {
523
+ const done = (outcome) => {
529
524
  if (settled) return;
530
525
  settled = true;
531
- resolve(result);
526
+ resolve(outcome);
532
527
  };
533
- const timer = setTimeout(() => done({ consent: "denied" }), timeoutMs + 500);
534
- const finish = (result) => {
528
+ const timer = setTimeout(() => done({
529
+ result: { consent: "granted" },
530
+ retriable: true
531
+ }), timeout + 1500);
532
+ const finish = (outcome) => {
535
533
  clearTimeout(timer);
536
- done(result);
534
+ done(outcome);
537
535
  };
538
536
  try {
539
537
  geo.getCurrentPosition((position) => finish({
540
- latitude: String(position.coords.latitude),
541
- longitude: String(position.coords.longitude),
542
- accuracyMeters: typeof position.coords.accuracy === "number" ? position.coords.accuracy : void 0,
543
- consent: "granted"
544
- }), () => finish({ consent: "denied" }), {
545
- enableHighAccuracy: true,
546
- timeout: timeoutMs,
538
+ result: {
539
+ latitude: String(position.coords.latitude),
540
+ longitude: String(position.coords.longitude),
541
+ accuracyMeters: typeof position.coords.accuracy === "number" ? position.coords.accuracy : void 0,
542
+ consent: "granted"
543
+ },
544
+ retriable: false
545
+ }), (err) => {
546
+ const code = err?.code;
547
+ const denied = typeof code === "number" && code === PERMISSION_DENIED;
548
+ finish({
549
+ result: { consent: denied ? "denied" : "granted" },
550
+ retriable: !denied
551
+ });
552
+ }, {
553
+ enableHighAccuracy: highAccuracy,
554
+ timeout,
547
555
  maximumAge: 0
548
556
  });
549
557
  } catch {
550
- finish({ consent: "denied" });
558
+ finish({
559
+ result: { consent: "granted" },
560
+ retriable: true
561
+ });
551
562
  }
552
563
  });
553
564
  }
565
+ /**
566
+ * Capture the browser's precise location. Resolves to a consent-labelled result and never
567
+ * throws/rejects. High-accuracy first; on a no-fix (not a denial) retries once at low accuracy.
568
+ */
569
+ async function capturePreciseLocation(consented, opts = {}) {
570
+ if (!consented) return { consent: "denied" };
571
+ const geo = opts.geolocation ?? (typeof navigator !== "undefined" ? navigator.geolocation : void 0);
572
+ if (!geo) return { consent: "denied" };
573
+ const highAccuracyMs = opts.timeoutMs ?? 8e3;
574
+ const lowAccuracyMs = opts.fallbackTimeoutMs ?? 12e3;
575
+ const first = await attemptCapture(geo, true, highAccuracyMs);
576
+ if (first.result.latitude !== void 0 || !first.retriable) return first.result;
577
+ return (await attemptCapture(geo, false, lowAccuracyMs)).result;
578
+ }
554
579
  //#endregion
555
580
  //#region src/components/sign-in.tsx
556
581
  const DEFAULT_LABELS$3 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poly-x/react",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.17",
4
4
  "description": "PolyX SDK for React SPAs - provider, hooks, drop-in auth UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "module": "./dist/index.mjs",
28
28
  "types": "./dist/index.d.cts",
29
29
  "dependencies": {
30
- "@poly-x/core": "0.1.0-alpha.16"
30
+ "@poly-x/core": "0.1.0-alpha.17"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18",