diff --git a/docs/my-account-v2-prototype-brief.md b/docs/my-account-v2-prototype-brief.md
index a1f78835d5..2096fa11d9 100644
--- a/docs/my-account-v2-prototype-brief.md
+++ b/docs/my-account-v2-prototype-brief.md
@@ -150,7 +150,7 @@ Before writing any code: clone the repo via `n`, install deps (`npm install` and
## 3. Scope of screens
-Three priority surfaces, each with several variant states. Everything else (account settings, delete account, signed-out, email-unverified) is **reused from v1 as-is** for the prototype.
+Four priority surfaces, each with several variant states. Everything else (account settings, delete account, signed-out, email-unverified) is **reused from v1 as-is** for the prototype.
**Newsletters** (Figma section `2636:46703`)
@@ -182,6 +182,10 @@ v1 already extends the WooCommerce Subscriptions endpoint, but the design is bei
- Detail variants: `active`, `active (no fees)`, `cancelled`, `expiring`, `renewed`.
- Modals: `Cancel subscription – Init/Success`, `Renew subscription` and its `Success`, `Change subscription – Init / Monthly selected / Plan selected / Transaction modal`.
+**Payment methods** (no Figma — reproduce v1 as-is)
+
+v1 already renders `/my-account/payment-methods/` as a `
` of saved cards with action buttons (Make default / Delete) plus an "Add payment method" CTA. The v2 prototype reproduces the exact v1 DOM under the `?v2-demo` flag, fed by fake data instead of real `wc_get_customer_saved_methods_list()` — no new design, no new components. This means v2-demo readers can see and click through the payment-methods experience without needing real WC payment tokens on the demo site. Phase 6 ships this; details in §10 → Phase 6.
+
Reused from v1 unchanged: account-page page template, sidebar/menu, account settings, delete-account flow, signed-out state.
## 4. The `?v2-demo` mechanism
@@ -615,11 +619,15 @@ All 5 detail variants. Figma section `2636:46116`. v1 already has a subscription
All six modal flows (cancel donation, modify donation, restart donation, cancel subscription, renew subscription, change subscription) wired to client-side handlers + toast confirmations. All use `Newspack / Modal` → `.newspack-ui__modal*` (see §6).
-### Phase 6 — Polish + scenario fixtures (~0.5 day)
+### Phase 6 — Payment methods (~1–1.5 days)
+
+Reproduce v1's `/my-account/payment-methods/` surface byte-for-byte under the v2-demo flag, fed entirely by fake data (no real WooCommerce payment-token storage). The v1 page is a saved-cards table backed by `wc_get_account_payment_methods_columns()` + `wc_get_account_payment_methods_types()` plus an "Add payment method" CTA; the v2 prototype renders the same DOM (`
+ . */
+ __( 'We have just sent a confirmation email to %s.', 'newspack-plugin' ),
+ '' . esc_html( $reader_email ) . ''
+ ),
+ [ 'strong' => [] ]
+ );
+ ?>
+
+
+
+
+
+
+
+
diff --git a/includes/plugins/woocommerce/my-account/templates/v2-demo/partials/cancel-subscription-modal.php b/includes/plugins/woocommerce/my-account/templates/v2-demo/partials/cancel-subscription-modal.php
new file mode 100644
index 0000000000..8b2c8cecf6
--- /dev/null
+++ b/includes/plugins/woocommerce/my-account/templates/v2-demo/partials/cancel-subscription-modal.php
@@ -0,0 +1,125 @@
+ the subscription row this modal is for
+ * (id, status, expires_on, next_payment).
+ * - `reader_email` => email address surfaced in the success
+ * copy (matches the page-level fake data).
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+$subscription = isset( $args['subscription'] ) ? $args['subscription'] : [];
+$reader_email = isset( $args['reader_email'] ) ? (string) $args['reader_email'] : '';
+$subscription_id = isset( $subscription['id'] ) ? (string) $subscription['id'] : '';
+
+// "active until X" date: prefer next_payment for actively-renewing subs; fall
+// back to expires_on for the expiring variant. Either way the source is the
+// fake-data fixture, formatted with WP's locale-aware date helper.
+$end_iso = '';
+if ( ! empty( $subscription['next_payment'] ) ) {
+ $end_iso = (string) $subscription['next_payment'];
+} elseif ( ! empty( $subscription['expires_on'] ) ) {
+ $end_iso = (string) $subscription['expires_on'];
+}
+$end_date_label = '';
+if ( '' !== $end_iso ) {
+ $end_ts = strtotime( $end_iso );
+ $end_date_label = $end_ts ? date_i18n( 'F j, Y', $end_ts ) : $end_iso;
+}
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+ . */
+ __( 'If you cancel now, your subscription will remain active until %s.', 'newspack-plugin' ),
+ '' . esc_html( $end_date_label ) . ''
+ ),
+ [ 'strong' => [] ]
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ . */
+ __( 'We have just sent a confirmation email to %s.', 'newspack-plugin' ),
+ '' . esc_html( $reader_email ) . ''
+ ),
+ [ 'strong' => [] ]
+ );
+ ?>
+
+
+
+
+
+
+
+
diff --git a/includes/plugins/woocommerce/my-account/templates/v2-demo/partials/modify-donation-modal.php b/includes/plugins/woocommerce/my-account/templates/v2-demo/partials/modify-donation-modal.php
new file mode 100644
index 0000000000..169bc6b2d0
--- /dev/null
+++ b/includes/plugins/woocommerce/my-account/templates/v2-demo/partials/modify-donation-modal.php
@@ -0,0 +1,268 @@
+ the donation row (active recurring).
+ * - `currency_symbol` => currency symbol from fake data.
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+$donation = isset( $args['donation'] ) ? $args['donation'] : [];
+$currency_symbol = isset( $args['currency_symbol'] ) ? (string) $args['currency_symbol'] : '$';
+$donation_id = isset( $donation['id'] ) ? (string) $donation['id'] : '';
+
+$initial_amount = isset( $donation['amount'] ) ? (float) $donation['amount'] : 0.0;
+$initial_frequency = isset( $donation['frequency'] ) ? (string) $donation['frequency'] : 'month';
+$fees_covered = ! empty( $donation['fees_covered'] );
+$next_payment_iso = isset( $donation['next_payment'] ) ? (string) $donation['next_payment'] : '';
+$next_payment_date = '';
+if ( '' !== $next_payment_iso ) {
+ $ts = strtotime( $next_payment_iso );
+ $next_payment_date = $ts ? date_i18n( 'F j, Y', $ts ) : $next_payment_iso;
+}
+
+// Frequency catalogue (label = tab text, unit = "/ unit" suffix).
+$frequencies = [
+ [
+ 'id' => 'month',
+ 'label' => __( 'Monthly', 'newspack-plugin' ),
+ 'unit' => __( 'month', 'newspack-plugin' ),
+ ],
+ [
+ 'id' => 'year',
+ 'label' => __( 'Annually', 'newspack-plugin' ),
+ 'unit' => __( 'year', 'newspack-plugin' ),
+ ],
+];
+
+$unit_labels = [];
+foreach ( $frequencies as $freq ) {
+ $unit_labels[ $freq['id'] ] = $freq['unit'];
+}
+$initial_unit = isset( $unit_labels[ $initial_frequency ] ) ? $unit_labels[ $initial_frequency ] : $initial_frequency;
+
+// Initial breakdown values, mirroring the live JS recompute. Vat rate matches
+// the fake-data fixture (subtotal/total ratio); kept here as a single source
+// for the data attributes the JS reads.
+$vat_rate = 0.20;
+$fee_rate = 0.02;
+$initial_subtotal = $initial_amount / ( 1 + $vat_rate );
+$initial_vat = $initial_amount - $initial_subtotal;
+$initial_fee = $fees_covered ? $initial_amount * $fee_rate : null;
+$initial_total = $fees_covered ? $initial_amount + ( null === $initial_fee ? 0 : $initial_fee ) : $initial_amount;
+
+$format_amount = static function ( $value ) use ( $currency_symbol ) {
+ return $currency_symbol . number_format_i18n( (float) $value, 2 );
+};
+
+// Per-frequency next-donation dates. The detail page only ships a single
+// next_payment for the donation's current frequency; for the other tab we
+// suppress the date so we don't lie to the reader. JSON-encoded for JS.
+$next_dates = [];
+if ( '' !== $next_payment_date ) {
+ $next_dates[ $initial_frequency ] = $next_payment_date;
+}
+
+// Confirm button label is always "Confirm donation: $X / unit". Initial render
+// matches the donation's current values; JS keeps it in sync afterwards.
+/* translators: %1$s: amount with currency, %2$s: frequency unit. */
+$initial_confirm_label = sprintf( __( '%1$s / %2$s', 'newspack-plugin' ), $format_amount( $initial_amount ), $initial_unit );
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ " — the unit span is updated in JS as the
+ // active tab changes, so it stays in sync with the segmented
+ // control (Figma 2636:46578).
+ esc_html_e( 'Amount /', 'newspack-plugin' );
+ ?>
+
+
+ ` requires a dot-decimal value regardless
+ // of locale; the JS `parseFloat()` in donations.js can't handle
+ // `9,99` from a comma-decimal locale. Use plain `number_format`
+ // with explicit `.` decimal sep + no thousands separator for both
+ // the input value and the data-initial-amount attribute.
+ $amount_machine = number_format( $initial_amount, 2, '.', '' );
+ ?>
+