@poly-x/react 0.1.0-alpha.13 → 0.1.0-alpha.15

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
@@ -433,6 +433,9 @@ var BffSessionStore = class {
433
433
  status: "signed-out",
434
434
  reason: "signed-out"
435
435
  });
436
+ if (options?.onSignOut) try {
437
+ await options.onSignOut();
438
+ } catch {}
436
439
  window.location.assign(options?.redirectTo ?? "/");
437
440
  };
438
441
  /** Null by design: under BFF custody the token never reaches the browser (FR-SES-3). */
@@ -511,6 +514,45 @@ function Protected({ children, fallback = null, loading = null }) {
511
514
  return isSignedIn ? children : fallback;
512
515
  }
513
516
  //#endregion
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;
527
+ return new Promise((resolve) => {
528
+ let settled = false;
529
+ const done = (result) => {
530
+ if (settled) return;
531
+ settled = true;
532
+ resolve(result);
533
+ };
534
+ const timer = setTimeout(() => done({ consent: "denied" }), timeoutMs + 500);
535
+ const finish = (result) => {
536
+ clearTimeout(timer);
537
+ done(result);
538
+ };
539
+ try {
540
+ 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,
548
+ maximumAge: 0
549
+ });
550
+ } catch {
551
+ finish({ consent: "denied" });
552
+ }
553
+ });
554
+ }
555
+ //#endregion
514
556
  //#region src/components/sign-in.tsx
