From 2b010ac8b33649c8e65e92f3f865e285792ef81d Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 27 Apr 2026 12:28:18 -0700 Subject: [PATCH 001/114] fix: dedupe pending checkout attempt construction (#11622) ## Summary Deduplicates pending subscription checkout attempt construction so storage and fallback paths share the same payload creation. ## Changes - **What**: Build the `PendingSubscriptionCheckoutAttempt` once in `recordPendingSubscriptionCheckoutAttempt()` and reuse it for unavailable-storage, failed-write, and successful-write paths. - **Dependencies**: None. ## Review Focus This is intended as a no-behavior-change cleanup: unavailable storage still returns an attempt, failed `setItem()` still returns that attempt without dispatching, and the pending-checkout event only fires after a successful storage write. Linear: FE-209 ## Screenshots (if applicable) N/A --------- Co-authored-by: GitHub Action --- .../utils/subscriptionCheckoutTracker.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/platform/cloud/subscription/utils/subscriptionCheckoutTracker.ts b/src/platform/cloud/subscription/utils/subscriptionCheckoutTracker.ts index 364af345b2..9b7f0079eb 100644 --- a/src/platform/cloud/subscription/utils/subscriptionCheckoutTracker.ts +++ b/src/platform/cloud/subscription/utils/subscriptionCheckoutTracker.ts @@ -228,18 +228,6 @@ export const recordPendingSubscriptionCheckoutAttempt = ( input: RecordPendingSubscriptionCheckoutAttemptInput ): PendingSubscriptionCheckoutAttempt => { const storage = getStorage() - if (!storage) { - return { - attempt_id: createAttemptId(), - started_at_ms: Date.now(), - tier: input.tier, - cycle: input.cycle, - checkout_type: input.checkout_type, - ...(input.previous_tier ? { previous_tier: input.previous_tier } : {}), - ...(input.previous_cycle ? { previous_cycle: input.previous_cycle } : {}) - } - } - const attempt: PendingSubscriptionCheckoutAttempt = { attempt_id: createAttemptId(), started_at_ms: Date.now(), @@ -250,6 +238,10 @@ export const recordPendingSubscriptionCheckoutAttempt = ( ...(input.previous_cycle ? { previous_cycle: input.previous_cycle } : {}) } + if (!storage) { + return attempt + } + try { storage.setItem( PENDING_SUBSCRIPTION_CHECKOUT_STORAGE_KEY, From 7ee667c1d19ac428c79c7623c7a7732029f33071 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Mon, 27 Apr 2026 12:46:01 -0700 Subject: [PATCH 002/114] fix: avoid escaped secret date labels (#11480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Global i18n config has escapeParameter as true. This explicitly turns it to false. I opened a Linear ticket to reconsider changing this back to false as default globally. ## Summary Fix the Secrets panel so created and last-used dates render as plain text instead of HTML-escaped slash entities. ## Changes - **What**: Compute the Secrets date labels with `t(..., { escapeParameter: false })` after formatting the date, so vue-i18n does not escape `/` into `/` for plain-text output. - **What**: Replace the mocked translation setup in `SecretListItem.test.ts` with a real `vue-i18n` instance and add a regression test that asserts the rendered dates do not contain escaped slash entities. ## Review Focus This intentionally fixes the i18n interpolation issue shown in the bug screenshot. It does not change the separate RFC3339Nano parsing behavior discussed in #11358. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11480-fix-avoid-escaped-secret-date-labels-3486d73d365081c890ecd2a6992d7879) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action --- .../secrets/components/SecretListItem.test.ts | 71 ++++++++++++++++--- .../secrets/components/SecretListItem.vue | 39 +++++++--- 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/src/platform/secrets/components/SecretListItem.test.ts b/src/platform/secrets/components/SecretListItem.test.ts index 8c63188059..bbe3cf7867 100644 --- a/src/platform/secrets/components/SecretListItem.test.ts +++ b/src/platform/secrets/components/SecretListItem.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { render, screen } from '@testing-library/vue' import userEvent from '@testing-library/user-event' +import { createI18n } from 'vue-i18n' import type { SecretMetadata } from '../types' import SecretListItem from './SecretListItem.vue' @@ -15,6 +16,24 @@ vi.mock('../providers', () => ({ getProviderLogo: () => undefined })) +const i18n = createI18n({ + legacy: false, + locale: 'en', + escapeParameter: true, + messages: { + en: { + g: { + edit: 'Edit', + delete: 'Delete' + }, + secrets: { + createdAt: 'Created {date}', + lastUsed: 'Last used {date}' + } + } + } +}) + function createMockSecret( overrides: Partial = {} ): SecretMetadata { @@ -36,6 +55,7 @@ function renderComponent(props: { return render(SecretListItem, { props, global: { + plugins: [i18n], stubs: { Button: { template: @@ -45,10 +65,6 @@ function renderComponent(props: { }, directives: { tooltip: () => {} - }, - mocks: { - $t: (key: string, params?: object) => - `${key}${params ? JSON.stringify(params) : ''}` } } }) @@ -89,21 +105,44 @@ describe('SecretListItem', () => { const secret = createMockSecret({ created_at: '2024-01-15T10:00:00Z' }) renderComponent({ secret }) - expect(screen.getByText(/secrets\.createdAt/)).toBeInTheDocument() + expect( + screen.getByText( + `Created ${new Date(secret.created_at).toLocaleDateString()}` + ) + ).toBeInTheDocument() }) it('displays last used date when available', () => { const secret = createMockSecret({ last_used_at: '2024-01-20T10:00:00Z' }) renderComponent({ secret }) - expect(screen.getByText(/secrets\.lastUsed/)).toBeInTheDocument() + expect( + screen.getByText( + `Last used ${new Date(secret.last_used_at!).toLocaleDateString()}` + ) + ).toBeInTheDocument() }) it('hides last used when not available', () => { const secret = createMockSecret({ last_used_at: undefined }) renderComponent({ secret }) - expect(screen.queryByText(/secrets\.lastUsed/)).not.toBeInTheDocument() + expect(screen.queryByText(/^Last used /)).not.toBeInTheDocument() + }) + + it('renders formatted dates without escaped slash entities', () => { + const secret = createMockSecret({ + created_at: '2026-02-06T10:00:00Z', + last_used_at: '2026-04-17T10:00:00Z' + }) + renderComponent({ secret }) + + expect(screen.queryByText(///)).not.toBeInTheDocument() + expect( + screen.getByText( + `Created ${new Date(secret.created_at).toLocaleDateString()}` + ) + ).toBeInTheDocument() }) it('renders created date for ISO string with 4-digit fractional seconds', () => { @@ -112,7 +151,11 @@ describe('SecretListItem', () => { }) renderComponent({ secret }) - expect(screen.getByText(/secrets\.createdAt/)).toBeInTheDocument() + expect( + screen.getByText( + `Created ${new Date(secret.created_at).toLocaleDateString()}` + ) + ).toBeInTheDocument() expect(screen.queryByText(/Invalid Date/)).not.toBeInTheDocument() }) @@ -122,7 +165,11 @@ describe('SecretListItem', () => { }) renderComponent({ secret }) - expect(screen.getByText(/secrets\.createdAt/)).toBeInTheDocument() + expect( + screen.getByText( + `Created ${new Date(secret.created_at).toLocaleDateString()}` + ) + ).toBeInTheDocument() expect(screen.queryByText(/Invalid Date/)).not.toBeInTheDocument() }) @@ -148,7 +195,11 @@ describe('SecretListItem', () => { }) renderComponent({ secret }) - expect(screen.getByText(/secrets\.lastUsed/)).toBeInTheDocument() + expect( + screen.getByText( + `Last used ${new Date(secret.last_used_at!).toLocaleDateString()}` + ) + ).toBeInTheDocument() expect(screen.queryByText(/Invalid Date/)).not.toBeInTheDocument() }) }) diff --git a/src/platform/secrets/components/SecretListItem.vue b/src/platform/secrets/components/SecretListItem.vue index 54fd474594..3d7a5723d4 100644 --- a/src/platform/secrets/components/SecretListItem.vue +++ b/src/platform/secrets/components/SecretListItem.vue @@ -19,11 +19,9 @@
- - {{ $t('secrets.createdAt', { date: createdDate }) }} - - - {{ $t('secrets.lastUsed', { date: lastUsedDate }) }} + {{ createdAtLabel }} + + {{ lastUsedLabel }}
@@ -31,20 +29,20 @@