additional stripe changes and Billing info cleanup
This commit is contained in:
+101
-45
@@ -4,6 +4,7 @@ import { findParrotWeightReference, parrotSpeciesOptions, type ParrotWeightRefer
|
||||
|
||||
type BillingPlan = 'rescue_free' | 'household_basic' | 'household_plus' | 'household_macaw';
|
||||
type HouseholdBillingPlan = Exclude<BillingPlan, 'rescue_free'>;
|
||||
type BillingInterval = 'monthly' | 'yearly';
|
||||
type WorkspaceType = 'standard' | 'rescue';
|
||||
type WorkspaceRole = 'owner' | 'assistant' | 'caregiver' | 'viewer';
|
||||
type SubscriptionStatus = 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid' | 'none';
|
||||
@@ -52,6 +53,7 @@ type Workspace = {
|
||||
workspaceType: WorkspaceType;
|
||||
billingEmail: string | null;
|
||||
billingPlan: BillingPlan;
|
||||
billingInterval: BillingInterval;
|
||||
subscriptionStatus: SubscriptionStatus;
|
||||
stripeCustomerId: string | null;
|
||||
stripeSubscriptionId: string | null;
|
||||
@@ -152,6 +154,7 @@ type WorkspaceFormState = {
|
||||
workspaceType: WorkspaceType;
|
||||
billingEmail: string;
|
||||
billingPlan: HouseholdBillingPlan;
|
||||
billingInterval: BillingInterval;
|
||||
};
|
||||
|
||||
type WorkspaceMemberFormState = {
|
||||
@@ -165,6 +168,7 @@ type WorkspaceCreateFormState = {
|
||||
workspaceType: WorkspaceType;
|
||||
billingEmail: string;
|
||||
billingPlan: HouseholdBillingPlan;
|
||||
billingInterval: BillingInterval;
|
||||
};
|
||||
|
||||
type AuthFormState = {
|
||||
@@ -247,6 +251,7 @@ const emptyWorkspaceForm: WorkspaceFormState = {
|
||||
workspaceType: 'standard',
|
||||
billingEmail: '',
|
||||
billingPlan: 'household_basic',
|
||||
billingInterval: 'monthly',
|
||||
};
|
||||
|
||||
const emptyWorkspaceMemberForm: WorkspaceMemberFormState = {
|
||||
@@ -260,6 +265,7 @@ const emptyWorkspaceCreateForm: WorkspaceCreateFormState = {
|
||||
workspaceType: 'standard',
|
||||
billingEmail: '',
|
||||
billingPlan: 'household_basic',
|
||||
billingInterval: 'monthly',
|
||||
};
|
||||
|
||||
const emptyAuthForm: AuthFormState = {
|
||||
@@ -481,6 +487,8 @@ const oauthStartUrl = (providerKey: AuthProvider['providerKey']) => {
|
||||
const isHouseholdPlan = (billingPlan: BillingPlan): billingPlan is HouseholdBillingPlan =>
|
||||
billingPlan === 'household_basic' || billingPlan === 'household_plus' || billingPlan === 'household_macaw';
|
||||
|
||||
const formatBillingIntervalName = (billingInterval: BillingInterval) => (billingInterval === 'yearly' ? 'Annual' : 'Monthly');
|
||||
|
||||
const formatBillingPlanName = (billingPlan: BillingPlan) => {
|
||||
if (billingPlan === 'rescue_free') {
|
||||
return 'Rescue Free';
|
||||
@@ -548,6 +556,15 @@ const formatSubscriptionStatus = (status: SubscriptionStatus) => {
|
||||
return 'Active';
|
||||
};
|
||||
|
||||
const subscriptionAllowsFlockWrites = (status: SubscriptionStatus) => status === 'active' || status === 'trialing';
|
||||
|
||||
const formatFlockAccessStatus = (status: SubscriptionStatus) => (subscriptionAllowsFlockWrites(status) ? formatSubscriptionStatus(status) : 'Read-only');
|
||||
|
||||
const formatFlockAccessDescription = (status: SubscriptionStatus) =>
|
||||
subscriptionAllowsFlockWrites(status)
|
||||
? 'This flock is writable while the subscription is active.'
|
||||
: `This flock is read-only until billing is restored. Current subscription status: ${formatSubscriptionStatus(status)}.`;
|
||||
|
||||
const formatRescueVerificationStatus = (status: RescueVerificationStatus) => {
|
||||
if (status === 'approved') {
|
||||
return 'Active';
|
||||
@@ -1122,6 +1139,7 @@ function App() {
|
||||
workspaceType: session.activeWorkspace.workspaceType,
|
||||
billingEmail: session.activeWorkspace.billingEmail ?? '',
|
||||
billingPlan: isHouseholdPlan(session.activeWorkspace.billingPlan) ? session.activeWorkspace.billingPlan : 'household_basic',
|
||||
billingInterval: session.activeWorkspace.billingInterval,
|
||||
});
|
||||
setWorkspaceCreateForm((current) => ({
|
||||
...current,
|
||||
@@ -1626,6 +1644,7 @@ function App() {
|
||||
workspaceType: workspaceCreateForm.workspaceType,
|
||||
billingEmail: workspaceCreateForm.billingEmail,
|
||||
billingPlan: workspaceCreateForm.workspaceType === 'rescue' ? undefined : workspaceCreateForm.billingPlan,
|
||||
billingInterval: workspaceCreateForm.workspaceType === 'rescue' ? undefined : workspaceCreateForm.billingInterval,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2216,6 +2235,7 @@ function App() {
|
||||
body: JSON.stringify({
|
||||
...workspaceForm,
|
||||
billingPlan: workspaceForm.workspaceType === 'rescue' ? undefined : workspaceForm.billingPlan,
|
||||
billingInterval: workspaceForm.workspaceType === 'rescue' ? undefined : workspaceForm.billingInterval,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2248,6 +2268,7 @@ function App() {
|
||||
workspaceType: savedWorkspace.workspaceType,
|
||||
billingEmail: savedWorkspace.billingEmail ?? '',
|
||||
billingPlan: isHouseholdPlan(savedWorkspace.billingPlan) ? savedWorkspace.billingPlan : 'household_basic',
|
||||
billingInterval: savedWorkspace.billingInterval,
|
||||
});
|
||||
} catch (workspaceError) {
|
||||
setError(workspaceError instanceof Error ? workspaceError.message : 'Unable to save flock settings.');
|
||||
@@ -2339,6 +2360,7 @@ function App() {
|
||||
workspaceType: savedWorkspace.workspaceType,
|
||||
billingEmail: savedWorkspace.billingEmail ?? '',
|
||||
billingPlan: isHouseholdPlan(savedWorkspace.billingPlan) ? savedWorkspace.billingPlan : 'household_basic',
|
||||
billingInterval: savedWorkspace.billingInterval,
|
||||
});
|
||||
} catch (workspaceError) {
|
||||
setError(workspaceError instanceof Error ? workspaceError.message : 'Unable to cancel rescue status request.');
|
||||
@@ -2359,7 +2381,7 @@ function App() {
|
||||
const response = await apiFetch('/billing/checkout-session', authToken, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ billingPlan: workspace.billingPlan }),
|
||||
body: JSON.stringify({ billingPlan: workspace.billingPlan, billingInterval: workspace.billingInterval }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -3291,12 +3313,11 @@ function App() {
|
||||
<div className="panel-header">
|
||||
<div>
|
||||
<p className="eyebrow">Flock</p>
|
||||
<h2>Flock profile and billing</h2>
|
||||
<h2>Flock profile</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p className="muted">
|
||||
Each flock carries its own billing and collaboration rules. That lets one person keep a personal household flock while also
|
||||
participating in a rescue flock without mixing billing or bird ownership.
|
||||
Manage this flock's name and type. Household billing details live in the Billing info card below.
|
||||
</p>
|
||||
<form className="form-panel" onSubmit={handleWorkspaceSubmit}>
|
||||
<label>
|
||||
@@ -3327,44 +3348,12 @@ function App() {
|
||||
</span>
|
||||
</article>
|
||||
) : null}
|
||||
{workspaceForm.workspaceType === 'standard' ? (
|
||||
<>
|
||||
<label>
|
||||
Household plan
|
||||
<select
|
||||
value={workspaceForm.billingPlan}
|
||||
onChange={(event) =>
|
||||
setWorkspaceForm({
|
||||
...workspaceForm,
|
||||
billingPlan: event.target.value as WorkspaceFormState['billingPlan'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="household_basic">Conure</option>
|
||||
<option value="household_plus">Indian Ringneck</option>
|
||||
<option value="household_macaw">Macaw</option>
|
||||
</select>
|
||||
</label>
|
||||
<article className="summary-card">
|
||||
<strong>{formatBillingPlanName(workspaceForm.billingPlan)}</strong>
|
||||
<span>{formatBillingPlanCapacity(workspaceForm.billingPlan)}</span>
|
||||
</article>
|
||||
</>
|
||||
) : (
|
||||
{workspaceForm.workspaceType === 'rescue' ? (
|
||||
<article className="summary-card">
|
||||
<strong>{formatBillingPlanName('rescue_free')}</strong>
|
||||
<span>Rescue flocks stay free while still supporting shared team access.</span>
|
||||
</article>
|
||||
)}
|
||||
<label>
|
||||
Billing contact email
|
||||
<input
|
||||
type="email"
|
||||
value={workspaceForm.billingEmail}
|
||||
onChange={(event) => setWorkspaceForm({ ...workspaceForm, billingEmail: event.target.value })}
|
||||
placeholder="Optional for later billing and account management"
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
<button className="primary-button" type="submit" disabled={savingWorkspace}>
|
||||
{savingWorkspace
|
||||
? 'Saving flock...'
|
||||
@@ -3390,15 +3379,64 @@ function App() {
|
||||
<h2>Billing info</h2>
|
||||
</div>
|
||||
</div>
|
||||
{workspace?.workspaceType !== 'rescue' ? (
|
||||
<form className="form-panel" onSubmit={handleWorkspaceSubmit}>
|
||||
<label>
|
||||
Household plan
|
||||
<select
|
||||
value={workspaceForm.billingPlan}
|
||||
onChange={(event) =>
|
||||
setWorkspaceForm({
|
||||
...workspaceForm,
|
||||
billingPlan: event.target.value as WorkspaceFormState['billingPlan'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="household_basic">Conure - up to 4 birds</option>
|
||||
<option value="household_plus">Indian Ringneck - up to 10 birds</option>
|
||||
<option value="household_macaw">Macaw - 11+ birds</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Billing frequency
|
||||
<select
|
||||
value={workspaceForm.billingInterval}
|
||||
onChange={(event) =>
|
||||
setWorkspaceForm({
|
||||
...workspaceForm,
|
||||
billingInterval: event.target.value as WorkspaceFormState['billingInterval'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Annual</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Billing contact email
|
||||
<input
|
||||
type="email"
|
||||
value={workspaceForm.billingEmail}
|
||||
onChange={(event) => setWorkspaceForm({ ...workspaceForm, billingEmail: event.target.value })}
|
||||
placeholder="Optional for billing and account management"
|
||||
/>
|
||||
</label>
|
||||
<button className="primary-button" type="submit" disabled={savingWorkspace}>
|
||||
{savingWorkspace ? 'Saving billing...' : 'Save billing settings'}
|
||||
</button>
|
||||
</form>
|
||||
) : null}
|
||||
<div className="summary-grid">
|
||||
<article className="summary-card">
|
||||
<strong>{workspace ? formatBillingPlanName(workspace.billingPlan) : 'No plan yet'}</strong>
|
||||
<strong>
|
||||
{workspace ? `${formatBillingPlanName(workspace.billingPlan)} • ${formatBillingIntervalName(workspace.billingInterval)}` : 'No plan yet'}
|
||||
</strong>
|
||||
<span>{workspace ? formatBillingPlanCapacity(workspace.billingPlan) : 'Pick a flock plan to see bird capacity.'}</span>
|
||||
</article>
|
||||
{workspace?.workspaceType !== 'rescue' ? (
|
||||
<article className="summary-card">
|
||||
<strong>{workspace ? formatSubscriptionStatus(workspace.subscriptionStatus) : 'Unknown'}</strong>
|
||||
<span>Flock write access will follow subscription health once billing is connected.</span>
|
||||
<strong>{workspace ? formatFlockAccessStatus(workspace.subscriptionStatus) : 'Unknown'}</strong>
|
||||
<span>{workspace ? formatFlockAccessDescription(workspace.subscriptionStatus) : 'Subscription status controls flock write access.'}</span>
|
||||
</article>
|
||||
) : null}
|
||||
{workspace?.workspaceType === 'rescue' ? (
|
||||
@@ -3725,13 +3763,31 @@ function App() {
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="household_basic">Conure</option>
|
||||
<option value="household_plus">Indian Ringneck</option>
|
||||
<option value="household_macaw">Macaw</option>
|
||||
<option value="household_basic">Conure - up to 4 birds</option>
|
||||
<option value="household_plus">Indian Ringneck - up to 10 birds</option>
|
||||
<option value="household_macaw">Macaw - 11+ birds</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Billing frequency
|
||||
<select
|
||||
value={workspaceCreateForm.billingInterval}
|
||||
onChange={(event) =>
|
||||
setWorkspaceCreateForm({
|
||||
...workspaceCreateForm,
|
||||
billingInterval: event.target.value as WorkspaceCreateFormState['billingInterval'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Annual</option>
|
||||
</select>
|
||||
</label>
|
||||
<article className="summary-card">
|
||||
<strong>{formatBillingPlanName(workspaceCreateForm.billingPlan)}</strong>
|
||||
<strong>
|
||||
{formatBillingPlanName(workspaceCreateForm.billingPlan)} •{' '}
|
||||
{formatBillingIntervalName(workspaceCreateForm.billingInterval)}
|
||||
</strong>
|
||||
<span>{formatBillingPlanCapacity(workspaceCreateForm.billingPlan)}</span>
|
||||
</article>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user