create-nextblock 0.16.4 → 0.17.0
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/CLAUDE.md +22 -0
- package/docker-template/.env.docker.example +69 -69
- package/docker-template/docker/db/init/99-jwt.sql +6 -6
- package/docker-template/docker/db/init/99-roles.sql +25 -25
- package/docker-template/docker/kong/kong.yml +112 -112
- package/docker-template/scripts/docker-setup.mjs +310 -310
- package/package.json +1 -1
- package/templates/nextblock-template/.browserslistrc +11 -11
- package/templates/nextblock-template/.swcrc +30 -30
- package/templates/nextblock-template/CLAUDE.md +26 -0
- package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
- package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
- package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -16
- package/templates/nextblock-template/app/actions/consent.ts +57 -57
- package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
- package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
- package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
- package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
- package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -29
- package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
- package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
- package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
- package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +270 -205
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +8096 -7198
- package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
- package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
- package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
- package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
- package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
- package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -47
- package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -47
- package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +442 -441
- package/templates/nextblock-template/app/article/[slug]/page.utils.ts +4 -1
- package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -20
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +241 -241
- package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
- package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
- package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
- package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -23
- package/templates/nextblock-template/app/cms/components/SeoScoreBadge.tsx +3 -3
- package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
- package/templates/nextblock-template/app/cms/components/TablePagination.tsx +136 -136
- package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
- package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
- package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
- package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -16
- package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
- package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
- package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
- package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
- package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -200
- package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
- package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
- package/templates/nextblock-template/app/cms/pages/page.tsx +273 -273
- package/templates/nextblock-template/app/cms/posts/page.tsx +251 -251
- package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
- package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
- package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
- package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
- package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
- package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
- package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
- package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
- package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +276 -276
- package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
- package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
- package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
- package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
- package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
- package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
- package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
- package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -190
- package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -56
- package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
- package/templates/nextblock-template/app/setup/layout.tsx +13 -13
- package/templates/nextblock-template/app/setup/page.tsx +111 -111
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -38
- package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -68
- package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -35
- package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -34
- package/templates/nextblock-template/components/CartTranslator.tsx +210 -210
- package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -51
- package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -68
- package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
- package/templates/nextblock-template/components/GlobalSearch.tsx +557 -557
- package/templates/nextblock-template/components/Header.tsx +38 -38
- package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
- package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
- package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
- package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +48 -48
- package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +9 -9
- package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -25
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -92
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +11 -0
- package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +24 -24
- package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -57
- package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +2 -2
- package/templates/nextblock-template/components/media/YouTubeFacade.tsx +105 -105
- package/templates/nextblock-template/components/media/youtube-embed-replace.tsx +32 -32
- package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
- package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
- package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
- package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +471 -471
- package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
- package/templates/nextblock-template/context/language-rest-client.ts +32 -32
- package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
- package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
- package/templates/nextblock-template/docker/kong/kong.yml +112 -112
- package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
- package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +11 -0
- package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +19 -0
- package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
- package/templates/nextblock-template/lib/app-secrets.ts +39 -39
- package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
- package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
- package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
- package/templates/nextblock-template/lib/blocks/README.md +13 -13
- package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
- package/templates/nextblock-template/lib/cms/payments-reminder.test.ts +135 -135
- package/templates/nextblock-template/lib/cms/payments-reminder.ts +105 -105
- package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
- package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
- package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
- package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
- package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
- package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
- package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
- package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
- package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
- package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
- package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
- package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
- package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
- package/templates/nextblock-template/lib/email/branding.ts +76 -76
- package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
- package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
- package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
- package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +56 -54
- package/templates/nextblock-template/lib/media/youtube.ts +99 -99
- package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
- package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
- package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
- package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
- package/templates/nextblock-template/lib/privacy/types.ts +69 -69
- package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
- package/templates/nextblock-template/lib/promotions/server.ts +741 -741
- package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
- package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
- package/templates/nextblock-template/lib/seo/page-document.ts +4 -4
- package/templates/nextblock-template/lib/setup/actions.ts +460 -460
- package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
- package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +42 -12
- package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
- package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
- package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
- package/templates/nextblock-template/lib/setup/types.ts +18 -18
- package/templates/nextblock-template/lib/storage/provider.ts +66 -66
- package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
- package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
- package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
- package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -105
- package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -42
- package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -143
- package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -94
- package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -81
- package/templates/nextblock-template/lib/zod-config.ts +5 -5
- package/templates/nextblock-template/next-env.d.ts +1 -0
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/public/images/cortex_post.webp +0 -0
- package/templates/nextblock-template/public/images/update_nextblock.webp +0 -0
- package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
- package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -112
- package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
- package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
- package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
- package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
- package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
- package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
- package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
- package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
- package/templates/nextblock-template/tools/deploy-supabase.js +159 -159
- package/templates/nextblock-template/types/jsdom.d.ts +6 -6
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
type ProductRow = {
|
|
4
|
-
product_type: 'physical' | 'digital';
|
|
5
|
-
status: 'active' | 'draft';
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const mocks = vi.hoisted(() => ({
|
|
9
|
-
getStoreReadiness: vi.fn(),
|
|
10
|
-
verifyPackageOnline: vi.fn(),
|
|
11
|
-
products: [] as ProductRow[],
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
vi.mock('server-only', () => ({}));
|
|
15
|
-
|
|
16
|
-
vi.mock('@nextblock-cms/ecommerce/server', () => ({
|
|
17
|
-
getStoreReadiness: mocks.getStoreReadiness,
|
|
18
|
-
}));
|
|
19
|
-
|
|
20
|
-
vi.mock('@nextblock-cms/db/server', () => ({
|
|
21
|
-
verifyPackageOnline: mocks.verifyPackageOnline,
|
|
22
|
-
createClient: () => ({
|
|
23
|
-
auth: {
|
|
24
|
-
getUser: async () => ({ data: { user: { id: 'admin-1' } } }),
|
|
25
|
-
},
|
|
26
|
-
from: (table: string) => {
|
|
27
|
-
if (table === 'profiles') {
|
|
28
|
-
return {
|
|
29
|
-
select: () => ({
|
|
30
|
-
eq: () => ({
|
|
31
|
-
maybeSingle: async () => ({ data: { role: 'ADMIN' } }),
|
|
32
|
-
}),
|
|
33
|
-
}),
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (table === 'products') {
|
|
38
|
-
return {
|
|
39
|
-
select: () => {
|
|
40
|
-
const filters: Array<(product: ProductRow) => boolean> = [];
|
|
41
|
-
const query = {
|
|
42
|
-
eq(column: keyof ProductRow, value: string) {
|
|
43
|
-
filters.push((product) => product[column] === value);
|
|
44
|
-
return query;
|
|
45
|
-
},
|
|
46
|
-
then(onFulfilled: (value: { count: number }) => unknown) {
|
|
47
|
-
const count = mocks.products.filter((product) =>
|
|
48
|
-
filters.every((filter) => filter(product)),
|
|
49
|
-
).length;
|
|
50
|
-
return Promise.resolve({ count }).then(onFulfilled);
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
return query;
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
throw new Error(`Unexpected table in test: ${table}`);
|
|
59
|
-
},
|
|
60
|
-
}),
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
import { getPaymentsReminder } from './payments-reminder';
|
|
64
|
-
|
|
65
|
-
function providerReadiness({
|
|
66
|
-
stripeReady,
|
|
67
|
-
freemiusReady,
|
|
68
|
-
}: {
|
|
69
|
-
stripeReady: boolean;
|
|
70
|
-
freemiusReady: boolean;
|
|
71
|
-
}) {
|
|
72
|
-
return {
|
|
73
|
-
stripe: {
|
|
74
|
-
provider: 'stripe',
|
|
75
|
-
label: 'Stripe',
|
|
76
|
-
enabled: true,
|
|
77
|
-
hasKeys: stripeReady,
|
|
78
|
-
ready: stripeReady,
|
|
79
|
-
missing: stripeReady ? [] : ['Secret key'],
|
|
80
|
-
},
|
|
81
|
-
freemius: {
|
|
82
|
-
provider: 'freemius',
|
|
83
|
-
label: 'Freemius',
|
|
84
|
-
enabled: true,
|
|
85
|
-
hasKeys: freemiusReady,
|
|
86
|
-
ready: freemiusReady,
|
|
87
|
-
missing: freemiusReady ? [] : ['Secret key'],
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
describe('CMS payments reminder', () => {
|
|
93
|
-
beforeEach(() => {
|
|
94
|
-
vi.clearAllMocks();
|
|
95
|
-
mocks.products.length = 0;
|
|
96
|
-
mocks.verifyPackageOnline.mockResolvedValue(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('ignores an unconfigured Stripe toggle when the store has only digital products', async () => {
|
|
100
|
-
mocks.products.push({ product_type: 'digital', status: 'active' });
|
|
101
|
-
mocks.getStoreReadiness.mockResolvedValue(
|
|
102
|
-
providerReadiness({ stripeReady: false, freemiusReady: true }),
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
await expect(getPaymentsReminder()).resolves.toBeNull();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('warns about Stripe when any physical product needs it', async () => {
|
|
109
|
-
mocks.products.push({ product_type: 'physical', status: 'draft' });
|
|
110
|
-
mocks.getStoreReadiness.mockResolvedValue(
|
|
111
|
-
providerReadiness({ stripeReady: false, freemiusReady: true }),
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
await expect(getPaymentsReminder()).resolves.toEqual({
|
|
115
|
-
blocked: [
|
|
116
|
-
{ provider: 'stripe', label: 'Stripe', missing: ['Secret key'] },
|
|
117
|
-
],
|
|
118
|
-
affectedProducts: 0,
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('warns about Freemius when a digital product needs it', async () => {
|
|
123
|
-
mocks.products.push({ product_type: 'digital', status: 'active' });
|
|
124
|
-
mocks.getStoreReadiness.mockResolvedValue(
|
|
125
|
-
providerReadiness({ stripeReady: true, freemiusReady: false }),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
await expect(getPaymentsReminder()).resolves.toEqual({
|
|
129
|
-
blocked: [
|
|
130
|
-
{ provider: 'freemius', label: 'Freemius', missing: ['Secret key'] },
|
|
131
|
-
],
|
|
132
|
-
affectedProducts: 1,
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
});
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
type ProductRow = {
|
|
4
|
+
product_type: 'physical' | 'digital';
|
|
5
|
+
status: 'active' | 'draft';
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const mocks = vi.hoisted(() => ({
|
|
9
|
+
getStoreReadiness: vi.fn(),
|
|
10
|
+
verifyPackageOnline: vi.fn(),
|
|
11
|
+
products: [] as ProductRow[],
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('server-only', () => ({}));
|
|
15
|
+
|
|
16
|
+
vi.mock('@nextblock-cms/ecommerce/server', () => ({
|
|
17
|
+
getStoreReadiness: mocks.getStoreReadiness,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@nextblock-cms/db/server', () => ({
|
|
21
|
+
verifyPackageOnline: mocks.verifyPackageOnline,
|
|
22
|
+
createClient: () => ({
|
|
23
|
+
auth: {
|
|
24
|
+
getUser: async () => ({ data: { user: { id: 'admin-1' } } }),
|
|
25
|
+
},
|
|
26
|
+
from: (table: string) => {
|
|
27
|
+
if (table === 'profiles') {
|
|
28
|
+
return {
|
|
29
|
+
select: () => ({
|
|
30
|
+
eq: () => ({
|
|
31
|
+
maybeSingle: async () => ({ data: { role: 'ADMIN' } }),
|
|
32
|
+
}),
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (table === 'products') {
|
|
38
|
+
return {
|
|
39
|
+
select: () => {
|
|
40
|
+
const filters: Array<(product: ProductRow) => boolean> = [];
|
|
41
|
+
const query = {
|
|
42
|
+
eq(column: keyof ProductRow, value: string) {
|
|
43
|
+
filters.push((product) => product[column] === value);
|
|
44
|
+
return query;
|
|
45
|
+
},
|
|
46
|
+
then(onFulfilled: (value: { count: number }) => unknown) {
|
|
47
|
+
const count = mocks.products.filter((product) =>
|
|
48
|
+
filters.every((filter) => filter(product)),
|
|
49
|
+
).length;
|
|
50
|
+
return Promise.resolve({ count }).then(onFulfilled);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
return query;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error(`Unexpected table in test: ${table}`);
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
import { getPaymentsReminder } from './payments-reminder';
|
|
64
|
+
|
|
65
|
+
function providerReadiness({
|
|
66
|
+
stripeReady,
|
|
67
|
+
freemiusReady,
|
|
68
|
+
}: {
|
|
69
|
+
stripeReady: boolean;
|
|
70
|
+
freemiusReady: boolean;
|
|
71
|
+
}) {
|
|
72
|
+
return {
|
|
73
|
+
stripe: {
|
|
74
|
+
provider: 'stripe',
|
|
75
|
+
label: 'Stripe',
|
|
76
|
+
enabled: true,
|
|
77
|
+
hasKeys: stripeReady,
|
|
78
|
+
ready: stripeReady,
|
|
79
|
+
missing: stripeReady ? [] : ['Secret key'],
|
|
80
|
+
},
|
|
81
|
+
freemius: {
|
|
82
|
+
provider: 'freemius',
|
|
83
|
+
label: 'Freemius',
|
|
84
|
+
enabled: true,
|
|
85
|
+
hasKeys: freemiusReady,
|
|
86
|
+
ready: freemiusReady,
|
|
87
|
+
missing: freemiusReady ? [] : ['Secret key'],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('CMS payments reminder', () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
vi.clearAllMocks();
|
|
95
|
+
mocks.products.length = 0;
|
|
96
|
+
mocks.verifyPackageOnline.mockResolvedValue(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('ignores an unconfigured Stripe toggle when the store has only digital products', async () => {
|
|
100
|
+
mocks.products.push({ product_type: 'digital', status: 'active' });
|
|
101
|
+
mocks.getStoreReadiness.mockResolvedValue(
|
|
102
|
+
providerReadiness({ stripeReady: false, freemiusReady: true }),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await expect(getPaymentsReminder()).resolves.toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('warns about Stripe when any physical product needs it', async () => {
|
|
109
|
+
mocks.products.push({ product_type: 'physical', status: 'draft' });
|
|
110
|
+
mocks.getStoreReadiness.mockResolvedValue(
|
|
111
|
+
providerReadiness({ stripeReady: false, freemiusReady: true }),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await expect(getPaymentsReminder()).resolves.toEqual({
|
|
115
|
+
blocked: [
|
|
116
|
+
{ provider: 'stripe', label: 'Stripe', missing: ['Secret key'] },
|
|
117
|
+
],
|
|
118
|
+
affectedProducts: 0,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('warns about Freemius when a digital product needs it', async () => {
|
|
123
|
+
mocks.products.push({ product_type: 'digital', status: 'active' });
|
|
124
|
+
mocks.getStoreReadiness.mockResolvedValue(
|
|
125
|
+
providerReadiness({ stripeReady: true, freemiusReady: false }),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await expect(getPaymentsReminder()).resolves.toEqual({
|
|
129
|
+
blocked: [
|
|
130
|
+
{ provider: 'freemius', label: 'Freemius', missing: ['Secret key'] },
|
|
131
|
+
],
|
|
132
|
+
affectedProducts: 1,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
import 'server-only';
|
|
2
|
-
|
|
3
|
-
import { createClient, verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
4
|
-
import { getStoreReadiness } from '@nextblock-cms/ecommerce/server';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* A store that has commerce switched on but cannot actually take money. This is easy to
|
|
8
|
-
* end up in — the products, prices and pages all look finished — and the only symptom
|
|
9
|
-
* used to be a shopper hitting a payment error at checkout. The banner makes the gap
|
|
10
|
-
* visible in the CMS instead of on the storefront.
|
|
11
|
-
*/
|
|
12
|
-
export interface PaymentsReminder {
|
|
13
|
-
/** Providers that are not ready, with what each is missing. */
|
|
14
|
-
blocked: Array<{
|
|
15
|
-
provider: 'stripe' | 'freemius';
|
|
16
|
-
label: string;
|
|
17
|
-
missing: string[];
|
|
18
|
-
}>;
|
|
19
|
-
/** Published products that currently cannot be bought because of the above. */
|
|
20
|
-
affectedProducts: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Best-effort reminder state for the CMS layout. Returns null (no banner) whenever
|
|
25
|
-
* nagging would be wrong or the answer can't be determined — an unmigrated database,
|
|
26
|
-
* an instance without commerce, or the sandbox.
|
|
27
|
-
*/
|
|
28
|
-
export async function getPaymentsReminder(): Promise<PaymentsReminder | null> {
|
|
29
|
-
// The sandbox refuses to store live credentials at all (savePaymentCredentials throws),
|
|
30
|
-
// so a "finish setting up payments" nag there is noise the operator cannot act on.
|
|
31
|
-
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') return null;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
if (!(await verifyPackageOnline('ecommerce'))) return null;
|
|
35
|
-
|
|
36
|
-
const supabase = createClient();
|
|
37
|
-
|
|
38
|
-
// Only ADMINs can act on this (the /cms/payments route is admin-only), so don't do
|
|
39
|
-
// the work for anyone else.
|
|
40
|
-
const {
|
|
41
|
-
data: { user },
|
|
42
|
-
} = await supabase.auth.getUser();
|
|
43
|
-
if (!user) return null;
|
|
44
|
-
const { data: profile } = await supabase
|
|
45
|
-
.from('profiles')
|
|
46
|
-
.select('role')
|
|
47
|
-
.eq('id', user.id)
|
|
48
|
-
.maybeSingle();
|
|
49
|
-
if (profile?.role !== 'ADMIN') return null;
|
|
50
|
-
|
|
51
|
-
const readiness = await getStoreReadiness();
|
|
52
|
-
const unready = (['stripe', 'freemius'] as const)
|
|
53
|
-
.map((provider) => readiness[provider])
|
|
54
|
-
.filter((entry) => !entry.ready);
|
|
55
|
-
|
|
56
|
-
if (unready.length === 0) return null;
|
|
57
|
-
|
|
58
|
-
// A provider matters to this reminder only when the catalogue contains products
|
|
59
|
-
// of the type routed through it. Do not treat a stale/accidental provider toggle as
|
|
60
|
-
// usage: that produced a Stripe warning in digital-only stores with no Stripe keys.
|
|
61
|
-
// Count across ALL statuses so a shop building a draft catalogue is warned before
|
|
62
|
-
// it publishes rather than after.
|
|
63
|
-
const usage = await Promise.all(
|
|
64
|
-
unready.map(async (entry) => {
|
|
65
|
-
const productType =
|
|
66
|
-
entry.provider === 'stripe' ? 'physical' : 'digital';
|
|
67
|
-
const [{ count: totalCount }, { count: activeCount }] =
|
|
68
|
-
await Promise.all([
|
|
69
|
-
supabase
|
|
70
|
-
.from('products')
|
|
71
|
-
.select('id', { count: 'exact', head: true })
|
|
72
|
-
.eq('product_type', productType),
|
|
73
|
-
supabase
|
|
74
|
-
.from('products')
|
|
75
|
-
.select('id', { count: 'exact', head: true })
|
|
76
|
-
.eq('status', 'active')
|
|
77
|
-
.eq('product_type', productType),
|
|
78
|
-
]);
|
|
79
|
-
return {
|
|
80
|
-
entry,
|
|
81
|
-
inUse: (totalCount ?? 0) > 0,
|
|
82
|
-
activeProducts: activeCount ?? 0,
|
|
83
|
-
};
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const relevant = usage.filter((item) => item.inUse);
|
|
88
|
-
if (relevant.length === 0) return null;
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
blocked: relevant.map(({ entry }) => ({
|
|
92
|
-
provider: entry.provider,
|
|
93
|
-
label: entry.label,
|
|
94
|
-
missing: entry.missing,
|
|
95
|
-
})),
|
|
96
|
-
affectedProducts: relevant.reduce(
|
|
97
|
-
(total, item) => total + item.activeProducts,
|
|
98
|
-
0,
|
|
99
|
-
),
|
|
100
|
-
};
|
|
101
|
-
} catch {
|
|
102
|
-
// Never let a reminder break the CMS shell.
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { createClient, verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
4
|
+
import { getStoreReadiness } from '@nextblock-cms/ecommerce/server';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A store that has commerce switched on but cannot actually take money. This is easy to
|
|
8
|
+
* end up in — the products, prices and pages all look finished — and the only symptom
|
|
9
|
+
* used to be a shopper hitting a payment error at checkout. The banner makes the gap
|
|
10
|
+
* visible in the CMS instead of on the storefront.
|
|
11
|
+
*/
|
|
12
|
+
export interface PaymentsReminder {
|
|
13
|
+
/** Providers that are not ready, with what each is missing. */
|
|
14
|
+
blocked: Array<{
|
|
15
|
+
provider: 'stripe' | 'freemius';
|
|
16
|
+
label: string;
|
|
17
|
+
missing: string[];
|
|
18
|
+
}>;
|
|
19
|
+
/** Published products that currently cannot be bought because of the above. */
|
|
20
|
+
affectedProducts: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Best-effort reminder state for the CMS layout. Returns null (no banner) whenever
|
|
25
|
+
* nagging would be wrong or the answer can't be determined — an unmigrated database,
|
|
26
|
+
* an instance without commerce, or the sandbox.
|
|
27
|
+
*/
|
|
28
|
+
export async function getPaymentsReminder(): Promise<PaymentsReminder | null> {
|
|
29
|
+
// The sandbox refuses to store live credentials at all (savePaymentCredentials throws),
|
|
30
|
+
// so a "finish setting up payments" nag there is noise the operator cannot act on.
|
|
31
|
+
if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') return null;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (!(await verifyPackageOnline('ecommerce'))) return null;
|
|
35
|
+
|
|
36
|
+
const supabase = createClient();
|
|
37
|
+
|
|
38
|
+
// Only ADMINs can act on this (the /cms/payments route is admin-only), so don't do
|
|
39
|
+
// the work for anyone else.
|
|
40
|
+
const {
|
|
41
|
+
data: { user },
|
|
42
|
+
} = await supabase.auth.getUser();
|
|
43
|
+
if (!user) return null;
|
|
44
|
+
const { data: profile } = await supabase
|
|
45
|
+
.from('profiles')
|
|
46
|
+
.select('role')
|
|
47
|
+
.eq('id', user.id)
|
|
48
|
+
.maybeSingle();
|
|
49
|
+
if (profile?.role !== 'ADMIN') return null;
|
|
50
|
+
|
|
51
|
+
const readiness = await getStoreReadiness();
|
|
52
|
+
const unready = (['stripe', 'freemius'] as const)
|
|
53
|
+
.map((provider) => readiness[provider])
|
|
54
|
+
.filter((entry) => !entry.ready);
|
|
55
|
+
|
|
56
|
+
if (unready.length === 0) return null;
|
|
57
|
+
|
|
58
|
+
// A provider matters to this reminder only when the catalogue contains products
|
|
59
|
+
// of the type routed through it. Do not treat a stale/accidental provider toggle as
|
|
60
|
+
// usage: that produced a Stripe warning in digital-only stores with no Stripe keys.
|
|
61
|
+
// Count across ALL statuses so a shop building a draft catalogue is warned before
|
|
62
|
+
// it publishes rather than after.
|
|
63
|
+
const usage = await Promise.all(
|
|
64
|
+
unready.map(async (entry) => {
|
|
65
|
+
const productType =
|
|
66
|
+
entry.provider === 'stripe' ? 'physical' : 'digital';
|
|
67
|
+
const [{ count: totalCount }, { count: activeCount }] =
|
|
68
|
+
await Promise.all([
|
|
69
|
+
supabase
|
|
70
|
+
.from('products')
|
|
71
|
+
.select('id', { count: 'exact', head: true })
|
|
72
|
+
.eq('product_type', productType),
|
|
73
|
+
supabase
|
|
74
|
+
.from('products')
|
|
75
|
+
.select('id', { count: 'exact', head: true })
|
|
76
|
+
.eq('status', 'active')
|
|
77
|
+
.eq('product_type', productType),
|
|
78
|
+
]);
|
|
79
|
+
return {
|
|
80
|
+
entry,
|
|
81
|
+
inUse: (totalCount ?? 0) > 0,
|
|
82
|
+
activeProducts: activeCount ?? 0,
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const relevant = usage.filter((item) => item.inUse);
|
|
88
|
+
if (relevant.length === 0) return null;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
blocked: relevant.map(({ entry }) => ({
|
|
92
|
+
provider: entry.provider,
|
|
93
|
+
label: entry.label,
|
|
94
|
+
missing: entry.missing,
|
|
95
|
+
})),
|
|
96
|
+
affectedProducts: relevant.reduce(
|
|
97
|
+
(total, item) => total + item.activeProducts,
|
|
98
|
+
0,
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
} catch {
|
|
102
|
+
// Never let a reminder break the CMS shell.
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|