Skip to content

Commit 496a5f0

Browse files
iisaclaude
andauthored
feat: add PayPal vault flow for editing payment method (#71)
* feat: add PayPal vault flow for editing payment method - Add PayPal vault button rendering in ia-mgc-braintree-manager - Wire paypalBlockerSelected to set PaymentProvider.PayPal - Auto-dispatch UpdatePaymentMethod on PayPal authorization (no submit button needed) - Fix setNewPaymentMethod() to correctly set paypalEmail, clear card fields, update paymentMethodType - Fix 'Paypal' casing in plans.ts to match PaymentProvider.PayPal - Show PayPal email in payment method view mode - Hide CC fields via CSS when PayPal is selected - Add PayPalVaultError event with UI error message on SDK failure - Update demo to show PayPal nonce and email on UpdatePaymentMethod - Add integration tests for PayPal selection and UpdatePaymentMethod dispatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: handle unhandled rejection in renderPayPalVaultButton Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve ESLint errors in PayPal payment method changes - Fix import order for PaymentProvider in plan.ts and plans.ts - Add file-level no-console disable in braintree-manager.ts - Add console.log to empty PayPal delegate methods - Add eslint-disable-next-line for console.log in payment-method.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve high priority code review issues in PayPal payment flow - Fix race condition in braintree-manager: set paypalButtonRendered optimistically before async call, reset on error to prevent infinite retry - Fix plans.ts: Venmo display was showing paypalEmail instead of venmoUsername - Add tests: PayPal email fallbacks, old btData snapshot, PayPalVaultError, hidden submit button, Venmo provider switch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: correct PayPal email fallback test to assert 'not_found' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve remaining code review issues - Use PaymentProvider enum consistently in plans.ts (replace string literals) - Remove debug console.log from contactFormSection getter - Replace self=this alias with arrow functions in PayPal delegate - Remove unused setupPaymentHandlers() method Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add session notes for Apr 6, 2026 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: render PayPal button eagerly on BraintreeManagerSetupComplete Align with iaux-donation-form pattern: render the Braintree PayPal iframe into the slot immediately when setup completes, rather than lazily on user selection. Removes the displayPayPal property from ia-mgc-braintree-manager since it is no longer needed. Adds test verifying the eager render is triggered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove unused displayContactForm variable in contactFormSection getter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: apply prettier formatting to PayPal branch changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: apply prettier formatting to plans.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: fix failing tests for PayPal selection event and Venmo payment method - Fix test dispatching wrong event name (paypalBlockerSelected → paypalSelected) - Add explicit Venmo branch in setNewPaymentMethod to map details.username to venmoUsername and clear card fields (cardType, last4, expiration) - Add username field to BTPaymentMethodUpdate.details type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add new tests for PayPal slot, cancel button, braintree getter, and Venmo fields - PayPal slot (#ia-mgc-paypal-button) exists in DOM when currentlyEditing is true - Cancel button resets selectedPaymentProvider and closes the form - braintreeManagerElement getter returns ia-mgc-braintree-manager element - Venmo branch clears expirationMonth and expirationYear from previous btdata Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 160f422 commit 496a5f0

14 files changed

Lines changed: 793 additions & 104 deletions

.claude/Apr 6, 2026.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Session Notes - Apr 6, 2026
2+
3+
## Branch: `claude/competent-swanson` (PayPal vault flow)
4+
5+
### Work Done
6+
7+
**Code review fixes (post-feature):**
8+
- Fixed race condition in `braintree-manager.ts`: `paypalButtonRendered` now set optimistically before async call, reset to `false` on error
9+
- Fixed copy-paste bug in `plans.ts`: Venmo link was displaying `paypalEmail` instead of `venmoUsername`
10+
- Replaced `self = this` alias with arrow functions in PayPal delegate
11+
- Removed unused `setupPaymentHandlers()` method
12+
- Replaced string literals (`'creditCard'`, `'Venmo'`) with `PaymentProvider` enum in `plans.ts`
13+
- Removed debug `console.log('contactFormSectioncontactFormSection', ...)` from `payment-method.ts`
14+
- Fixed import order ESLint errors in `plan.ts` and `plans.ts`
15+
16+
**Tests added:**
17+
- `plan.test.ts`: PayPal email `'not_found'` fallback, old btData snapshot on PayPal switch, Venmo provider switch
18+
- `edit-plan-flow-payment-method.test.ts`: `PayPalVaultError` sets `updateStatus = 'fail'`, submit button hidden for PayPal
19+
20+
### Key Decisions
21+
- `paypalButtonRendered` set optimistically (before async) to prevent infinite retry; reset on error so retries are possible after failure
22+
- `PaymentProvider` enum used consistently throughout — no more string literals for payment method types
23+
- Tests bypass Braintree SDK entirely by dispatching events directly on components
24+
25+
### Commits on this branch
26+
- `fix: correct PayPal email fallback test to assert 'not_found'`
27+
- `fix: resolve high priority code review issues in PayPal payment flow`
28+
- `fix: resolve remaining code review issues`
29+
30+
### Pending
31+
- PRs not yet created for any of the feature branches (`competent-swanson`, `venmo-integration`, `google-pay-integration`, `apple-pay-integration`)
32+
- `npm run test` not yet verified (sandbox blocks port 8000 — must be run manually)

.claude/Mar 16, 2026.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Session Notes — Mar 16, 2026
2+
3+
## Branch: `display-cents`
4+
5+
## Summary
6+
Code reviewed the `display-cents` branch and addressed all review items from the initial review.
7+
8+
## Code Review Items & Resolutions
9+
10+
1. **`Receipt.amount` and `Receipt.amountFormatted` identical** — Removed `amountFormatted`, kept `amount`. Updated `receipts.ts` reference. (commit `2d3c8ff`)
11+
12+
2. **`Receipt.currencySymbol` / `MonthlyPlan.currencySymbol` dead code** — Removed both getters and their tests since `formatCurrency` handles symbols via `Intl.NumberFormat`. (commit `bdf4a2e`)
13+
14+
3. **Unsafe `as number` cast in `coveredFeesText`** — Replaced with falsy check. `undefined`, `null`, and `0` now return fallback text `"I'll generously cover the fees."`. Added 3 unit tests. (commit `df00ed3`)
15+
16+
4. **"Invalid amount" as user-facing text** — Decided to keep current behavior.
17+
18+
5. **Hardcoded `'en-US'` locale** — Added `currencyToLocale` map (USD only) with `en-US` fallback. Added unit tests for `formatCurrency`. (commit `04605e0`)
19+
20+
6. **No unit tests for `formatCurrency()`** — Added `test/utils/currency-format.test.ts` with 11 tests covering USD, EUR, GBP, zero, negative, NaN, Infinity, rounding, and `currencyToSymbol` map. (commit `04605e0`)
21+
22+
7. **Hardcoded `$` prefix in input form (nit)** — Added `currencyToSymbol` map, replaced hardcoded `$` with map lookup. (commit `62a5dcc`)
23+
24+
## Key Decisions
25+
- Keep `amount` over `amountFormatted` on Receipt model
26+
- `coveredFeesText` uses fallback text when fee is `0` (not `$0.00`)
27+
- `currencyToLocale` and `currencyToSymbol` maps only contain USD for now
28+
- "Invalid amount" string kept as-is for non-finite inputs
29+
30+
## Commits (5 new)
31+
- `04605e0` add currency-to-locale map and unit tests for formatCurrency
32+
- `df00ed3` fix unsafe cast in coveredFeesText and add unit tests
33+
- `bdf4a2e` remove dead currencySymbol getters from Plan and Receipt models
34+
- `2d3c8ff` remove duplicate amountFormatted getter from Receipt model
35+
- `62a5dcc` replace hardcoded $ prefix with currencyToSymbol map lookup

.claude/Mar 17, 2026.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Session Notes - Mar 17, 2026
2+
3+
## Branch: `display-cents`
4+
5+
## Changes Made
6+
7+
### coveredFeesText refactor
8+
- Updated `coveredFeesText` getter in `amount.ts` to derive fee from `this.newAmount` using `DonationPaymentInfo.calculateFeeAmount()` instead of `this.donationPaymentInfo?.feeAmountCovered`
9+
- Shows generic text when `newAmount` is 0, dynamic fee text when filled
10+
- Updated tests accordingly, removed unused `DonationType` import
11+
- Commit: `0a9f138`
12+
13+
### Currency code display
14+
- Added currency code (e.g. `USD`) before `amountFormatted` in three locations:
15+
- Plan info display in `amount.ts` (line 71)
16+
- "Current donation amount:" in edit form in `amount.ts` (line 272)
17+
- "Total:" in edit form in `amount.ts` (line 307)
18+
- Plans list in `plans.ts` (line 36)
19+
- Added unit tests for each display area
20+
- Commits: `4923843`, `f90a9f6`, `236eacc`
21+
22+
### Receipts cleanup
23+
- Renamed `donation` parameter to `receipt` in `.map()` callback in `receipts.ts`
24+
- No test changes needed (tests reference DOM selectors, not variable names)
25+
- Commit: `40dd2f4`
26+
27+
### Demo data fix
28+
- Fixed duplicate receipt IDs in `demo/index.html` — first two receipts shared the same JWT token as `id`, causing "Sent" label to appear on wrong row
29+
- Gave them unique IDs: `foo-id-1`/`foo-token-1` and `foo-id-4`/`foo-token-4`
30+
- Commit: `e6ce922`
31+
32+
## Branch: `edit-plan-flow-error-handling-tests`
33+
34+
### State persistence across views tests
35+
- Created `test/state-persistence-across-views.test.ts` with 4 tests covering view transitions
36+
- Extracted `navigateBackToPlans` into shared helpers (`test/helpers/edit-plan-helpers.ts`)
37+
- Refactored `edit-plan-flow.test.ts` to use shared helper
38+
- Commit: `f489140`
39+
40+
### Cancel / hasBeenCancelled tests
41+
- Added 2 tests to `edit-plan-flow.test.ts` covering the cancel code path in `updateReceived` (lines 125-128 of `monthly-giving-circle.ts`)
42+
1. `action: 'cancel'` — clears `editingThisPlan`, navigates to plans view
43+
2. `plan.cancelPlan()` + non-cancel action — exercises `hasBeenCancelled` branch, verifies `cancelled` CSS class on plan `<li>`
44+
- Added `IauxMgcPlans` type import for proper typing
45+
- Commit: `d41fa0b`
46+
- All 110 tests passing
47+
48+
### Remaining cleanup items
49+
1. ~~**`beforeEach` in `edit-plan-flow.test.ts`** — all 5 tests repeat the same fixture setup~~ (done in `ffb4f96`)
50+
2. ~~**Missing test: `dateUpdate` success closes date form**~~ (done in `ffb4f96`)
51+
3. ~~**Missing test: `cancel` via `updateReceived`**~~ (done in `d41fa0b`)
52+
4. ~~**`import type` in `state-persistence`**~~ (already fixed — currently uses `import type`)
53+
54+
### Session 3 (this session)
55+
- Implemented cancel/hasBeenCancelled tests per plan from plan mode
56+
- Confirmed `import type` in state-persistence was already fixed
57+
- All 4 cleanup items now complete
58+
- Saved preference: never disable sandbox without explicit permission; use local `node_modules/.bin/`
59+
60+
## Previous session (display-cents branch)
61+
62+
### Changes Made
63+
- coveredFeesText refactor, currency code display, receipts cleanup, demo data fix
64+
- Cover fees checkbox `?disabled` attribute was added then removed per user request
65+
- All 80 tests passing

.claude/Mar 9, 2026.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Session Notes - Mar 9, 2026
2+
3+
## Task
4+
Standardize currency amount formatting across all donation displays in `iaux-monthly-giving-circle`.
5+
6+
## Key Decisions
7+
- Created shared `formatCurrency` utility in `src/utils/currency-format.ts` using `new Intl.NumberFormat` with `style: 'currency'`
8+
- Added `Number.isFinite` guard that returns `'Invalid amount'` for non-finite values (not silently defaulting to $0.00)
9+
- Kept existing `currencySymbol` getters in Plan and Receipt models (not removed)
10+
- Removed hardcoded `USD $` prefixes from templates — `Intl.NumberFormat` handles currency symbol automatically
11+
12+
## Files Changed
13+
- **Created**: `src/utils/currency-format.ts`
14+
- **Modified**: `src/models/plan.ts`, `src/models/receipt.ts`, `src/form-sections/amount.ts`, `src/plans.ts`
15+
- **Tests updated**: `test/models/plan.test.ts`, `test/models/receipt.test.ts`
16+
17+
## Progress
18+
- All 71 tests passing
19+
- Lint, prettier, and circular dependency checks all pass
20+
- Branch: `display-cents`
21+
22+
## Notes
23+
- `dist/test/receipts.test.js` was a stale compiled artifact with no source `.ts` file on current branch or main (only existed on `unit-tests-2` branch). Was deleted during debugging — gets regenerated by `tsc` if source exists.
24+
- `DonationPaymentInfo` (feeAmountCovered, calculateTotal) comes from external package `@internetarchive/donation-form-data-models` — cannot be modified in this repo.
25+
26+
---
27+
28+
## Session 2 - Time Display Audit & Fixes
29+
30+
### Task
31+
Audit all places where time/dates are displayed and fix locale/timezone issues.
32+
33+
### Key Decisions
34+
- Fixed invalid `'US-EN'` locale tags to `'en-US'` (BCP 47) in `demo/index.html`
35+
- Switched `nextBillingDateLocale` and `lastBillingDateLocale` from `Date.toLocaleDateString()` to `Intl.DateTimeFormat` with `timeZone: 'UTC'` to prevent off-by-one-day display for users west of UTC
36+
- Added try/catch to return `"Invalid date"` instead of throwing `RangeError` on bad date strings
37+
- Added early return pattern to `lastBillingDateLocale` to match `nextBillingDateLocale` style
38+
39+
### Files Changed
40+
- **Modified**: `demo/index.html`, `src/models/plan.ts`, `test/models/plan.test.ts`
41+
42+
### Progress
43+
- All 38 plan model tests passing (4 new tests added)
44+
- Full test suite (73+ tests) passing
45+
- Squashed into single commit `8ae7c84` on branch `pin-us-locale`
46+
47+
### Audit Findings (for future work)
48+
- `date.ts:217-228`: "second donation this month" validation compares local-time user input against UTC server date — potential timezone mismatch near month boundaries
49+
- Receipt dates (`receipt.ts`) are raw strings passed through from consumers — no formatting done in the model

demo/index.html

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
"timezone": "UTC"
134134
},
135135
"status": "Active",
136-
"paymentMethodType": "Paypal",
136+
"paymentMethodType": "PayPal",
137137
"last4": null,
138138
"cardType": null,
139139
"expirationMonth": null,
@@ -423,14 +423,20 @@
423423
});
424424

