feat(member): account/profile polish + tier upgrade flow

- Timezone: curated USelectMenu dropdown (app/config/timezones.js), preserves unknown saved values
- Profile save now uses useToast() for success/error; remove inline save banner
- Nav onboarding dot nudged down 1px for optical alignment with lowercase text
- Onboarding: skip a suggestion with POST /api/onboarding/track {skip}; member.onboarding.skipped map; does not affect graduation
- CirclePicker takes :saved-value so 'Current' badge stays until save completes
- PrivacyToggle is binary (USwitch labeled Private); member schema enum reduced to ['members','private']; zod coerces legacy 'public'
- New /member/payment-setup page: HelcimPay $0 verify + update-contribution, wired from account.vue via requiresPaymentSetup redirect
- Helcim portal: NUXT_PUBLIC_HELCIM_PORTAL_URL env + account.vue 'Manage billing in Helcim' link
- Migration script: scripts/migrate-privacy-public-to-members.js
This commit is contained in:
Jennie Robinson Faber 2026-04-14 20:35:37 +01:00
parent 08fc3884da
commit 7292b11c0b
18 changed files with 604 additions and 122 deletions

View file

@ -68,6 +68,15 @@
}}</span>
</div>
</div>
<a
v-if="helcimPortalUrl && memberData.helcimCustomerId"
:href="helcimPortalUrl"
target="_blank"
rel="noopener"
class="billing-link"
>
Manage billing in Helcim &rarr;
</a>
</PageSection>
<PageSection divider="top">
@ -178,6 +187,7 @@
<CirclePicker
v-model="selectedCircle"
:saved-value="memberData.circle"
:circles="circleOptions"
/>
<button
@ -204,6 +214,7 @@ definePageMeta({
const { memberData, checkMemberStatus } = useAuth();
const { openLoginModal } = useLoginModal();
const toast = useToast();
const helcimPortalUrl = useRuntimeConfig().public.helcimPortalUrl || '';
const selectedTier = ref(0);
const selectedCircle = ref("");
@ -276,13 +287,22 @@ const handleUpdateTier = async () => {
body: { contributionTier: String(selectedTier.value) },
});
await checkMemberStatus();
toast.add({ title: "Contribution updated", color: "green" });
toast.add({ title: "Contribution updated", color: "success" });
} catch (err) {
// Paid upgrade without a saved card route to payment setup instead of erroring.
if (err.data?.data?.requiresPaymentSetup) {
await navigateTo(
`/member/payment-setup?tier=${selectedTier.value}&circle=${
selectedCircle.value || memberData.value?.circle || 'community'
}`,
);
return;
}
selectedTier.value = Number(memberData.value?.contributionTier || 0);
toast.add({
title: "Update failed",
description: err.data?.statusMessage || "Please try again.",
color: "red",
color: "error",
});
} finally {
isUpdating.value = false;
@ -297,13 +317,13 @@ const handleUpdateCircle = async () => {
body: { circle: selectedCircle.value },
});
await checkMemberStatus();
toast.add({ title: "Circle updated", color: "green" });
toast.add({ title: "Circle updated", color: "success" });
} catch (err) {
selectedCircle.value = memberData.value?.circle || "community";
toast.add({
title: "Update failed",
description: err.data?.statusMessage || "Please try again.",
color: "red",
color: "error",
});
} finally {
isUpdating.value = false;
@ -326,12 +346,12 @@ const handleUpdateEmail = async () => {
});
await checkMemberStatus();
cancelEmailEdit();
toast.add({ title: "Email updated", color: "green" });
toast.add({ title: "Email updated", color: "success" });
} catch (err) {
toast.add({
title: "Update failed",
description: err.data?.statusMessage || "Please try again.",
color: "red",
color: "error",
});
} finally {
isUpdatingEmail.value = false;
@ -359,13 +379,13 @@ const confirmCancelMembership = async () => {
color: "neutral",
});
} else {
toast.add({ title: "Membership cancelled", color: "orange" });
toast.add({ title: "Membership cancelled", color: "warning" });
}
} catch (err) {
toast.add({
title: "Cancellation failed",
description: err.data?.statusMessage || "Please try again.",
color: "red",
color: "error",
});
} finally {
isCancelling.value = false;
@ -528,4 +548,16 @@ const confirmCancelMembership = async () => {
text-align: center;
}
.billing-link {
display: inline-block;
margin-top: 10px;
font-size: 12px;
color: var(--candle);
text-decoration: none;
}
.billing-link:hover {
text-decoration: underline;
}
</style>