diff --git a/package-lock.json b/package-lock.json index 2a1a644a4..80cf6883a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@foxy.io/sdk": "^1.16.2", + "@foxy.io/sdk": "^1.17.1", "@open-wc/lit-helpers": "^0.3.12", "@open-wc/scoped-elements": "^1.2.1", "@polymer/iron-icons": "^3.0.1", @@ -1822,9 +1822,9 @@ } }, "node_modules/@foxy.io/sdk": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/@foxy.io/sdk/-/sdk-1.16.2.tgz", - "integrity": "sha512-jaBtjKIx3u9zzAXZ+5T52KC4rzuBMbGIcrN/ys0JGtQDjxIFV6rnnAvm56htN8N1TyF8T28TfLlNxo/VViBK+w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@foxy.io/sdk/-/sdk-1.17.1.tgz", + "integrity": "sha512-7FHL934LhsZBZVngRP0bkO+iv2nmLf6+b3yuHB1vxbfj3fNdZCxkkI4MwpZg3cCMuOLGrnmDZKInONZ2mi8lew==", "license": "MIT", "dependencies": { "@types/jsdom": "^16.2.5", diff --git a/package.json b/package.json index a5658ce8c..f73432aa7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "prepack": "npm run lint && rimraf dist && node ./.build/compile-for-npm.js && rollup -c" }, "dependencies": { - "@foxy.io/sdk": "^1.16.2", + "@foxy.io/sdk": "^1.17.1", "@open-wc/lit-helpers": "^0.3.12", "@open-wc/scoped-elements": "^1.2.1", "@polymer/iron-icons": "^3.0.1", diff --git a/src/elements/public/StoreForm/StoreForm.test.ts b/src/elements/public/StoreForm/StoreForm.test.ts index cb009be08..9677d08e2 100644 --- a/src/elements/public/StoreForm/StoreForm.test.ts +++ b/src/elements/public/StoreForm/StoreForm.test.ts @@ -2469,4 +2469,61 @@ describe('StoreForm', () => { expect(element.errors).to.include('error:store_domain_exists'); }); + it('produces the auto-anonymize-days:v8n_too_small error when the value is under 90', () => { + const form = new Form(); + expect(form.errors).to.not.include('auto-anonymize-days:v8n_too_small'); + + form.edit({ data_retention: { auto_anonymize: false, auto_anonymize_days: 89 } }); + expect(form.errors).to.include('auto-anonymize-days:v8n_too_small'); + + form.edit({ data_retention: { auto_anonymize: false, auto_anonymize_days: 90 } }); + expect(form.errors).to.not.include('auto-anonymize-days:v8n_too_small'); + }); + + it('produces the auto-anonymize-days:v8n_required error when auto-anonymization is on but days are unset', () => { + const form = new Form(); + expect(form.errors).to.not.include('auto-anonymize-days:v8n_required'); + + form.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: null } }); + expect(form.errors).to.include('auto-anonymize-days:v8n_required'); + + form.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: 365 } }); + expect(form.errors).to.not.include('auto-anonymize-days:v8n_required'); + }); + + it('renders a switch control for auto-anonymization in the Data Retention section', async () => { + const element = await fixture
(html``); + const control = element.renderRoot.querySelector( + '[infer="data-retention"] foxy-internal-switch-control[infer="auto-anonymize"]' + ) as InternalSwitchControl; + + expect(control).to.exist; + + element.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: 120 } }); + expect(control.getValue()).to.equal(true); + + control.setValue(false); + expect(element).to.have.nested.property('form.data_retention.auto_anonymize', false); + expect(element).to.have.nested.property('form.data_retention.auto_anonymize_days', 120); + }); + + it('renders a number control for days of inactivity in the Data Retention section when auto-anonymization is on', async () => { + const element = await fixture(html``); + const selector = + '[infer="data-retention"] foxy-internal-number-control[infer="auto-anonymize-days"]'; + + expect(element.renderRoot.querySelector(selector)).to.not.exist; + + element.edit({ data_retention: { auto_anonymize: true, auto_anonymize_days: 365 } }); + await element.requestUpdate(); + + const control = element.renderRoot.querySelector(selector) as InternalNumberControl; + + expect(control).to.exist; + expect(control).to.have.attribute('min', '90'); + expect(control.getValue()).to.equal(365); + + control.setValue(120); + expect(element).to.have.nested.property('form.data_retention.auto_anonymize_days', 120); + }); }); diff --git a/src/elements/public/StoreForm/StoreForm.ts b/src/elements/public/StoreForm/StoreForm.ts index 988017268..adb894b57 100644 --- a/src/elements/public/StoreForm/StoreForm.ts +++ b/src/elements/public/StoreForm/StoreForm.ts @@ -9,6 +9,7 @@ import type { Rels } from '@foxy.io/sdk/backend'; import type { ParsedCustomDisplayIdConfig, + ParsedDataRetention, ParsedSmtpConfig, ParsedWebhookKey, Data, @@ -164,6 +165,18 @@ export class StoreForm extends Base { ({ unified_order_entry_password: v }) => { return !v || String(v).length <= 100 || 'unified-order-entry-password:v8n_too_long'; }, + + ({ data_retention: v }) => { + const days = v?.auto_anonymize_days; + if (days === null || days === undefined) return true; + return (Number.isInteger(days) && days >= 90) || 'auto-anonymize-days:v8n_too_small'; + }, + + ({ data_retention: v }) => { + if (!v?.auto_anonymize) return true; + const days = v.auto_anonymize_days; + return (typeof days === 'number' && days >= 90) || 'auto-anonymize-days:v8n_required'; + }, ]; } @@ -288,6 +301,31 @@ export class StoreForm extends Base { this.edit({ store_name: newValue }); }; + private readonly __getDataRetention = (): ParsedDataRetention => { + const config = this.form.data_retention; + + return { + auto_anonymize: config?.auto_anonymize ?? false, + auto_anonymize_days: config?.auto_anonymize_days ?? null, + }; + }; + + private readonly __getAutoAnonymizeValue = (): boolean => { + return this.__getDataRetention().auto_anonymize; + }; + + private readonly __setAutoAnonymizeValue = (newValue: boolean) => { + this.edit({ data_retention: { ...this.__getDataRetention(), auto_anonymize: newValue } }); + }; + + private readonly __getAutoAnonymizeDaysValue = (): number | null => { + return this.__getDataRetention().auto_anonymize_days; + }; + + private readonly __setAutoAnonymizeDaysValue = (newValue: number) => { + this.edit({ data_retention: { ...this.__getDataRetention(), auto_anonymize_days: newValue } }); + }; + private readonly __getStoreEmailValue = (): Item[] => { const emails = this.form.store_email ?? ''; return emails @@ -1017,6 +1055,28 @@ export class StoreForm extends Base { : ''} + + + + ${this.form.data_retention?.auto_anonymize + ? html` + + + ` + : ''} + + ${this.renderTemplateOrSlot()} ${this.href || !this.hCaptchaSiteKey ? '' diff --git a/src/elements/public/StoreForm/types.ts b/src/elements/public/StoreForm/types.ts index 3e2fb66c8..0251f3fa8 100644 --- a/src/elements/public/StoreForm/types.ts +++ b/src/elements/public/StoreForm/types.ts @@ -13,3 +13,8 @@ export type Data = Omit, 'custom_display_id_config'> & { export type ParsedWebhookKey = StoreWebhookKeyJson; export type ParsedSmtpConfig = StoreSmtpConfigJson; export type ParsedCustomDisplayIdConfig = StoreCustomDisplayIdConfigJson; + +export type ParsedDataRetention = { + auto_anonymize: boolean; + auto_anonymize_days: number | null; +}; diff --git a/src/server/hapi/createDataset.ts b/src/server/hapi/createDataset.ts index 662b12729..b8cb1e1d2 100644 --- a/src/server/hapi/createDataset.ts +++ b/src/server/hapi/createDataset.ts @@ -652,6 +652,7 @@ export const createDataset: () => Dataset = () => ({ }, }, }, + data_retention: { auto_anonymize: true, auto_anonymize_days: 365 }, affiliate_id: 0, is_maintenance_mode: false, is_active: true, diff --git a/src/static/translations/store-form/en.json b/src/static/translations/store-form/en.json index 5030f965e..c5560d3fd 100644 --- a/src/static/translations/store-form/en.json +++ b/src/static/translations/store-form/en.json @@ -440,6 +440,23 @@ } } }, + "data-retention": { + "label": "Data retention", + "helper_text": "", + "auto-anonymize": { + "label": "Auto-anonymize inactive customers", + "helper_text": "When enabled, customers with no activity for the configured number of days are automatically and irreversibly anonymized.", + "checked": "Yes", + "unchecked": "No" + }, + "auto-anonymize-days": { + "label": "Days of inactivity", + "placeholder": "365", + "helper_text": "Minimum 90 days. Customers inactive this long will be anonymized.", + "v8n_required": "Please enter the number of inactive days (at least 90).", + "v8n_too_small": "Please enter at least 90 days." + } + }, "hcaptcha": { "disclaimer": "We use hCaptcha to protect this page from spam and abuse.", "privacy_policy": "Privacy Policy", @@ -469,4 +486,4 @@ "create": { "caption": "Create" } -} \ No newline at end of file +}