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.
Files changed (60) hide show
  1. package/README.md +234 -24
  2. package/dist/components/app/RequestMagicLink.d.ts +21 -4
  3. package/dist/components/app/RequestMagicLink.js +10 -42
  4. package/dist/components/app/RequestMagicLink.js.map +1 -1
  5. package/dist/components/app/RequestOrSubscribe.d.ts +21 -4
  6. package/dist/components/app/RequestOrSubscribe.js +5 -8
  7. package/dist/components/app/RequestOrSubscribe.js.map +1 -1
  8. package/dist/components/app/SelectOptInChannels.d.ts +26 -3
  9. package/dist/components/app/SelectOptInChannels.js +4 -1
  10. package/dist/components/app/SelectOptInChannels.js.map +1 -1
  11. package/dist/components/app/Subscribe.d.ts +26 -5
  12. package/dist/components/app/Subscribe.js +63 -81
  13. package/dist/components/app/Subscribe.js.map +1 -1
  14. package/dist/components/app/SubscriberMenu.d.ts +18 -6
  15. package/dist/components/app/SubscriberMenu.js +9 -8
  16. package/dist/components/app/SubscriberMenu.js.map +1 -1
  17. package/dist/components/app/Unsubscribe.d.ts +45 -0
  18. package/dist/components/app/Unsubscribe.js +91 -0
  19. package/dist/components/app/Unsubscribe.js.map +1 -0
  20. package/dist/components/app/VerifyMagicLink.d.ts +32 -14
  21. package/dist/components/app/VerifyMagicLink.js +60 -114
  22. package/dist/components/app/VerifyMagicLink.js.map +1 -1
  23. package/dist/contexts/SubscriberProvider.js +1 -1
  24. package/dist/contexts/SubscriberProvider.js.map +1 -1
  25. package/dist/endpoints/requestMagicLink.d.ts +6 -2
  26. package/dist/endpoints/requestMagicLink.js +26 -19
  27. package/dist/endpoints/requestMagicLink.js.map +1 -1
  28. package/dist/endpoints/subscribe.d.ts +6 -2
  29. package/dist/endpoints/subscribe.js +29 -13
  30. package/dist/endpoints/subscribe.js.map +1 -1
  31. package/dist/endpoints/unsubscribe.d.ts +21 -0
  32. package/dist/endpoints/unsubscribe.js +120 -0
  33. package/dist/endpoints/unsubscribe.js.map +1 -0
  34. package/dist/endpoints/verifyMagicLink.js +3 -2
  35. package/dist/endpoints/verifyMagicLink.js.map +1 -1
  36. package/dist/exports/ui.d.ts +2 -0
  37. package/dist/exports/ui.js +1 -0
  38. package/dist/exports/ui.js.map +1 -1
  39. package/dist/helpers/token.d.ts +3 -0
  40. package/dist/helpers/token.js +12 -2
  41. package/dist/helpers/token.js.map +1 -1
  42. package/dist/helpers/utilities.d.ts +1 -0
  43. package/dist/helpers/utilities.js +6 -0
  44. package/dist/helpers/utilities.js.map +1 -0
  45. package/dist/hooks/useRequestMagicLink.d.ts +35 -0
  46. package/dist/hooks/useRequestMagicLink.js +61 -0
  47. package/dist/hooks/useRequestMagicLink.js.map +1 -0
  48. package/dist/hooks/useSubscribe.d.ts +38 -0
  49. package/dist/hooks/useSubscribe.js +62 -0
  50. package/dist/hooks/useSubscribe.js.map +1 -0
  51. package/dist/hooks/useUnsubscribe.d.ts +43 -0
  52. package/dist/hooks/useUnsubscribe.js +86 -0
  53. package/dist/hooks/useUnsubscribe.js.map +1 -0
  54. package/dist/hooks/useVerifyMagicLink.d.ts +31 -0
  55. package/dist/hooks/useVerifyMagicLink.js +74 -0
  56. package/dist/hooks/useVerifyMagicLink.js.map +1 -0
  57. package/dist/index.d.ts +8 -0
  58. package/dist/index.js +16 -2
  59. package/dist/index.js.map +1 -1
  60. 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 '../..//endpoints/requestMagicLink.js';
