@pradip1995/segment-login-popup 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/segment-login-popup",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  isAuthRequiredError,
12
12
  promptLoginForWishlist,
13
13
  requestLoginPopup,
14
+ requireLoginForWishlist,
14
15
  } from "./request-login"
15
16
  export {
16
17
  DEFAULT_LOGIN_POPUP_FEATURE_CARDS,
@@ -5,6 +5,7 @@ import {
5
5
  useCallback,
6
6
  useContext,
7
7
  useEffect,
8
+ useLayoutEffect,
8
9
  useMemo,
9
10
  useRef,
10
11
  useState,
@@ -78,7 +79,7 @@ export function LoginPopupProvider({
78
79
  setIsPopupOpen(true)
79
80
  }, [])
80
81
 
81
- useEffect(() => {
82
+ useLayoutEffect(() => {
82
83
  if (isLoggedIn) return
83
84
 
84
85
  const onOpen = (event: Event) => {
@@ -5,11 +5,13 @@ import {
5
5
  clearMedusaAuthCookies,
6
6
  GOOGLE_LOGIN_COUNTRY_CODE_KEY,
7
7
  } from "@pradip1995/commerce-auth/util/google-auth-client"
8
+ import "./segment.css"
8
9
 
9
10
  export {
10
11
  isAuthRequiredError,
11
12
  promptLoginForWishlist,
12
13
  requestLoginPopup,
14
+ requireLoginForWishlist,
13
15
  } from "./request-login"
14
16
 
15
17
  type LoginRequiredModalProps = {
@@ -49,41 +51,39 @@ export default function LoginRequiredModal({
49
51
  }
50
52
 
51
53
  return (
52
- <div className="fixed inset-0 z-[99999] flex items-center justify-center p-4">
54
+ <div className="login-required-modal">
53
55
  <button
54
56
  type="button"
55
- className="absolute inset-0 bg-black/50 backdrop-blur-[2px]"
57
+ className="login-required-modal__backdrop"
56
58
  aria-label="Close login required dialog"
57
59
  onClick={onClose}
58
60
  />
59
- <div className="relative w-full max-w-sm bg-surface border border-cart-border shadow-xl p-6 text-center">
60
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-brand-accent/10 text-brand-accent">
61
+ <div className="login-required-modal__dialog">
62
+ <div className="login-required-modal__icon" aria-hidden>
61
63
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75">
62
64
  <rect x="3" y="11" width="18" height="11" rx="2" />
63
65
  <path d="M7 11V7a5 5 0 0 1 10 0v4" />
64
66
  </svg>
65
67
  </div>
66
- <h2 className="font-heading text-lg font-bold text-heading uppercase tracking-[0.08em] mb-2">
67
- Login required
68
- </h2>
69
- <p className="text-sm text-muted mb-5">{message}</p>
68
+ <h2 className="login-required-modal__title">Login required</h2>
69
+ <p className="login-required-modal__message">{message}</p>
70
70
 
71
- <div className="mb-4">
71
+ <div className="login-required-modal__google">
72
72
  <GoogleAuthSection countryCode={countryCode} />
73
73
  </div>
74
74
 
75
- <div className="flex gap-2">
75
+ <div className="login-required-modal__actions">
76
76
  <button
77
77
  type="button"
78
78
  onClick={onClose}
79
- className="flex-1 py-2.5 text-xs font-bold uppercase tracking-widest border border-cart-border text-muted hover:text-heading"
79
+ className="login-required-modal__btn login-required-modal__btn--ghost"
80
80
  >
81
81
  Cancel
82
82
  </button>
83
83
  <button
84
84
  type="button"
85
85
  onClick={goToAccount}
86
- className="flex-1 py-2.5 text-xs font-bold uppercase tracking-widest bg-brand-primary text-inverse hover:bg-brand-accent"
86
+ className="login-required-modal__btn login-required-modal__btn--primary"
87
87
  >
88
88
  Login
89
89
  </button>
@@ -1,8 +1,13 @@
1
1
  import assert from "node:assert/strict"
2
2
  import { describe, it } from "node:test"
3
- import { isAuthRequiredError } from "./request-login.ts"
3
+ import {
4
+ isAuthRequiredError,
5
+ LOGIN_POPUP_OPEN_EVENT,
6
+ requestLoginPopup,
7
+ requireLoginForWishlist,
8
+ } from "./request-login.ts"
4
9
 
5
- const CASES: Array<{
10
+ const AUTH_CASES: Array<{
6
11
  name: string
7
12
  error: string | null | undefined
8
13
  want: boolean
@@ -19,9 +24,49 @@ const CASES: Array<{
19
24
  ]
20
25
 
21
26
  describe("isAuthRequiredError", () => {
22
- for (const row of CASES) {
27
+ for (const row of AUTH_CASES) {
23
28
  it(row.name, () => {
24
29
  assert.equal(isAuthRequiredError(row.error), row.want)
25
30
  })
26
31
  }
27
32
  })
33
+
34
+ const POPUP_CASES: Array<{
35
+ name: string
36
+ dispatcher: "none" | "silent" | "guest"
37
+ want: boolean
38
+ }> = [
39
+ { name: "no dispatcher cannot open popup", dispatcher: "none", want: false },
40
+ { name: "logged-in shopper has no listener", dispatcher: "silent", want: false },
41
+ { name: "guest listener preventDefault opens popup", dispatcher: "guest", want: true },
42
+ ]
43
+
44
+ function makeDispatcher(kind: "silent" | "guest"): EventTarget {
45
+ const target = new EventTarget()
46
+ if (kind === "guest") {
47
+ target.addEventListener(LOGIN_POPUP_OPEN_EVENT, (event) => {
48
+ event.preventDefault()
49
+ })
50
+ }
51
+ return target
52
+ }
53
+
54
+ describe("requestLoginPopup", () => {
55
+ for (const row of POPUP_CASES) {
56
+ it(row.name, () => {
57
+ const dispatcher =
58
+ row.dispatcher === "none" ? null : makeDispatcher(row.dispatcher)
59
+ assert.equal(requestLoginPopup(dispatcher), row.want)
60
+ })
61
+ }
62
+ })
63
+
64
+ describe("requireLoginForWishlist", () => {
65
+ for (const row of POPUP_CASES) {
66
+ it(`aborts wishlist when ${row.name}`, () => {
67
+ const dispatcher =
68
+ row.dispatcher === "none" ? null : makeDispatcher(row.dispatcher)
69
+ assert.equal(requireLoginForWishlist(dispatcher), row.want)
70
+ })
71
+ }
72
+ })
@@ -10,13 +10,30 @@ export function isAuthRequiredError(error?: string | null): boolean {
10
10
  )
11
11
  }
12
12
 
13
- export function requestLoginPopup(): boolean {
14
- if (typeof window === "undefined") return false
13
+ type LoginPopupDispatcher = Pick<EventTarget, "dispatchEvent">
14
+
15
+ export function requestLoginPopup(
16
+ dispatcher?: LoginPopupDispatcher | null
17
+ ): boolean {
18
+ const target =
19
+ dispatcher ?? (typeof window === "undefined" ? null : window)
20
+ if (!target) return false
15
21
  const event = new CustomEvent(LOGIN_POPUP_OPEN_EVENT, { cancelable: true })
16
- window.dispatchEvent(event)
22
+ target.dispatchEvent(event)
17
23
  return event.defaultPrevented
18
24
  }
19
25
 
26
+ /**
27
+ * Opens the chrome login popup for guests (provider calls preventDefault).
28
+ * Returns true when the wishlist mutation should abort.
29
+ * Logged-in shoppers have no listener, so this returns false and the API proceeds.
30
+ */
31
+ export function requireLoginForWishlist(
32
+ dispatcher?: LoginPopupDispatcher | null
33
+ ): boolean {
34
+ return requestLoginPopup(dispatcher)
35
+ }
36
+
20
37
  export function promptLoginForWishlist(fallback: () => void) {
21
38
  if (!requestLoginPopup()) fallback()
22
39
  }
package/src/segment.css CHANGED
@@ -385,6 +385,92 @@ body.login-popup-open {
385
385
  text-underline-offset: 2px;
386
386
  }
387
387
 
388
+ .login-required-modal {
389
+ position: fixed;
390
+ inset: 0;
391
+ z-index: 99999;
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ padding: 1rem;
396
+ }
397
+
398
+ .login-required-modal__backdrop {
399
+ position: absolute;
400
+ inset: 0;
401
+ border: none;
402
+ background: rgba(0, 0, 0, 0.5);
403
+ cursor: pointer;
404
+ }
405
+
406
+ .login-required-modal__dialog {
407
+ position: relative;
408
+ width: min(24rem, 100%);
409
+ padding: 1.5rem;
410
+ text-align: center;
411
+ background: var(--color-surface, #fff);
412
+ border: 1px solid var(--color-cart-border, #e5e5e5);
413
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.16);
414
+ }
415
+
416
+ .login-required-modal__icon {
417
+ display: flex;
418
+ align-items: center;
419
+ justify-content: center;
420
+ width: 3rem;
421
+ height: 3rem;
422
+ margin: 0 auto 1rem;
423
+ border-radius: 999px;
424
+ color: var(--color-brand-accent, #111);
425
+ background: color-mix(in srgb, var(--color-brand-accent, #111) 10%, transparent);
426
+ }
427
+
428
+ .login-required-modal__title {
429
+ margin: 0 0 0.5rem;
430
+ font-size: 1.125rem;
431
+ font-weight: 700;
432
+ letter-spacing: 0.08em;
433
+ text-transform: uppercase;
434
+ color: var(--color-heading, #111);
435
+ }
436
+
437
+ .login-required-modal__message {
438
+ margin: 0 0 1.25rem;
439
+ font-size: 0.875rem;
440
+ color: var(--color-muted, #666);
441
+ }
442
+
443
+ .login-required-modal__google {
444
+ margin-bottom: 1rem;
445
+ }
446
+
447
+ .login-required-modal__actions {
448
+ display: flex;
449
+ gap: 0.5rem;
450
+ }
451
+
452
+ .login-required-modal__btn {
453
+ flex: 1;
454
+ padding: 0.625rem 0.75rem;
455
+ font-size: 0.75rem;
456
+ font-weight: 700;
457
+ letter-spacing: 0.16em;
458
+ text-transform: uppercase;
459
+ cursor: pointer;
460
+ }
461
+
462
+ .login-required-modal__btn--ghost {
463
+ border: 1px solid var(--color-cart-border, #e5e5e5);
464
+ background: transparent;
465
+ color: var(--color-muted, #666);
466
+ }
467
+
468
+ .login-required-modal__btn--primary {
469
+ border: none;
470
+ background: var(--color-brand-primary, #111);
471
+ color: var(--color-inverse, #fff);
472
+ }
473
+
388
474
  @media (min-width: 768px) {
389
475
  .login-popup__panel {
390
476
  flex-direction: row;