425425
mgcComponent.addEventListener('UpdatePaymentMethod', (e) => {
426+
console.log('UpdatePaymentMethod received:', e.detail);
426427
const { plan, newPaymentMethodRequest } = e.detail;
427428

428-
429-
const heads = flipCoin() === 1;
429+
const isPayPal = newPaymentMethodRequest?.paymentProvider === 'PayPal';
430+
const heads = isPayPal ? true : flipCoin() === 1;
430431
const successOrFail = heads ? 'success' : 'fail';
431432
const returnTiming = heads ? 1500 : 4000;
432433

433-
uxMessageInfoArea.innerText = `Updating payment method for plan: ${plan ? plan.id : 'no plan'} -- Update will return ${successOrFail} in ${returnTiming} ms`;
434+
const paypalEmail = newPaymentMethodRequest?.paymentMethodInfo?.details?.email;
435+
const paypalNonce = newPaymentMethodRequest?.paymentMethodInfo?.nonce;
436+
const methodLabel = isPayPal
437+
? `PayPal (${paypalEmail}, nonce: ${paypalNonce})`
438+
: 'payment method';
439+
uxMessageInfoArea.innerText = `Updating ${methodLabel} for plan: ${plan ? plan.id : 'no plan'} -- Update will return ${successOrFail} in ${returnTiming} ms`;
434440
const message = successOrFail === 'success' ? 'Payment method updated' : 'Payment method failed to update';
435441

436442
if (heads && plan) {
@@ -450,7 +456,7 @@
450456

451457
setTimeout(() => {
452458
mgcComponent.updateReceived(update);
453-
console.log('Amount Update Request --- index.html ----', update);
459+
console.log('UpdatePaymentMethod --- index.html ----', update);
454460
uxMessageInfoArea.innerText = '';
455461
}, returnTiming);
456462
});

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@internetarchive/donation-monthly-portal",
3-
"version": "7.6.1",
3+
"version": "0.0.0-paypal-payment-method-4",
44
"description": "The Internet Archive Monthly Portal",
55
"license": "AGPL-3.0-only",
66
"main": "dist/index.js",

src/form-sections/parts/braintree-manager.ts

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-console */
12
import { LitElement, html, css, CSSResult, PropertyValueMap } from 'lit';
23
import { customElement, property, state } from 'lit/decorators.js';
34

@@ -10,6 +11,10 @@ import {
1011
BraintreeManagerInterface,
1112
HostingEnvironment,
1213
} from '@internetarchive/donation-form';
14+
import {
15+
DonationPaymentInfo,
16+
DonationType,
17+
} from '@internetarchive/donation-form-data-models';
1318
import type { BraintreeEndpointManagerInterface } from '@internetarchive/donation-form/dist/src/braintree-manager/braintree-interfaces.js';
1419
import type { PaymentClientsInterface } from '@internetarchive/donation-form/dist/src/braintree-manager/payment-clients.js';
1520

@@ -249,6 +254,73 @@ export class MGCBraintreeManager extends LitElement {
249254
`;
250255
}
251256

257+
async renderPayPalVaultButton(): Promise<void> {
258+
console.log('[PayPal] renderPayPalVaultButton called');
259+
260+
const handler =
261+
await this.braintreeManager?.paymentProviders.paypalHandler.get();
262+
console.log('[PayPal] handler:', handler);
263+
if (!handler) return;
264+
265+
const container = document.querySelector('#ia-mgc-paypal-button');
266+
console.log('[PayPal] container element:', container);
267+
268+
const donationInfo = new DonationPaymentInfo({
269+
donationType: DonationType.Monthly,
270+
amount: this.plan?.amount ?? 0,
271+
coverFees: false,
272+
});
273+
274+
const dataSource = await handler.renderPayPalButton({
275+
selector: '#ia-mgc-paypal-button',
276+
style: { color: 'blue', shape: 'rect', size: 'medium' },
277+
donationInfo,
278+
});
279+
console.log('[PayPal] dataSource:', dataSource);
280+
281+
if (!dataSource) return;
282+
283+
dataSource.delegate = {
284+
payPalPaymentStarted: async () => {
285+
console.log('PayPal payment started');
286+
},
287+
payPalPaymentAuthorized: async (_ds: any, payload: any) => {
288+
this.handlePayPalAuthorized(payload);
289+
},
290+
payPalPaymentConfirmed: async (_ds: any, payload: any) => {
291+
this.handlePayPalAuthorized(payload);
292+
},
293+
payPalPaymentCancelled: async () => {
294+
console.log('PayPal payment cancelled');
295+
},
296+
payPalPaymentError: async (_ds: any, error: unknown) => {
297+
console.error('PayPal vault error:', error);
298+
this.dispatchEvent(
299+
new CustomEvent('PayPalVaultError', { detail: { error } }),
300+
);
301+
},
302+
};
303+
}
304+
305+
private handlePayPalAuthorized(payload: {
306+
nonce: string;
307+
type: string;
308+
details: { email: string };
309+
}): void {
310+
this.dispatchEvent(
311+
new CustomEvent('PayPalVaultAuthorized', {
312+
detail: {
313+
paymentMethodInfo: {
314+
description: `PayPal - ${payload.details.email}`,
315+
nonce: payload.nonce,
316+
type: payload.type,
317+
details: { email: payload.details.email },
318+
},
319+
},
320+
}),
321+
);
322+
}
323+
252324
private async setupBraintreeManager(): Promise<void> {
253325
this.braintreeManager = new BraintreeManager({
254326
paymentClients:
@@ -313,14 +385,4 @@ export class MGCBraintreeManager extends LitElement {
313385
get contactForm(): HTMLFormElement | null {
314386
return this.querySelector('form[name="contact-form"]');
315387
}
316-
317-
async setupPaymentHandlers() {
318-
// const creditCardFlowHandler = this.paymentConfig?.paymentFlowHandlers?.creditCardHandler;
319-
const creditCardHandler =
320-
await this.braintreeManager?.paymentProviders.creditCardHandler.get();
321-
creditCardHandler?.hideErrorMessage();
322-
// const valid = this.contactForm?.reportValidity();
323-
// const hostedFieldsResponse = await creditCardFlowHandler?.tokenizeFields();
324-
// console.log("CC hostedFieldsResponse", hostedFieldsResponse);
325-
}
326388
}

0 commit comments

Comments
 (0)