feat(member): pending_payment retains access, soften status copy
pending_payment now grants the same RSVP/peer-support capabilities as active,
and status banner/label copy is rewritten to be non-threatening ("Setting up
payment", "Paused", "Closed"). Aligns member-facing copy across the account
page with the capability model.
This commit is contained in:
parent
15329e3e84
commit
37a58cb0eb
3 changed files with 43 additions and 43 deletions
|
|
@ -12,16 +12,16 @@ export const MEMBER_STATUSES = {
|
||||||
|
|
||||||
export const MEMBER_STATUS_CONFIG = {
|
export const MEMBER_STATUS_CONFIG = {
|
||||||
pending_payment: {
|
pending_payment: {
|
||||||
label: "Payment Pending",
|
label: "Setting up payment",
|
||||||
color: "orange",
|
color: "orange",
|
||||||
bgColor: "bg-orange-500/10",
|
bgColor: "bg-orange-500/10",
|
||||||
borderColor: "border-orange-500/30",
|
borderColor: "border-orange-500/30",
|
||||||
textColor: "text-orange-300",
|
textColor: "text-orange-300",
|
||||||
icon: "heroicons:exclamation-triangle",
|
icon: "heroicons:exclamation-triangle",
|
||||||
severity: "warning",
|
severity: "warning",
|
||||||
canRSVP: false,
|
canRSVP: true,
|
||||||
canAccessMembers: true,
|
canAccessMembers: true,
|
||||||
canPeerSupport: false,
|
canPeerSupport: true,
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
label: "Active Member",
|
label: "Active Member",
|
||||||
|
|
@ -126,24 +126,21 @@ export const useMemberStatus = () => {
|
||||||
// Get banner message based on status
|
// Get banner message based on status
|
||||||
const getBannerMessage = () => {
|
const getBannerMessage = () => {
|
||||||
if (isPendingPayment.value) {
|
if (isPendingPayment.value) {
|
||||||
return "Your membership is pending payment. Please complete your payment to unlock full features.";
|
return "Your payment setup isn't finished yet. Your membership and access aren't affected — finish whenever you're ready, or reach out if there's a snag.";
|
||||||
}
|
}
|
||||||
if (isSuspended.value) {
|
if (isSuspended.value) {
|
||||||
return "Your membership has been suspended. Please contact support to reactivate your account.";
|
return "Your account is paused while we work through a community issue. We'll be in touch.";
|
||||||
}
|
}
|
||||||
if (isCancelled.value) {
|
if (isCancelled.value) {
|
||||||
return "Your membership has been cancelled. Would you like to reactivate?";
|
return "Your account is closed. Reach out if you'd like to come back.";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get RSVP restriction message
|
// Get RSVP restriction message
|
||||||
const getRSVPMessage = () => {
|
const getRSVPMessage = () => {
|
||||||
if (isPendingPayment.value) {
|
|
||||||
return "Complete your payment to register for events";
|
|
||||||
}
|
|
||||||
if (isSuspended.value || isCancelled.value) {
|
if (isSuspended.value || isCancelled.value) {
|
||||||
return "Your membership status prevents RSVP. Please reactivate your account.";
|
return "Your account isn't active right now. Reach out if you have questions.";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
<span
|
<span
|
||||||
class="status-dot"
|
class="status-dot"
|
||||||
:class="memberData.status || 'active'"
|
:class="memberData.status || 'active'"
|
||||||
></span>
|
/>
|
||||||
<span>{{
|
<span>{{
|
||||||
formatStatus(memberData.status || "active")
|
formatStatus(memberData.status || "active")
|
||||||
}}</span>
|
}}</span>
|
||||||
|
|
@ -93,26 +93,26 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>New email address</label>
|
<label>New email address</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
|
||||||
v-model="newEmail"
|
v-model="newEmail"
|
||||||
|
type="email"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
|
autofocus
|
||||||
@keydown.enter="handleUpdateEmail"
|
@keydown.enter="handleUpdateEmail"
|
||||||
@keydown.escape="cancelEmailEdit"
|
@keydown.escape="cancelEmailEdit"
|
||||||
autofocus
|
>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="email-edit-actions">
|
<div class="email-edit-actions">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
@click="handleUpdateEmail"
|
|
||||||
:disabled="isUpdatingEmail || !newEmail.trim()"
|
:disabled="isUpdatingEmail || !newEmail.trim()"
|
||||||
|
@click="handleUpdateEmail"
|
||||||
>
|
>
|
||||||
{{ isUpdatingEmail ? "Saving…" : "Save" }}
|
{{ isUpdatingEmail ? "Saving…" : "Save" }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn"
|
||||||
@click="cancelEmailEdit"
|
|
||||||
:disabled="isUpdatingEmail"
|
:disabled="isUpdatingEmail"
|
||||||
|
@click="cancelEmailEdit"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -128,9 +128,12 @@
|
||||||
<div class="section-label danger">Danger Zone</div>
|
<div class="section-label danger">Danger Zone</div>
|
||||||
<div class="danger-zone">
|
<div class="danger-zone">
|
||||||
<p>
|
<p>
|
||||||
Cancelling your membership will immediately revoke access to
|
Cancelling closes your account and ends access to member-only
|
||||||
member-only resources, events, and the Slack workspace.
|
spaces, including Slack. If you're cancelling because of a
|
||||||
<strong>This action cannot be easily undone.</strong>
|
money issue, the
|
||||||
|
<NuxtLink to="/community-guidelines">Solidarity Fund</NuxtLink>
|
||||||
|
and the $0 tier are always available — reach out before you
|
||||||
|
go.
|
||||||
</p>
|
</p>
|
||||||
<div v-if="showCancelConfirm" class="cancel-confirm">
|
<div v-if="showCancelConfirm" class="cancel-confirm">
|
||||||
<p class="cancel-confirm-prompt">
|
<p class="cancel-confirm-prompt">
|
||||||
|
|
@ -139,8 +142,8 @@
|
||||||
<div class="cancel-confirm-actions">
|
<div class="cancel-confirm-actions">
|
||||||
<button
|
<button
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
@click="confirmCancelMembership"
|
|
||||||
:disabled="isCancelling"
|
:disabled="isCancelling"
|
||||||
|
@click="confirmCancelMembership"
|
||||||
>
|
>
|
||||||
{{ isCancelling ? "Cancelling…" : "Yes, Cancel" }}
|
{{ isCancelling ? "Cancelling…" : "Yes, Cancel" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -152,8 +155,8 @@
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
@click="handleCancelMembership"
|
|
||||||
:disabled="isCancelling"
|
:disabled="isCancelling"
|
||||||
|
@click="handleCancelMembership"
|
||||||
>
|
>
|
||||||
Cancel Membership
|
Cancel Membership
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -172,11 +175,11 @@
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-section"
|
class="btn btn-primary btn-section"
|
||||||
@click="handleUpdateTier"
|
|
||||||
:disabled="
|
:disabled="
|
||||||
selectedTier === Number(memberData.contributionTier || 0) ||
|
selectedTier === Number(memberData.contributionTier || 0) ||
|
||||||
isUpdating
|
isUpdating
|
||||||
"
|
"
|
||||||
|
@click="handleUpdateTier"
|
||||||
>
|
>
|
||||||
{{ isUpdating ? "Updating…" : "Update Contribution" }}
|
{{ isUpdating ? "Updating…" : "Update Contribution" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -192,8 +195,8 @@
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-section"
|
class="btn btn-primary btn-section"
|
||||||
@click="handleUpdateCircle"
|
|
||||||
:disabled="selectedCircle === memberData.circle || isUpdating"
|
:disabled="selectedCircle === memberData.circle || isUpdating"
|
||||||
|
@click="handleUpdateCircle"
|
||||||
>
|
>
|
||||||
{{ isUpdating ? "Updating…" : "Update Circle" }}
|
{{ isUpdating ? "Updating…" : "Update Circle" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -254,9 +257,9 @@ const circleOptions = [
|
||||||
|
|
||||||
const STATUS_LABELS = {
|
const STATUS_LABELS = {
|
||||||
active: "Active",
|
active: "Active",
|
||||||
pending_payment: "Pending",
|
pending_payment: "Setting up payment",
|
||||||
suspended: "Suspended",
|
suspended: "Paused",
|
||||||
cancelled: "Cancelled",
|
cancelled: "Closed",
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatStatus = (s) => STATUS_LABELS[s] || s;
|
const formatStatus = (s) => STATUS_LABELS[s] || s;
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,11 @@ describe("MEMBER_STATUS_CONFIG", () => {
|
||||||
expect(cfg.canPeerSupport).toBe(true);
|
expect(cfg.canPeerSupport).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pending_payment can access members but not RSVP or peer support", () => {
|
it("pending_payment has full permissions (payment is decoupled from access)", () => {
|
||||||
const cfg = MEMBER_STATUS_CONFIG.pending_payment;
|
const cfg = MEMBER_STATUS_CONFIG.pending_payment;
|
||||||
expect(cfg.canRSVP).toBe(false);
|
expect(cfg.canRSVP).toBe(true);
|
||||||
expect(cfg.canAccessMembers).toBe(true);
|
expect(cfg.canAccessMembers).toBe(true);
|
||||||
expect(cfg.canPeerSupport).toBe(false);
|
expect(cfg.canPeerSupport).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("suspended has all permissions false", () => {
|
it("suspended has all permissions false", () => {
|
||||||
|
|
@ -122,10 +122,10 @@ describe("useMemberStatus composable", () => {
|
||||||
expect(canRSVP.value).toBe(true);
|
expect(canRSVP.value).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("canRSVP is false when pending_payment", () => {
|
it("canRSVP is true when pending_payment (decoupled from payment)", () => {
|
||||||
memberData.value = { status: "pending_payment" };
|
memberData.value = { status: "pending_payment" };
|
||||||
const { canRSVP } = useMemberStatus();
|
const { canRSVP } = useMemberStatus();
|
||||||
expect(canRSVP.value).toBe(false);
|
expect(canRSVP.value).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("canAccessMembers is true for active and pending_payment", () => {
|
it("canAccessMembers is true for active and pending_payment", () => {
|
||||||
|
|
@ -182,22 +182,22 @@ describe("useMemberStatus composable", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getBannerMessage", () => {
|
describe("getBannerMessage", () => {
|
||||||
it("returns payment message for pending_payment", () => {
|
it("returns payment-setup message for pending_payment", () => {
|
||||||
memberData.value = { status: "pending_payment" };
|
memberData.value = { status: "pending_payment" };
|
||||||
const { getBannerMessage } = useMemberStatus();
|
const { getBannerMessage } = useMemberStatus();
|
||||||
expect(getBannerMessage()).toContain("pending payment");
|
expect(getBannerMessage()).toContain("payment setup");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns suspended message for suspended", () => {
|
it("returns paused message for suspended", () => {
|
||||||
memberData.value = { status: "suspended" };
|
memberData.value = { status: "suspended" };
|
||||||
const { getBannerMessage } = useMemberStatus();
|
const { getBannerMessage } = useMemberStatus();
|
||||||
expect(getBannerMessage()).toContain("suspended");
|
expect(getBannerMessage()).toContain("paused");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns cancelled message for cancelled", () => {
|
it("returns closed message for cancelled", () => {
|
||||||
memberData.value = { status: "cancelled" };
|
memberData.value = { status: "cancelled" };
|
||||||
const { getBannerMessage } = useMemberStatus();
|
const { getBannerMessage } = useMemberStatus();
|
||||||
expect(getBannerMessage()).toContain("cancelled");
|
expect(getBannerMessage()).toContain("closed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null for active", () => {
|
it("returns null for active", () => {
|
||||||
|
|
@ -208,22 +208,22 @@ describe("useMemberStatus composable", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getRSVPMessage", () => {
|
describe("getRSVPMessage", () => {
|
||||||
it("returns payment message for pending_payment", () => {
|
it("returns null for pending_payment (decoupled from RSVP)", () => {
|
||||||
memberData.value = { status: "pending_payment" };
|
memberData.value = { status: "pending_payment" };
|
||||||
const { getRSVPMessage } = useMemberStatus();
|
const { getRSVPMessage } = useMemberStatus();
|
||||||
expect(getRSVPMessage()).toContain("payment");
|
expect(getRSVPMessage()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns restriction message for suspended", () => {
|
it("returns inactive-account message for suspended", () => {
|
||||||
memberData.value = { status: "suspended" };
|
memberData.value = { status: "suspended" };
|
||||||
const { getRSVPMessage } = useMemberStatus();
|
const { getRSVPMessage } = useMemberStatus();
|
||||||
expect(getRSVPMessage()).toContain("reactivate");
|
expect(getRSVPMessage()).toContain("isn't active");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns restriction message for cancelled", () => {
|
it("returns inactive-account message for cancelled", () => {
|
||||||
memberData.value = { status: "cancelled" };
|
memberData.value = { status: "cancelled" };
|
||||||
const { getRSVPMessage } = useMemberStatus();
|
const { getRSVPMessage } = useMemberStatus();
|
||||||
expect(getRSVPMessage()).toContain("reactivate");
|
expect(getRSVPMessage()).toContain("isn't active");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null for active", () => {
|
it("returns null for active", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue