feat(contribution): port accept-invite.vue to ContributionAmountField
Replace cadence radios, contribution input, preset chips, guidance label, and billing summary block with a single ContributionAmountField usage. Default contribution updated to 180 to preserve the previous $15/mo suggested annual default (cadence-unit value now). Updated flowSummary to format cadence-unit directly. Updated e2e selectors to use the data-testids the component exposes and new summary copy.
This commit is contained in:
parent
3126ddb8ea
commit
e0e7da5cca
2 changed files with 29 additions and 181 deletions
|
|
@ -124,77 +124,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label class="form-label">Billing Cadence</label>
|
<ContributionAmountField
|
||||||
<div class="cadence-radios">
|
v-model="form.contributionAmount"
|
||||||
<div class="circle-radio">
|
v-model:cadence="cadence"
|
||||||
<input
|
/>
|
||||||
id="accept-cadence-annual"
|
|
||||||
v-model="cadence"
|
|
||||||
type="radio"
|
|
||||||
name="cadence"
|
|
||||||
value="annual"
|
|
||||||
>
|
|
||||||
<label for="accept-cadence-annual">
|
|
||||||
<span class="circle-label-name">Per Year</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="circle-radio">
|
|
||||||
<input
|
|
||||||
id="accept-cadence-monthly"
|
|
||||||
v-model="cadence"
|
|
||||||
type="radio"
|
|
||||||
name="cadence"
|
|
||||||
value="monthly"
|
|
||||||
>
|
|
||||||
<label for="accept-cadence-monthly">
|
|
||||||
<span class="circle-label-name">Per Month</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group full-width">
|
|
||||||
<label class="form-label" for="accept-contribution">
|
|
||||||
Monthly Contribution
|
|
||||||
</label>
|
|
||||||
<div class="contribution-input-row">
|
|
||||||
<span class="contribution-currency">$</span>
|
|
||||||
<input
|
|
||||||
id="accept-contribution"
|
|
||||||
v-model.number="form.contributionAmount"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
inputmode="numeric"
|
|
||||||
class="contribution-input"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="contribution-presets" role="group" aria-label="Suggested amounts">
|
|
||||||
<button
|
|
||||||
v-for="preset in CONTRIBUTION_PRESETS"
|
|
||||||
:key="preset.amount"
|
|
||||||
type="button"
|
|
||||||
class="contribution-preset-chip"
|
|
||||||
@click="form.contributionAmount = preset.amount"
|
|
||||||
>
|
|
||||||
${{ preset.amount }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p v-if="guidanceLabel" class="contribution-guidance">{{ guidanceLabel }}</p>
|
|
||||||
<p class="field-note">Pay what you can. If you can pay more, you're making room for someone who can't.</p>
|
<p class="field-note">Pay what you can. If you can pay more, you're making room for someone who can't.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="form.contributionAmount > 0" class="form-group full-width">
|
|
||||||
<div class="billing-summary">
|
|
||||||
<p class="billing-summary-line">
|
|
||||||
You'll be charged <strong>${{ firstCharge }} today</strong><span v-if="cadence === 'annual'"> (${{ form.contributionAmount }}/month × 12)</span>.
|
|
||||||
</p>
|
|
||||||
<p class="billing-summary-line">
|
|
||||||
Then <strong>${{ firstCharge }} every {{ cadence === 'annual' ? 'year' : 'month' }}</strong>, until you cancel.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group full-width">
|
<div class="form-group full-width">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input
|
<input
|
||||||
|
|
@ -236,11 +172,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { requiresPayment } from "~/config/contributions";
|
||||||
requiresPayment,
|
|
||||||
CONTRIBUTION_PRESETS,
|
|
||||||
getGuidanceLabel,
|
|
||||||
} from "~/config/contributions";
|
|
||||||
|
|
||||||
definePageMeta({ layout: false });
|
definePageMeta({ layout: false });
|
||||||
useSiteMeta({ title: "Accept Invitation", noindex: true });
|
useSiteMeta({ title: "Accept Invitation", noindex: true });
|
||||||
|
|
@ -265,7 +197,7 @@ const form = reactive({
|
||||||
location: "",
|
location: "",
|
||||||
circle: "community",
|
circle: "community",
|
||||||
motivation: "",
|
motivation: "",
|
||||||
contributionAmount: 15,
|
contributionAmount: 180,
|
||||||
agreedToGuidelines: false,
|
agreedToGuidelines: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -283,26 +215,16 @@ const needsPayment = computed(() => {
|
||||||
return requiresPayment(form.contributionAmount);
|
return requiresPayment(form.contributionAmount);
|
||||||
});
|
});
|
||||||
|
|
||||||
const guidanceLabel = computed(() => getGuidanceLabel(form.contributionAmount));
|
const flowSummary = computed(() => {
|
||||||
|
|
||||||
const firstCharge = computed(() => {
|
|
||||||
const amount = form.contributionAmount || 0;
|
const amount = form.contributionAmount || 0;
|
||||||
return cadence.value === "annual" ? amount * 12 : amount;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatContributionAmount = (amount) => {
|
|
||||||
if (!amount || amount === 0) return "$0";
|
|
||||||
const display = cadence.value === "annual" ? amount * 12 : amount;
|
|
||||||
const suffix = cadence.value === "annual" ? "/yr" : "/mo";
|
const suffix = cadence.value === "annual" ? "/yr" : "/mo";
|
||||||
return `$${display}${suffix}`;
|
return {
|
||||||
};
|
|
||||||
|
|
||||||
const flowSummary = computed(() => ({
|
|
||||||
name: form.name,
|
name: form.name,
|
||||||
email: preRegEmail.value,
|
email: preRegEmail.value,
|
||||||
circle: form.circle,
|
circle: form.circle,
|
||||||
contribution: formatContributionAmount(form.contributionAmount),
|
contribution: amount > 0 ? `$${amount}${suffix}` : "$0",
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const closeFlowOverlay = () => {
|
const closeFlowOverlay = () => {
|
||||||
flowState.value = "idle";
|
flowState.value = "idle";
|
||||||
|
|
@ -525,72 +447,6 @@ textarea.form-input {
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- CONTRIBUTION AMOUNT INPUT + CHIPS ---- */
|
|
||||||
.contribution-input-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
.contribution-currency {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.contribution-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
background: var(--input-bg);
|
|
||||||
border: 1px solid var(--parch);
|
|
||||||
font-family: 'Commit Mono', monospace;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.contribution-input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--candle);
|
|
||||||
}
|
|
||||||
.contribution-presets {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.contribution-preset-chip {
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px dashed var(--parch);
|
|
||||||
font-family: 'Commit Mono', monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.contribution-preset-chip:hover {
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--candle);
|
|
||||||
}
|
|
||||||
.contribution-guidance {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-style: italic;
|
|
||||||
color: var(--ink-soft, currentColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- BILLING SUMMARY ---- */
|
|
||||||
.billing-summary {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 1px dashed var(--border);
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
.billing-summary-line {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text);
|
|
||||||
line-height: 1.5;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.billing-summary-line + .billing-summary-line {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
.billing-summary-line strong {
|
|
||||||
color: var(--text-bright);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- CIRCLE RADIOS ---- */
|
/* ---- CIRCLE RADIOS ---- */
|
||||||
.circle-radios {
|
.circle-radios {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -598,12 +454,6 @@ textarea.form-input {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cadence-radios {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle-radio {
|
.circle-radio {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
@ -721,9 +571,5 @@ textarea.form-input {
|
||||||
.circle-radios {
|
.circle-radios {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cadence-radios {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,9 @@ test.describe('Accept Invite — pre-registrant signup', () => {
|
||||||
await expect(page.locator('#circle-community')).toBeAttached()
|
await expect(page.locator('#circle-community')).toBeAttached()
|
||||||
await expect(page.locator('#circle-founder')).toBeAttached()
|
await expect(page.locator('#circle-founder')).toBeAttached()
|
||||||
await expect(page.locator('#circle-practitioner')).toBeAttached()
|
await expect(page.locator('#circle-practitioner')).toBeAttached()
|
||||||
await expect(page.locator('#accept-cadence-monthly')).toBeAttached()
|
await expect(page.getByTestId('cadence-monthly')).toBeVisible()
|
||||||
await expect(page.locator('#accept-cadence-annual')).toBeAttached()
|
await expect(page.getByTestId('cadence-annual')).toBeVisible()
|
||||||
await expect(page.locator('#accept-contribution')).toBeVisible()
|
await expect(page.getByTestId('contribution-amount')).toBeVisible()
|
||||||
await expect(page.locator('.contribution-preset-chip').first()).toBeVisible()
|
await expect(page.locator('.contribution-preset-chip').first()).toBeVisible()
|
||||||
await expect(page.locator('.form-submit')).toBeVisible()
|
await expect(page.locator('.form-submit')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
@ -110,15 +110,17 @@ test.describe('Accept Invite — pre-registrant signup', () => {
|
||||||
await mockVerifyOk(page)
|
await mockVerifyOk(page)
|
||||||
await gotoAcceptInvite(page)
|
await gotoAcceptInvite(page)
|
||||||
|
|
||||||
await expect(page.locator('#accept-contribution')).toBeVisible()
|
await expect(page.getByTestId('contribution-amount')).toBeVisible()
|
||||||
await page.locator('#accept-contribution').fill('10')
|
await page.getByTestId('cadence-monthly').click()
|
||||||
|
await page.getByTestId('contribution-amount').fill('10')
|
||||||
|
|
||||||
await page.locator('label[for="accept-cadence-monthly"]').click()
|
const summary = page.locator('.billing-summary')
|
||||||
await expect(page.locator('.billing-summary')).toContainText('$10 today')
|
await expect(summary).toContainText('$10 today')
|
||||||
|
await expect(summary).toContainText('each month')
|
||||||
|
|
||||||
await page.locator('label[for="accept-cadence-annual"]').click()
|
await page.getByTestId('cadence-annual').click()
|
||||||
await expect(page.locator('.billing-summary')).toContainText('$120 today')
|
await expect(summary).toContainText('$120 today')
|
||||||
await expect(page.locator('.billing-summary')).toContainText('$10/month')
|
await expect(summary).toContainText('at each annual renewal')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('preset chip sets contribution amount', async ({ page }) => {
|
test('preset chip sets contribution amount', async ({ page }) => {
|
||||||
|
|
@ -131,7 +133,7 @@ test.describe('Accept Invite — pre-registrant signup', () => {
|
||||||
const expected = chipText.replace(/[^0-9]/g, '')
|
const expected = chipText.replace(/[^0-9]/g, '')
|
||||||
|
|
||||||
await chip.click()
|
await chip.click()
|
||||||
await expect(page.locator('#accept-contribution')).toHaveValue(expected)
|
await expect(page.getByTestId('contribution-amount')).toHaveValue(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('free tier happy path shows welcome state', async ({ page }) => {
|
test('free tier happy path shows welcome state', async ({ page }) => {
|
||||||
|
|
@ -141,7 +143,7 @@ test.describe('Accept Invite — pre-registrant signup', () => {
|
||||||
|
|
||||||
await expect(page.locator('#accept-name')).toHaveValue('Free Tester')
|
await expect(page.locator('#accept-name')).toHaveValue('Free Tester')
|
||||||
await page.locator('#circle-community').check({ force: true })
|
await page.locator('#circle-community').check({ force: true })
|
||||||
await page.locator('#accept-contribution').fill('0')
|
await page.getByTestId('contribution-amount').fill('0')
|
||||||
await page.getByRole('checkbox', { name: /Community Guidelines/ }).check()
|
await page.getByRole('checkbox', { name: /Community Guidelines/ }).check()
|
||||||
|
|
||||||
await expect(page.locator('.form-submit')).toBeEnabled()
|
await expect(page.locator('.form-submit')).toBeEnabled()
|
||||||
|
|
@ -158,7 +160,7 @@ test.describe('Accept Invite — pre-registrant signup', () => {
|
||||||
await mockVerifyOk(page)
|
await mockVerifyOk(page)
|
||||||
await gotoAcceptInvite(page)
|
await gotoAcceptInvite(page)
|
||||||
|
|
||||||
await page.locator('#accept-contribution').fill('10')
|
await page.getByTestId('contribution-amount').fill('10')
|
||||||
await page.getByRole('checkbox', { name: /Community Guidelines/ }).check()
|
await page.getByRole('checkbox', { name: /Community Guidelines/ }).check()
|
||||||
await expect(page.locator('.form-submit')).toContainText(/Continue to Payment/)
|
await expect(page.locator('.form-submit')).toContainText(/Continue to Payment/)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue