@varshylinc/onboarding-consent-engine 0.1.0 → 0.1.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/dist/client/components/ConsentBlock.d.ts +13 -0
- package/dist/client/components/ConsentBlock.d.ts.map +1 -0
- package/dist/client/components/ConsentBlock.js +14 -0
- package/dist/client/components/ConsentBlock.js.map +1 -0
- package/dist/client/components/ConsentCheckbox.d.ts +11 -0
- package/dist/client/components/ConsentCheckbox.d.ts.map +1 -0
- package/dist/client/components/ConsentCheckbox.js +5 -0
- package/dist/client/components/ConsentCheckbox.js.map +1 -0
- package/dist/client/components/ConsentUpdateModal.d.ts +12 -0
- package/dist/client/components/ConsentUpdateModal.d.ts.map +1 -0
- package/dist/client/components/ConsentUpdateModal.js +9 -0
- package/dist/client/components/ConsentUpdateModal.js.map +1 -0
- package/dist/client/components/EmptyState.d.ts +8 -0
- package/dist/client/components/EmptyState.d.ts.map +1 -0
- package/dist/client/components/EmptyState.js +5 -0
- package/dist/client/components/EmptyState.js.map +1 -0
- package/dist/client/components/WelcomeScreen.d.ts +15 -0
- package/dist/client/components/WelcomeScreen.d.ts.map +1 -0
- package/dist/client/components/WelcomeScreen.js +7 -0
- package/dist/client/components/WelcomeScreen.js.map +1 -0
- package/{src/client/components/index.ts → dist/client/components/index.d.ts} +1 -0
- package/dist/client/components/index.d.ts.map +1 -0
- package/dist/client/components/index.js +6 -0
- package/dist/client/components/index.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +2 -0
- package/dist/client/index.js.map +1 -0
- package/package.json +11 -6
- package/.eslintrc.cjs +0 -18
- package/CHANGELOG.md +0 -11
- package/MODULE.md +0 -130
- package/src/client/components/ConsentBlock.tsx +0 -73
- package/src/client/components/ConsentCheckbox.tsx +0 -53
- package/src/client/components/ConsentUpdateModal.tsx +0 -65
- package/src/client/components/EmptyState.tsx +0 -38
- package/src/client/components/WelcomeScreen.tsx +0 -64
- package/src/client/index.ts +0 -19
- package/src/index.ts +0 -20
- package/src/server/index.ts +0 -143
- package/src/server/lib/getAuditTrail.ts +0 -20
- package/src/server/lib/getCurrentConsents.ts +0 -25
- package/src/server/lib/getUserLatestConsents.ts +0 -27
- package/src/server/lib/hasUserConsented.ts +0 -18
- package/src/server/lib/index.ts +0 -7
- package/src/server/lib/needsConsentUpdate.ts +0 -28
- package/src/server/lib/recordConsent.ts +0 -13
- package/src/server/lib/recordSignupConsents.ts +0 -32
- package/src/server/migrations/0001_create_oce_schema_migrations.sql +0 -7
- package/src/server/migrations/0002_create_oce_consent_definitions.sql +0 -12
- package/src/server/migrations/0003_create_oce_user_consents.sql +0 -14
- package/src/server/migrations/0004_create_oce_consent_version_log.sql +0 -12
- package/src/server/templates/applyProductName.ts +0 -10
- package/src/server/templates/index.ts +0 -3
- package/src/server/templates/standardConsents.ts +0 -37
- package/src/shared/types.ts +0 -85
- package/tests/integration/integration.test.ts +0 -162
- package/tests/setup/global-setup.ts +0 -16
- package/tests/unit/applyProductName.test.ts +0 -20
- package/tests/unit/getAuditTrail.test.ts +0 -33
- package/tests/unit/hasUserConsented.test.ts +0 -24
- package/tests/unit/needsConsentUpdate.test.ts +0 -24
- package/tests/unit/recordConsent.test.ts +0 -41
- package/tsconfig.client.json +0 -15
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -9
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConsentDefinition } from '../../shared/types.js';
|
|
2
|
+
export interface ConsentBlockProps {
|
|
3
|
+
requiredConsents: ConsentDefinition[];
|
|
4
|
+
optionalConsents: ConsentDefinition[];
|
|
5
|
+
/** Array of definition keys the user has currently checked/granted */
|
|
6
|
+
value: string[];
|
|
7
|
+
onChange: (keys: string[]) => void;
|
|
8
|
+
productName: string;
|
|
9
|
+
legalLinks?: Record<string, string>;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function ConsentBlock({ requiredConsents, optionalConsents, value, onChange, legalLinks, disabled, }: ConsentBlockProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
//# sourceMappingURL=ConsentBlock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsentBlock.d.ts","sourceRoot":"","sources":["../../../src/client/components/ConsentBlock.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG/D,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;IACtC,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;IACtC,sEAAsE;IACtE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,YAAY,CAAC,EAC3B,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,EACL,QAAQ,EACR,UAAU,EACV,QAAQ,GACT,EAAE,iBAAiB,2CAkDnB"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ConsentCheckbox } from './ConsentCheckbox.js';
|
|
3
|
+
export function ConsentBlock({ requiredConsents, optionalConsents, value, onChange, legalLinks, disabled, }) {
|
|
4
|
+
const toggle = (key, checked) => {
|
|
5
|
+
if (checked) {
|
|
6
|
+
onChange([...value, key]);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
onChange(value.filter((k) => k !== key));
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
return (_jsxs("div", { className: "space-y-1", children: [requiredConsents.length > 0 && (_jsxs("div", { children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-gray-500 mb-1", children: "Required" }), requiredConsents.map((def) => (_jsx(ConsentCheckbox, { id: `consent-${def.key}`, checked: value.includes(def.key), onChange: (checked) => toggle(def.key, checked), label: def.display_text, required: true, legalUrl: legalLinks?.[def.key] ?? def.legal_url, disabled: disabled }, def.key)))] })), optionalConsents.length > 0 && (_jsxs("div", { className: "mt-3", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-gray-500 mb-1", children: "Optional" }), optionalConsents.map((def) => (_jsx(ConsentCheckbox, { id: `consent-${def.key}`, checked: value.includes(def.key), onChange: (checked) => toggle(def.key, checked), label: def.display_text, legalUrl: legalLinks?.[def.key] ?? def.legal_url, disabled: disabled }, def.key)))] }))] }));
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=ConsentBlock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsentBlock.js","sourceRoot":"","sources":["../../../src/client/components/ConsentBlock.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAavD,MAAM,UAAU,YAAY,CAAC,EAC3B,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,EACL,QAAQ,EACR,UAAU,EACV,QAAQ,GACU;IAClB,MAAM,MAAM,GAAG,CAAC,GAAW,EAAE,OAAgB,EAAE,EAAE;QAC/C,IAAI,OAAO,EAAE,CAAC;YACZ,QAAQ,CAAC,CAAC,GAAG,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CACL,eAAK,SAAS,EAAC,WAAW,aACvB,gBAAgB,CAAC,MAAM,GAAG,CAAC,IAAI,CAC9B,0BACE,YAAG,SAAS,EAAC,kEAAkE,yBAE3E,EACH,gBAAgB,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAC7B,KAAC,eAAe,IAEd,EAAE,EAAE,WAAW,GAAG,CAAC,GAAG,EAAE,EACxB,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAChC,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,EAC/C,KAAK,EAAE,GAAG,CAAC,YAAY,EACvB,QAAQ,QACR,QAAQ,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,SAAS,EAChD,QAAQ,EAAE,QAAQ,IAPb,GAAG,CAAC,GAAG,CAQZ,CACH,CAAC,IACE,CACP,EACA,gBAAgB,CAAC,MAAM,GAAG,CAAC,IAAI,CAC9B,eAAK,SAAS,EAAC,MAAM,aACnB,YAAG,SAAS,EAAC,kEAAkE,yBAE3E,EACH,gBAAgB,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAC7B,KAAC,eAAe,IAEd,EAAE,EAAE,WAAW,GAAG,CAAC,GAAG,EAAE,EACxB,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAChC,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,EAC/C,KAAK,EAAE,GAAG,CAAC,YAAY,EACvB,QAAQ,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,SAAS,EAChD,QAAQ,EAAE,QAAQ,IANb,GAAG,CAAC,GAAG,CAOZ,CACH,CAAC,IACE,CACP,IACG,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ConsentCheckboxProps {
|
|
2
|
+
id: string;
|
|
3
|
+
checked: boolean;
|
|
4
|
+
onChange: (checked: boolean) => void;
|
|
5
|
+
label: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
legalUrl?: string | null;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function ConsentCheckbox({ id, checked, onChange, label, required, legalUrl, disabled, }: ConsentCheckboxProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=ConsentCheckbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsentCheckbox.d.ts","sourceRoot":"","sources":["../../../src/client/components/ConsentCheckbox.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,EAC9B,EAAE,EACF,OAAO,EACP,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,2CAgCtB"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function ConsentCheckbox({ id, checked, onChange, label, required, legalUrl, disabled, }) {
|
|
3
|
+
return (_jsxs("div", { className: "flex items-start gap-3 py-2", children: [_jsx("input", { id: id, type: "checkbox", checked: checked, onChange: (e) => onChange(e.target.checked), disabled: disabled, "aria-required": required, className: "mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" }), _jsxs("label", { htmlFor: id, className: "text-sm text-gray-700 leading-snug", children: [label, required && (_jsx("span", { className: "ml-1 text-red-500", "aria-hidden": "true", children: "*" })), legalUrl && (_jsx("a", { href: legalUrl, target: "_blank", rel: "noopener noreferrer", className: "ml-1 text-blue-600 underline", children: "(view)" }))] })] }));
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=ConsentCheckbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsentCheckbox.js","sourceRoot":"","sources":["../../../src/client/components/ConsentCheckbox.tsx"],"names":[],"mappings":";AAYA,MAAM,UAAU,eAAe,CAAC,EAC9B,EAAE,EACF,OAAO,EACP,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,QAAQ,GACa;IACrB,OAAO,CACL,eAAK,SAAS,EAAC,6BAA6B,aAC1C,gBACE,EAAE,EAAE,EAAE,EACN,IAAI,EAAC,UAAU,EACf,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAC3C,QAAQ,EAAE,QAAQ,mBACH,QAAQ,EACvB,SAAS,EAAC,wEAAwE,GAClF,EACF,iBAAO,OAAO,EAAE,EAAE,EAAE,SAAS,EAAC,oCAAoC,aAC/D,KAAK,EACL,QAAQ,IAAI,CACX,eAAM,SAAS,EAAC,mBAAmB,iBAAa,MAAM,kBAE/C,CACR,EACA,QAAQ,IAAI,CACX,YACE,IAAI,EAAE,QAAQ,EACd,MAAM,EAAC,QAAQ,EACf,GAAG,EAAC,qBAAqB,EACzB,SAAS,EAAC,8BAA8B,uBAGtC,CACL,IACK,IACJ,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConsentDefinition } from '../../shared/types.js';
|
|
2
|
+
export interface ConsentUpdateModalProps {
|
|
3
|
+
productName: string;
|
|
4
|
+
updatedConsents: ConsentDefinition[];
|
|
5
|
+
value: string[];
|
|
6
|
+
onChange: (keys: string[]) => void;
|
|
7
|
+
onAccept: () => void;
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
legalLinks?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function ConsentUpdateModal({ productName, updatedConsents, value, onChange, onAccept, loading, legalLinks, }: ConsentUpdateModalProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=ConsentUpdateModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsentUpdateModal.d.ts","sourceRoot":"","sources":["../../../src/client/components/ConsentUpdateModal.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG/D,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,iBAAiB,EAAE,CAAC;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED,wBAAgB,kBAAkB,CAAC,EACjC,WAAW,EACX,eAAe,EACf,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,UAAU,GACX,EAAE,uBAAuB,2CA0CzB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ConsentBlock } from './ConsentBlock.js';
|
|
3
|
+
export function ConsentUpdateModal({ productName, updatedConsents, value, onChange, onAccept, loading, legalLinks, }) {
|
|
4
|
+
const required = updatedConsents.filter((d) => d.required);
|
|
5
|
+
const optional = updatedConsents.filter((d) => !d.required);
|
|
6
|
+
const allRequiredGranted = required.every((d) => value.includes(d.key));
|
|
7
|
+
return (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4", role: "dialog", "aria-modal": "true", children: _jsxs("div", { className: "w-full max-w-md bg-white rounded-2xl shadow-xl p-8", children: [_jsxs("h2", { className: "text-xl font-bold text-gray-900 mb-2", children: [productName, " has updated its policies"] }), _jsx("p", { className: "text-sm text-gray-500 mb-5", children: "Please review and accept the updated terms to continue." }), _jsx(ConsentBlock, { requiredConsents: required, optionalConsents: optional, value: value, onChange: onChange, productName: productName, legalLinks: legalLinks, disabled: loading }), _jsx("button", { onClick: onAccept, disabled: !allRequiredGranted || loading, className: "mt-6 w-full rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white\n hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed\n focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\n transition-colors", children: loading ? 'Saving…' : 'Accept & Continue' })] }) }));
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=ConsentUpdateModal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConsentUpdateModal.js","sourceRoot":"","sources":["../../../src/client/components/ConsentUpdateModal.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAYjD,MAAM,UAAU,kBAAkB,CAAC,EACjC,WAAW,EACX,eAAe,EACf,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,UAAU,GACc;IACxB,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3D,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC5D,MAAM,kBAAkB,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAExE,OAAO,CACL,cACE,SAAS,EAAC,qEAAqE,EAC/E,IAAI,EAAC,QAAQ,gBACF,MAAM,YAEjB,eAAK,SAAS,EAAC,oDAAoD,aACjE,cAAI,SAAS,EAAC,sCAAsC,aACjD,WAAW,iCACT,EACL,YAAG,SAAS,EAAC,4BAA4B,wEAErC,EAEJ,KAAC,YAAY,IACX,gBAAgB,EAAE,QAAQ,EAC1B,gBAAgB,EAAE,QAAQ,EAC1B,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,WAAW,EACxB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,OAAO,GACjB,EAEF,iBACE,OAAO,EAAE,QAAQ,EACjB,QAAQ,EAAE,CAAC,kBAAkB,IAAI,OAAO,EACxC,SAAS,EAAC,6SAGmB,YAE5B,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,mBAAmB,GACnC,IACL,GACF,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface EmptyStateProps {
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
action?: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare function EmptyState({ title, description, action, }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=EmptyState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmptyState.d.ts","sourceRoot":"","sources":["../../../src/client/components/EmptyState.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B;AAED,wBAAgB,UAAU,CAAC,EACzB,KAA0B,EAC1B,WAAqD,EACrD,MAAM,GACP,EAAE,eAAe,2CAyBjB"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function EmptyState({ title = 'Nothing here yet', description = 'Data will appear here once available.', action, }) {
|
|
3
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-16 px-4 text-center", children: [_jsx("div", { className: "mb-4 text-gray-300", children: _jsx("svg", { xmlns: "http://www.w3.org/2000/svg", className: "h-16 w-16", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" }) }) }), _jsx("h3", { className: "text-lg font-semibold text-gray-700 mb-1", children: title }), _jsx("p", { className: "text-sm text-gray-500 max-w-xs", children: description }), action && _jsx("div", { className: "mt-4", children: action })] }));
|
|
4
|
+
}
|
|
5
|
+
//# sourceMappingURL=EmptyState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EmptyState.js","sourceRoot":"","sources":["../../../src/client/components/EmptyState.tsx"],"names":[],"mappings":";AAQA,MAAM,UAAU,UAAU,CAAC,EACzB,KAAK,GAAG,kBAAkB,EAC1B,WAAW,GAAG,uCAAuC,EACrD,MAAM,GACU;IAChB,OAAO,CACL,eAAK,SAAS,EAAC,kEAAkE,aAC/E,cAAK,SAAS,EAAC,oBAAoB,YACjC,cACE,KAAK,EAAC,4BAA4B,EAClC,SAAS,EAAC,WAAW,EACrB,IAAI,EAAC,MAAM,EACX,OAAO,EAAC,WAAW,EACnB,MAAM,EAAC,cAAc,iBACT,MAAM,YAElB,eACE,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,GAAG,EAChB,CAAC,EAAC,qMAAqM,GACvM,GACE,GACF,EACN,aAAI,SAAS,EAAC,0CAA0C,YAAE,KAAK,GAAM,EACrE,YAAG,SAAS,EAAC,gCAAgC,YAAE,WAAW,GAAK,EAC9D,MAAM,IAAI,cAAK,SAAS,EAAC,MAAM,YAAE,MAAM,GAAO,IAC3C,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ConsentDefinition } from '../../shared/types.js';
|
|
3
|
+
export interface WelcomeScreenProps {
|
|
4
|
+
productName: string;
|
|
5
|
+
requiredConsents: ConsentDefinition[];
|
|
6
|
+
optionalConsents: ConsentDefinition[];
|
|
7
|
+
value: string[];
|
|
8
|
+
onChange: (keys: string[]) => void;
|
|
9
|
+
onContinue: () => void;
|
|
10
|
+
legalLinks?: Record<string, string>;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
logo?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
export declare function WelcomeScreen({ productName, requiredConsents, optionalConsents, value, onChange, onContinue, legalLinks, loading, logo, }: WelcomeScreenProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=WelcomeScreen.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WelcomeScreen.d.ts","sourceRoot":"","sources":["../../../src/client/components/WelcomeScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG/D,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;IACtC,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;IACtC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACnC,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CACxB;AAED,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,EACL,QAAQ,EACR,UAAU,EACV,UAAU,EACV,OAAO,EACP,IAAI,GACL,EAAE,kBAAkB,2CAqCpB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ConsentBlock } from './ConsentBlock.js';
|
|
3
|
+
export function WelcomeScreen({ productName, requiredConsents, optionalConsents, value, onChange, onContinue, legalLinks, loading, logo, }) {
|
|
4
|
+
const allRequiredGranted = requiredConsents.every((d) => value.includes(d.key));
|
|
5
|
+
return (_jsx("div", { className: "min-h-screen flex items-center justify-center bg-gray-50 p-4", children: _jsxs("div", { className: "w-full max-w-md bg-white rounded-2xl shadow-lg p-8", children: [logo && _jsx("div", { className: "flex justify-center mb-6", children: logo }), _jsxs("h1", { className: "text-2xl font-bold text-gray-900 text-center mb-2", children: ["Welcome to ", productName] }), _jsx("p", { className: "text-sm text-gray-500 text-center mb-6", children: "Before you get started, please review and accept the following." }), _jsx(ConsentBlock, { requiredConsents: requiredConsents, optionalConsents: optionalConsents, value: value, onChange: onChange, productName: productName, legalLinks: legalLinks, disabled: loading }), _jsx("button", { onClick: onContinue, disabled: !allRequiredGranted || loading, className: "mt-6 w-full rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white\n hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed\n focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\n transition-colors", children: loading ? 'Saving…' : 'Continue' })] }) }));
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=WelcomeScreen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WelcomeScreen.js","sourceRoot":"","sources":["../../../src/client/components/WelcomeScreen.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAcjD,MAAM,UAAU,aAAa,CAAC,EAC5B,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,EACL,QAAQ,EACR,UAAU,EACV,UAAU,EACV,OAAO,EACP,IAAI,GACe;IACnB,MAAM,kBAAkB,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAEhF,OAAO,CACL,cAAK,SAAS,EAAC,8DAA8D,YAC3E,eAAK,SAAS,EAAC,oDAAoD,aAChE,IAAI,IAAI,cAAK,SAAS,EAAC,0BAA0B,YAAE,IAAI,GAAO,EAC/D,cAAI,SAAS,EAAC,mDAAmD,4BACnD,WAAW,IACpB,EACL,YAAG,SAAS,EAAC,wCAAwC,gFAEjD,EAEJ,KAAC,YAAY,IACX,gBAAgB,EAAE,gBAAgB,EAClC,gBAAgB,EAAE,gBAAgB,EAClC,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,WAAW,EACxB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,OAAO,GACjB,EAEF,iBACE,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,kBAAkB,IAAI,OAAO,EACxC,SAAS,EAAC,6SAGmB,YAE5B,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,GAC1B,IACL,GACF,CACP,CAAC;AACJ,CAAC"}
|
|
@@ -8,3 +8,4 @@ export { EmptyState } from './EmptyState.js';
|
|
|
8
8
|
export type { EmptyStateProps } from './EmptyState.js';
|
|
9
9
|
export { ConsentUpdateModal } from './ConsentUpdateModal.js';
|
|
10
10
|
export type { ConsentUpdateModalProps } from './ConsentUpdateModal.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ConsentCheckbox } from './ConsentCheckbox.js';
|
|
2
|
+
export { ConsentBlock } from './ConsentBlock.js';
|
|
3
|
+
export { WelcomeScreen } from './WelcomeScreen.js';
|
|
4
|
+
export { EmptyState } from './EmptyState.js';
|
|
5
|
+
export { ConsentUpdateModal } from './ConsentUpdateModal.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ConsentCheckbox, ConsentBlock, WelcomeScreen, EmptyState, ConsentUpdateModal, } from './components/index.js';
|
|
2
|
+
export type { ConsentCheckboxProps, ConsentBlockProps, WelcomeScreenProps, EmptyStateProps, ConsentUpdateModalProps, } from './components/index.js';
|
|
3
|
+
export type { ConsentDefinition, ConsentStatus, AuditEntry, } from '../shared/types.js';
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,YAAY,EACZ,aAAa,EACb,UAAU,EACV,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,UAAU,GACX,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,YAAY,EACZ,aAAa,EACb,UAAU,EACV,kBAAkB,GACnB,MAAM,uBAAuB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@varshylinc/onboarding-consent-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Consent collection, audit trail, welcome screen, and empty state for Varshyl products",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
6
9
|
"exports": {
|
|
7
10
|
".": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.js"
|
|
10
14
|
},
|
|
11
15
|
"./client": {
|
|
12
|
-
"
|
|
13
|
-
"
|
|
16
|
+
"types": "./dist/client/index.d.ts",
|
|
17
|
+
"require": "./dist/client/index.js",
|
|
18
|
+
"import": "./dist/client/index.js"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"main": "./dist/index.js",
|
|
@@ -38,7 +43,7 @@
|
|
|
38
43
|
"registry": "https://registry.npmjs.org/"
|
|
39
44
|
},
|
|
40
45
|
"scripts": {
|
|
41
|
-
"build": "tsc -p tsconfig.json && mkdir -p dist/server/migrations && cp src/server/migrations/*.sql dist/server/migrations/",
|
|
46
|
+
"build": "tsc -p tsconfig.json && tsc -p tsconfig.client.build.json && mkdir -p dist/server/migrations && cp src/server/migrations/*.sql dist/server/migrations/",
|
|
42
47
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
43
48
|
"lint": "eslint src --ext .ts,.tsx --max-warnings 0",
|
|
44
49
|
"test": "vitest run",
|
package/.eslintrc.cjs
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
root: true,
|
|
3
|
-
parser: '@typescript-eslint/parser',
|
|
4
|
-
parserOptions: {
|
|
5
|
-
project: ['./tsconfig.json', './tsconfig.client.json'],
|
|
6
|
-
tsconfigRootDir: __dirname,
|
|
7
|
-
},
|
|
8
|
-
plugins: ['@typescript-eslint'],
|
|
9
|
-
extends: [
|
|
10
|
-
'eslint:recommended',
|
|
11
|
-
'plugin:@typescript-eslint/recommended',
|
|
12
|
-
],
|
|
13
|
-
rules: {
|
|
14
|
-
'@typescript-eslint/no-explicit-any': 'warn',
|
|
15
|
-
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
16
|
-
},
|
|
17
|
-
env: { node: true, browser: true, es2022: true },
|
|
18
|
-
};
|
package/CHANGELOG.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# @varshylinc/onboarding-consent-engine
|
|
2
|
-
|
|
3
|
-
## 0.1.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- Initial release: consent collection, audit trail, welcome screen, empty state.
|
|
8
|
-
- 4 SQL migrations (0001–0004) using `oce_` table prefix.
|
|
9
|
-
- Server API: `runMigrations`, `createConsentModule`, `seedStandardConsents`.
|
|
10
|
-
- Client components: `ConsentCheckbox`, `ConsentBlock`, `WelcomeScreen`, `EmptyState`, `ConsentUpdateModal`.
|
|
11
|
-
- Shared types: `ConsentDefinition`, `UserConsent`, `ConsentStatus`, `AuditEntry`.
|
package/MODULE.md
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# MODULE: @varshylinc/onboarding-consent-engine
|
|
2
|
-
|
|
3
|
-
**Status:** ✅ RELEASED — v0.1.0
|
|
4
|
-
**Git tag:** `onboarding-consent-engine-v0.1.0`
|
|
5
|
-
**First module to ship MODULE.md** — this document sets the precedent for all future `packages/*` in varshyl-toolkit.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Overview
|
|
10
|
-
|
|
11
|
-
Shared consent collection, audit trail, welcome screen, and empty state. Intended to be adopted by all Varshyl products at onboarding.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Public API
|
|
16
|
-
|
|
17
|
-
### Server (import from `@varshylinc/onboarding-consent-engine`)
|
|
18
|
-
|
|
19
|
-
| Export | Signature | Description |
|
|
20
|
-
|--------|-----------|-------------|
|
|
21
|
-
| `runMigrations` | `(pool: Pool, logger?) → Promise<{applied, skipped}>` | Apply 0001–0004 migrations idempotently |
|
|
22
|
-
| `createConsentModule` | `(config: ConsentModuleConfig) → ConsentModule` | Create module instance |
|
|
23
|
-
| `seedStandardConsents` | `(pool: Pool, productName: string) → Promise<void>` | Upsert standard consent definitions with product name applied |
|
|
24
|
-
| `STANDARD_CONSENTS` | `readonly array` | Canonical 4-consent set (ToS, Privacy, Marketing, AI Training) |
|
|
25
|
-
| `applyProductName` | `(template, productName) → string` | Substitute `{{PRODUCT_NAME}}` at seed time only |
|
|
26
|
-
|
|
27
|
-
### ConsentModule instance methods
|
|
28
|
-
|
|
29
|
-
| Method | Description |
|
|
30
|
-
|--------|-------------|
|
|
31
|
-
| `recordConsent(input)` | Insert one consent record |
|
|
32
|
-
| `recordSignupConsents(input)` | Insert one record per consent key at signup |
|
|
33
|
-
| `hasUserConsented(userId, key)` | Latest grant status for one key |
|
|
34
|
-
| `needsConsentUpdate(userId)` | Required definitions not yet at current version |
|
|
35
|
-
| `getCurrentConsents(userId)` | Latest status for all definitions |
|
|
36
|
-
| `getAuditTrail(userId, limit?)` | Full consent history, newest-first |
|
|
37
|
-
| `getUserLatestConsents(userIds[])` | Batch latest status by user |
|
|
38
|
-
|
|
39
|
-
### Client (import from `@varshylinc/onboarding-consent-engine/client`)
|
|
40
|
-
|
|
41
|
-
| Component | Props | Description |
|
|
42
|
-
|-----------|-------|-------------|
|
|
43
|
-
| `ConsentCheckbox` | `{id, checked, onChange, label, required?, legalUrl?, disabled?}` | Single checkbox with label |
|
|
44
|
-
| `ConsentBlock` | `{requiredConsents, optionalConsents, value, onChange, productName, legalLinks?, disabled?}` | Grouped checkbox list |
|
|
45
|
-
| `WelcomeScreen` | `{productName, requiredConsents, optionalConsents, value, onChange, onContinue, legalLinks?, loading?, logo?}` | Full-page first-run screen |
|
|
46
|
-
| `EmptyState` | `{title?, description?, action?}` | Empty state placeholder |
|
|
47
|
-
| `ConsentUpdateModal` | `{productName, updatedConsents, value, onChange, onAccept, loading?, legalLinks?}` | Modal for policy updates |
|
|
48
|
-
|
|
49
|
-
### Shared types (import from `@varshylinc/onboarding-consent-engine`)
|
|
50
|
-
|
|
51
|
-
`ConsentDefinition`, `UserConsent`, `ConsentVersionLog`, `RecordConsentInput`, `RecordSignupConsentsInput`, `ConsentStatus`, `AuditEntry`, `ConsentModuleAdapter`, `ConsentModuleConfig`
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## Owned Tables
|
|
56
|
-
|
|
57
|
-
All tables use the `oce_` prefix to namespace away from other modules.
|
|
58
|
-
|
|
59
|
-
| Table | Purpose |
|
|
60
|
-
|-------|---------|
|
|
61
|
-
| `oce_schema_migrations` | Migration ledger (idempotency) |
|
|
62
|
-
| `oce_consent_definitions` | Consent templates — one row per consent type |
|
|
63
|
-
| `oce_user_consents` | Immutable append-only consent records |
|
|
64
|
-
| `oce_consent_version_log` | Version history when a definition's text changes |
|
|
65
|
-
|
|
66
|
-
**`user_id` is `TEXT` — no FK, no constraint on format.** Products stringify their own PK before passing it in (see Adapter Contract below).
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## Host Requirements
|
|
71
|
-
|
|
72
|
-
The consuming app (e.g., `apps/demo-host`) must:
|
|
73
|
-
|
|
74
|
-
1. Provide a PostgreSQL `Pool` from `pg`.
|
|
75
|
-
2. Call `await runMigrations(pool)` on startup before serving requests.
|
|
76
|
-
3. Call `await seedStandardConsents(pool, 'ProductName')` on startup (idempotent).
|
|
77
|
-
4. Expose an API route (e.g., `GET /api/consent/definitions`) to serve definitions to the client.
|
|
78
|
-
5. Expose an API route (e.g., `POST /api/consent/record`) that calls `recordSignupConsents` or `recordConsent`.
|
|
79
|
-
|
|
80
|
-
No email-sending, no authentication, no account creation. This module only collects and audits consent.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Adapter Contract
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
interface ConsentModuleAdapter {
|
|
88
|
-
onConsentRecorded?: (userId: string, key: string, granted: boolean) => void | Promise<void>;
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Optional hook called after every consent record is inserted. Use for analytics, CRM sync, etc.
|
|
93
|
-
|
|
94
|
-
**`user_id` format by product:**
|
|
95
|
-
|
|
96
|
-
| Product | PK type | What to pass |
|
|
97
|
-
|---------|---------|--------------|
|
|
98
|
-
| ConstructInv | integer | `String(user.id)` |
|
|
99
|
-
| DailyLog | UUID string | pass as-is |
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## Version / Git Tag Plan
|
|
104
|
-
|
|
105
|
-
Releases follow SemVer with the prefix `onboarding-consent-engine-vX.Y.Z`.
|
|
106
|
-
|
|
107
|
-
| Release | Tag | Notes |
|
|
108
|
-
|---------|-----|-------|
|
|
109
|
-
| v0.1.0 | `onboarding-consent-engine-v0.1.0` | Initial release |
|
|
110
|
-
| v0.2.0 | `onboarding-consent-engine-v0.2.0` | Planned: IP/UA encryption via libsodium sealed box |
|
|
111
|
-
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
## Retroactive Adoption Guide
|
|
115
|
-
|
|
116
|
-
For products that already have users and want to adopt this module:
|
|
117
|
-
|
|
118
|
-
1. Run `runMigrations(pool)` — creates tables idempotently, no data loss.
|
|
119
|
-
2. Run `seedStandardConsents(pool, 'YourProductName')` — upserts definitions.
|
|
120
|
-
3. Wrap your signup handler to call `recordSignupConsents` for new users.
|
|
121
|
-
4. For existing users: call `needsConsentUpdate(userId)` at login — redirect to `ConsentUpdateModal` if non-empty.
|
|
122
|
-
5. Existing users who never consented will always appear in `needsConsentUpdate` until they go through the modal. This is intentional — it surfaces the gap for retroactive collection.
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## Future Work (v0.2.0+)
|
|
127
|
-
|
|
128
|
-
- **IP/UA encryption** — encrypt `ip_address` and `user_agent` at rest using libsodium sealed box. Not in v0.1.0. No encryption key env var required yet.
|
|
129
|
-
- **Consent export** — `exportUserConsents(userId)` for GDPR data portability.
|
|
130
|
-
- **Consent withdrawal** — `withdrawConsent(userId, key)` records a revocation.
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { ConsentDefinition } from '../../shared/types.js';
|
|
3
|
-
import { ConsentCheckbox } from './ConsentCheckbox.js';
|
|
4
|
-
|
|
5
|
-
export interface ConsentBlockProps {
|
|
6
|
-
requiredConsents: ConsentDefinition[];
|
|
7
|
-
optionalConsents: ConsentDefinition[];
|
|
8
|
-
/** Array of definition keys the user has currently checked/granted */
|
|
9
|
-
value: string[];
|
|
10
|
-
onChange: (keys: string[]) => void;
|
|
11
|
-
productName: string;
|
|
12
|
-
legalLinks?: Record<string, string>;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function ConsentBlock({
|
|
17
|
-
requiredConsents,
|
|
18
|
-
optionalConsents,
|
|
19
|
-
value,
|
|
20
|
-
onChange,
|
|
21
|
-
legalLinks,
|
|
22
|
-
disabled,
|
|
23
|
-
}: ConsentBlockProps) {
|
|
24
|
-
const toggle = (key: string, checked: boolean) => {
|
|
25
|
-
if (checked) {
|
|
26
|
-
onChange([...value, key]);
|
|
27
|
-
} else {
|
|
28
|
-
onChange(value.filter((k) => k !== key));
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<div className="space-y-1">
|
|
34
|
-
{requiredConsents.length > 0 && (
|
|
35
|
-
<div>
|
|
36
|
-
<p className="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-1">
|
|
37
|
-
Required
|
|
38
|
-
</p>
|
|
39
|
-
{requiredConsents.map((def) => (
|
|
40
|
-
<ConsentCheckbox
|
|
41
|
-
key={def.key}
|
|
42
|
-
id={`consent-${def.key}`}
|
|
43
|
-
checked={value.includes(def.key)}
|
|
44
|
-
onChange={(checked) => toggle(def.key, checked)}
|
|
45
|
-
label={def.display_text}
|
|
46
|
-
required
|
|
47
|
-
legalUrl={legalLinks?.[def.key] ?? def.legal_url}
|
|
48
|
-
disabled={disabled}
|
|
49
|
-
/>
|
|
50
|
-
))}
|
|
51
|
-
</div>
|
|
52
|
-
)}
|
|
53
|
-
{optionalConsents.length > 0 && (
|
|
54
|
-
<div className="mt-3">
|
|
55
|
-
<p className="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-1">
|
|
56
|
-
Optional
|
|
57
|
-
</p>
|
|
58
|
-
{optionalConsents.map((def) => (
|
|
59
|
-
<ConsentCheckbox
|
|
60
|
-
key={def.key}
|
|
61
|
-
id={`consent-${def.key}`}
|
|
62
|
-
checked={value.includes(def.key)}
|
|
63
|
-
onChange={(checked) => toggle(def.key, checked)}
|
|
64
|
-
label={def.display_text}
|
|
65
|
-
legalUrl={legalLinks?.[def.key] ?? def.legal_url}
|
|
66
|
-
disabled={disabled}
|
|
67
|
-
/>
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
)}
|
|
71
|
-
</div>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
export interface ConsentCheckboxProps {
|
|
4
|
-
id: string;
|
|
5
|
-
checked: boolean;
|
|
6
|
-
onChange: (checked: boolean) => void;
|
|
7
|
-
label: string;
|
|
8
|
-
required?: boolean;
|
|
9
|
-
legalUrl?: string | null;
|
|
10
|
-
disabled?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function ConsentCheckbox({
|
|
14
|
-
id,
|
|
15
|
-
checked,
|
|
16
|
-
onChange,
|
|
17
|
-
label,
|
|
18
|
-
required,
|
|
19
|
-
legalUrl,
|
|
20
|
-
disabled,
|
|
21
|
-
}: ConsentCheckboxProps) {
|
|
22
|
-
return (
|
|
23
|
-
<div className="flex items-start gap-3 py-2">
|
|
24
|
-
<input
|
|
25
|
-
id={id}
|
|
26
|
-
type="checkbox"
|
|
27
|
-
checked={checked}
|
|
28
|
-
onChange={(e) => onChange(e.target.checked)}
|
|
29
|
-
disabled={disabled}
|
|
30
|
-
aria-required={required}
|
|
31
|
-
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
32
|
-
/>
|
|
33
|
-
<label htmlFor={id} className="text-sm text-gray-700 leading-snug">
|
|
34
|
-
{label}
|
|
35
|
-
{required && (
|
|
36
|
-
<span className="ml-1 text-red-500" aria-hidden="true">
|
|
37
|
-
*
|
|
38
|
-
</span>
|
|
39
|
-
)}
|
|
40
|
-
{legalUrl && (
|
|
41
|
-
<a
|
|
42
|
-
href={legalUrl}
|
|
43
|
-
target="_blank"
|
|
44
|
-
rel="noopener noreferrer"
|
|
45
|
-
className="ml-1 text-blue-600 underline"
|
|
46
|
-
>
|
|
47
|
-
(view)
|
|
48
|
-
</a>
|
|
49
|
-
)}
|
|
50
|
-
</label>
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import type { ConsentDefinition } from '../../shared/types.js';
|
|
3
|
-
import { ConsentBlock } from './ConsentBlock.js';
|
|
4
|
-
|
|
5
|
-
export interface ConsentUpdateModalProps {
|
|
6
|
-
productName: string;
|
|
7
|
-
updatedConsents: ConsentDefinition[];
|
|
8
|
-
value: string[];
|
|
9
|
-
onChange: (keys: string[]) => void;
|
|
10
|
-
onAccept: () => void;
|
|
11
|
-
loading?: boolean;
|
|
12
|
-
legalLinks?: Record<string, string>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function ConsentUpdateModal({
|
|
16
|
-
productName,
|
|
17
|
-
updatedConsents,
|
|
18
|
-
value,
|
|
19
|
-
onChange,
|
|
20
|
-
onAccept,
|
|
21
|
-
loading,
|
|
22
|
-
legalLinks,
|
|
23
|
-
}: ConsentUpdateModalProps) {
|
|
24
|
-
const required = updatedConsents.filter((d) => d.required);
|
|
25
|
-
const optional = updatedConsents.filter((d) => !d.required);
|
|
26
|
-
const allRequiredGranted = required.every((d) => value.includes(d.key));
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div
|
|
30
|
-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
|
31
|
-
role="dialog"
|
|
32
|
-
aria-modal="true"
|
|
33
|
-
>
|
|
34
|
-
<div className="w-full max-w-md bg-white rounded-2xl shadow-xl p-8">
|
|
35
|
-
<h2 className="text-xl font-bold text-gray-900 mb-2">
|
|
36
|
-
{productName} has updated its policies
|
|
37
|
-
</h2>
|
|
38
|
-
<p className="text-sm text-gray-500 mb-5">
|
|
39
|
-
Please review and accept the updated terms to continue.
|
|
40
|
-
</p>
|
|
41
|
-
|
|
42
|
-
<ConsentBlock
|
|
43
|
-
requiredConsents={required}
|
|
44
|
-
optionalConsents={optional}
|
|
45
|
-
value={value}
|
|
46
|
-
onChange={onChange}
|
|
47
|
-
productName={productName}
|
|
48
|
-
legalLinks={legalLinks}
|
|
49
|
-
disabled={loading}
|
|
50
|
-
/>
|
|
51
|
-
|
|
52
|
-
<button
|
|
53
|
-
onClick={onAccept}
|
|
54
|
-
disabled={!allRequiredGranted || loading}
|
|
55
|
-
className="mt-6 w-full rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white
|
|
56
|
-
hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed
|
|
57
|
-
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
|
58
|
-
transition-colors"
|
|
59
|
-
>
|
|
60
|
-
{loading ? 'Saving…' : 'Accept & Continue'}
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
}
|