payload-subscribers-plugin 0.0.1
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 +305 -0
- package/dist/collections/OptInChannels.d.ts +3 -0
- package/dist/collections/OptInChannels.js +44 -0
- package/dist/collections/OptInChannels.js.map +1 -0
- package/dist/collections/Subscribers.d.ts +8 -0
- package/dist/collections/Subscribers.js +88 -0
- package/dist/collections/Subscribers.js.map +1 -0
- package/dist/collections/fields/OptedInChannels.d.ts +2 -0
- package/dist/collections/fields/OptedInChannels.js +12 -0
- package/dist/collections/fields/OptedInChannels.js.map +1 -0
- package/dist/components/BeforeDashboardClient.d.ts +1 -0
- package/dist/components/BeforeDashboardClient.js +40 -0
- package/dist/components/BeforeDashboardClient.js.map +1 -0
- package/dist/components/BeforeDashboardServer.d.ts +2 -0
- package/dist/components/BeforeDashboardServer.js +22 -0
- package/dist/components/BeforeDashboardServer.js.map +1 -0
- package/dist/components/BeforeDashboardServer.module.css +5 -0
- package/dist/components/app/RequestMagicLink.d.ts +16 -0
- package/dist/components/app/RequestMagicLink.js +114 -0
- package/dist/components/app/RequestMagicLink.js.map +1 -0
- package/dist/components/app/RequestMagicLink.module.css +5 -0
- package/dist/components/app/RequestOrSubscribe.d.ts +17 -0
- package/dist/components/app/RequestOrSubscribe.js +28 -0
- package/dist/components/app/RequestOrSubscribe.js.map +1 -0
- package/dist/components/app/SelectOptInChannels.d.ts +20 -0
- package/dist/components/app/SelectOptInChannels.js +120 -0
- package/dist/components/app/SelectOptInChannels.js.map +1 -0
- package/dist/components/app/SelectOptInChannels.module.css +5 -0
- package/dist/components/app/Subscribe.d.ts +18 -0
- package/dist/components/app/Subscribe.js +169 -0
- package/dist/components/app/Subscribe.js.map +1 -0
- package/dist/components/app/Subscribe.module.css +5 -0
- package/dist/components/app/SubscriberMenu.d.ts +7 -0
- package/dist/components/app/SubscriberMenu.js +44 -0
- package/dist/components/app/SubscriberMenu.js.map +1 -0
- package/dist/components/app/VerifyMagicLink.d.ts +23 -0
- package/dist/components/app/VerifyMagicLink.js +169 -0
- package/dist/components/app/VerifyMagicLink.js.map +1 -0
- package/dist/components/app/VerifyMagicLink.module.css +5 -0
- package/dist/components/app/helpers.d.ts +1 -0
- package/dist/components/app/helpers.js +5 -0
- package/dist/components/app/helpers.js.map +1 -0
- package/dist/components/app/shared.module.css +14 -0
- package/dist/contexts/SubscriberProvider.d.ts +15 -0
- package/dist/contexts/SubscriberProvider.js +105 -0
- package/dist/contexts/SubscriberProvider.js.map +1 -0
- package/dist/copied/payload-types.d.ts +395 -0
- package/dist/copied/payload-types.js +15 -0
- package/dist/copied/payload-types.js.map +1 -0
- package/dist/copied/payload.config.d.ts +2 -0
- package/dist/endpoints/customEndpointHandler.d.ts +2 -0
- package/dist/endpoints/customEndpointHandler.js +7 -0
- package/dist/endpoints/customEndpointHandler.js.map +1 -0
- package/dist/endpoints/getOptInChannels.d.ts +19 -0
- package/dist/endpoints/getOptInChannels.js +42 -0
- package/dist/endpoints/getOptInChannels.js.map +1 -0
- package/dist/endpoints/logout.d.ts +20 -0
- package/dist/endpoints/logout.js +60 -0
- package/dist/endpoints/logout.js.map +1 -0
- package/dist/endpoints/requestMagicLink.d.ts +20 -0
- package/dist/endpoints/requestMagicLink.js +122 -0
- package/dist/endpoints/requestMagicLink.js.map +1 -0
- package/dist/endpoints/subscribe.d.ts +24 -0
- package/dist/endpoints/subscribe.js +343 -0
- package/dist/endpoints/subscribe.js.map +1 -0
- package/dist/endpoints/subscriberAuth.d.ts +22 -0
- package/dist/endpoints/subscriberAuth.js +69 -0
- package/dist/endpoints/subscriberAuth.js.map +1 -0
- package/dist/endpoints/verifyMagicLink.d.ts +20 -0
- package/dist/endpoints/verifyMagicLink.js +142 -0
- package/dist/endpoints/verifyMagicLink.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +3 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/index.d.ts +1 -0
- package/dist/exports/index.js +3 -0
- package/dist/exports/index.js.map +1 -0
- package/dist/exports/rsc.d.ts +1 -0
- package/dist/exports/rsc.js +3 -0
- package/dist/exports/rsc.js.map +1 -0
- package/dist/exports/ui.d.ts +11 -0
- package/dist/exports/ui.js +9 -0
- package/dist/exports/ui.js.map +1 -0
- package/dist/helpers/serverConfig.d.ts +4 -0
- package/dist/helpers/serverConfig.js +22 -0
- package/dist/helpers/serverConfig.js.map +1 -0
- package/dist/helpers/testData.d.ts +2 -0
- package/dist/helpers/testData.js +4 -0
- package/dist/helpers/testData.js.map +1 -0
- package/dist/helpers/token.d.ts +9 -0
- package/dist/helpers/token.js +20 -0
- package/dist/helpers/token.js.map +1 -0
- package/dist/helpers/verifyOptIns.d.ts +5 -0
- package/dist/helpers/verifyOptIns.js +33 -0
- package/dist/helpers/verifyOptIns.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/dist/react-hooks/useServerUrl.d.ts +3 -0
- package/dist/react-hooks/useServerUrl.js +19 -0
- package/dist/react-hooks/useServerUrl.js.map +1 -0
- package/dist/server-functions/serverUrl.d.ts +3 -0
- package/dist/server-functions/serverUrl.js +31 -0
- package/dist/server-functions/serverUrl.js.map +1 -0
- package/dist/server-functions/subscriberAuth.d.ts +11 -0
- package/package.json +94 -0
- package/src/collections/OptInChannels.ts +45 -0
- package/src/collections/Subscribers.ts +99 -0
- package/src/collections/fields/OptedInChannels.ts +12 -0
- package/src/components/app/RequestMagicLink.tsx +129 -0
- package/src/components/app/RequestOrSubscribe.tsx +58 -0
- package/src/components/app/SelectOptInChannels.tsx +147 -0
- package/src/components/app/Subscribe.tsx +190 -0
- package/src/components/app/SubscriberMenu.tsx +46 -0
- package/src/components/app/VerifyMagicLink.tsx +197 -0
- package/src/components/app/helpers.ts +6 -0
- package/src/components/app/shared.module.css +14 -0
- package/src/contexts/SubscriberProvider.tsx +122 -0
- package/src/copied/payload-types.ts +478 -0
- package/src/endpoints/getOptInChannels.ts +56 -0
- package/src/endpoints/logout.ts +104 -0
- package/src/endpoints/requestMagicLink.ts +139 -0
- package/src/endpoints/subscribe.ts +435 -0
- package/src/endpoints/subscriberAuth.ts +100 -0
- package/src/endpoints/verifyMagicLink.ts +164 -0
- package/src/exports/index.ts +1 -0
- package/src/exports/ui.ts +17 -0
- package/src/helpers/testData.ts +2 -0
- package/src/helpers/token.ts +14 -0
- package/src/helpers/verifyOptIns.ts +39 -0
- package/src/index.ts +207 -0
- package/src/react-hooks/useServerUrl.tsx +18 -0
- package/src/server-functions/serverUrl.ts +38 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
const getServerSideURL = ()=>{
|
|
3
|
+
const serverSideURL = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` : process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : process.env.NEXT_PUBLIC_DEV_URL ? `http://${process.env.NEXT_PUBLIC_DEV_URL}` : 'http://localhost:3000';
|
|
4
|
+
// console.log(`process.env.NEXT_PUBLIC_DEV_URL: ${process.env.NEXT_PUBLIC_DEV_URL}`)
|
|
5
|
+
// console.log(`serverSideURL: ${serverSideURL}`)
|
|
6
|
+
return serverSideURL;
|
|
7
|
+
};
|
|
8
|
+
// const canUseDOM = !!(
|
|
9
|
+
// typeof window !== 'undefined' &&
|
|
10
|
+
// window.document &&
|
|
11
|
+
// window.document.createElement
|
|
12
|
+
// )
|
|
13
|
+
// const getClientSideURL = () => {
|
|
14
|
+
// if (canUseDOM) {
|
|
15
|
+
// const protocol = window.location.protocol
|
|
16
|
+
// const domain = window.location.hostname
|
|
17
|
+
// const port = window.location.port
|
|
18
|
+
// // `${window.location.protocol}//${window.location.host}
|
|
19
|
+
// const clientSideURL = `${protocol}//${domain}${port ? `:${port}` : ''}`
|
|
20
|
+
// // console.log(`clientSideURL: ${clientSideURL}`)
|
|
21
|
+
// return clientSideURL
|
|
22
|
+
// }
|
|
23
|
+
// return getServerSideURL()
|
|
24
|
+
// }
|
|
25
|
+
export const getServerUrl = async ()=>{
|
|
26
|
+
return {
|
|
27
|
+
serverURL: getServerSideURL()
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=serverUrl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/server-functions/serverUrl.ts"],"sourcesContent":["'use server'\n\nconst getServerSideURL = () => {\n const serverSideURL = process.env.NEXT_PUBLIC_VERCEL_URL\n ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`\n : process.env.VERCEL_PROJECT_PRODUCTION_URL\n ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`\n : process.env.NEXT_PUBLIC_DEV_URL\n ? `http://${process.env.NEXT_PUBLIC_DEV_URL}`\n : 'http://localhost:3000'\n // console.log(`process.env.NEXT_PUBLIC_DEV_URL: ${process.env.NEXT_PUBLIC_DEV_URL}`)\n // console.log(`serverSideURL: ${serverSideURL}`)\n return serverSideURL\n}\n\n// const canUseDOM = !!(\n// typeof window !== 'undefined' &&\n// window.document &&\n// window.document.createElement\n// )\n\n// const getClientSideURL = () => {\n// if (canUseDOM) {\n// const protocol = window.location.protocol\n// const domain = window.location.hostname\n// const port = window.location.port\n// // `${window.location.protocol}//${window.location.host}\n// const clientSideURL = `${protocol}//${domain}${port ? `:${port}` : ''}`\n// // console.log(`clientSideURL: ${clientSideURL}`)\n// return clientSideURL\n// }\n\n// return getServerSideURL()\n// }\n\nexport const getServerUrl = async (): Promise<{ serverURL: string }> => {\n return { serverURL: getServerSideURL() }\n}\n"],"names":["getServerSideURL","serverSideURL","process","env","NEXT_PUBLIC_VERCEL_URL","VERCEL_PROJECT_PRODUCTION_URL","NEXT_PUBLIC_DEV_URL","getServerUrl","serverURL"],"mappings":"AAAA;AAEA,MAAMA,mBAAmB;IACvB,MAAMC,gBAAgBC,QAAQC,GAAG,CAACC,sBAAsB,GACpD,CAAC,QAAQ,EAAEF,QAAQC,GAAG,CAACC,sBAAsB,EAAE,GAC/CF,QAAQC,GAAG,CAACE,6BAA6B,GACvC,CAAC,QAAQ,EAAEH,QAAQC,GAAG,CAACE,6BAA6B,EAAE,GACtDH,QAAQC,GAAG,CAACG,mBAAmB,GAC7B,CAAC,OAAO,EAAEJ,QAAQC,GAAG,CAACG,mBAAmB,EAAE,GAC3C;IACR,qFAAqF;IACrF,iDAAiD;IACjD,OAAOL;AACT;AAEA,wBAAwB;AACxB,qCAAqC;AACrC,uBAAuB;AACvB,kCAAkC;AAClC,IAAI;AAEJ,mCAAmC;AACnC,qBAAqB;AACrB,gDAAgD;AAChD,8CAA8C;AAC9C,wCAAwC;AACxC,+DAA+D;AAC/D,8EAA8E;AAC9E,wDAAwD;AACxD,2BAA2B;AAC3B,MAAM;AAEN,8BAA8B;AAC9B,IAAI;AAEJ,OAAO,MAAMM,eAAe;IAC1B,OAAO;QAAEC,WAAWR;IAAmB;AACzC,EAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type SubscriberAuthReturn = {
|
|
2
|
+
error: any;
|
|
3
|
+
} | {
|
|
4
|
+
permissions: any;
|
|
5
|
+
subscriber: any;
|
|
6
|
+
};
|
|
7
|
+
export declare const subscriberAuth: () => Promise<SubscriberAuthReturn>;
|
|
8
|
+
export declare function logoutAction(): Promise<{
|
|
9
|
+
message: string;
|
|
10
|
+
success: boolean;
|
|
11
|
+
}>;
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "payload-subscribers-plugin",
|
|
3
|
+
"description": "A Payload CMS (3.0) plugin to add subscriber features into a site or app",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Chad Crume",
|
|
6
|
+
"url": "https://github.com/chadcrume"
|
|
7
|
+
},
|
|
8
|
+
"repository": "chadcrume/payload-subscribers-plugin",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./ui": {
|
|
18
|
+
"import": "./dist/exports/ui.js",
|
|
19
|
+
"types": "./dist/exports/ui.d.ts",
|
|
20
|
+
"default": "./dist/exports/ui.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@eslint/eslintrc": "^3.3.3",
|
|
31
|
+
"@payloadcms/db-mongodb": "3.73.0",
|
|
32
|
+
"@payloadcms/eslint-config": "3.9.0",
|
|
33
|
+
"@payloadcms/next": "3.73.0",
|
|
34
|
+
"@payloadcms/sdk": "3.73.0",
|
|
35
|
+
"@payloadcms/ui": "3.73.0",
|
|
36
|
+
"@playwright/test": "1.56.1",
|
|
37
|
+
"@swc-node/register": "1.10.9",
|
|
38
|
+
"@swc/cli": "0.6.0",
|
|
39
|
+
"@types/node": "^22.19.7",
|
|
40
|
+
"@types/react": "19.2.10",
|
|
41
|
+
"@types/react-dom": "19.2.3",
|
|
42
|
+
"copyfiles": "2.4.1",
|
|
43
|
+
"cross-env": "^7.0.3",
|
|
44
|
+
"eslint": "^9.39.2",
|
|
45
|
+
"eslint-config-next": "15.4.7",
|
|
46
|
+
"mongodb-memory-server": "10.1.4",
|
|
47
|
+
"next": "16.1.6",
|
|
48
|
+
"open": "^10.2.0",
|
|
49
|
+
"payload": "3.73.0",
|
|
50
|
+
"prettier": "^3.8.1",
|
|
51
|
+
"qs-esm": "7.0.2",
|
|
52
|
+
"react": "19.2.4",
|
|
53
|
+
"react-dom": "19.2.4",
|
|
54
|
+
"sharp": "0.34.2",
|
|
55
|
+
"sort-package-json": "^2.15.1",
|
|
56
|
+
"typescript": "5.7.3",
|
|
57
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
58
|
+
"vitest": "^3.2.4"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"next": "^15.4.10",
|
|
62
|
+
"payload": "^3.71.1",
|
|
63
|
+
"@payloadcms/sdk": "^3.71.1",
|
|
64
|
+
"react": "^19.0.0",
|
|
65
|
+
"react-dom": "^19.0.0"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": "^18.20.2 || >=20.9.0",
|
|
69
|
+
"pnpm": "^9 || ^10"
|
|
70
|
+
},
|
|
71
|
+
"registry": "https://registry.npmjs.org/",
|
|
72
|
+
"dependencies": {},
|
|
73
|
+
"version": "0.0.1",
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
76
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
77
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
78
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
79
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
80
|
+
"dev": "pnpm dev:copyfiles && pnpm dev:next",
|
|
81
|
+
"dev:copyfiles": "copyfiles -u 1 \"dev/{payload-types}.ts\" src/copied/",
|
|
82
|
+
"dev:next": "next dev dev --turbo",
|
|
83
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
84
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
85
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
86
|
+
"generate:importmap": "pnpm dev:generate-importmap",
|
|
87
|
+
"generate:types": "pnpm dev:generate-types && pnpm dev:copyfiles",
|
|
88
|
+
"lint": "eslint",
|
|
89
|
+
"lint:fix": "eslint ./src --fix",
|
|
90
|
+
"test": "pnpm test:int && pnpm test:e2e",
|
|
91
|
+
"test:e2e": "playwright test",
|
|
92
|
+
"test:int": "vitest"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CollectionConfig } from 'payload'
|
|
2
|
+
|
|
3
|
+
export const OptInChannels: CollectionConfig = {
|
|
4
|
+
slug: 'opt-in-channels',
|
|
5
|
+
access: {
|
|
6
|
+
// Public access for creation (signup form)
|
|
7
|
+
create: () => true,
|
|
8
|
+
// Admin-only access for reading, updating, and deleting
|
|
9
|
+
delete: ({ req }) => (req.user ? true : false),
|
|
10
|
+
// read: ({ req }) => (req.user ? true : false),
|
|
11
|
+
read: () => true,
|
|
12
|
+
update: ({ req }) => (req.user ? true : false),
|
|
13
|
+
},
|
|
14
|
+
admin: {
|
|
15
|
+
useAsTitle: 'title', // Specify the field to use as the title
|
|
16
|
+
},
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: 'title',
|
|
20
|
+
type: 'text', // Enforces valid email format
|
|
21
|
+
label: 'Title',
|
|
22
|
+
required: true,
|
|
23
|
+
unique: true, // Ensures no duplicate titles
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'description',
|
|
27
|
+
type: 'text',
|
|
28
|
+
label: 'Description',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'active',
|
|
32
|
+
type: 'checkbox',
|
|
33
|
+
defaultValue: true, // Default to pending until verified
|
|
34
|
+
label: 'Active',
|
|
35
|
+
required: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'slug',
|
|
39
|
+
type: 'text',
|
|
40
|
+
label: 'slug',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default OptInChannels
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { CollectionConfig, CollectionSlug, Field } from 'payload'
|
|
2
|
+
|
|
3
|
+
import { OptedInChannels } from './fields/OptedInChannels.js'
|
|
4
|
+
|
|
5
|
+
export const defaultTokenExpiration = 30 * 60 // 30 minutes
|
|
6
|
+
|
|
7
|
+
export const defaultCollectionSlug = 'subscribers'
|
|
8
|
+
|
|
9
|
+
export const SubscribersCollectionFactory = ({
|
|
10
|
+
slug,
|
|
11
|
+
tokenExpiration = defaultTokenExpiration,
|
|
12
|
+
}: {
|
|
13
|
+
slug?: CollectionSlug
|
|
14
|
+
tokenExpiration?: number
|
|
15
|
+
}) => {
|
|
16
|
+
const Subscribers: CollectionConfig = {
|
|
17
|
+
slug: slug ? slug : defaultCollectionSlug,
|
|
18
|
+
access: {
|
|
19
|
+
// Public access for creation (signup form)
|
|
20
|
+
create: () => true,
|
|
21
|
+
// Admin-only access for reading, updating, and deleting
|
|
22
|
+
delete: ({ req }) => (req.user ? true : false),
|
|
23
|
+
read: ({ req }) => (req.user ? true : false),
|
|
24
|
+
update: ({ req }) => (req.user ? true : false),
|
|
25
|
+
},
|
|
26
|
+
admin: { useAsTitle: 'email' },
|
|
27
|
+
auth: {
|
|
28
|
+
tokenExpiration,
|
|
29
|
+
// verify: true, // Require email verification before being allowed to authenticate
|
|
30
|
+
// maxLoginAttempts: 5, // Automatically lock a user out after X amount of failed logins
|
|
31
|
+
// lockTime: 600 * 1000, // Time period to allow the max login attempts
|
|
32
|
+
},
|
|
33
|
+
fields: [...subscribersCollectionFields],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Subscribers
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const subscribersCollectionFields: Field[] = [
|
|
40
|
+
{
|
|
41
|
+
name: 'email',
|
|
42
|
+
type: 'email', // Enforces valid email format
|
|
43
|
+
label: 'Email Address',
|
|
44
|
+
required: true,
|
|
45
|
+
unique: true, // Ensures no duplicate emails
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'firstName',
|
|
49
|
+
type: 'text',
|
|
50
|
+
label: 'First Name',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'status',
|
|
54
|
+
type: 'select',
|
|
55
|
+
defaultValue: 'pending', // Default to pending until verified
|
|
56
|
+
label: 'Subscription Status',
|
|
57
|
+
options: [
|
|
58
|
+
{
|
|
59
|
+
label: 'Subscribed',
|
|
60
|
+
value: 'subscribed',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: 'Unsubscribed',
|
|
64
|
+
value: 'unsubscribed',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: 'Pending Verification',
|
|
68
|
+
value: 'pending',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
required: true,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'source',
|
|
75
|
+
type: 'text', // e.g., 'Homepage form', 'Blog post A', etc.
|
|
76
|
+
label: 'Signup Source',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'verificationToken',
|
|
80
|
+
type: 'text',
|
|
81
|
+
admin: {
|
|
82
|
+
hidden: true, // Hide this field in the admin panel for security/cleanliness
|
|
83
|
+
},
|
|
84
|
+
label: 'Verification Token',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'verificationTokenExpires',
|
|
88
|
+
type: 'date',
|
|
89
|
+
admin: {
|
|
90
|
+
hidden: true, // Hide this field in the admin panel for security/cleanliness
|
|
91
|
+
},
|
|
92
|
+
label: 'Verification Token Expiration',
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Plugin field relationship to optinchannels
|
|
97
|
+
*/
|
|
98
|
+
OptedInChannels,
|
|
99
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { PayloadSDK } from '@payloadcms/sdk'
|
|
4
|
+
import { type ChangeEvent, type SubmitEvent, useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import type { Config } from '../../copied/payload-types.js'
|
|
7
|
+
import type { RequestMagicLinkResponse } from '../../endpoints/requestMagicLink.js'
|
|
8
|
+
|
|
9
|
+
import { useSubscriber } from '../../contexts/SubscriberProvider.js'
|
|
10
|
+
import { useServerUrl } from '../../react-hooks/useServerUrl.js'
|
|
11
|
+
import { mergeClassNames } from './helpers.js'
|
|
12
|
+
import styles from './shared.module.css'
|
|
13
|
+
|
|
14
|
+
export { RequestMagicLinkResponse }
|
|
15
|
+
|
|
16
|
+
export interface IRequestMagicLink {
|
|
17
|
+
classNames?: RequestMagicLinkClasses
|
|
18
|
+
handleMagicLinkRequested?: (result: RequestMagicLinkResponse) => void
|
|
19
|
+
props?: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type RequestMagicLinkClasses = {
|
|
23
|
+
button?: string
|
|
24
|
+
container?: string
|
|
25
|
+
emailInput?: string
|
|
26
|
+
error?: string
|
|
27
|
+
form?: string
|
|
28
|
+
message?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type statusValues = 'default' | 'error' | 'sent'
|
|
32
|
+
|
|
33
|
+
export const RequestMagicLink = ({
|
|
34
|
+
classNames = {
|
|
35
|
+
button: '',
|
|
36
|
+
container: '',
|
|
37
|
+
emailInput: '',
|
|
38
|
+
error: '',
|
|
39
|
+
form: '',
|
|
40
|
+
message: '',
|
|
41
|
+
},
|
|
42
|
+
handleMagicLinkRequested,
|
|
43
|
+
}: IRequestMagicLink) => {
|
|
44
|
+
const { subscriber } = useSubscriber()
|
|
45
|
+
const { serverURL } = useServerUrl()
|
|
46
|
+
|
|
47
|
+
const [status, setStatus] = useState<statusValues>('default')
|
|
48
|
+
|
|
49
|
+
const sdk = new PayloadSDK<Config>({
|
|
50
|
+
baseURL: serverURL || '',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const [result, setResult] = useState<string>()
|
|
54
|
+
const [email, setEmail] = useState(subscriber?.email || '')
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setEmail(subscriber?.email || '')
|
|
58
|
+
}, [subscriber])
|
|
59
|
+
|
|
60
|
+
const handleSubmit = async (e: SubmitEvent<HTMLFormElement>) => {
|
|
61
|
+
e.preventDefault()
|
|
62
|
+
const forwardUrl = window.location.pathname + '?now=' + new Date().toISOString()
|
|
63
|
+
const emailTokenResponse = await sdk.request({
|
|
64
|
+
json: {
|
|
65
|
+
email,
|
|
66
|
+
forwardUrl,
|
|
67
|
+
},
|
|
68
|
+
method: 'POST',
|
|
69
|
+
path: '/api/emailToken',
|
|
70
|
+
})
|
|
71
|
+
if (emailTokenResponse.ok) {
|
|
72
|
+
const emailTokenResponseJson: RequestMagicLinkResponse = await emailTokenResponse.json()
|
|
73
|
+
if (handleMagicLinkRequested) {
|
|
74
|
+
handleMagicLinkRequested(emailTokenResponseJson)
|
|
75
|
+
}
|
|
76
|
+
// @ts-expect-error One or the other exists
|
|
77
|
+
const { emailResult, error } = emailTokenResponseJson
|
|
78
|
+
if (error) {
|
|
79
|
+
setStatus('error')
|
|
80
|
+
setResult(`An error occured. Please try again. \n ${error}`)
|
|
81
|
+
} else if (emailResult) {
|
|
82
|
+
setStatus('sent')
|
|
83
|
+
setResult('An email has been sent containing your magic link.')
|
|
84
|
+
} else {
|
|
85
|
+
setStatus('error')
|
|
86
|
+
setResult(`An error occured. Please try again. \nResult unknown`)
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
const emailTokenResponseText = await emailTokenResponse.text()
|
|
90
|
+
setStatus('error')
|
|
91
|
+
setResult(`An error occured. Please try again. \n${emailTokenResponseText}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={mergeClassNames([styles.container, classNames.container])}>
|
|
97
|
+
{result ? (
|
|
98
|
+
<p
|
|
99
|
+
className={mergeClassNames([
|
|
100
|
+
styles.message,
|
|
101
|
+
classNames.message,
|
|
102
|
+
status == 'error' ? [styles.error, classNames.error] : [],
|
|
103
|
+
])}
|
|
104
|
+
>
|
|
105
|
+
{result}
|
|
106
|
+
</p>
|
|
107
|
+
) : (
|
|
108
|
+
<></>
|
|
109
|
+
)}
|
|
110
|
+
<form
|
|
111
|
+
className={mergeClassNames([styles.form, classNames.form])}
|
|
112
|
+
method="POST"
|
|
113
|
+
onSubmit={handleSubmit}
|
|
114
|
+
>
|
|
115
|
+
<input
|
|
116
|
+
aria-label="enter your email"
|
|
117
|
+
className={mergeClassNames([styles.emailInput, classNames.emailInput])}
|
|
118
|
+
onChange={(e: ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
|
|
119
|
+
placeholder="enter your email"
|
|
120
|
+
type="email"
|
|
121
|
+
value={email}
|
|
122
|
+
/>
|
|
123
|
+
<button className={mergeClassNames([styles.button, classNames.button])} type="submit">
|
|
124
|
+
Request magic link
|
|
125
|
+
</button>
|
|
126
|
+
</form>
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useSubscriber } from '../../contexts/SubscriberProvider.js'
|
|
4
|
+
import {
|
|
5
|
+
RequestMagicLink,
|
|
6
|
+
type RequestMagicLinkResponse,
|
|
7
|
+
Subscribe,
|
|
8
|
+
type SubscribeResponse,
|
|
9
|
+
} from '../../exports/ui.js'
|
|
10
|
+
|
|
11
|
+
export type { RequestMagicLinkResponse, SubscribeResponse }
|
|
12
|
+
|
|
13
|
+
export type RequestOrSubscribeClasses = {
|
|
14
|
+
button?: string
|
|
15
|
+
container?: string
|
|
16
|
+
emailInput?: string
|
|
17
|
+
error?: string
|
|
18
|
+
form?: string
|
|
19
|
+
loading?: string
|
|
20
|
+
message?: string
|
|
21
|
+
section?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function RequestOrSubscribe({
|
|
25
|
+
classNames = {
|
|
26
|
+
button: '',
|
|
27
|
+
container: '',
|
|
28
|
+
emailInput: '',
|
|
29
|
+
error: '',
|
|
30
|
+
form: '',
|
|
31
|
+
loading: '',
|
|
32
|
+
message: '',
|
|
33
|
+
section: '',
|
|
34
|
+
},
|
|
35
|
+
handleMagicLinkRequested,
|
|
36
|
+
handleSubscribe,
|
|
37
|
+
}: {
|
|
38
|
+
classNames?: RequestOrSubscribeClasses
|
|
39
|
+
handleMagicLinkRequested?: (result: RequestMagicLinkResponse) => void
|
|
40
|
+
handleSubscribe?: (result: SubscribeResponse) => void
|
|
41
|
+
}) {
|
|
42
|
+
const { subscriber } = useSubscriber()
|
|
43
|
+
|
|
44
|
+
// Example: Conditionally render something or pass the state to children
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
{subscriber ? (
|
|
48
|
+
<Subscribe classNames={classNames} handleSubscribe={handleSubscribe} />
|
|
49
|
+
) : (
|
|
50
|
+
<RequestMagicLink
|
|
51
|
+
classNames={classNames}
|
|
52
|
+
handleMagicLinkRequested={handleMagicLinkRequested}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
{/* <div>subscriber = {JSON.stringify(subscriber)}</div> */}
|
|
56
|
+
</>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { PayloadSDK } from '@payloadcms/sdk'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import type { Config, OptInChannel } from '../../copied/payload-types.js'
|
|
7
|
+
import type { GetOptInChannelsResponse } from '../../endpoints/getOptInChannels.js'
|
|
8
|
+
|
|
9
|
+
import { useServerUrl } from '../../react-hooks/useServerUrl.js'
|
|
10
|
+
import { mergeClassNames } from './helpers.js'
|
|
11
|
+
import styles from './shared.module.css'
|
|
12
|
+
|
|
13
|
+
// const payload = await getPayload({
|
|
14
|
+
// config: configPromise,
|
|
15
|
+
// })
|
|
16
|
+
|
|
17
|
+
// Pass your config from generated types as generic
|
|
18
|
+
|
|
19
|
+
export interface ISelectOptInChannels {
|
|
20
|
+
classNames?: SelectOptInChannelsClasses
|
|
21
|
+
handleOptInChannelsSelected?: (result: OptInChannel[]) => void
|
|
22
|
+
props?: any
|
|
23
|
+
selectedOptInChannelIDs?: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SelectOptInChannelsClasses = {
|
|
27
|
+
button?: string
|
|
28
|
+
container?: string
|
|
29
|
+
error?: string
|
|
30
|
+
form?: string
|
|
31
|
+
loading?: string
|
|
32
|
+
message?: string
|
|
33
|
+
optInCheckbox?: string
|
|
34
|
+
optInCheckboxItem?: string
|
|
35
|
+
optInCheckboxLabel?: string
|
|
36
|
+
optionsGroup?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const SelectOptInChannels = ({
|
|
40
|
+
classNames = {
|
|
41
|
+
button: '',
|
|
42
|
+
container: '',
|
|
43
|
+
error: '',
|
|
44
|
+
form: '',
|
|
45
|
+
loading: '',
|
|
46
|
+
message: '',
|
|
47
|
+
optInCheckbox: '',
|
|
48
|
+
optInCheckboxItem: '',
|
|
49
|
+
optInCheckboxLabel: '',
|
|
50
|
+
optionsGroup: '',
|
|
51
|
+
},
|
|
52
|
+
handleOptInChannelsSelected,
|
|
53
|
+
selectedOptInChannelIDs,
|
|
54
|
+
}: ISelectOptInChannels) => {
|
|
55
|
+
const { serverURL } = useServerUrl()
|
|
56
|
+
// const { serverURL } = { serverURL: 'http://localhost:3001' }
|
|
57
|
+
type OptInChannelCheckbox = {
|
|
58
|
+
isChecked: boolean
|
|
59
|
+
} & OptInChannel
|
|
60
|
+
const [result, setResult] = useState<any>()
|
|
61
|
+
const [allOptInChannels, setAllOptInChannels] = useState<OptInChannelCheckbox[]>([])
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
async function verify() {
|
|
65
|
+
const sdk = new PayloadSDK<Config>({
|
|
66
|
+
baseURL: serverURL || '',
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
console.log('calling optinchannels endpoint')
|
|
70
|
+
const result = await sdk.request({
|
|
71
|
+
method: 'GET',
|
|
72
|
+
path: '/api/optinchannels',
|
|
73
|
+
})
|
|
74
|
+
if (result.ok) {
|
|
75
|
+
const resultJson: GetOptInChannelsResponse = await result.json()
|
|
76
|
+
setResult(resultJson)
|
|
77
|
+
} else {
|
|
78
|
+
const resultText = await result.text()
|
|
79
|
+
setResult(resultText)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
void verify()
|
|
83
|
+
}, [serverURL])
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const channels = result?.optInChannels?.map((channel: OptInChannel) => ({
|
|
87
|
+
...channel,
|
|
88
|
+
isChecked: selectedOptInChannelIDs?.includes(channel.id),
|
|
89
|
+
}))
|
|
90
|
+
setAllOptInChannels(channels)
|
|
91
|
+
}, [result, selectedOptInChannelIDs])
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className={mergeClassNames([styles.container, classNames.container])}>
|
|
95
|
+
<h3>Opt-in Channels</h3>
|
|
96
|
+
{!result ? (
|
|
97
|
+
<p className={mergeClassNames([styles.loading, classNames.loading])}>verifying...</p>
|
|
98
|
+
) : (
|
|
99
|
+
<div className={mergeClassNames([styles.optionsGroup, classNames.optionsGroup])}>
|
|
100
|
+
{// Map over the tasks array to render each checkbox
|
|
101
|
+
allOptInChannels?.map((channel) => (
|
|
102
|
+
<div
|
|
103
|
+
className={mergeClassNames([styles.optInCheckboxItem, classNames.optInCheckboxItem])}
|
|
104
|
+
key={channel.id}
|
|
105
|
+
>
|
|
106
|
+
<label
|
|
107
|
+
className={mergeClassNames([
|
|
108
|
+
styles.optInCheckboxLabel,
|
|
109
|
+
classNames.optInCheckboxLabel,
|
|
110
|
+
])}
|
|
111
|
+
>
|
|
112
|
+
<input
|
|
113
|
+
aria-label={channel.title}
|
|
114
|
+
// The checked prop is controlled by the state
|
|
115
|
+
checked={channel.isChecked}
|
|
116
|
+
className={mergeClassNames([styles.optInCheckbox, classNames.optInCheckbox])}
|
|
117
|
+
// The onChange handler calls the update function with the item's ID
|
|
118
|
+
onChange={(event) => {
|
|
119
|
+
event.preventDefault()
|
|
120
|
+
|
|
121
|
+
const checked = event.target.checked
|
|
122
|
+
|
|
123
|
+
if (handleOptInChannelsSelected) {
|
|
124
|
+
handleOptInChannelsSelected(
|
|
125
|
+
allOptInChannels
|
|
126
|
+
.map((channel) => ({
|
|
127
|
+
...channel,
|
|
128
|
+
isChecked:
|
|
129
|
+
channel.title == event.target.value ? checked : channel.isChecked,
|
|
130
|
+
}))
|
|
131
|
+
.filter((c) => c.isChecked)
|
|
132
|
+
.map((channel) => ({ ...channel, isChecked: undefined })),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}}
|
|
136
|
+
type="checkbox"
|
|
137
|
+
value={channel.title}
|
|
138
|
+
/>
|
|
139
|
+
{channel.title}
|
|
140
|
+
</label>
|
|
141
|
+
</div>
|
|
142
|
+
))}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|