From 23de73e7ece6ad011b1f19b70698488bf463c280 Mon Sep 17 00:00:00 2001 From: ClaydeCode Date: Tue, 22 Sep 2026 08:11:19 +0000 Subject: [PATCH 1/2] settings, lib: show the exact next-charge timestamp The subscription card rendered next_billing_date through formatDateHumanize, so an active subscriber read "Next charge in a month." With a paid subscription the length of the current term and the date of the next charge have to be unambiguous; a humanised interval is the kind of statement a competitor can attack under the UWG. The card now reads "Next charge: 2026-07-01 13:30", which is also the wording the PayPal billing design spec asks for. next_billing_date is a timestamptz taken from PayPal's next_billing_time, so rendering a time next to the date reflects the data rather than padding it. formatDate was the only absolute-date precedent in the codebase and had no call sites. It formatted with hh, a 12-hour clock with no meridiem, which renders 13:30 as 01:30 and would have carried the ambiguity straight back into the billing line. Its body moves to src/lib/dates.js on HH, where the unit suite can reach it, following the same extraction as lib/pricing.js. --- src/lib/dates.js | 5 +++++ src/main.js | 5 ++--- src/views/Settings.vue | 2 +- tests/unit/dates.spec.js | 12 ++++++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 src/lib/dates.js create mode 100644 tests/unit/dates.spec.js diff --git a/src/lib/dates.js b/src/lib/dates.js new file mode 100644 index 0000000..9bc861f --- /dev/null +++ b/src/lib/dates.js @@ -0,0 +1,5 @@ +import moment from "moment/moment"; + +export function formatDateTime(value) { + return moment(String(value)).format('YYYY-MM-DD HH:mm'); +} diff --git a/src/main.js b/src/main.js index 1e93209..c9247a9 100644 --- a/src/main.js +++ b/src/main.js @@ -11,6 +11,7 @@ import 'vue-tour/dist/vue-tour.css' import './assets/css/main.css' import QrcodeVue from "qrcode.vue"; import moment from "moment/moment"; +import { formatDateTime } from "./lib/dates"; Vue.use(BootstrapVue) Vue.use(BootstrapVueIcons) @@ -28,9 +29,7 @@ Vue.filter('uppercase', function (value) { return value.toUpperCase(); }); -Vue.filter('formatDate', function (value) { - return moment(String(value)).format('YYYY-MM-DD hh:mm'); -}) +Vue.filter('formatDate', formatDateTime) Vue.filter('formatDateHumanize', function (value) { const now = new moment(); diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 76f1c07..c7b8dcc 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -89,7 +89,7 @@ incl. 19% VAT ({{ formatPrice(vatAmountEur($store.state.profile.subscription.price_cents)) }}) - Next charge {{ $store.state.profile.subscription.next_billing_date | formatDateHumanize }}. + Next charge: {{ $store.state.profile.subscription.next_billing_date | formatDate }}. Billed to {{ $store.state.profile.subscription.payer_email }}. diff --git a/tests/unit/dates.spec.js b/tests/unit/dates.spec.js new file mode 100644 index 0000000..9367a4a --- /dev/null +++ b/tests/unit/dates.spec.js @@ -0,0 +1,12 @@ +import { formatDateTime } from '@/lib/dates'; + +describe('formatDateTime', () => { + test('renders the absolute date and time', () => { + expect(formatDateTime('2026-07-01T13:30:00')).toBe('2026-07-01 13:30'); + }); + + test('tells afternoon from morning', () => { + expect(formatDateTime('2026-07-01T13:30:00')) + .not.toBe(formatDateTime('2026-07-01T01:30:00')); + }); +}); From 44925ba21602a2210851948eba5faa8e36db8c4b Mon Sep 17 00:00:00 2001 From: ClaydeCode Date: Tue, 22 Sep 2026 08:27:29 +0000 Subject: [PATCH 2/2] settings, lib: show the charge date without a clock time, and the exact date on hover Max' call on the first version: for the next charge only the day matters, not the hour. formatDate loses the time component, which also settles the hh-versus-HH question the previous commit had to solve, since there is no clock left to be ambiguous about. The billing line reads "Next charge: 2026-07-01." The card states seven other dates as intervals, from "in a month" to "3 days ago". None of them is a charge falling due, so none needed the same treatment, but an interval is still the wrong thing to read when the question is which day something happens. Hovering now shows the calendar date. bootstrap-vue is already a dependency and b-tooltip is already used in the navbar, so this is the v-b-tooltip directive plus an optional prop on TextField, whose three About-overlay fields take their content as a string and had nowhere to hang a title. --- src/components/TextField.vue | 4 ++-- src/lib/dates.js | 4 ++-- src/main.js | 4 ++-- src/views/Settings.vue | 24 ++++++++++++++++-------- tests/unit/dates.spec.js | 14 +++++++------- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/components/TextField.vue b/src/components/TextField.vue index 82e9e37..d9bf3fc 100644 --- a/src/components/TextField.vue +++ b/src/components/TextField.vue @@ -1,13 +1,13 @@ diff --git a/src/lib/dates.js b/src/lib/dates.js index 9bc861f..5561411 100644 --- a/src/lib/dates.js +++ b/src/lib/dates.js @@ -1,5 +1,5 @@ import moment from "moment/moment"; -export function formatDateTime(value) { - return moment(String(value)).format('YYYY-MM-DD HH:mm'); +export function formatDate(value) { + return moment(String(value)).format('YYYY-MM-DD'); } diff --git a/src/main.js b/src/main.js index c9247a9..62e52ce 100644 --- a/src/main.js +++ b/src/main.js @@ -11,7 +11,7 @@ import 'vue-tour/dist/vue-tour.css' import './assets/css/main.css' import QrcodeVue from "qrcode.vue"; import moment from "moment/moment"; -import { formatDateTime } from "./lib/dates"; +import { formatDate } from "./lib/dates"; Vue.use(BootstrapVue) Vue.use(BootstrapVueIcons) @@ -29,7 +29,7 @@ Vue.filter('uppercase', function (value) { return value.toUpperCase(); }); -Vue.filter('formatDate', formatDateTime) +Vue.filter('formatDate', formatDate) Vue.filter('formatDateHumanize', function (value) { const now = new moment(); diff --git a/src/views/Settings.vue b/src/views/Settings.vue index c7b8dcc..5a2b2cc 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -49,7 +49,8 @@ Your shard will be deleted - {{ $store.state.profile.delete_after | formatDateHumanize }}. + {{ + $store.state.profile.delete_after | formatDateHumanize }}. That happens on its own if you do nothing: you are not charged, and there is nothing to cancel. @@ -120,18 +121,21 @@ @@ -235,7 +239,8 @@

Your passphrase was last revealed - {{ backupInfo.last_passphrase_access_info.time | formatDateHumanize }} + {{ + backupInfo.last_passphrase_access_info.time | formatDateHumanize }} from device {{ backupInfo.last_passphrase_access_info.terminal_name }} (with ID: {{ backupInfo.last_passphrase_access_info.terminal_id }}).

@@ -341,12 +346,15 @@ + :content="$store.state.profile.time_created | formatDateHumanize" + :tooltip="$store.state.profile.time_created | formatDate"/> + :content="$store.state.profile.time_assigned | formatDateHumanize" + :tooltip="$store.state.profile.time_assigned | formatDate"/>
+ :content="$store.state.profile.delete_after | formatDateHumanize" + :tooltip="$store.state.profile.delete_after | formatDate"/>
diff --git a/tests/unit/dates.spec.js b/tests/unit/dates.spec.js index 9367a4a..12608a5 100644 --- a/tests/unit/dates.spec.js +++ b/tests/unit/dates.spec.js @@ -1,12 +1,12 @@ -import { formatDateTime } from '@/lib/dates'; +import { formatDate } from '@/lib/dates'; -describe('formatDateTime', () => { - test('renders the absolute date and time', () => { - expect(formatDateTime('2026-07-01T13:30:00')).toBe('2026-07-01 13:30'); +describe('formatDate', () => { + test('renders the calendar day the charge falls on', () => { + expect(formatDate('2026-07-01T13:30:00')).toBe('2026-07-01'); }); - test('tells afternoon from morning', () => { - expect(formatDateTime('2026-07-01T13:30:00')) - .not.toBe(formatDateTime('2026-07-01T01:30:00')); + test('carries no clock time, which the billing line does not need', () => { + expect(formatDate('2026-07-01T13:30:00')) + .toBe(formatDate('2026-07-01T01:30:00')); }); });