@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 +1 -1
- package/src/index.ts +1 -0
- package/src/login-popup-provider.tsx +2 -1
- package/src/login-required-modal.tsx +12 -12
- package/src/request-login.test.ts +48 -3
- package/src/request-login.ts +20 -3
- package/src/segment.css +86 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -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
|
-
|
|
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="
|
|
54
|
+
<div className="login-required-modal">
|
|
53
55
|
<button
|
|
54
56
|
type="button"
|
|
55
|
-
className="
|
|
57
|
+
className="login-required-modal__backdrop"
|
|
56
58
|
aria-label="Close login required dialog"
|
|
57
59
|
onClick={onClose}
|
|
58
60
|
/>
|
|
59
|
-
<div className="
|
|
60
|
-
<div className="
|
|
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="
|
|
67
|
-
|
|
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="
|
|
71
|
+
<div className="login-required-modal__google">
|
|
72
72
|
<GoogleAuthSection countryCode={countryCode} />
|
|
73
73
|
</div>
|
|
74
74
|
|
|
75
|
-
<div className="
|
|
75
|
+
<div className="login-required-modal__actions">
|
|
76
76
|
<button
|
|
77
77
|
type="button"
|
|
78
78
|
onClick={onClose}
|
|
79
|
-
className="
|
|
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="
|
|
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 {
|
|
3
|
+
import {
|
|
4
|
+
isAuthRequiredError,
|
|
5
|
+
LOGIN_POPUP_OPEN_EVENT,
|
|
6
|
+
requestLoginPopup,
|
|
7
|
+
requireLoginForWishlist,
|
|
8
|
+
} from "./request-login.ts"
|
|
4
9
|
|
|
5
|
-
const
|
|
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
|
|
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
|
+
})
|
package/src/request-login.ts
CHANGED
|
@@ -10,13 +10,30 @@ export function isAuthRequiredError(error?: string | null): boolean {
|
|
|
10
10
|
)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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;
|