258 lines
12 KiB
Plaintext
258 lines
12 KiB
Plaintext
@model ManageSubscriptionViewModel
|
|
@{
|
|
ViewData["Title"] = "Subscription";
|
|
var subscription = Model.CurrentSubscription;
|
|
var usage = Model.Usage;
|
|
}
|
|
|
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
|
<a asp-controller="ManageAccount" asp-action="Index">Manage Account</a>
|
|
<span>Subscription</span>
|
|
</nav>
|
|
|
|
<div class="page-heading compact">
|
|
<div>
|
|
<p class="eyebrow">Account</p>
|
|
<h1>Subscription</h1>
|
|
<p class="lead-text">@Model.User.Email</p>
|
|
</div>
|
|
<div class="button-row">
|
|
<a class="btn btn-outline-secondary" asp-action="Index">Back to account</a>
|
|
</div>
|
|
</div>
|
|
|
|
@if (TempData["SubscriptionMessage"] is string subscriptionMessage)
|
|
{
|
|
<div class="alert alert-info">@subscriptionMessage</div>
|
|
}
|
|
|
|
<section class="edit-panel">
|
|
<h2>Current subscription</h2>
|
|
@if (subscription is null)
|
|
{
|
|
<p class="muted">No active subscription is assigned.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<p class="eyebrow">Plan</p>
|
|
<p>@subscription.DisplayName</p>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<p class="eyebrow">Description</p>
|
|
<p>@(string.IsNullOrWhiteSpace(subscription.Description) ? "No description available." : subscription.Description)</p>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<p class="eyebrow">Status</p>
|
|
<p>@(string.IsNullOrWhiteSpace(subscription.StripeStatus) ? (subscription.IsActive ? "Active" : "Inactive") : subscription.StripeStatus)</p>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<p class="eyebrow">Billing interval</p>
|
|
<p>@(string.IsNullOrWhiteSpace(subscription.BillingInterval) ? "Not set" : subscription.BillingInterval)</p>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<p class="eyebrow">Trial</p>
|
|
<p>@(subscription.IsTrial ? "Trial" : "No")</p>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<p class="eyebrow">Trial expiry</p>
|
|
<p>@FormatDate(subscription.TrialExpiryDateUTC)</p>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<p class="eyebrow">Started</p>
|
|
<p>@FormatDate(subscription.StartDateUTC)</p>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<p class="eyebrow">Ends</p>
|
|
<p>@FormatDate(subscription.EndDateUTC)</p>
|
|
</div>
|
|
</div>
|
|
|
|
@if (subscription.CancelAtPeriodEnd)
|
|
{
|
|
<div class="alert alert-warning mt-3">This subscription is scheduled to cancel at the end of the current billing period.</div>
|
|
}
|
|
@if (subscription.LastPaymentFailureDateUTC.HasValue)
|
|
{
|
|
<div class="alert alert-warning mt-3">Stripe has reported a payment issue for this subscription.</div>
|
|
}
|
|
|
|
<div class="button-row mt-3">
|
|
@if (!string.IsNullOrWhiteSpace(subscription.StripeCustomerId))
|
|
{
|
|
<form asp-action="ManageBilling" method="post">
|
|
<button class="btn btn-outline-secondary" type="submit">Manage Billing</button>
|
|
</form>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(subscription.StripeSubscriptionId) && !subscription.CancelAtPeriodEnd)
|
|
{
|
|
<form asp-action="CancelSubscription" method="post" data-confirm-message="Cancel this subscription at the end of the current billing period? Your existing data will remain available.">
|
|
<button class="btn btn-outline-danger" type="submit">Cancel Subscription</button>
|
|
</form>
|
|
}
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
<section class="edit-panel">
|
|
<h2>Usage</h2>
|
|
@if (subscription is null || usage is null)
|
|
{
|
|
<p class="muted">Usage is available after a subscription is assigned.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="table-responsive">
|
|
<table class="table align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th>Limit</th>
|
|
<th>Usage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Books</td>
|
|
<td>@FormatUsage(usage.BooksUsed, subscription.MaxBooks, "books")</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Storage</td>
|
|
<td>@FormatStorageUsage(usage.StorageUsedBytes, usage.StorageMaximumBytes)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Collaborators</td>
|
|
<td>@FormatUsage(usage.CollaboratorsUsed, subscription.MaxCollaborators, "collaborators")</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Highest chapters in a book</td>
|
|
<td>@FormatUsage(usage.HighestChaptersInBook, subscription.MaxChaptersPerBook, "chapters")</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Highest scenes in a book</td>
|
|
<td>@FormatUsage(usage.HighestScenesInBook, subscription.MaxScenesPerBook, "scenes")</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Highest characters in a project</td>
|
|
<td>@FormatUsage(usage.HighestCharactersInProject, subscription.MaxCharactersPerProject, "characters")</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
<section class="list-section">
|
|
<h2>Available plans</h2>
|
|
@if (!Model.AvailableLevels.Any())
|
|
{
|
|
<p class="muted">No active subscription levels are available.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="content-grid">
|
|
@foreach (var level in Model.AvailableLevels)
|
|
{
|
|
var isCurrent = subscription is not null && level.SubscriptionLevelID == subscription.SubscriptionLevelID;
|
|
<article class="plain-card">
|
|
<h3>@level.DisplayName</h3>
|
|
<p class="eyebrow">@(isCurrent ? "Current plan" : "Available plan")</p>
|
|
<p>@(string.IsNullOrWhiteSpace(level.Description) ? "No description available." : level.Description)</p>
|
|
@if (!level.RequiresStripeSubscription)
|
|
{
|
|
<p class="lead-text">Free / trial / application managed</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="row g-2 mb-3">
|
|
@if (level.MonthlyPricePence.HasValue)
|
|
{
|
|
<div class="col-sm-6">
|
|
<p class="eyebrow">Monthly</p>
|
|
<p>@FormatMoney(level.MonthlyPricePence.Value, level.CurrencyCode)</p>
|
|
</div>
|
|
}
|
|
@if (level.AnnualPricePence.HasValue)
|
|
{
|
|
<div class="col-sm-6">
|
|
<p class="eyebrow">Annual</p>
|
|
<p>@FormatMoney(level.AnnualPricePence.Value, level.CurrencyCode)</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
<dl class="row small">
|
|
<dt class="col-7">Books</dt>
|
|
<dd class="col-5">@FormatLimit(level.MaxBooks)</dd>
|
|
<dt class="col-7">Chapters / book</dt>
|
|
<dd class="col-5">@FormatLimit(level.MaxChaptersPerBook)</dd>
|
|
<dt class="col-7">Scenes / book</dt>
|
|
<dd class="col-5">@FormatLimit(level.MaxScenesPerBook)</dd>
|
|
<dt class="col-7">Characters / project</dt>
|
|
<dd class="col-5">@FormatLimit(level.MaxCharactersPerProject)</dd>
|
|
<dt class="col-7">Storage</dt>
|
|
<dd class="col-5">@FormatStorageAllowance(level.MaxStorageMB)</dd>
|
|
<dt class="col-7">Collaborators</dt>
|
|
<dd class="col-5">@FormatLimit(level.MaxCollaborators)</dd>
|
|
</dl>
|
|
@if (!level.RequiresStripeSubscription)
|
|
{
|
|
<p class="muted">Draft Desk is managed inside PlotWeaver Studio.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="button-row compact-buttons">
|
|
@if (!string.IsNullOrWhiteSpace(level.StripeMonthlyPriceId))
|
|
{
|
|
<form asp-action="SubscriptionCheckout" method="post">
|
|
<input type="hidden" name="SubscriptionLevelID" value="@level.SubscriptionLevelID" />
|
|
<input type="hidden" name="BillingInterval" value="Monthly" />
|
|
<button class="btn @(isCurrent && string.Equals(subscription?.BillingInterval, "Monthly", StringComparison.OrdinalIgnoreCase) ? "btn-outline-secondary" : "btn-outline-primary") btn-sm" type="submit">Monthly</button>
|
|
</form>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(level.StripeAnnualPriceId))
|
|
{
|
|
<form asp-action="SubscriptionCheckout" method="post">
|
|
<input type="hidden" name="SubscriptionLevelID" value="@level.SubscriptionLevelID" />
|
|
<input type="hidden" name="BillingInterval" value="Annual" />
|
|
<button class="btn @(isCurrent && string.Equals(subscription?.BillingInterval, "Annual", StringComparison.OrdinalIgnoreCase) ? "btn-outline-secondary" : "btn-outline-primary") btn-sm" type="submit">Annual</button>
|
|
</form>
|
|
}
|
|
</div>
|
|
}
|
|
</article>
|
|
}
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
@functions {
|
|
private const int PracticalUnlimited = 999999;
|
|
|
|
private static string FormatDate(DateTime? value) =>
|
|
value.HasValue ? value.Value.ToLocalTime().ToString("yyyy-MM-dd") : "Not set";
|
|
|
|
private static string FormatLimit(int value) =>
|
|
value >= PracticalUnlimited ? "Unlimited" : value.ToString("N0");
|
|
|
|
private static string FormatUsage(int used, int maximum, string label) =>
|
|
maximum >= PracticalUnlimited
|
|
? $"{used:N0} / Unlimited {label}"
|
|
: $"{used:N0} / {maximum:N0} {label}";
|
|
|
|
private static string FormatStorageAllowance(int megabytes) =>
|
|
megabytes >= PracticalUnlimited
|
|
? "Unlimited"
|
|
: StorageUsage.FormatBytes(megabytes * 1024L * 1024L);
|
|
|
|
private static string FormatStorageUsage(long usedBytes, long maximumBytes) =>
|
|
maximumBytes >= PracticalUnlimited * 1024L * 1024L
|
|
? $"{StorageUsage.FormatBytes(usedBytes)} / Unlimited storage"
|
|
: $"{StorageUsage.FormatBytes(usedBytes)} / {StorageUsage.FormatBytes(maximumBytes)} storage";
|
|
|
|
private static string FormatMoney(int pence, string? currencyCode) =>
|
|
string.Equals(currencyCode, "GBP", StringComparison.OrdinalIgnoreCase)
|
|
? $"£{pence / 100m:0.00}"
|
|
: $"{pence / 100m:0.00} {(string.IsNullOrWhiteSpace(currencyCode) ? "GBP" : currencyCode.ToUpperInvariant())}";
|
|
}
|