payload-subscribers-plugin 0.0.8 → 0.0.10
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/README.md +234 -24
- package/dist/components/app/RequestMagicLink.d.ts +21 -4
- package/dist/components/app/RequestMagicLink.js +10 -42
- package/dist/components/app/RequestMagicLink.js.map +1 -1
- package/dist/components/app/RequestOrSubscribe.d.ts +21 -4
- package/dist/components/app/RequestOrSubscribe.js +5 -8
- package/dist/components/app/RequestOrSubscribe.js.map +1 -1
- package/dist/components/app/SelectOptInChannels.d.ts +26 -3
- package/dist/components/app/SelectOptInChannels.js +4 -1
- package/dist/components/app/SelectOptInChannels.js.map +1 -1
- package/dist/components/app/Subscribe.d.ts +26 -5
- package/dist/components/app/Subscribe.js +63 -81
- package/dist/components/app/Subscribe.js.map +1 -1
- package/dist/components/app/SubscriberMenu.d.ts +18 -6
- package/dist/components/app/SubscriberMenu.js +9 -8
- package/dist/components/app/SubscriberMenu.js.map +1 -1
- package/dist/components/app/Unsubscribe.d.ts +45 -0
- package/dist/components/app/Unsubscribe.js +91 -0
- package/dist/components/app/Unsubscribe.js.map +1 -0
- package/dist/components/app/VerifyMagicLink.d.ts +32 -14
- package/dist/components/app/VerifyMagicLink.js +60 -114
- package/dist/components/app/VerifyMagicLink.js.map +1 -1
- package/dist/contexts/SubscriberProvider.js +1 -1
- package/dist/contexts/SubscriberProvider.js.map +1 -1
- package/dist/endpoints/requestMagicLink.d.ts +6 -2
- package/dist/endpoints/requestMagicLink.js +26 -19
- package/dist/endpoints/requestMagicLink.js.map +1 -1
- package/dist/endpoints/subscribe.d.ts +6 -2
- package/dist/endpoints/subscribe.js +29 -13
- package/dist/endpoints/subscribe.js.map +1 -1
- package/dist/endpoints/unsubscribe.d.ts +21 -0
- package/dist/endpoints/unsubscribe.js +120 -0
- package/dist/endpoints/unsubscribe.js.map +1 -0
- package/dist/endpoints/verifyMagicLink.js +3 -2
- package/dist/endpoints/verifyMagicLink.js.map +1 -1
- package/dist/exports/ui.d.ts +2 -0
- package/dist/exports/ui.js +1 -0
- package/dist/exports/ui.js.map +1 -1
- package/dist/helpers/token.d.ts +3 -0
- package/dist/helpers/token.js +12 -2
- package/dist/helpers/token.js.map +1 -1
- package/dist/helpers/utilities.d.ts +1 -0
- package/dist/helpers/utilities.js +6 -0
- package/dist/helpers/utilities.js.map +1 -0
- package/dist/hooks/useRequestMagicLink.d.ts +35 -0
- package/dist/hooks/useRequestMagicLink.js +61 -0
- package/dist/hooks/useRequestMagicLink.js.map +1 -0
- package/dist/hooks/useSubscribe.d.ts +38 -0
- package/dist/hooks/useSubscribe.js +62 -0
- package/dist/hooks/useSubscribe.js.map +1 -0
- package/dist/hooks/useUnsubscribe.d.ts +43 -0
- package/dist/hooks/useUnsubscribe.js +86 -0
- package/dist/hooks/useUnsubscribe.js.map +1 -0
- package/dist/hooks/useVerifyMagicLink.d.ts +31 -0
- package/dist/hooks/useVerifyMagicLink.js +74 -0
- package/dist/hooks/useVerifyMagicLink.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useSearchParams } from 'next/navigation.js';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
import { useUnsubscribe } from '../../hooks/useUnsubscribe.js';
|
|
6
|
+
import { mergeClassNames } from './helpers.js';
|
|
7
|
+
import styles from './shared.module.css';
|
|
8
|
+
/**
|
|
9
|
+
* Handles the unsubscribe action, for use with unsubscribe URLs in emails, etc.
|
|
10
|
+
* Uses URL params email and hash to call POST /api/unsubscribe. Displays children after attempt.
|
|
11
|
+
*
|
|
12
|
+
* @param props - Component props (see IUnsubscribe)
|
|
13
|
+
* @param props.children - Optional React nodes rendered after unsubscribe is attempted
|
|
14
|
+
* @param props.classNames - Optional class overrides for the component elements
|
|
15
|
+
* @param props.handleUnsubscribe - Callback when unsubscribe is attempted (success or error)
|
|
16
|
+
* @returns Loading status, result message, and children
|
|
17
|
+
*/ export const Unsubscribe = ({ children, classNames = {
|
|
18
|
+
button: '',
|
|
19
|
+
container: '',
|
|
20
|
+
emailInput: '',
|
|
21
|
+
error: '',
|
|
22
|
+
form: '',
|
|
23
|
+
loading: '',
|
|
24
|
+
message: ''
|
|
25
|
+
}, handleUnsubscribe })=>{
|
|
26
|
+
const { isError, isLoading, result, unsubscribe } = useUnsubscribe({
|
|
27
|
+
handleUnsubscribe
|
|
28
|
+
});
|
|
29
|
+
const searchParams = useSearchParams();
|
|
30
|
+
const email = searchParams.get('email');
|
|
31
|
+
const hash = searchParams.get('hash');
|
|
32
|
+
useEffect(()=>{
|
|
33
|
+
async function callUnsubscribe() {
|
|
34
|
+
if (email && hash) {
|
|
35
|
+
await unsubscribe({
|
|
36
|
+
email,
|
|
37
|
+
hash
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
void callUnsubscribe();
|
|
42
|
+
}, [
|
|
43
|
+
email,
|
|
44
|
+
hash,
|
|
45
|
+
unsubscribe
|
|
46
|
+
]);
|
|
47
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
48
|
+
className: mergeClassNames([
|
|
49
|
+
'subscribers-callUnsubscribe subscribers-container',
|
|
50
|
+
styles.container,
|
|
51
|
+
classNames.container
|
|
52
|
+
]),
|
|
53
|
+
children: [
|
|
54
|
+
isLoading && /*#__PURE__*/ _jsx("p", {
|
|
55
|
+
className: mergeClassNames([
|
|
56
|
+
'subscribers-loading',
|
|
57
|
+
styles.loading,
|
|
58
|
+
classNames.loading
|
|
59
|
+
]),
|
|
60
|
+
children: "unsubscribing..."
|
|
61
|
+
}),
|
|
62
|
+
!isLoading && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
63
|
+
children: [
|
|
64
|
+
/*#__PURE__*/ _jsx("p", {
|
|
65
|
+
className: mergeClassNames([
|
|
66
|
+
'subscribers-message',
|
|
67
|
+
styles.message,
|
|
68
|
+
classNames.message,
|
|
69
|
+
isError ? [
|
|
70
|
+
'subscribers-error',
|
|
71
|
+
styles.error,
|
|
72
|
+
classNames.error
|
|
73
|
+
] : []
|
|
74
|
+
]),
|
|
75
|
+
children: result
|
|
76
|
+
}),
|
|
77
|
+
/*#__PURE__*/ _jsx("div", {
|
|
78
|
+
className: mergeClassNames([
|
|
79
|
+
'subscribers-form',
|
|
80
|
+
styles.form,
|
|
81
|
+
classNames.form
|
|
82
|
+
]),
|
|
83
|
+
children: children
|
|
84
|
+
})
|
|
85
|
+
]
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=Unsubscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/app/Unsubscribe.tsx"],"sourcesContent":["'use client'\n\nimport { useSearchParams } from 'next/navigation.js'\nimport { useEffect } from 'react'\n\nimport type { UnsubscribeResponse } from '../../endpoints/unsubscribe.js'\n\nexport { UnsubscribeResponse }\nimport { useSubscriber } from '../../exports/ui.js'\nimport { useUnsubscribe } from '../../hooks/useUnsubscribe.js'\nimport { mergeClassNames } from './helpers.js'\nimport styles from './shared.module.css'\n\n// const payload = await getPayload({\n// config: configPromise,\n// })\n\n/**\n * Props for the Unsubscribe component.\n *\n * @property children - Optional React nodes rendered after unsubscribe is attempted\n * @property classNames - Optional CSS class overrides for the component elements\n * @property handleUnsubscribe - Callback when unsubscribe is attempted (success or error)\n */\nexport interface IUnsubscribe {\n children?: React.ReactNode\n classNames?: UnsubscribeClasses\n handleUnsubscribe?: (result: UnsubscribeResponse) => void\n}\n\n/**\n * Optional CSS class overrides for Unsubscribe elements.\n *\n * @property button - Class for buttons\n * @property container - Class for the main container\n * @property emailInput - Class for the email input field\n * @property error - Class for error messages\n * @property form - Class for the form\n * @property loading - Class for loading state\n * @property message - Class for result message text\n */\nexport type UnsubscribeClasses = {\n button?: string\n container?: string\n emailInput?: string\n error?: string\n form?: string\n loading?: string\n message?: string\n}\n\n/**\n * Handles the unsubscribe action, for use with unsubscribe URLs in emails, etc.\n * Uses URL params email and hash to call POST /api/unsubscribe. Displays children after attempt.\n *\n * @param props - Component props (see IUnsubscribe)\n * @param props.children - Optional React nodes rendered after unsubscribe is attempted\n * @param props.classNames - Optional class overrides for the component elements\n * @param props.handleUnsubscribe - Callback when unsubscribe is attempted (success or error)\n * @returns Loading status, result message, and children\n */\nexport const Unsubscribe = ({\n children,\n classNames = {\n button: '',\n container: '',\n emailInput: '',\n error: '',\n form: '',\n loading: '',\n message: '',\n },\n handleUnsubscribe,\n}: IUnsubscribe) => {\n const { isError, isLoading, result, unsubscribe } = useUnsubscribe({ handleUnsubscribe })\n\n const searchParams = useSearchParams()\n const email = searchParams.get('email')\n const hash = searchParams.get('hash')\n\n useEffect(() => {\n async function callUnsubscribe() {\n if (email && hash) {\n await unsubscribe({ email, hash })\n }\n }\n void callUnsubscribe()\n }, [email, hash, unsubscribe])\n\n return (\n <div\n className={mergeClassNames([\n 'subscribers-callUnsubscribe subscribers-container',\n styles.container,\n classNames.container,\n ])}\n >\n {isLoading && (\n <p className={mergeClassNames(['subscribers-loading', styles.loading, classNames.loading])}>\n unsubscribing...\n </p>\n )}\n {!isLoading && (\n <>\n <p\n className={mergeClassNames([\n 'subscribers-message',\n styles.message,\n classNames.message,\n isError ? ['subscribers-error', styles.error, classNames.error] : [],\n ])}\n >\n {result}\n </p>\n <div className={mergeClassNames(['subscribers-form', styles.form, classNames.form])}>\n {children}\n </div>\n </>\n )}\n </div>\n )\n}\n"],"names":["useSearchParams","useEffect","useUnsubscribe","mergeClassNames","styles","Unsubscribe","children","classNames","button","container","emailInput","error","form","loading","message","handleUnsubscribe","isError","isLoading","result","unsubscribe","searchParams","email","get","hash","callUnsubscribe","div","className","p"],"mappings":"AAAA;;AAEA,SAASA,eAAe,QAAQ,qBAAoB;AACpD,SAASC,SAAS,QAAQ,QAAO;AAMjC,SAASC,cAAc,QAAQ,gCAA+B;AAC9D,SAASC,eAAe,QAAQ,eAAc;AAC9C,OAAOC,YAAY,sBAAqB;AAwCxC;;;;;;;;;CASC,GACD,OAAO,MAAMC,cAAc,CAAC,EAC1BC,QAAQ,EACRC,aAAa;IACXC,QAAQ;IACRC,WAAW;IACXC,YAAY;IACZC,OAAO;IACPC,MAAM;IACNC,SAAS;IACTC,SAAS;AACX,CAAC,EACDC,iBAAiB,EACJ;IACb,MAAM,EAAEC,OAAO,EAAEC,SAAS,EAAEC,MAAM,EAAEC,WAAW,EAAE,GAAGjB,eAAe;QAAEa;IAAkB;IAEvF,MAAMK,eAAepB;IACrB,MAAMqB,QAAQD,aAAaE,GAAG,CAAC;IAC/B,MAAMC,OAAOH,aAAaE,GAAG,CAAC;IAE9BrB,UAAU;QACR,eAAeuB;YACb,IAAIH,SAASE,MAAM;gBACjB,MAAMJ,YAAY;oBAAEE;oBAAOE;gBAAK;YAClC;QACF;QACA,KAAKC;IACP,GAAG;QAACH;QAAOE;QAAMJ;KAAY;IAE7B,qBACE,MAACM;QACCC,WAAWvB,gBAAgB;YACzB;YACAC,OAAOK,SAAS;YAChBF,WAAWE,SAAS;SACrB;;YAEAQ,2BACC,KAACU;gBAAED,WAAWvB,gBAAgB;oBAAC;oBAAuBC,OAAOS,OAAO;oBAAEN,WAAWM,OAAO;iBAAC;0BAAG;;YAI7F,CAACI,2BACA;;kCACE,KAACU;wBACCD,WAAWvB,gBAAgB;4BACzB;4BACAC,OAAOU,OAAO;4BACdP,WAAWO,OAAO;4BAClBE,UAAU;gCAAC;gCAAqBZ,OAAOO,KAAK;gCAAEJ,WAAWI,KAAK;6BAAC,GAAG,EAAE;yBACrE;kCAEAO;;kCAEH,KAACO;wBAAIC,WAAWvB,gBAAgB;4BAAC;4BAAoBC,OAAOQ,IAAI;4BAAEL,WAAWK,IAAI;yBAAC;kCAC/EN;;;;;;AAMb,EAAC"}
|
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
import type { RequestMagicLinkResponse } from '
|
|
1
|
+
import type { RequestMagicLinkResponse } from '../../endpoints/requestMagicLink.js';
|
|
2
2
|
import type { VerifyMagicLinkResponse } from '../../endpoints/verifyMagicLink.js';
|
|
3
3
|
export { VerifyMagicLinkResponse };
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Props for the VerifyMagicLink component.
|
|
6
|
+
*
|
|
7
|
+
* @property children - Optional React nodes rendered after the verify action (e.g. when error)
|
|
8
|
+
* @property classNames - Optional CSS class overrides for the component elements
|
|
9
|
+
* @property handleMagicLinkRequested - Callback when a new magic link is requested
|
|
10
|
+
* @property handleMagicLinkVerified - Callback when the magic link is verified
|
|
11
|
+
* @property verifyData - Optional data for verification (e.g. email/token from URL)
|
|
12
|
+
*/
|
|
5
13
|
export interface IVerifyMagicLink {
|
|
6
14
|
children?: React.ReactNode;
|
|
7
15
|
classNames?: VerifyMagicLinkClasses;
|
|
8
16
|
handleMagicLinkRequested?: (result: RequestMagicLinkResponse) => void;
|
|
9
|
-
handleMagicLinkVerified?: (result:
|
|
10
|
-
|
|
11
|
-
name?: string;
|
|
12
|
-
onClick?: () => any;
|
|
13
|
-
text?: string;
|
|
14
|
-
}) => React.ReactNode;
|
|
15
|
-
verifyUrl?: string | URL;
|
|
17
|
+
handleMagicLinkVerified?: (result: string) => void;
|
|
18
|
+
verifyData?: string;
|
|
16
19
|
}
|
|
17
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Optional CSS class overrides for VerifyMagicLink elements.
|
|
22
|
+
*
|
|
23
|
+
* @property button - Class for buttons
|
|
24
|
+
* @property container - Class for the main container
|
|
25
|
+
* @property emailInput - Class for the email input field
|
|
26
|
+
* @property error - Class for error messages
|
|
27
|
+
* @property form - Class for the form
|
|
28
|
+
* @property loading - Class for loading state
|
|
29
|
+
* @property message - Class for result message text
|
|
30
|
+
*/
|
|
18
31
|
export type VerifyMagicLinkClasses = {
|
|
19
32
|
button?: string;
|
|
20
33
|
container?: string;
|
|
@@ -27,9 +40,14 @@ export type VerifyMagicLinkClasses = {
|
|
|
27
40
|
/**
|
|
28
41
|
* Handles the verify step of magic-link flow. When URL has email and token query params, calls
|
|
29
42
|
* POST /api/verifyToken to verify and log in; otherwise shows RequestMagicLink. Supports
|
|
30
|
-
* "Request another magic link"
|
|
43
|
+
* "Request another magic link" and optional callbacks.
|
|
31
44
|
*
|
|
32
|
-
* @param props -
|
|
33
|
-
* @
|
|
45
|
+
* @param props - Component props (see IVerifyMagicLink)
|
|
46
|
+
* @param props.children - Optional React nodes rendered after the verify action (e.g. when error)
|
|
47
|
+
* @param props.classNames - Optional class overrides for the component elements
|
|
48
|
+
* @param props.handleMagicLinkRequested - Callback when a new magic link is requested
|
|
49
|
+
* @param props.handleMagicLinkVerified - Callback when the magic link is verified
|
|
50
|
+
* @param props.verifyData - Optional data for verification (e.g. email/token from URL)
|
|
51
|
+
* @returns Loading status, error/result message, and children. Shows RequestMagicLink when no token/email.
|
|
34
52
|
*/
|
|
35
|
-
export declare const VerifyMagicLink: ({ children, classNames, handleMagicLinkRequested, handleMagicLinkVerified,
|
|
53
|
+
export declare const VerifyMagicLink: ({ children, classNames, handleMagicLinkRequested, handleMagicLinkVerified, verifyData, }: IVerifyMagicLink) => import("react").JSX.Element;
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { PayloadSDK } from '@payloadcms/sdk';
|
|
4
3
|
import { useSearchParams } from 'next/navigation.js';
|
|
5
|
-
import {
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
6
5
|
import { RequestMagicLink, useSubscriber } from '../../exports/ui.js';
|
|
7
|
-
import {
|
|
6
|
+
import { useRequestMagicLink } from '../../hooks/useRequestMagicLink.js';
|
|
7
|
+
import { useVerifyMagicLink } from '../../hooks/useVerifyMagicLink.js';
|
|
8
8
|
import { mergeClassNames } from './helpers.js';
|
|
9
9
|
import styles from './shared.module.css';
|
|
10
10
|
/**
|
|
11
11
|
* Handles the verify step of magic-link flow. When URL has email and token query params, calls
|
|
12
12
|
* POST /api/verifyToken to verify and log in; otherwise shows RequestMagicLink. Supports
|
|
13
|
-
* "Request another magic link"
|
|
13
|
+
* "Request another magic link" and optional callbacks.
|
|
14
14
|
*
|
|
15
|
-
* @param props -
|
|
16
|
-
* @
|
|
15
|
+
* @param props - Component props (see IVerifyMagicLink)
|
|
16
|
+
* @param props.children - Optional React nodes rendered after the verify action (e.g. when error)
|
|
17
|
+
* @param props.classNames - Optional class overrides for the component elements
|
|
18
|
+
* @param props.handleMagicLinkRequested - Callback when a new magic link is requested
|
|
19
|
+
* @param props.handleMagicLinkVerified - Callback when the magic link is verified
|
|
20
|
+
* @param props.verifyData - Optional data for verification (e.g. email/token from URL)
|
|
21
|
+
* @returns Loading status, error/result message, and children. Shows RequestMagicLink when no token/email.
|
|
17
22
|
*/ export const VerifyMagicLink = ({ children, classNames = {
|
|
18
23
|
button: '',
|
|
19
24
|
container: '',
|
|
@@ -22,122 +27,57 @@ import styles from './shared.module.css';
|
|
|
22
27
|
form: '',
|
|
23
28
|
loading: '',
|
|
24
29
|
message: ''
|
|
25
|
-
}, handleMagicLinkRequested, handleMagicLinkVerified,
|
|
26
|
-
className: mergeClassNames([
|
|
27
|
-
'subscribers-button',
|
|
28
|
-
styles.button,
|
|
29
|
-
classNames.button
|
|
30
|
-
]),
|
|
31
|
-
name: name,
|
|
32
|
-
onClick: onClick,
|
|
33
|
-
type: "button",
|
|
34
|
-
children: text
|
|
35
|
-
}), verifyUrl })=>{
|
|
36
|
-
if (typeof verifyUrl == 'string') {
|
|
37
|
-
verifyUrl = new URL(verifyUrl);
|
|
38
|
-
}
|
|
39
|
-
const { serverURL } = useServerUrl();
|
|
40
|
-
const { // refreshSubscriber,
|
|
41
|
-
subscriber } = useSubscriber();
|
|
30
|
+
}, handleMagicLinkRequested, handleMagicLinkVerified, verifyData })=>{
|
|
42
31
|
const searchParams = useSearchParams();
|
|
43
32
|
const email = searchParams.get('email');
|
|
44
33
|
const token = searchParams.get('token');
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!email || !token) {
|
|
51
|
-
console.info('Invalid input');
|
|
52
|
-
return {
|
|
53
|
-
error: 'Invalid input'
|
|
54
|
-
};
|
|
34
|
+
const { subscriber } = useSubscriber();
|
|
35
|
+
const { isError: verifyIsError, isLoading: verifyIsLoading, result: verifyResult, verify } = useVerifyMagicLink();
|
|
36
|
+
useEffect(()=>{
|
|
37
|
+
async function asyncVerify() {
|
|
38
|
+
await verify();
|
|
55
39
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const verifyEndpointResult = await fetch(serverURL + '/api/verifyToken', {
|
|
62
|
-
body: JSON.stringify({
|
|
63
|
-
email,
|
|
64
|
-
token
|
|
65
|
-
}),
|
|
66
|
-
method: 'POST'
|
|
67
|
-
});
|
|
68
|
-
// return verifyEndpointResult
|
|
69
|
-
if (verifyEndpointResult && verifyEndpointResult.json) {
|
|
70
|
-
console.log(1);
|
|
71
|
-
const resultJson = await verifyEndpointResult.json();
|
|
72
|
-
return {
|
|
73
|
-
error: resultJson.error,
|
|
74
|
-
message: resultJson.message
|
|
75
|
-
};
|
|
76
|
-
} else if (verifyEndpointResult && verifyEndpointResult.text) {
|
|
77
|
-
console.log(2);
|
|
78
|
-
const resultText = await verifyEndpointResult.text();
|
|
79
|
-
return {
|
|
80
|
-
error: resultText
|
|
81
|
-
};
|
|
82
|
-
} else {
|
|
83
|
-
console.log(3);
|
|
84
|
-
return {
|
|
85
|
-
error: verifyEndpointResult.status
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.log('catch');
|
|
90
|
-
return {
|
|
91
|
-
error
|
|
92
|
-
};
|
|
40
|
+
if (!subscriber) {
|
|
41
|
+
void asyncVerify();
|
|
42
|
+
} else {
|
|
43
|
+
setIsError(false);
|
|
44
|
+
setResult('Already logged in');
|
|
93
45
|
}
|
|
94
46
|
}, [
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
token
|
|
47
|
+
subscriber,
|
|
48
|
+
verify
|
|
98
49
|
]);
|
|
99
50
|
useEffect(()=>{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
if (!subscriber) {
|
|
107
|
-
void verify();
|
|
51
|
+
setResult(verifyResult);
|
|
52
|
+
setIsError(verifyIsError);
|
|
53
|
+
setIsLoading(verifyIsLoading);
|
|
54
|
+
if (!verifyIsError && handleMagicLinkVerified) {
|
|
55
|
+
handleMagicLinkVerified(verifyResult);
|
|
108
56
|
}
|
|
109
57
|
}, [
|
|
110
|
-
callVerify,
|
|
111
|
-
serverURL,
|
|
112
|
-
email,
|
|
113
58
|
handleMagicLinkVerified,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
59
|
+
verifyResult,
|
|
60
|
+
verifyIsError,
|
|
61
|
+
verifyIsLoading
|
|
117
62
|
]);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
// const resultText = await emailResult.text()
|
|
139
|
-
setResult('An error occured. Please try again.');
|
|
140
|
-
setIsError(true);
|
|
63
|
+
const [result, setResult] = useState();
|
|
64
|
+
const [isError, setIsError] = useState(false);
|
|
65
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
66
|
+
const { result: requestResult, sendMagicLink, status: requestStatus } = useRequestMagicLink({
|
|
67
|
+
handleMagicLinkRequested,
|
|
68
|
+
verifyData
|
|
69
|
+
});
|
|
70
|
+
useEffect(()=>{
|
|
71
|
+
setIsError(requestStatus == 'error');
|
|
72
|
+
setResult(requestResult);
|
|
73
|
+
setIsLoading(false);
|
|
74
|
+
}, [
|
|
75
|
+
requestResult,
|
|
76
|
+
requestStatus
|
|
77
|
+
]);
|
|
78
|
+
const handleRequestAnother = ()=>{
|
|
79
|
+
if (email) {
|
|
80
|
+
void sendMagicLink(email);
|
|
141
81
|
}
|
|
142
82
|
};
|
|
143
83
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
@@ -152,7 +92,7 @@ import styles from './shared.module.css';
|
|
|
152
92
|
classNames.container
|
|
153
93
|
]),
|
|
154
94
|
children: [
|
|
155
|
-
|
|
95
|
+
isLoading && /*#__PURE__*/ _jsx("p", {
|
|
156
96
|
className: mergeClassNames([
|
|
157
97
|
'subscribers-loading',
|
|
158
98
|
styles.loading,
|
|
@@ -160,7 +100,7 @@ import styles from './shared.module.css';
|
|
|
160
100
|
]),
|
|
161
101
|
children: "verifying..."
|
|
162
102
|
}),
|
|
163
|
-
result && /*#__PURE__*/ _jsx("p", {
|
|
103
|
+
!isLoading && result && /*#__PURE__*/ _jsx("p", {
|
|
164
104
|
className: mergeClassNames([
|
|
165
105
|
'subscribers-message',
|
|
166
106
|
styles.message,
|
|
@@ -180,10 +120,16 @@ import styles from './shared.module.css';
|
|
|
180
120
|
classNames.form
|
|
181
121
|
]),
|
|
182
122
|
children: [
|
|
183
|
-
result && isError &&
|
|
123
|
+
result && isError && /*#__PURE__*/ _jsx("button", {
|
|
124
|
+
className: mergeClassNames([
|
|
125
|
+
'subscribers-button',
|
|
126
|
+
styles.button,
|
|
127
|
+
classNames.button
|
|
128
|
+
]),
|
|
184
129
|
name: 'request',
|
|
185
130
|
onClick: handleRequestAnother,
|
|
186
|
-
|
|
131
|
+
type: "button",
|
|
132
|
+
children: 'Request another magic link'
|
|
187
133
|
}),
|
|
188
134
|
result && children
|
|
189
135
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/app/VerifyMagicLink.tsx"],"sourcesContent":["'use client'\n\nimport { PayloadSDK } from '@payloadcms/sdk'\nimport { useSearchParams } from 'next/navigation.js'\nimport { useCallback, useEffect, useState } from 'react'\n\nimport type { RequestMagicLinkResponse } from '../..//endpoints/requestMagicLink.js'\nimport type { Config } from '../../copied/payload-types.js'\nimport type { VerifyMagicLinkResponse } from '../../endpoints/verifyMagicLink.js'\n\nexport { VerifyMagicLinkResponse }\nimport { RequestMagicLink, useSubscriber } from '../../exports/ui.js'\nimport { useServerUrl } from '../../react-hooks/useServerUrl.js'\nimport { mergeClassNames } from './helpers.js'\nimport styles from './shared.module.css'\n\n// const payload = await getPayload({\n// config: configPromise,\n// })\n\n/** Props for the VerifyMagicLink component. */\nexport interface IVerifyMagicLink {\n children?: React.ReactNode\n classNames?: VerifyMagicLinkClasses\n handleMagicLinkRequested?: (result: RequestMagicLinkResponse) => void\n handleMagicLinkVerified?: (result: VerifyMagicLinkResponse) => void\n renderButton?: (props: { name?: string; onClick?: () => any; text?: string }) => React.ReactNode\n verifyUrl?: string | URL\n}\n\n/** Optional CSS class overrides for VerifyMagicLink elements. */\nexport type VerifyMagicLinkClasses = {\n button?: string\n container?: string\n emailInput?: string\n error?: string\n form?: string\n loading?: string\n message?: string\n}\n\n/**\n * Handles the verify step of magic-link flow. When URL has email and token query params, calls\n * POST /api/verifyToken to verify and log in; otherwise shows RequestMagicLink. Supports\n * \"Request another magic link\" via renderButton and optional callbacks.\n *\n * @param props - See IVerifyMagicLink\n * @returns RequestMagicLink when no token/email; otherwise verifying state, result message, and optional button/children\n */\nexport const VerifyMagicLink = ({\n children,\n classNames = {\n button: '',\n container: '',\n emailInput: '',\n error: '',\n form: '',\n loading: '',\n message: '',\n },\n handleMagicLinkRequested,\n handleMagicLinkVerified,\n renderButton = ({ name, onClick, text }) => (\n <button\n className={mergeClassNames(['subscribers-button', styles.button, classNames.button])}\n name={name}\n onClick={onClick}\n type=\"button\"\n >\n {text}\n </button>\n ),\n verifyUrl,\n}: IVerifyMagicLink) => {\n if (typeof verifyUrl == 'string') {\n verifyUrl = new URL(verifyUrl)\n }\n const { serverURL } = useServerUrl()\n const {\n // refreshSubscriber,\n subscriber,\n } = useSubscriber()\n\n const searchParams = useSearchParams()\n const email = searchParams.get('email')\n const token = searchParams.get('token')\n\n const [result, setResult] = useState<string>()\n const [isError, setIsError] = useState<boolean>(false)\n // const [email, setEmail] = useState('')\n\n const { refreshSubscriber } = useSubscriber()\n\n const callVerify = useCallback(async () => {\n if (!email || !token) {\n console.info('Invalid input')\n return { error: 'Invalid input' }\n }\n try {\n // I tried using PayloadSDK.request, but when the endpoint\n // returns a not-okay status, PayloadSDK.request returns its\n // own \"Bad request\" error, and doesn't share the endpoint\n // result data.\n const verifyEndpointResult = await fetch(serverURL + '/api/verifyToken', {\n body: JSON.stringify({\n email,\n token,\n }),\n method: 'POST',\n })\n\n // return verifyEndpointResult\n if (verifyEndpointResult && verifyEndpointResult.json) {\n console.log(1)\n const resultJson = await verifyEndpointResult.json()\n return { error: resultJson.error, message: resultJson.message }\n } else if (verifyEndpointResult && verifyEndpointResult.text) {\n console.log(2)\n const resultText = await verifyEndpointResult.text()\n return { error: resultText }\n } else {\n console.log(3)\n return { error: verifyEndpointResult.status }\n }\n } catch (error: unknown) {\n console.log('catch')\n return { error }\n }\n }, [email, serverURL, token])\n\n useEffect(() => {\n async function verify() {\n const { error, message } = await callVerify()\n setResult(message || `An error occured. Please try again. (${error})`)\n setIsError(error && !message)\n // console.info('callVerify not okay', { error, message })\n }\n if (!subscriber) {\n void verify()\n }\n }, [callVerify, serverURL, email, handleMagicLinkVerified, refreshSubscriber, subscriber, token])\n\n const handleRequestAnother = async () => {\n const sdk = new PayloadSDK<Config>({\n baseURL: serverURL || '',\n })\n\n const emailResult = await sdk.request({\n json: {\n email,\n verifyUrl: verifyUrl?.href,\n },\n method: 'POST',\n path: '/api/emailToken',\n })\n if (emailResult.ok) {\n const resultJson = await emailResult.json()\n setResult('An email has been sent containing your magic link.')\n setIsError(false)\n if (handleMagicLinkRequested) {\n handleMagicLinkRequested(resultJson)\n }\n } else {\n // const resultText = await emailResult.text()\n setResult('An error occured. Please try again.')\n setIsError(true)\n }\n }\n\n return (\n <>\n {(!email || !token) && <RequestMagicLink classNames={classNames} />}\n {email && token && (\n <div\n className={mergeClassNames([\n 'subscribers-verify subscribers-container',\n styles.container,\n classNames.container,\n ])}\n >\n {!result && (\n <p\n className={mergeClassNames([\n 'subscribers-loading',\n styles.loading,\n classNames.loading,\n ])}\n >\n verifying...\n </p>\n )}\n {result && (\n <p\n className={mergeClassNames([\n 'subscribers-message',\n styles.message,\n classNames.message,\n isError ? ['subscribers-error', styles.error, classNames.error] : [],\n ])}\n >\n {result}\n </p>\n )}\n <div className={mergeClassNames(['subscribers-form', styles.form, classNames.form])}>\n {result &&\n isError &&\n renderButton &&\n renderButton({\n name: 'request',\n onClick: handleRequestAnother,\n text: 'Request another magic link',\n })}\n {result && children}\n </div>\n </div>\n )}\n </>\n )\n}\n"],"names":["PayloadSDK","useSearchParams","useCallback","useEffect","useState","RequestMagicLink","useSubscriber","useServerUrl","mergeClassNames","styles","VerifyMagicLink","children","classNames","button","container","emailInput","error","form","loading","message","handleMagicLinkRequested","handleMagicLinkVerified","renderButton","name","onClick","text","className","type","verifyUrl","URL","serverURL","subscriber","searchParams","email","get","token","result","setResult","isError","setIsError","refreshSubscriber","callVerify","console","info","verifyEndpointResult","fetch","body","JSON","stringify","method","json","log","resultJson","resultText","status","verify","handleRequestAnother","sdk","baseURL","emailResult","request","href","path","ok","div","p"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/components/app/VerifyMagicLink.tsx"],"sourcesContent":["'use client'\n\nimport { useSearchParams } from 'next/navigation.js'\nimport { useEffect, useState } from 'react'\n\nimport type { RequestMagicLinkResponse } from '../../endpoints/requestMagicLink.js'\nimport type { VerifyMagicLinkResponse } from '../../endpoints/verifyMagicLink.js'\n\nexport { VerifyMagicLinkResponse }\nimport { RequestMagicLink, useSubscriber } from '../../exports/ui.js'\nimport { useRequestMagicLink } from '../../hooks/useRequestMagicLink.js'\nimport { useVerifyMagicLink } from '../../hooks/useVerifyMagicLink.js'\nimport { mergeClassNames } from './helpers.js'\nimport styles from './shared.module.css'\n\n// const payload = await getPayload({\n// config: configPromise,\n// })\n\n/**\n * Props for the VerifyMagicLink component.\n *\n * @property children - Optional React nodes rendered after the verify action (e.g. when error)\n * @property classNames - Optional CSS class overrides for the component elements\n * @property handleMagicLinkRequested - Callback when a new magic link is requested\n * @property handleMagicLinkVerified - Callback when the magic link is verified\n * @property verifyData - Optional data for verification (e.g. email/token from URL)\n */\nexport interface IVerifyMagicLink {\n children?: React.ReactNode\n classNames?: VerifyMagicLinkClasses\n handleMagicLinkRequested?: (result: RequestMagicLinkResponse) => void\n handleMagicLinkVerified?: (result: string) => void\n verifyData?: string\n}\n\n/**\n * Optional CSS class overrides for VerifyMagicLink elements.\n *\n * @property button - Class for buttons\n * @property container - Class for the main container\n * @property emailInput - Class for the email input field\n * @property error - Class for error messages\n * @property form - Class for the form\n * @property loading - Class for loading state\n * @property message - Class for result message text\n */\nexport type VerifyMagicLinkClasses = {\n button?: string\n container?: string\n emailInput?: string\n error?: string\n form?: string\n loading?: string\n message?: string\n}\n\n/**\n * Handles the verify step of magic-link flow. When URL has email and token query params, calls\n * POST /api/verifyToken to verify and log in; otherwise shows RequestMagicLink. Supports\n * \"Request another magic link\" and optional callbacks.\n *\n * @param props - Component props (see IVerifyMagicLink)\n * @param props.children - Optional React nodes rendered after the verify action (e.g. when error)\n * @param props.classNames - Optional class overrides for the component elements\n * @param props.handleMagicLinkRequested - Callback when a new magic link is requested\n * @param props.handleMagicLinkVerified - Callback when the magic link is verified\n * @param props.verifyData - Optional data for verification (e.g. email/token from URL)\n * @returns Loading status, error/result message, and children. Shows RequestMagicLink when no token/email.\n */\nexport const VerifyMagicLink = ({\n children,\n classNames = {\n button: '',\n container: '',\n emailInput: '',\n error: '',\n form: '',\n loading: '',\n message: '',\n },\n handleMagicLinkRequested,\n handleMagicLinkVerified,\n verifyData,\n}: IVerifyMagicLink) => {\n const searchParams = useSearchParams()\n const email = searchParams.get('email')\n const token = searchParams.get('token')\n\n const { subscriber } = useSubscriber()\n\n const {\n isError: verifyIsError,\n isLoading: verifyIsLoading,\n result: verifyResult,\n verify,\n } = useVerifyMagicLink()\n\n useEffect(() => {\n async function asyncVerify() {\n await verify()\n }\n if (!subscriber) {\n void asyncVerify()\n } else {\n setIsError(false)\n setResult('Already logged in')\n }\n }, [subscriber, verify])\n\n useEffect(() => {\n setResult(verifyResult)\n setIsError(verifyIsError)\n setIsLoading(verifyIsLoading)\n if (!verifyIsError && handleMagicLinkVerified) {\n handleMagicLinkVerified(verifyResult)\n }\n }, [handleMagicLinkVerified, verifyResult, verifyIsError, verifyIsLoading])\n\n const [result, setResult] = useState<string>()\n const [isError, setIsError] = useState<boolean>(false)\n const [isLoading, setIsLoading] = useState<boolean>(false)\n\n const {\n result: requestResult,\n sendMagicLink,\n status: requestStatus,\n } = useRequestMagicLink({\n handleMagicLinkRequested,\n verifyData,\n })\n\n useEffect(() => {\n setIsError(requestStatus == 'error')\n setResult(requestResult)\n setIsLoading(false)\n }, [requestResult, requestStatus])\n\n const handleRequestAnother = () => {\n if (email) {\n void sendMagicLink(email)\n }\n }\n\n return (\n <>\n {(!email || !token) && <RequestMagicLink classNames={classNames} />}\n {email && token && (\n <div\n className={mergeClassNames([\n 'subscribers-verify subscribers-container',\n styles.container,\n classNames.container,\n ])}\n >\n {isLoading && (\n <p\n className={mergeClassNames([\n 'subscribers-loading',\n styles.loading,\n classNames.loading,\n ])}\n >\n verifying...\n </p>\n )}\n {!isLoading && result && (\n <p\n className={mergeClassNames([\n 'subscribers-message',\n styles.message,\n classNames.message,\n isError ? ['subscribers-error', styles.error, classNames.error] : [],\n ])}\n >\n {result}\n </p>\n )}\n <div className={mergeClassNames(['subscribers-form', styles.form, classNames.form])}>\n {result && isError && (\n <button\n className={mergeClassNames([\n 'subscribers-button',\n styles.button,\n classNames.button,\n ])}\n name={'request'}\n onClick={handleRequestAnother}\n type=\"button\"\n >\n {'Request another magic link'}\n </button>\n )}\n {result && children}\n </div>\n </div>\n )}\n </>\n )\n}\n"],"names":["useSearchParams","useEffect","useState","RequestMagicLink","useSubscriber","useRequestMagicLink","useVerifyMagicLink","mergeClassNames","styles","VerifyMagicLink","children","classNames","button","container","emailInput","error","form","loading","message","handleMagicLinkRequested","handleMagicLinkVerified","verifyData","searchParams","email","get","token","subscriber","isError","verifyIsError","isLoading","verifyIsLoading","result","verifyResult","verify","asyncVerify","setIsError","setResult","setIsLoading","requestResult","sendMagicLink","status","requestStatus","handleRequestAnother","div","className","p","name","onClick","type"],"mappings":"AAAA;;AAEA,SAASA,eAAe,QAAQ,qBAAoB;AACpD,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAM3C,SAASC,gBAAgB,EAAEC,aAAa,QAAQ,sBAAqB;AACrE,SAASC,mBAAmB,QAAQ,qCAAoC;AACxE,SAASC,kBAAkB,QAAQ,oCAAmC;AACtE,SAASC,eAAe,QAAQ,eAAc;AAC9C,OAAOC,YAAY,sBAAqB;AA4CxC;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC,kBAAkB,CAAC,EAC9BC,QAAQ,EACRC,aAAa;IACXC,QAAQ;IACRC,WAAW;IACXC,YAAY;IACZC,OAAO;IACPC,MAAM;IACNC,SAAS;IACTC,SAAS;AACX,CAAC,EACDC,wBAAwB,EACxBC,uBAAuB,EACvBC,UAAU,EACO;IACjB,MAAMC,eAAetB;IACrB,MAAMuB,QAAQD,aAAaE,GAAG,CAAC;IAC/B,MAAMC,QAAQH,aAAaE,GAAG,CAAC;IAE/B,MAAM,EAAEE,UAAU,EAAE,GAAGtB;IAEvB,MAAM,EACJuB,SAASC,aAAa,EACtBC,WAAWC,eAAe,EAC1BC,QAAQC,YAAY,EACpBC,MAAM,EACP,GAAG3B;IAEJL,UAAU;QACR,eAAeiC;YACb,MAAMD;QACR;QACA,IAAI,CAACP,YAAY;YACf,KAAKQ;QACP,OAAO;YACLC,WAAW;YACXC,UAAU;QACZ;IACF,GAAG;QAACV;QAAYO;KAAO;IAEvBhC,UAAU;QACRmC,UAAUJ;QACVG,WAAWP;QACXS,aAAaP;QACb,IAAI,CAACF,iBAAiBR,yBAAyB;YAC7CA,wBAAwBY;QAC1B;IACF,GAAG;QAACZ;QAAyBY;QAAcJ;QAAeE;KAAgB;IAE1E,MAAM,CAACC,QAAQK,UAAU,GAAGlC;IAC5B,MAAM,CAACyB,SAASQ,WAAW,GAAGjC,SAAkB;IAChD,MAAM,CAAC2B,WAAWQ,aAAa,GAAGnC,SAAkB;IAEpD,MAAM,EACJ6B,QAAQO,aAAa,EACrBC,aAAa,EACbC,QAAQC,aAAa,EACtB,GAAGpC,oBAAoB;QACtBc;QACAE;IACF;IAEApB,UAAU;QACRkC,WAAWM,iBAAiB;QAC5BL,UAAUE;QACVD,aAAa;IACf,GAAG;QAACC;QAAeG;KAAc;IAEjC,MAAMC,uBAAuB;QAC3B,IAAInB,OAAO;YACT,KAAKgB,cAAchB;QACrB;IACF;IAEA,qBACE;;YACI,CAAA,CAACA,SAAS,CAACE,KAAI,mBAAM,KAACtB;gBAAiBQ,YAAYA;;YACpDY,SAASE,uBACR,MAACkB;gBACCC,WAAWrC,gBAAgB;oBACzB;oBACAC,OAAOK,SAAS;oBAChBF,WAAWE,SAAS;iBACrB;;oBAEAgB,2BACC,KAACgB;wBACCD,WAAWrC,gBAAgB;4BACzB;4BACAC,OAAOS,OAAO;4BACdN,WAAWM,OAAO;yBACnB;kCACF;;oBAIF,CAACY,aAAaE,wBACb,KAACc;wBACCD,WAAWrC,gBAAgB;4BACzB;4BACAC,OAAOU,OAAO;4BACdP,WAAWO,OAAO;4BAClBS,UAAU;gCAAC;gCAAqBnB,OAAOO,KAAK;gCAAEJ,WAAWI,KAAK;6BAAC,GAAG,EAAE;yBACrE;kCAEAgB;;kCAGL,MAACY;wBAAIC,WAAWrC,gBAAgB;4BAAC;4BAAoBC,OAAOQ,IAAI;4BAAEL,WAAWK,IAAI;yBAAC;;4BAC/Ee,UAAUJ,yBACT,KAACf;gCACCgC,WAAWrC,gBAAgB;oCACzB;oCACAC,OAAOI,MAAM;oCACbD,WAAWC,MAAM;iCAClB;gCACDkC,MAAM;gCACNC,SAASL;gCACTM,MAAK;0CAEJ;;4BAGJjB,UAAUrB;;;;;;;AAMvB,EAAC"}
|
|
@@ -75,7 +75,7 @@ const SubscriberContext = /*#__PURE__*/ createContext(undefined);
|
|
|
75
75
|
void refreshSubscriber();
|
|
76
76
|
}, [
|
|
77
77
|
refreshSubscriber
|
|
78
|
-
]);
|
|
78
|
+
]);
|
|
79
79
|
// Memoize the value to prevent unnecessary re-renders in consumers
|
|
80
80
|
const contextValue = useMemo(()=>({
|
|
81
81
|
isLoaded,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/contexts/SubscriberProvider.tsx"],"sourcesContent":["'use client'\n\nimport { PayloadSDK } from '@payloadcms/sdk'\nimport { type ReactNode, useCallback, useEffect } from 'react'\nimport { createContext, useContext, useMemo, useState } from 'react'\n\nimport type { Config, Subscriber } from '../copied/payload-types.js'\n\nimport { useServerUrl } from '../react-hooks/useServerUrl.js'\n\n/** Value provided by SubscriberProvider: current subscriber, auth state, and actions. */\nexport type SubscriberContextType = {\n isLoaded: boolean\n logOut: () => void\n permissions: any\n refreshSubscriber: () => void\n subscriber: null | Subscriber\n}\n\nconst SubscriberContext = createContext<SubscriberContextType | undefined>(undefined)\n\n/** Props for SubscriberProvider. */\ninterface ProviderProps {\n children?: ReactNode\n}\n\n/**\n * Provider that fetches and holds the current subscriber auth state (via POST /api/subscriberAuth).\n * Exposes subscriber, permissions, refreshSubscriber, and logOut to descendants. Must wrap any\n * component that uses useSubscriber().\n *\n * @param props.children - React tree to wrap\n * @returns SubscriberContext.Provider with current auth state and actions\n */\nexport function SubscriberProvider({ children }: ProviderProps) {\n // eslint-disable-next-line\n const [subscriber, setSubscriber] = useState<null | (Subscriber & { optIns: string[] })>(null)\n\n const { serverURL } = useServerUrl()\n\n // Keep track of if the selection content is loaded yet\n const [isLoaded, setIsLoaded] = useState(false)\n\n const [permissions, setPermissions] = useState<any>()\n\n const refreshSubscriber = useCallback(async () => {\n const initSubscriber = async () => {\n setIsLoaded(false)\n try {\n const authResponse = await fetch('/api/subscriberAuth', {\n // body: JSON.stringify({}),\n method: 'POST',\n })\n\n if (authResponse.ok) {\n // Call the server function to get the user data\n const { permissions, subscriber } = await authResponse.json()\n // console.log(`subscriber = `, subscriber)\n // console.log(`permissions = `, permissions)\n setPermissions(permissions)\n setSubscriber(subscriber)\n } else {\n setPermissions(null)\n setSubscriber(null)\n }\n } catch (error: unknown) {\n console.log(`authResponse error`, error)\n }\n setIsLoaded(true)\n }\n await initSubscriber()\n }, [serverURL])\n\n const logOut = useCallback(async () => {\n setIsLoaded(false)\n try {\n // const sdk = new PayloadSDK<Config>({\n // baseURL: serverURL || '',\n // })\n // const logoutResponse = await sdk.request({\n // json: {},\n // method: 'POST',\n // path: '/api/logout',\n // })\n // Unsure why sdk isn't working here\n const logoutResponse = await fetch('/api/logout', {\n method: 'POST',\n })\n\n // console.log(`logoutResponse`, logoutResponse)\n\n if (logoutResponse.ok) {\n setSubscriber(null)\n setPermissions(null)\n }\n } catch (error: unknown) {\n console.log(`logoutResponse error`, error)\n }\n setIsLoaded(true)\n }, [])\n\n useEffect(() => {\n void refreshSubscriber()\n }, [refreshSubscriber])
|
|
1
|
+
{"version":3,"sources":["../../src/contexts/SubscriberProvider.tsx"],"sourcesContent":["'use client'\n\nimport { PayloadSDK } from '@payloadcms/sdk'\nimport { type ReactNode, useCallback, useEffect } from 'react'\nimport { createContext, useContext, useMemo, useState } from 'react'\n\nimport type { Config, Subscriber } from '../copied/payload-types.js'\n\nimport { useServerUrl } from '../react-hooks/useServerUrl.js'\n\n/** Value provided by SubscriberProvider: current subscriber, auth state, and actions. */\nexport type SubscriberContextType = {\n isLoaded: boolean\n logOut: () => void\n permissions: any\n refreshSubscriber: () => void\n subscriber: null | Subscriber\n}\n\nconst SubscriberContext = createContext<SubscriberContextType | undefined>(undefined)\n\n/** Props for SubscriberProvider. */\ninterface ProviderProps {\n children?: ReactNode\n}\n\n/**\n * Provider that fetches and holds the current subscriber auth state (via POST /api/subscriberAuth).\n * Exposes subscriber, permissions, refreshSubscriber, and logOut to descendants. Must wrap any\n * component that uses useSubscriber().\n *\n * @param props.children - React tree to wrap\n * @returns SubscriberContext.Provider with current auth state and actions\n */\nexport function SubscriberProvider({ children }: ProviderProps) {\n // eslint-disable-next-line\n const [subscriber, setSubscriber] = useState<null | (Subscriber & { optIns: string[] })>(null)\n\n const { serverURL } = useServerUrl()\n\n // Keep track of if the selection content is loaded yet\n const [isLoaded, setIsLoaded] = useState(false)\n\n const [permissions, setPermissions] = useState<any>()\n\n const refreshSubscriber = useCallback(async () => {\n const initSubscriber = async () => {\n setIsLoaded(false)\n try {\n const authResponse = await fetch('/api/subscriberAuth', {\n // body: JSON.stringify({}),\n method: 'POST',\n })\n\n if (authResponse.ok) {\n // Call the server function to get the user data\n const { permissions, subscriber } = await authResponse.json()\n // console.log(`subscriber = `, subscriber)\n // console.log(`permissions = `, permissions)\n setPermissions(permissions)\n setSubscriber(subscriber)\n } else {\n setPermissions(null)\n setSubscriber(null)\n }\n } catch (error: unknown) {\n console.log(`authResponse error`, error)\n }\n setIsLoaded(true)\n }\n await initSubscriber()\n }, [serverURL])\n\n const logOut = useCallback(async () => {\n setIsLoaded(false)\n try {\n // const sdk = new PayloadSDK<Config>({\n // baseURL: serverURL || '',\n // })\n // const logoutResponse = await sdk.request({\n // json: {},\n // method: 'POST',\n // path: '/api/logout',\n // })\n // Unsure why sdk isn't working here\n const logoutResponse = await fetch('/api/logout', {\n method: 'POST',\n })\n\n // console.log(`logoutResponse`, logoutResponse)\n\n if (logoutResponse.ok) {\n setSubscriber(null)\n setPermissions(null)\n }\n } catch (error: unknown) {\n console.log(`logoutResponse error`, error)\n }\n setIsLoaded(true)\n }, [])\n\n useEffect(() => {\n void refreshSubscriber()\n }, [refreshSubscriber])\n\n // Memoize the value to prevent unnecessary re-renders in consumers\n const contextValue: SubscriberContextType = useMemo(\n () => ({\n isLoaded,\n logOut,\n permissions,\n refreshSubscriber,\n subscriber,\n }),\n [isLoaded, logOut, permissions, refreshSubscriber, subscriber],\n )\n\n return <SubscriberContext.Provider value={contextValue}>{children}</SubscriberContext.Provider>\n}\n\n/**\n * Consumes SubscriberContext. Use only inside a SubscriberProvider.\n *\n * @returns Current subscriber (or null), permissions, isLoaded, refreshSubscriber, and logOut\n * @throws Error if used outside SubscriberProvider\n */\nexport function useSubscriber() {\n const context = useContext(SubscriberContext)\n if (context === undefined) {\n throw new Error('useSubscriber must be used within a SubscriberProvider')\n }\n return context\n}\n"],"names":["useCallback","useEffect","createContext","useContext","useMemo","useState","useServerUrl","SubscriberContext","undefined","SubscriberProvider","children","subscriber","setSubscriber","serverURL","isLoaded","setIsLoaded","permissions","setPermissions","refreshSubscriber","initSubscriber","authResponse","fetch","method","ok","json","error","console","log","logOut","logoutResponse","contextValue","Provider","value","useSubscriber","context","Error"],"mappings":"AAAA;;AAGA,SAAyBA,WAAW,EAAEC,SAAS,QAAQ,QAAO;AAC9D,SAASC,aAAa,EAAEC,UAAU,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAIpE,SAASC,YAAY,QAAQ,iCAAgC;AAW7D,MAAMC,kCAAoBL,cAAiDM;AAO3E;;;;;;;CAOC,GACD,OAAO,SAASC,mBAAmB,EAAEC,QAAQ,EAAiB;IAC5D,2BAA2B;IAC3B,MAAM,CAACC,YAAYC,cAAc,GAAGP,SAAqD;IAEzF,MAAM,EAAEQ,SAAS,EAAE,GAAGP;IAEtB,uDAAuD;IACvD,MAAM,CAACQ,UAAUC,YAAY,GAAGV,SAAS;IAEzC,MAAM,CAACW,aAAaC,eAAe,GAAGZ;IAEtC,MAAMa,oBAAoBlB,YAAY;QACpC,MAAMmB,iBAAiB;YACrBJ,YAAY;YACZ,IAAI;gBACF,MAAMK,eAAe,MAAMC,MAAM,uBAAuB;oBACtD,4BAA4B;oBAC5BC,QAAQ;gBACV;gBAEA,IAAIF,aAAaG,EAAE,EAAE;oBACnB,gDAAgD;oBAChD,MAAM,EAAEP,WAAW,EAAEL,UAAU,EAAE,GAAG,MAAMS,aAAaI,IAAI;oBAC3D,2CAA2C;oBAC3C,6CAA6C;oBAC7CP,eAAeD;oBACfJ,cAAcD;gBAChB,OAAO;oBACLM,eAAe;oBACfL,cAAc;gBAChB;YACF,EAAE,OAAOa,OAAgB;gBACvBC,QAAQC,GAAG,CAAC,CAAC,kBAAkB,CAAC,EAAEF;YACpC;YACAV,YAAY;QACd;QACA,MAAMI;IACR,GAAG;QAACN;KAAU;IAEd,MAAMe,SAAS5B,YAAY;QACzBe,YAAY;QACZ,IAAI;YACF,uCAAuC;YACvC,8BAA8B;YAC9B,KAAK;YACL,6CAA6C;YAC7C,cAAc;YACd,oBAAoB;YACpB,yBAAyB;YACzB,KAAK;YACL,oCAAoC;YACpC,MAAMc,iBAAiB,MAAMR,MAAM,eAAe;gBAChDC,QAAQ;YACV;YAEA,gDAAgD;YAEhD,IAAIO,eAAeN,EAAE,EAAE;gBACrBX,cAAc;gBACdK,eAAe;YACjB;QACF,EAAE,OAAOQ,OAAgB;YACvBC,QAAQC,GAAG,CAAC,CAAC,oBAAoB,CAAC,EAAEF;QACtC;QACAV,YAAY;IACd,GAAG,EAAE;IAELd,UAAU;QACR,KAAKiB;IACP,GAAG;QAACA;KAAkB;IAEtB,mEAAmE;IACnE,MAAMY,eAAsC1B,QAC1C,IAAO,CAAA;YACLU;YACAc;YACAZ;YACAE;YACAP;QACF,CAAA,GACA;QAACG;QAAUc;QAAQZ;QAAaE;QAAmBP;KAAW;IAGhE,qBAAO,KAACJ,kBAAkBwB,QAAQ;QAACC,OAAOF;kBAAepB;;AAC3D;AAEA;;;;;CAKC,GACD,OAAO,SAASuB;IACd,MAAMC,UAAU/B,WAAWI;IAC3B,IAAI2B,YAAY1B,WAAW;QACzB,MAAM,IAAI2B,MAAM;IAClB;IACA,OAAOD;AACT"}
|
|
@@ -11,10 +11,14 @@ export type RequestMagicLinkResponse = {
|
|
|
11
11
|
* Sends a magic-link email to the given address (creates a pending subscriber if needed).
|
|
12
12
|
*
|
|
13
13
|
* @param options - Config options for the endpoint
|
|
14
|
-
* @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)
|
|
14
|
+
* @param options.subscribersCollectionSlug - (required) Collection slug for subscribers (default from Subscribers collection)
|
|
15
|
+
* @param options.unsubscribeURL - (optional) The URL to use for unsubscribe links
|
|
16
|
+
* @param options.verifyURL - (required) The URL to use for verify links
|
|
15
17
|
* @returns Payload Endpoint config for POST /emailToken
|
|
16
18
|
*/
|
|
17
|
-
declare function createEndpointRequestMagicLink({ subscribersCollectionSlug, }: {
|
|
19
|
+
declare function createEndpointRequestMagicLink({ subscribersCollectionSlug, unsubscribeURL, verifyURL, }: {
|
|
18
20
|
subscribersCollectionSlug: CollectionSlug;
|
|
21
|
+
unsubscribeURL?: URL;
|
|
22
|
+
verifyURL: URL;
|
|
19
23
|
}): Endpoint;
|
|
20
24
|
export default createEndpointRequestMagicLink;
|
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
1
|
import { defaultCollectionSlug } from '../collections/Subscribers.js';
|
|
3
|
-
import { getTokenAndHash } from '../helpers/token.js';
|
|
2
|
+
import { getHmacHash, getTokenAndHash } from '../helpers/token.js';
|
|
4
3
|
/**
|
|
5
4
|
* Factory that creates the request-magic-link endpoint config and handler.
|
|
6
5
|
* Sends a magic-link email to the given address (creates a pending subscriber if needed).
|
|
7
6
|
*
|
|
8
7
|
* @param options - Config options for the endpoint
|
|
9
|
-
* @param options.subscribersCollectionSlug - Collection slug for subscribers (default from Subscribers collection)
|
|
8
|
+
* @param options.subscribersCollectionSlug - (required) Collection slug for subscribers (default from Subscribers collection)
|
|
9
|
+
* @param options.unsubscribeURL - (optional) The URL to use for unsubscribe links
|
|
10
|
+
* @param options.verifyURL - (required) The URL to use for verify links
|
|
10
11
|
* @returns Payload Endpoint config for POST /emailToken
|
|
11
|
-
*/ function createEndpointRequestMagicLink({ subscribersCollectionSlug = defaultCollectionSlug }) {
|
|
12
|
+
*/ function createEndpointRequestMagicLink({ subscribersCollectionSlug = defaultCollectionSlug, unsubscribeURL, verifyURL }) {
|
|
13
|
+
// verifyURL required
|
|
14
|
+
if (!verifyURL || !verifyURL.href) {
|
|
15
|
+
throw new Error('A verify URL is required');
|
|
16
|
+
}
|
|
12
17
|
/**
|
|
13
|
-
* Handler for POST /emailToken.
|
|
18
|
+
* Handler for POST /emailToken. Takes an email parameter. Creates/updates a pending
|
|
14
19
|
* subscriber with a verification token, and sends a magic-link email.
|
|
15
20
|
*
|
|
16
|
-
* @param req - Payload request
|
|
21
|
+
* @param req - Payload request. Expects body to be a json object { email, verifyData }
|
|
17
22
|
* @returns 200 with `emailResult` and `now` on success; 400 with `error` and `now` on bad data or email failure
|
|
18
23
|
*/ const requestMagicLinkHandler = async (req)=>{
|
|
19
24
|
const data = req?.json ? await req.json() : {};
|
|
20
|
-
const { email,
|
|
25
|
+
const { email, verifyData } = data // if by POST data
|
|
21
26
|
;
|
|
22
|
-
|
|
23
|
-
if (!email || !verifyUrl) {
|
|
27
|
+
if (!email) {
|
|
24
28
|
return Response.json({
|
|
25
|
-
error: '
|
|
29
|
+
error: 'Email required',
|
|
26
30
|
now: new Date().toISOString()
|
|
27
31
|
}, {
|
|
28
32
|
status: 400
|
|
@@ -55,7 +59,7 @@ import { getTokenAndHash } from '../helpers/token.js';
|
|
|
55
59
|
});
|
|
56
60
|
if (!createResult) {
|
|
57
61
|
return Response.json({
|
|
58
|
-
error: '
|
|
62
|
+
error: 'Error creating subscriber',
|
|
59
63
|
now: new Date().toISOString()
|
|
60
64
|
}, {
|
|
61
65
|
status: 400
|
|
@@ -63,15 +67,15 @@ import { getTokenAndHash } from '../helpers/token.js';
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
// Update user with verificationToken
|
|
66
|
-
const token = crypto.randomBytes(32).toString('hex')
|
|
67
|
-
const tokenHash = crypto.createHash('sha256').update(token).digest('hex')
|
|
68
|
-
const expiresAt = new Date(Date.now() + 15 * 60 * 1000) // 15 mins
|
|
69
|
-
;
|
|
70
|
+
// const token = crypto.randomBytes(32).toString('hex')
|
|
71
|
+
// const tokenHash = crypto.createHash('sha256').update(token).digest('hex')
|
|
72
|
+
// const expiresAt = new Date(Date.now() + 15 * 60 * 1000) // 15 mins
|
|
73
|
+
const { expiresAt, token, tokenHash } = getTokenAndHash(15 * 60 * 1000);
|
|
70
74
|
await req.payload.update({
|
|
71
75
|
collection: subscribersCollectionSlug,
|
|
72
76
|
data: {
|
|
73
77
|
verificationToken: tokenHash,
|
|
74
|
-
verificationTokenExpires: expiresAt
|
|
78
|
+
verificationTokenExpires: expiresAt?.toISOString()
|
|
75
79
|
},
|
|
76
80
|
where: {
|
|
77
81
|
email: {
|
|
@@ -79,12 +83,15 @@ import { getTokenAndHash } from '../helpers/token.js';
|
|
|
79
83
|
}
|
|
80
84
|
}
|
|
81
85
|
});
|
|
86
|
+
const { hashToken: unsubscribeHash } = getHmacHash(email);
|
|
82
87
|
// Send email
|
|
83
|
-
const magicLink = `${
|
|
88
|
+
const magicLink = `${verifyURL?.href}${verifyURL?.search ? '&' : '?'}token=${encodeURIComponent(token)}&email=${encodeURIComponent(email)}&verifyData=${encodeURIComponent(verifyData)}`;
|
|
89
|
+
const unsubscribeLink = !unsubscribeURL ? undefined : `${unsubscribeURL?.href}${unsubscribeURL?.search ? '&' : '?'}email=${encodeURIComponent(email)}&hash=${encodeURIComponent(unsubscribeHash)}`;
|
|
84
90
|
const subject = data.subject || 'Your Magic Login Link';
|
|
85
91
|
const message = `
|
|
86
|
-
${data.message || '<p>
|
|
87
|
-
<p><a href="${magicLink}"><b>Login</b></a></p>
|
|
92
|
+
${data.message || '<p>You requested a magic link to log in. Click the button below</p>'}
|
|
93
|
+
<p><a href="${magicLink}"><button><b>Login</b></button></a></p>
|
|
94
|
+
${unsubscribeLink ? `<p>Click here to <a href="${unsubscribeLink}">unsubscribe</a></p>` : ``}
|
|
88
95
|
`;
|
|
89
96
|
const emailResult = await req.payload.sendEmail({
|
|
90
97
|
html: message,
|