@poly-x/react 0.1.0-alpha.15 → 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 = {
@@ -567,7 +592,6 @@ const DEFAULT_LABELS$3 = {
567
592
  submit: "Sign in",
568
593
  submitting: "Signing in…",
569
594
  forgotPassword: "Forgot password?",
570
- shareLocation: "Share my location for added account security",
571
595
  chooseWorkspace: "Choose a workspace",
572
596
  chooseWorkspaceSubheading: "You have access to more than one workspace.",
573
597
  securedBy: "Secured by PolyX",
@@ -681,7 +705,6 @@ function SignIn(props) {
681
705
  const [tenants, setTenants] = (0, react.useState)(null);
682
706
  const [submitting, setSubmitting] = (0, react.useState)(false);
683
707
  const [error, setError] = (0, react.useState)(null);
684
- const [shareLocation, setShareLocation] = (0, react.useState)(false);
685
708
  const polyx = (0, react.useContext)(PolyXContext);
686
709
  const [branding, setBranding] = (0, react.useState)({});
687
710
  (0, react.useEffect)(() => {
@@ -714,7 +737,7 @@ function SignIn(props) {
714
737
  setSubmitting(true);
715
738
  setError(null);
716
739
  try {
717
- const geo = props.captureLocation ? await capturePreciseLocation(shareLocation) : void 0;
740
+ const geo = props.captureLocation ? await capturePreciseLocation(true) : void 0;
718
741
  const body = await (await fetch(action, {
719
742
  method: "POST",
720
743
  headers: { "content-type": "application/json" },
@@ -927,19 +950,6 @@ function SignIn(props) {
927
950
  children: labels.forgotPassword
928
951
  })
929
952
  }) : null,
930
- props.captureLocation ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
931
- className: "polyx-signin__consent",
932
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
933
- type: "checkbox",
934
- className: "polyx-signin__checkbox",
935
- checked: shareLocation,
936
- disabled: submitting,
937
- onChange: (event) => setShareLocation(event.target.checked)
938
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
939
- className: "polyx-signin__consent-label",
940
- children: labels.shareLocation
941
- })]
942
- }) : null,
943
953
  errorNode,
944
954
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
945
955
  type: "submit",
package/dist/index.d.cts CHANGED
@@ -225,8 +225,6 @@ interface SignInLabels {
225
225
  submit: string;
226
226
  submitting: string;
227
227
  forgotPassword: string;
228
- /** Consent checkbox label shown when `captureLocation` is enabled (v6). */
229
- shareLocation: string;
230
228
  chooseWorkspace: string;
231
229
  chooseWorkspaceSubheading: string;
232
230
  securedBy: string;
@@ -265,9 +263,10 @@ interface SignInProps {
265
263
  */
266
264
  forgotPasswordUrl?: string;
267
265
  /**
268
- * v6 (FR-CNST): show an explicit, opt-in consent checkbox to share precise location for
269
- * security/presence. On submit, if checked, the browser captures a position and it rides to
270
- * the platform with the login. Denial/timeout never blocks sign-in (coarse fallback). Off by default.
266
+ * v6 (FR-CNST): capture precise location for security/presence. When enabled, sign-in asks the
267
+ * browser for a position the browser's own native permission prompt is the consent gate (no
268
+ * in-form checkbox), and it's remembered per-origin after the first choice. A block, denial, or
269
+ * timeout never blocks sign-in (coarse IP fallback). Off by default.
271
270
  */
272
271
  captureLocation?: boolean;
273
272
  /** Where to send the browser after success. Overrides the server's default redirect. */
@@ -411,11 +410,17 @@ declare function AuthCallback({
411
410
  /**
412
411
  * @poly-x/react/geolocation — browser precise-location capture for sign-in (v6, FR-CNST/FR-LOC).
413
412
  *
414
- * The consent-gated bridge to `navigator.geolocation`. It NEVER rejects: a decline, a browser
415
- * permission denial, a missing API, or a timeout all resolve to `{ consent: "denied" }` with no
416
- * coordinates, so the caller can always proceed — sign-in must never block on location (D52).
417
- * The captured value is posted to the BFF, which forwards it to the platform on the token
418
- * 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.
419
424
  */
420
425
  /** Minimal shape of the browser Geolocation API we depend on (injectable for testing). */
421
426
  interface GeolocationLike {
@@ -425,28 +430,33 @@ interface GeolocationLike {
425
430
  longitude: number;
426
431
  accuracy?: number;
427
432
  };
428
- }) => void, error?: (err: unknown) => void, options?: {
433
+ }) => void, error?: (err: {
434
+ code?: number;
435
+ } | unknown) => void, options?: {
429
436
  enableHighAccuracy?: boolean;
430
437
  timeout?: number;
431
438
  maximumAge?: number;
432
439
  }): void;
433
440
  }
434
- /** 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. */
435
442
  interface CapturedGeo {
436
443
  latitude?: string;
437
444
  longitude?: string;
438
445
  accuracyMeters?: number;
446
+ /** The browser PERMISSION decision only — never downgraded by a failed/timed-out fix. */
439
447
  consent: "granted" | "denied";
440
448
  }
441
449
  interface CaptureOptions {
442
- /** 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. */
443
451
  timeoutMs?: number;
452
+ /** Low-accuracy (network) retry budget when the first attempt gets no fix. Default 12000ms. */
453
+ fallbackTimeoutMs?: number;
444
454
  /** Override the geolocation source (tests). Defaults to `navigator.geolocation`. */
445
455
  geolocation?: GeolocationLike;
446
456
  }
447
457
  /**
448
- * Capture the browser's precise location IF the user consented. Resolves to a consent-labelled
449
- * 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.
450
460
  */
451
461
  declare function capturePreciseLocation(consented: boolean, opts?: CaptureOptions): Promise<CapturedGeo>;
452
462
  //#endregion
package/dist/index.d.mts CHANGED
@@ -225,8 +225,6 @@ interface SignInLabels {
225
225
  submit: string;
226
226
  submitting: string;
227
227
  forgotPassword: string;
228
- /** Consent checkbox label shown when `captureLocation` is enabled (v6). */
229
- shareLocation: string;
230
228
  chooseWorkspace: string;
231
229
  chooseWorkspaceSubheading: string;
232
230
  securedBy: string;
@@ -265,9 +263,10 @@ interface SignInProps {
265
263
  */
266
264
  forgotPasswordUrl?: string;
267
265
  /**
268
- * v6 (FR-CNST): show an explicit, opt-in consent checkbox to share precise location for
269
- * security/presence. On submit, if checked, the browser captures a position and it rides to
270
- * the platform with the login. Denial/timeout never blocks sign-in (coarse fallback). Off by default.
266
+ * v6 (FR-CNST): capture precise location for security/presence. When enabled, sign-in asks the
267
+ * browser for a position the browser's own native permission prompt is the consent gate (no
268
+ * in-form checkbox), and it's remembered per-origin after the first choice. A block, denial, or
269
+ * timeout never blocks sign-in (coarse IP fallback). Off by default.
271
270
  */
272
271
  captureLocation?: boolean;
273
272
  /** Where to send the browser after success. Overrides the server's default redirect. */
@@ -411,11 +410,17 @@ declare function AuthCallback({
411
410
  /**
412
411
  * @poly-x/react/geolocation — browser precise-location capture for sign-in (v6, FR-CNST/FR-LOC).
413
412
  *
414
- * The consent-gated bridge to `navigator.geolocation`. It NEVER rejects: a decline, a browser
415
- * permission denial, a missing API, or a timeout all resolve to `{ consent: "denied" }` with no
416
- * coordinates, so the caller can always proceed — sign-in must never block on location (D52).
417
- * The captured value is posted to the BFF, which forwards it to the platform on the token
418
- * 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.
419
424
  */
420
425
  /** Minimal shape of the browser Geolocation API we depend on (injectable for testing). */
421
426
  interface GeolocationLike {
@@ -425,28 +430,33 @@ interface GeolocationLike {
425
430
  longitude: number;
426
431
  accuracy?: number;
427
432
  };
428
- }) => void, error?: (err: unknown) => void, options?: {
433
+ }) => void, error?: (err: {
434
+ code?: number;
435
+ } | unknown) => void, options?: {
429
436
  enableHighAccuracy?: boolean;
430
437
  timeout?: number;
431
438
  maximumAge?: number;
432
439
  }): void;
433
440
  }
434
- /** 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. */
435
442
  interface CapturedGeo {
436
443
  latitude?: string;
437
444
  longitude?: string;
438
445
  accuracyMeters?: number;
446
+ /** The browser PERMISSION decision only — never downgraded by a failed/timed-out fix. */
439
447
  consent: "granted" | "denied";
440
448
  }
441
449
  interface CaptureOptions {
442
- /** 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. */
443
451
  timeoutMs?: number;
452
+ /** Low-accuracy (network) retry budget when the first attempt gets no fix. Default 12000ms. */
453
+ fallbackTimeoutMs?: number;
444
454
  /** Override the geolocation source (tests). Defaults to `navigator.geolocation`. */
445
455
  geolocation?: GeolocationLike;
446
456
  }
447
457
  /**
448
- * Capture the browser's precise location IF the user consented. Resolves to a consent-labelled
449
- * 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.
450
460
  */
451
461
  declare function capturePreciseLocation(consented: boolean, opts?: CaptureOptions): Promise<CapturedGeo>;
452
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 = {
@@ -566,7 +591,6 @@ const DEFAULT_LABELS$3 = {
566
591
  submit: "Sign in",
567
592
  submitting: "Signing in…",
568
593
  forgotPassword: "Forgot password?",
569
- shareLocation: "Share my location for added account security",
570
594
  chooseWorkspace: "Choose a workspace",
571
595
  chooseWorkspaceSubheading: "You have access to more than one workspace.",
572
596
  securedBy: "Secured by PolyX",
@@ -680,7 +704,6 @@ function SignIn(props) {
680
704
  const [tenants, setTenants] = useState(null);
681
705
  const [submitting, setSubmitting] = useState(false);
682
706
  const [error, setError] = useState(null);
683
- const [shareLocation, setShareLocation] = useState(false);
684
707
  const polyx = useContext(PolyXContext);
685
708
  const [branding, setBranding] = useState({});
686
709
  useEffect(() => {
@@ -713,7 +736,7 @@ function SignIn(props) {
713
736
  setSubmitting(true);
714
737
  setError(null);
715
738
  try {
716
- const geo = props.captureLocation ? await capturePreciseLocation(shareLocation) : void 0;
739
+ const geo = props.captureLocation ? await capturePreciseLocation(true) : void 0;
717
740
  const body = await (await fetch(action, {
718
741
  method: "POST",
719
742
  headers: { "content-type": "application/json" },
@@ -926,19 +949,6 @@ function SignIn(props) {
926
949
  children: labels.forgotPassword
927
950
  })
928
951
  }) : null,
929
- props.captureLocation ? /* @__PURE__ */ jsxs("label", {
930
- className: "polyx-signin__consent",
931
- children: [/* @__PURE__ */ jsx("input", {
932
- type: "checkbox",
933
- className: "polyx-signin__checkbox",
934
- checked: shareLocation,
935
- disabled: submitting,
936
- onChange: (event) => setShareLocation(event.target.checked)
937
- }), /* @__PURE__ */ jsx("span", {
938
- className: "polyx-signin__consent-label",
939
- children: labels.shareLocation
940
- })]
941
- }) : null,
942
952
  errorNode,
943
953
  /* @__PURE__ */ jsx("button", {
944
954
  type: "submit",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poly-x/react",
3
- "version": "0.1.0-alpha.15",
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.15"
30
+ "@poly-x/core": "0.1.0-alpha.17"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18",