515
557
  const DEFAULT_LABELS$3 = {
516
558
  heading: "Sign in",
@@ -525,6 +567,7 @@ const DEFAULT_LABELS$3 = {
525
567
  submit: "Sign in",
526
568
  submitting: "Signing in…",
527
569
  forgotPassword: "Forgot password?",
570
+ shareLocation: "Share my location for added account security",
528
571
  chooseWorkspace: "Choose a workspace",
529
572
  chooseWorkspaceSubheading: "You have access to more than one workspace.",
530
573
  securedBy: "Secured by PolyX",
@@ -638,6 +681,7 @@ function SignIn(props) {
638
681
  const [tenants, setTenants] = (0, react.useState)(null);
639
682
  const [submitting, setSubmitting] = (0, react.useState)(false);
640
683
  const [error, setError] = (0, react.useState)(null);
684
+ const [shareLocation, setShareLocation] = (0, react.useState)(false);
641
685
  const polyx = (0, react.useContext)(PolyXContext);
642
686
  const [branding, setBranding] = (0, react.useState)({});
643
687
  (0, react.useEffect)(() => {
@@ -670,6 +714,7 @@ function SignIn(props) {
670
714
  setSubmitting(true);
671
715
  setError(null);
672
716
  try {
717
+ const geo = props.captureLocation ? await capturePreciseLocation(shareLocation) : void 0;
673
718
  const body = await (await fetch(action, {
674
719
  method: "POST",
675
720
  headers: { "content-type": "application/json" },
@@ -678,6 +723,7 @@ function SignIn(props) {
678
723
  password,
679
724
  organizationCode: props.organizationCode,
680
725
  tenantId: options.tenantId,
726
+ ...geo ? { geo } : {},
681
727
  ...options.passwords
682
728
  })
683
729
  })).json().catch(() => ({}));
@@ -881,6 +927,19 @@ function SignIn(props) {
881
927
  children: labels.forgotPassword
882
928
  })
883
929
  }) : 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,
884
943
  errorNode,
885
944
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
886
945
  type: "submit",
@@ -1024,7 +1083,7 @@ const DEFAULT_LABELS$2 = {
1024
1083
  submit: "Send reset link",
1025
1084
  submitting: "Sending…",
1026
1085
  sentHeading: "Check your email",
1027
- sentSubheading: "If an account exists for {email}, a password reset link is on its way. It expires in 30 minutes.",
1086
+ sentSubheading: "If an account exists for {email}, a password reset link is on its way it expires in 30 minutes. If it doesn't arrive within a few minutes, check your spam folder, then contact your workspace administrator.",
1028
1087
  differentEmail: "Use a different email",
1029
1088
  backToSignIn: "Back to sign in",
1030
1089
  securedBy: "Secured by PolyX",
@@ -1684,7 +1743,10 @@ function UserButtonRoot(props) {
1684
1743
  key: "signOut",
1685
1744
  label: labels.signOut,
1686
1745
  labelIcon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExitIcon, {}),
1687
- onClick: () => void signOut({ redirectTo: props.afterSignOutUrl })
1746
+ onClick: () => void signOut({
1747
+ redirectTo: props.afterSignOutUrl,
1748
+ onSignOut: props.onSignOut
1749
+ })
1688
1750
  });
1689
1751
  if (props.showSignOutEverywhere) builtIns.set("signOutEverywhere", {
1690
1752
  key: "signOutEverywhere",
@@ -1692,7 +1754,8 @@ function UserButtonRoot(props) {
1692
1754
  labelIcon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExitIcon, {}),
1693
1755
  onClick: () => void signOut({
1694
1756
  scope: "everywhere",
1695
- redirectTo: props.afterSignOutUrl
1757
+ redirectTo: props.afterSignOutUrl,
1758
+ onSignOut: props.onSignOut
1696
1759
  })
1697
1760
  });
1698
1761
  const items = resolveItems(props.children, builtIns);
@@ -1878,6 +1941,7 @@ exports.UserAvatar = UserAvatar;
1878
1941
  exports.UserButton = UserButton;
1879
1942
  exports.WebLocksLock = WebLocksLock;
1880
1943
  exports.WindowBrowserBridge = WindowBrowserBridge;
1944
+ exports.capturePreciseLocation = capturePreciseLocation;
1881
1945
  exports.createLoginController = createLoginController;
1882
1946
  exports.initialsFrom = initialsFrom;
1883
1947
  exports.resolveSide = resolveSide;
package/dist/index.d.cts CHANGED
@@ -24,6 +24,13 @@ interface BffSignOutOptions {
24
24
  scope?: "device" | "everywhere";
25
25
  /** Where to land afterwards. Resolved here, client-side — the server takes no redirect target. */
26
26
  redirectTo?: string;
27
+ /**
28
+ * Run consumer cleanup after the session is revoked but BEFORE the post-sign-out
29
+ * redirect (the page is still alive at this point) — e.g. clear app cookies, reset
30
+ * local state, flush a store. Awaited; a thrown/rejected hook is swallowed so a
31
+ * failing callback can never trap the user in a signed-out-but-not-redirected state.
32
+ */
33
+ onSignOut?: () => void | Promise<void>;
27
34
  }
28
35
  //#endregion
29
36
  //#region src/auth-channel.d.ts
@@ -218,6 +225,8 @@ interface SignInLabels {
218
225
  submit: string;
219
226
  submitting: string;
220
227
  forgotPassword: string;
228
+ /** Consent checkbox label shown when `captureLocation` is enabled (v6). */
229
+ shareLocation: string;
221
230
  chooseWorkspace: string;
222
231
  chooseWorkspaceSubheading: string;
223
232
  securedBy: string;
@@ -255,6 +264,12 @@ interface SignInProps {
255
264
  * "Forgot password?" link appears under the password field. Omit to hide it.
256
265
  */
257
266
  forgotPasswordUrl?: string;
267
+ /**
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.
271
+ */
272
+ captureLocation?: boolean;
258
273
  /** Where to send the browser after success. Overrides the server's default redirect. */
259
274
  afterSignInPath?: string;
260
275
  /** Pre-scope the login to a single organization (skips org discovery). */
@@ -292,7 +307,13 @@ interface ForgotPasswordLabels {
292
307
  submitting: string;
293
308
  /** Shown after submit — always, regardless of whether the account exists. */
294
309
  sentHeading: string;
295
- /** Confirmation body. `{email}` is replaced with the entered address, emphasized. */
310
+ /**
311
+ * Confirmation body. `{email}` is replaced with the entered address, emphasized.
312
+ * Kept deliberately uniform (no account-existence / mail-config signal, per the
313
+ * anti-enumeration reset design) and points the user at their administrator for
314
+ * the cases where no mail can arrive (org has reset email disabled, no provider
315
+ * configured, or a send failure) — none of which the response is allowed to reveal.
316
+ */
296
317
  sentSubheading: string;
297
318
  differentEmail: string;
298
319
  backToSignIn: string;
@@ -386,6 +407,49 @@ declare function AuthCallback({
386
407
  children
387
408
  }: AuthCallbackProps): ReactNode;
388
409
  //#endregion
410
+ //#region src/geolocation.d.ts
411
+ /**
412
+ * @poly-x/react/geolocation — browser precise-location capture for sign-in (v6, FR-CNST/FR-LOC).
413
+ *
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).
419
+ */
420
+ /** Minimal shape of the browser Geolocation API we depend on (injectable for testing). */
421
+ interface GeolocationLike {
422
+ getCurrentPosition(success: (position: {
423
+ coords: {
424
+ latitude: number;
425
+ longitude: number;
426
+ accuracy?: number;
427
+ };
428
+ }) => void, error?: (err: unknown) => void, options?: {
429
+ enableHighAccuracy?: boolean;
430
+ timeout?: number;
431
+ maximumAge?: number;
432
+ }): void;
433
+ }
434
+ /** The consent-labelled result of a capture attempt. Coordinates present only when granted. */
435
+ interface CapturedGeo {
436
+ latitude?: string;
437
+ longitude?: string;
438
+ accuracyMeters?: number;
439
+ consent: "granted" | "denied";
440
+ }
441
+ interface CaptureOptions {
442
+ /** Max time to wait for a fix before giving up (coarse fallback). Default 8000ms. */
443
+ timeoutMs?: number;
444
+ /** Override the geolocation source (tests). Defaults to `navigator.geolocation`. */
445
+ geolocation?: GeolocationLike;
446
+ }
447
+ /**
448
+ * Capture the browser's precise location IF the user consented. Resolves to a consent-labelled
449
+ * result and never throws/rejects.
450
+ */
451
+ declare function capturePreciseLocation(consented: boolean, opts?: CaptureOptions): Promise<CapturedGeo>;
452
+ //#endregion
389
453
  //#region src/components/placement.d.ts
390
454
  /**
391
455
  * Collision-aware placement for the SDK's popovers.
@@ -457,6 +521,12 @@ interface UserButtonProps {
457
521
  showName?: boolean;
458
522
  /** Where to land after signing out. Default the app root. */
459
523
  afterSignOutUrl?: string;
524
+ /**
525
+ * Consumer cleanup run after the session is revoked but before the redirect —
526
+ * clear app cookies, reset local stores, etc. Awaited; errors are swallowed so a
527
+ * failing hook can't trap the user. Applies to both "Sign out" and "Sign out everywhere".
528
+ */
529
+ onSignOut?: () => void | Promise<void>;
460
530
  /**
461
531
  * Destination for the "Manage account" entry. Omitted entirely when unset — PolyX has no
462
532
  * hosted profile page, so the destination is the consumer's to own.
@@ -559,4 +629,4 @@ declare function UserAvatar({
559
629
  declare const PACKAGE_NAME = "@poly-x/react";
560
630
  declare const version = "0.0.0";
561
631
  //#endregion
562
- export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
632
+ export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type CaptureOptions, type CapturedGeo, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type GeolocationLike, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
package/dist/index.d.mts CHANGED
@@ -24,6 +24,13 @@ interface BffSignOutOptions {
24
24
  scope?: "device" | "everywhere";
25
25
  /** Where to land afterwards. Resolved here, client-side — the server takes no redirect target. */
26
26
  redirectTo?: string;
27
+ /**
28
+ * Run consumer cleanup after the session is revoked but BEFORE the post-sign-out
29
+ * redirect (the page is still alive at this point) — e.g. clear app cookies, reset
30
+ * local state, flush a store. Awaited; a thrown/rejected hook is swallowed so a
31
+ * failing callback can never trap the user in a signed-out-but-not-redirected state.
32
+ */
33
+ onSignOut?: () => void | Promise<void>;
27
34
  }
28
35
  //#endregion
29
36
  //#region src/auth-channel.d.ts
@@ -218,6 +225,8 @@ interface SignInLabels {
218
225
  submit: string;
219
226
  submitting: string;
220
227
  forgotPassword: string;
228
+ /** Consent checkbox label shown when `captureLocation` is enabled (v6). */
229
+ shareLocation: string;
221
230
  chooseWorkspace: string;
222
231
  chooseWorkspaceSubheading: string;
223
232
  securedBy: string;
@@ -255,6 +264,12 @@ interface SignInProps {
255
264
  * "Forgot password?" link appears under the password field. Omit to hide it.
256
265
  */
257
266
  forgotPasswordUrl?: string;
267
+ /**
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.
271
+ */
272
+ captureLocation?: boolean;
258
273
  /** Where to send the browser after success. Overrides the server's default redirect. */
259
274
  afterSignInPath?: string;
260
275
  /** Pre-scope the login to a single organization (skips org discovery). */
@@ -292,7 +307,13 @@ interface ForgotPasswordLabels {
292
307
  submitting: string;
293
308
  /** Shown after submit — always, regardless of whether the account exists. */
294
309
  sentHeading: string;
295
- /** Confirmation body. `{email}` is replaced with the entered address, emphasized. */
310
+ /**
311
+ * Confirmation body. `{email}` is replaced with the entered address, emphasized.
312
+ * Kept deliberately uniform (no account-existence / mail-config signal, per the
313
+ * anti-enumeration reset design) and points the user at their administrator for
314
+ * the cases where no mail can arrive (org has reset email disabled, no provider
315
+ * configured, or a send failure) — none of which the response is allowed to reveal.
316
+ */
296
317
  sentSubheading: string;
297
318
  differentEmail: string;
298
319
  backToSignIn: string;
@@ -386,6 +407,49 @@ declare function AuthCallback({
386
407
  children
387
408
  }: AuthCallbackProps): ReactNode;
388
409
  //#endregion
410
+ //#region src/geolocation.d.ts
411
+ /**
412
+ * @poly-x/react/geolocation — browser precise-location capture for sign-in (v6, FR-CNST/FR-LOC).
413
+ *
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).
419
+ */
420
+ /** Minimal shape of the browser Geolocation API we depend on (injectable for testing). */
421
+ interface GeolocationLike {
422
+ getCurrentPosition(success: (position: {
423
+ coords: {
424
+ latitude: number;
425
+ longitude: number;
426
+ accuracy?: number;
427
+ };
428
+ }) => void, error?: (err: unknown) => void, options?: {
429
+ enableHighAccuracy?: boolean;
430
+ timeout?: number;
431
+ maximumAge?: number;
432
+ }): void;
433
+ }
434
+ /** The consent-labelled result of a capture attempt. Coordinates present only when granted. */
435
+ interface CapturedGeo {
436
+ latitude?: string;
437
+ longitude?: string;
438
+ accuracyMeters?: number;
439
+ consent: "granted" | "denied";
440
+ }
441
+ interface CaptureOptions {
442
+ /** Max time to wait for a fix before giving up (coarse fallback). Default 8000ms. */
443
+ timeoutMs?: number;
444
+ /** Override the geolocation source (tests). Defaults to `navigator.geolocation`. */
445
+ geolocation?: GeolocationLike;
446
+ }
447
+ /**
448
+ * Capture the browser's precise location IF the user consented. Resolves to a consent-labelled
449
+ * result and never throws/rejects.
450
+ */
451
+ declare function capturePreciseLocation(consented: boolean, opts?: CaptureOptions): Promise<CapturedGeo>;
452
+ //#endregion
389
453
  //#region src/components/placement.d.ts
390
454
  /**
391
455
  * Collision-aware placement for the SDK's popovers.
@@ -457,6 +521,12 @@ interface UserButtonProps {
457
521
  showName?: boolean;
458
522
  /** Where to land after signing out. Default the app root. */
459
523
  afterSignOutUrl?: string;
524
+ /**
525
+ * Consumer cleanup run after the session is revoked but before the redirect —
526
+ * clear app cookies, reset local stores, etc. Awaited; errors are swallowed so a
527
+ * failing hook can't trap the user. Applies to both "Sign out" and "Sign out everywhere".
528
+ */
529
+ onSignOut?: () => void | Promise<void>;
460
530
  /**
461
531
  * Destination for the "Manage account" entry. Omitted entirely when unset — PolyX has no
462
532
  * hosted profile page, so the destination is the consumer's to own.
@@ -559,4 +629,4 @@ declare function UserAvatar({
559
629
  declare const PACKAGE_NAME = "@poly-x/react";
560
630
  declare const version = "0.0.0";
561
631
  //#endregion
562
- export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
632
+ export { type Align, AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type CaptureOptions, type CapturedGeo, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type GeolocationLike, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, type Side, type SideOption, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, UserAvatar, type UserAvatarProps, UserButton, type UserButtonActionProps, type UserButtonLabels, type UserButtonLinkProps, type UserButtonProps, type UserButtonSlotProps, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
package/dist/index.mjs CHANGED
@@ -432,6 +432,9 @@ var BffSessionStore = class {
432
432
  status: "signed-out",
433
433
  reason: "signed-out"
434
434
  });
435
+ if (options?.onSignOut) try {
436
+ await options.onSignOut();
437
+ } catch {}
435
438
  window.location.assign(options?.redirectTo ?? "/");
436
439
  };
437
440
  /** Null by design: under BFF custody the token never reaches the browser (FR-SES-3). */
@@ -510,6 +513,45 @@ function Protected({ children, fallback = null, loading = null }) {
510
513
  return isSignedIn ? children : fallback;
511
514
  }
512
515
  //#endregion
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;
526
+ return new Promise((resolve) => {
527
+ let settled = false;
528
+ const done = (result) => {
529
+ if (settled) return;
530
+ settled = true;
531
+ resolve(result);
532
+ };
533
+ const timer = setTimeout(() => done({ consent: "denied" }), timeoutMs + 500);
534
+ const finish = (result) => {
535
+ clearTimeout(timer);
536
+ done(result);
537
+ };
538
+ try {
539
+ 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,
547
+ maximumAge: 0
548
+ });
549
+ } catch {
550
+ finish({ consent: "denied" });
551
+ }
552
+ });
553
+ }
554
+ //#endregion
513
555
  //#region src/components/sign-in.tsx
514
556
  const DEFAULT_LABELS$3 = {
515
557
  heading: "Sign in",
@@ -524,6 +566,7 @@ const DEFAULT_LABELS$3 = {
524
566
  submit: "Sign in",
525
567
  submitting: "Signing in…",
526
568
  forgotPassword: "Forgot password?",
569
+ shareLocation: "Share my location for added account security",
527
570
  chooseWorkspace: "Choose a workspace",
528
571
  chooseWorkspaceSubheading: "You have access to more than one workspace.",
529
572
  securedBy: "Secured by PolyX",
@@ -637,6 +680,7 @@ function SignIn(props) {
637
680
  const [tenants, setTenants] = useState(null);
638
681
  const [submitting, setSubmitting] = useState(false);
639
682
  const [error, setError] = useState(null);
683
+ const [shareLocation, setShareLocation] = useState(false);
640
684
  const polyx = useContext(PolyXContext);
641
685
  const [branding, setBranding] = useState({});
642
686
  useEffect(() => {
@@ -669,6 +713,7 @@ function SignIn(props) {
669
713
  setSubmitting(true);
670
714
  setError(null);
671
715
  try {
716
+ const geo = props.captureLocation ? await capturePreciseLocation(shareLocation) : void 0;
672
717
  const body = await (await fetch(action, {
673
718
  method: "POST",
674
719
  headers: { "content-type": "application/json" },
@@ -677,6 +722,7 @@ function SignIn(props) {
677
722
  password,
678
723
  organizationCode: props.organizationCode,
679
724
  tenantId: options.tenantId,
725
+ ...geo ? { geo } : {},
680
726
  ...options.passwords
681
727
  })
682
728
  })).json().catch(() => ({}));
@@ -880,6 +926,19 @@ function SignIn(props) {
880
926
  children: labels.forgotPassword
881
927
  })
882
928
  }) : 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,
883
942
  errorNode,
884
943
  /* @__PURE__ */ jsx("button", {
885
944
  type: "submit",
@@ -1023,7 +1082,7 @@ const DEFAULT_LABELS$2 = {
1023
1082
  submit: "Send reset link",
1024
1083
  submitting: "Sending…",
1025
1084
  sentHeading: "Check your email",
1026
- sentSubheading: "If an account exists for {email}, a password reset link is on its way. It expires in 30 minutes.",
1085
+ sentSubheading: "If an account exists for {email}, a password reset link is on its way it expires in 30 minutes. If it doesn't arrive within a few minutes, check your spam folder, then contact your workspace administrator.",
1027
1086
  differentEmail: "Use a different email",
1028
1087
  backToSignIn: "Back to sign in",
1029
1088
  securedBy: "Secured by PolyX",
@@ -1683,7 +1742,10 @@ function UserButtonRoot(props) {
1683
1742
  key: "signOut",
1684
1743
  label: labels.signOut,
1685
1744
  labelIcon: /* @__PURE__ */ jsx(ExitIcon, {}),
1686
- onClick: () => void signOut({ redirectTo: props.afterSignOutUrl })
1745
+ onClick: () => void signOut({
1746
+ redirectTo: props.afterSignOutUrl,
1747
+ onSignOut: props.onSignOut
1748
+ })
1687
1749
  });
1688
1750
  if (props.showSignOutEverywhere) builtIns.set("signOutEverywhere", {
1689
1751
  key: "signOutEverywhere",
@@ -1691,7 +1753,8 @@ function UserButtonRoot(props) {
1691
1753
  labelIcon: /* @__PURE__ */ jsx(ExitIcon, {}),
1692
1754
  onClick: () => void signOut({
1693
1755
  scope: "everywhere",
1694
- redirectTo: props.afterSignOutUrl
1756
+ redirectTo: props.afterSignOutUrl,
1757
+ onSignOut: props.onSignOut
1695
1758
  })
1696
1759
  });
1697
1760
  const items = resolveItems(props.children, builtIns);
@@ -1861,4 +1924,4 @@ const UserButton = Object.assign(UserButtonRoot, {
1861
1924
  const PACKAGE_NAME = "@poly-x/react";
1862
1925
  const version = "0.0.0";
1863
1926
  //#endregion
1864
- export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, ForgotPassword, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, ResetPassword, SessionStoragePkceStore, SignIn, UserAvatar, UserButton, WebLocksLock, WindowBrowserBridge, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
1927
+ export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, ForgotPassword, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, ResetPassword, SessionStoragePkceStore, SignIn, UserAvatar, UserButton, WebLocksLock, WindowBrowserBridge, capturePreciseLocation, createLoginController, initialsFrom, resolveSide, useAuth, useAuthCallback, useSession, useUser, version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poly-x/react",
3
- "version": "0.1.0-alpha.13",
3
+ "version": "0.1.0-alpha.15",
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.13"
30
+ "@poly-x/core": "0.1.0-alpha.15"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18",