1
+ import type { RequestMagicLinkResponse } from '../../endpoints/requestMagicLink.js';
2
2
  import type { VerifyMagicLinkResponse } from '../../endpoints/verifyMagicLink.js';
3
3
  export { VerifyMagicLinkResponse };
4
- /** Props for the VerifyMagicLink component. */
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: VerifyMagicLinkResponse) => void;
10
- renderButton?: (props: {
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
- /** Optional CSS class overrides for VerifyMagicLink elements. */
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" via renderButton and optional callbacks.
43
+ * "Request another magic link" and optional callbacks.
31
44
  *
32
- * @param props - See IVerifyMagicLink
33
- * @returns RequestMagicLink when no token/email; otherwise verifying state, result message, and optional button/children
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, renderButton, verifyUrl, }: IVerifyMagicLink) => import("react").JSX.Element;
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 { useCallback, useEffect, useState } from 'react';
4
+ import { useEffect, useState } from 'react';
6
5
  import { RequestMagicLink, useSubscriber } from '../../exports/ui.js';
7
- import { useServerUrl } from '../../react-hooks/useServerUrl.js';
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" via renderButton and optional callbacks.
13
+ * "Request another magic link" and optional callbacks.
14
14
  *
15
- * @param props - See IVerifyMagicLink
16
- * @returns RequestMagicLink when no token/email; otherwise verifying state, result message, and optional button/children
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, renderButton = ({ name, onClick, text })=>/*#__PURE__*/ _jsx("button", {
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 [result, setResult] = useState();
46
- const [isError, setIsError] = useState(false);
47
- // const [email, setEmail] = useState('')
48
- const { refreshSubscriber } = useSubscriber();
49
- const callVerify = useCallback(async ()=>{
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
- try {
57
- // I tried using PayloadSDK.request, but when the endpoint
58
- // returns a not-okay status, PayloadSDK.request returns its
59
- // own "Bad request" error, and doesn't share the endpoint
60
- // result data.
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
- email,
96
- serverURL,
97
- token
47
+ subscriber,
48
+ verify
98
49
  ]);
99
50
  useEffect(()=>{
100
- async function verify() {
101
- const { error, message } = await callVerify();
102
- setResult(message || `An error occured. Please try again. (${error})`);
103
- setIsError(error && !message);
104
- // console.info('callVerify not okay', { error, message })
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
- refreshSubscriber,
115
- subscriber,
116
- token
59
+ verifyResult,
60
+ verifyIsError,
61
+ verifyIsLoading
117
62
  ]);
118
- const handleRequestAnother = async ()=>{
119
- const sdk = new PayloadSDK({
120
- baseURL: serverURL || ''
121
- });
122
- const emailResult = await sdk.request({
123
- json: {
124
- email,
125
- verifyUrl: verifyUrl?.href
126
- },
127
- method: 'POST',
128
- path: '/api/emailToken'
129
- });
130
- if (emailResult.ok) {
131
- const resultJson = await emailResult.json();
132
- setResult('An email has been sent containing your magic link.');
133
- setIsError(false);
134
- if (handleMagicLinkRequested) {
135
- handleMagicLinkRequested(resultJson);
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
- !result && /*#__PURE__*/ _jsx("p", {
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 && renderButton && renderButton({
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
- text: 'Request another magic link'
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
- ]); // Empty dependency array for mount/unmount
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]) // Empty dependency array for mount/unmount\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,GAAE,2CAA2C;IAEnE,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"}
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. Accepts email and verifyUrl, creates/updates a pending
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; body must include `email` and `verifyUrl`
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, verifyUrl } = data // if by POST data
25
+ const { email, verifyData } = data // if by POST data
21
26
  ;
22
- // const { email } = req.routeParams // if by path
23
- if (!email || !verifyUrl) {
27
+ if (!email) {
24
28
  return Response.json({
25
- error: 'Bad data',
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: 'Bad data',
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.toISOString()
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 = `${verifyUrl}${verifyUrl.search ? '&' : '?'}token=${token}&email=${email}`;
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>Use this link to log in:</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,