Review the PlotDirector subscription, pricing, signup and account-management areas and implement a complete trial-visibility pass.
This commit is contained in:
parent
61d16be6d7
commit
d4c886ce2f
@ -30,10 +30,12 @@ public sealed class ManageAccountController(
|
|||||||
return RedirectToAction("Login", "Account");
|
return RedirectToAction("Login", "Account");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID);
|
||||||
return View(new ManageAccountViewModel
|
return View(new ManageAccountViewModel
|
||||||
{
|
{
|
||||||
User = user,
|
User = user,
|
||||||
Subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID),
|
Subscription = subscription,
|
||||||
|
Trial = TrialVisibilityViewModel.FromSubscription(subscription, DateTime.UtcNow),
|
||||||
HasPaidBillingRisk = await subscriptions.HasPaidBillingRiskAsync(user.UserID)
|
HasPaidBillingRisk = await subscriptions.HasPaidBillingRiskAsync(user.UserID)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -104,6 +106,7 @@ public sealed class ManageAccountController(
|
|||||||
{
|
{
|
||||||
User = user,
|
User = user,
|
||||||
CurrentSubscription = subscription,
|
CurrentSubscription = subscription,
|
||||||
|
Trial = TrialVisibilityViewModel.FromSubscription(subscription, DateTime.UtcNow),
|
||||||
Usage = subscription is null ? null : await subscriptions.GetUsageSummaryAsync(user.UserID),
|
Usage = subscription is null ? null : await subscriptions.GetUsageSummaryAsync(user.UserID),
|
||||||
AvailableLevels = await subscriptions.GetActiveSubscriptionLevelsAsync()
|
AvailableLevels = await subscriptions.GetActiveSubscriptionLevelsAsync()
|
||||||
});
|
});
|
||||||
|
|||||||
@ -66,6 +66,7 @@ public sealed class UserSubscriptionDetail : UserSubscription
|
|||||||
public int? MonthlyPricePence { get; set; }
|
public int? MonthlyPricePence { get; set; }
|
||||||
public int? AnnualPricePence { get; set; }
|
public int? AnnualPricePence { get; set; }
|
||||||
public string CurrencyCode { get; set; } = "GBP";
|
public string CurrencyCode { get; set; } = "GBP";
|
||||||
|
public bool RequiresStripeSubscription { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SubscriptionLimitResult
|
public sealed class SubscriptionLimitResult
|
||||||
|
|||||||
@ -372,6 +372,7 @@ public sealed class ProjectService(
|
|||||||
IWarningRepository warnings,
|
IWarningRepository warnings,
|
||||||
IWritingScheduleRepository writingSchedule,
|
IWritingScheduleRepository writingSchedule,
|
||||||
IProjectActivityService activity,
|
IProjectActivityService activity,
|
||||||
|
ISubscriptionService subscriptions,
|
||||||
ICurrentUserService currentUser,
|
ICurrentUserService currentUser,
|
||||||
IHardDeleteFileCleanupService fileCleanup,
|
IHardDeleteFileCleanupService fileCleanup,
|
||||||
ILogger<ProjectService> logger) : IProjectService
|
ILogger<ProjectService> logger) : IProjectService
|
||||||
@ -386,7 +387,8 @@ public sealed class ProjectService(
|
|||||||
return new ProjectIndexViewModel
|
return new ProjectIndexViewModel
|
||||||
{
|
{
|
||||||
ActiveProjects = await projects.ListActiveForUserAsync(currentUser.UserId.Value),
|
ActiveProjects = await projects.ListActiveForUserAsync(currentUser.UserId.Value),
|
||||||
ArchivedProjects = await projects.ListArchivedForUserAsync(currentUser.UserId.Value)
|
ArchivedProjects = await projects.ListArchivedForUserAsync(currentUser.UserId.Value),
|
||||||
|
Trial = TrialVisibilityViewModel.FromSubscription(await subscriptions.GetCurrentSubscriptionAsync(currentUser.UserId.Value), DateTime.UtcNow)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ public sealed class ManageAccountViewModel
|
|||||||
{
|
{
|
||||||
public AppUser User { get; set; } = new();
|
public AppUser User { get; set; } = new();
|
||||||
public UserSubscriptionDetail? Subscription { get; set; }
|
public UserSubscriptionDetail? Subscription { get; set; }
|
||||||
|
public TrialVisibilityViewModel? Trial { get; set; }
|
||||||
public bool HasPaidBillingRisk { get; set; }
|
public bool HasPaidBillingRisk { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,10 +15,77 @@ public sealed class ManageSubscriptionViewModel
|
|||||||
{
|
{
|
||||||
public AppUser User { get; set; } = new();
|
public AppUser User { get; set; } = new();
|
||||||
public UserSubscriptionDetail? CurrentSubscription { get; set; }
|
public UserSubscriptionDetail? CurrentSubscription { get; set; }
|
||||||
|
public TrialVisibilityViewModel? Trial { get; set; }
|
||||||
public SubscriptionUsageSummary? Usage { get; set; }
|
public SubscriptionUsageSummary? Usage { get; set; }
|
||||||
public IReadOnlyList<SubscriptionLevel> AvailableLevels { get; set; } = [];
|
public IReadOnlyList<SubscriptionLevel> AvailableLevels { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class TrialVisibilityViewModel
|
||||||
|
{
|
||||||
|
public const int TrialDurationDays = 30;
|
||||||
|
|
||||||
|
public string PlanName { get; init; } = string.Empty;
|
||||||
|
public DateTime StartDateUtc { get; init; }
|
||||||
|
public DateTime ExpiryDateUtc { get; init; }
|
||||||
|
public int DaysRemaining { get; init; }
|
||||||
|
public int DaysElapsed { get; init; }
|
||||||
|
public int ProgressPercentage { get; init; }
|
||||||
|
public bool IsExpired { get; init; }
|
||||||
|
public int? WarningThresholdDays { get; init; }
|
||||||
|
|
||||||
|
public string StatusLabel => IsExpired
|
||||||
|
? "Trial expired"
|
||||||
|
: DaysRemaining == 1 ? "1 day remaining" : $"{DaysRemaining} days remaining";
|
||||||
|
|
||||||
|
public string WarningCssClass => WarningThresholdDays switch
|
||||||
|
{
|
||||||
|
<= 1 => "trial-status--one-day",
|
||||||
|
<= 3 => "trial-status--three-days",
|
||||||
|
<= 7 => "trial-status--seven-days",
|
||||||
|
<= 14 => "trial-status--fourteen-days",
|
||||||
|
_ => "trial-status--steady"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static TrialVisibilityViewModel? FromSubscription(UserSubscriptionDetail? subscription, DateTime utcNow)
|
||||||
|
{
|
||||||
|
if (subscription is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTrialPlan = subscription.IsTrial
|
||||||
|
|| (!subscription.RequiresStripeSubscription && string.IsNullOrWhiteSpace(subscription.StripeSubscriptionId));
|
||||||
|
if (!isTrialPlan)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var start = subscription.StartDateUTC;
|
||||||
|
var expiry = subscription.TrialExpiryDateUTC ?? start.AddDays(TrialDurationDays);
|
||||||
|
var totalDays = Math.Max(1, (int)Math.Ceiling((expiry - start).TotalDays));
|
||||||
|
var rawRemaining = (int)Math.Ceiling((expiry - utcNow).TotalDays);
|
||||||
|
var daysRemaining = Math.Max(0, rawRemaining);
|
||||||
|
var daysElapsed = Math.Clamp(totalDays - daysRemaining, 0, totalDays);
|
||||||
|
var progress = Math.Clamp((int)Math.Round(daysElapsed * 100m / totalDays), 0, 100);
|
||||||
|
|
||||||
|
return new TrialVisibilityViewModel
|
||||||
|
{
|
||||||
|
PlanName = subscription.DisplayName,
|
||||||
|
StartDateUtc = start,
|
||||||
|
ExpiryDateUtc = expiry,
|
||||||
|
DaysRemaining = daysRemaining,
|
||||||
|
DaysElapsed = daysElapsed,
|
||||||
|
ProgressPercentage = progress,
|
||||||
|
IsExpired = utcNow >= expiry,
|
||||||
|
WarningThresholdDays = daysRemaining <= 1 ? 1
|
||||||
|
: daysRemaining <= 3 ? 3
|
||||||
|
: daysRemaining <= 7 ? 7
|
||||||
|
: daysRemaining <= 14 ? 14
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class PricingViewModel
|
public sealed class PricingViewModel
|
||||||
{
|
{
|
||||||
public IReadOnlyList<SubscriptionLevel> Plans { get; set; } = [];
|
public IReadOnlyList<SubscriptionLevel> Plans { get; set; } = [];
|
||||||
|
|||||||
@ -25,6 +25,7 @@ public sealed class ProjectIndexViewModel
|
|||||||
{
|
{
|
||||||
public IReadOnlyList<Project> ActiveProjects { get; init; } = [];
|
public IReadOnlyList<Project> ActiveProjects { get; init; } = [];
|
||||||
public IReadOnlyList<Project> ArchivedProjects { get; init; } = [];
|
public IReadOnlyList<Project> ArchivedProjects { get; init; } = [];
|
||||||
|
public TrialVisibilityViewModel? Trial { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ProjectDetailViewModel
|
public sealed class ProjectDetailViewModel
|
||||||
|
|||||||
@ -7,10 +7,19 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Account</p>
|
<p class="eyebrow">Account</p>
|
||||||
<h1>Create your account</h1>
|
<h1>Create your account</h1>
|
||||||
<p class="lead-text">Create a PlotDirector account so your workspace can use the new authentication foundation.</p>
|
<p class="lead-text">Create your account to start a free 30-day PlotDirector trial immediately. No payment details are required.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="trial-status-panel trial-status-panel--signup">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Free trial</p>
|
||||||
|
<h2>30 days to plan, test and decide</h2>
|
||||||
|
<p>Your trial begins as soon as registration is complete. When it expires, your account will show that the trial has ended and you can upgrade from the subscription page.</p>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-outline-primary" asp-controller="Pricing" asp-action="Index">Compare plans</a>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="edit-panel">
|
<section class="edit-panel">
|
||||||
<form asp-action="Register" method="post" class="asset-create-form">
|
<form asp-action="Register" method="post" class="asset-create-form">
|
||||||
<div asp-validation-summary="ModelOnly" class="alert alert-warning"></div>
|
<div asp-validation-summary="ModelOnly" class="alert alert-warning"></div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Manage Account";
|
ViewData["Title"] = "Manage Account";
|
||||||
var subscription = Model.Subscription;
|
var subscription = Model.Subscription;
|
||||||
|
var trial = Model.Trial;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="page-heading compact">
|
<div class="page-heading compact">
|
||||||
@ -64,11 +65,39 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@if (trial is not null)
|
||||||
|
{
|
||||||
|
<div class="trial-status-panel @trial.WarningCssClass">
|
||||||
|
<div class="trial-status-panel__heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Free trial status</p>
|
||||||
|
<h3>@trial.StatusLabel</h3>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-outline-primary btn-sm" asp-action="Subscription">Upgrade</a>
|
||||||
|
</div>
|
||||||
|
<p>Your 30-day @trial.PlanName trial expires on @FormatDate(trial.ExpiryDateUtc).</p>
|
||||||
|
<div class="trial-progress" aria-label="Trial progress">
|
||||||
|
<div class="trial-progress__bar" style="width: @trial.ProgressPercentage%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<p class="eyebrow">Current plan</p>
|
<p class="eyebrow">Current plan</p>
|
||||||
<p>@subscription.DisplayName</p>
|
<p>@subscription.DisplayName</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Trial start</p>
|
||||||
|
<p>@(trial is null ? "Not on trial" : FormatDate(trial.StartDateUtc))</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Trial expiry</p>
|
||||||
|
<p>@(trial is null ? "Not on trial" : FormatDate(trial.ExpiryDateUtc))</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Days remaining</p>
|
||||||
|
<p>@(trial is null ? "Not on trial" : trial.StatusLabel)</p>
|
||||||
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<p class="eyebrow">Books</p>
|
<p class="eyebrow">Books</p>
|
||||||
<p>@FormatLimit(subscription.MaxBooks)</p>
|
<p>@FormatLimit(subscription.MaxBooks)</p>
|
||||||
@ -159,6 +188,8 @@
|
|||||||
private static string FormatLimit(int value) => value >= 999999 ? "Practical unlimited" : value.ToString("N0");
|
private static string FormatLimit(int value) => value >= 999999 ? "Practical unlimited" : value.ToString("N0");
|
||||||
|
|
||||||
private static string FormatStorage(int value) => value >= 1024 ? $"{value / 1024:N0} GB" : $"{value:N0} MB";
|
private static string FormatStorage(int value) => value >= 1024 ? $"{value / 1024:N0} GB" : $"{value:N0} MB";
|
||||||
|
|
||||||
|
private static string FormatDate(DateTime value) => value.ToLocalTime().ToString("yyyy-MM-dd");
|
||||||
}
|
}
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Subscription";
|
ViewData["Title"] = "Subscription";
|
||||||
var subscription = Model.CurrentSubscription;
|
var subscription = Model.CurrentSubscription;
|
||||||
|
var trial = Model.Trial;
|
||||||
var usage = Model.Usage;
|
var usage = Model.Usage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +35,23 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@if (trial is not null)
|
||||||
|
{
|
||||||
|
<div class="trial-status-panel @trial.WarningCssClass">
|
||||||
|
<div class="trial-status-panel__heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Free trial status</p>
|
||||||
|
<h3>@trial.StatusLabel</h3>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-primary btn-sm" asp-controller="Pricing" asp-action="Index">Upgrade</a>
|
||||||
|
</div>
|
||||||
|
<p>Your 30-day @trial.PlanName trial started on @FormatDate(trial.StartDateUtc) and expires on @FormatDate(trial.ExpiryDateUtc).</p>
|
||||||
|
<div class="trial-progress" aria-label="Trial progress">
|
||||||
|
<div class="trial-progress__bar" style="width: @trial.ProgressPercentage%"></div>
|
||||||
|
</div>
|
||||||
|
<p class="muted mb-0">@trial.DaysElapsed days used, @trial.DaysRemaining days remaining.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<p class="eyebrow">Plan</p>
|
<p class="eyebrow">Plan</p>
|
||||||
@ -53,11 +71,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<p class="eyebrow">Trial</p>
|
<p class="eyebrow">Trial</p>
|
||||||
<p>@(subscription.IsTrial ? "Trial" : "No")</p>
|
<p>@(trial is null ? "No" : "30-day free trial")</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<p class="eyebrow">Trial start</p>
|
||||||
|
<p>@(trial is null ? "Not set" : FormatDate(trial.StartDateUtc))</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<p class="eyebrow">Trial expiry</p>
|
<p class="eyebrow">Trial expiry</p>
|
||||||
<p>@FormatDate(subscription.TrialExpiryDateUTC)</p>
|
<p>@(trial is null ? FormatDate(subscription.TrialExpiryDateUTC) : FormatDate(trial.ExpiryDateUtc))</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<p class="eyebrow">Days remaining</p>
|
||||||
|
<p>@(trial is null ? "Not on trial" : trial.StatusLabel)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<p class="eyebrow">Started</p>
|
<p class="eyebrow">Started</p>
|
||||||
@ -160,7 +186,7 @@
|
|||||||
<p>@(string.IsNullOrWhiteSpace(level.Description) ? "No description available." : level.Description)</p>
|
<p>@(string.IsNullOrWhiteSpace(level.Description) ? "No description available." : level.Description)</p>
|
||||||
@if (!level.RequiresStripeSubscription)
|
@if (!level.RequiresStripeSubscription)
|
||||||
{
|
{
|
||||||
<p class="lead-text">Free / trial / application managed</p>
|
<p class="lead-text">30-day free trial / application managed</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -197,7 +223,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
@if (!level.RequiresStripeSubscription)
|
@if (!level.RequiresStripeSubscription)
|
||||||
{
|
{
|
||||||
<p class="muted">Draft Desk is managed inside PlotDirector.</p>
|
<p class="muted">Draft Desk is a 30-day free trial. No payment details are required.</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
<div class="marketing-container pricing-hero__inner">
|
<div class="marketing-container pricing-hero__inner">
|
||||||
<p class="marketing-eyebrow">Pricing</p>
|
<p class="marketing-eyebrow">Pricing</p>
|
||||||
<h1>Simple pricing for stories of every size.</h1>
|
<h1>Simple pricing for stories of every size.</h1>
|
||||||
<p class="marketing-lead">Start free, subscribe when you're ready, or choose a plan straight away.</p>
|
<p class="marketing-lead">Start with a 30-day free trial. No payment details are required until you choose a paid plan.</p>
|
||||||
<div class="marketing-actions">
|
<div class="marketing-actions">
|
||||||
<a class="marketing-btn marketing-btn--primary" asp-controller="Pricing" asp-action="StartTrial">Start Free Trial</a>
|
<a class="marketing-btn marketing-btn--primary" asp-controller="Pricing" asp-action="StartTrial">Start Free Trial</a>
|
||||||
<a class="marketing-btn marketing-btn--secondary" href="#plans">Compare Plans</a>
|
<a class="marketing-btn marketing-btn--secondary" href="#plans">Compare Plans</a>
|
||||||
@ -71,7 +71,12 @@
|
|||||||
{
|
{
|
||||||
var isCurrent = Model.CurrentSubscriptionLevelID == plan.SubscriptionLevelID;
|
var isCurrent = Model.CurrentSubscriptionLevelID == plan.SubscriptionLevelID;
|
||||||
var isSuggested = suggestedPlanId == plan.SubscriptionLevelID;
|
var isSuggested = suggestedPlanId == plan.SubscriptionLevelID;
|
||||||
<article class="pricing-card @(isSuggested ? "pricing-card--featured" : string.Empty)">
|
var isTrialPlan = !plan.RequiresStripeSubscription;
|
||||||
|
<article class="pricing-card @(isSuggested ? "pricing-card--featured" : string.Empty) @(isTrialPlan ? "pricing-card--trial" : string.Empty)">
|
||||||
|
@if (isTrialPlan)
|
||||||
|
{
|
||||||
|
<p class="pricing-badge pricing-badge--trial">30-day free trial</p>
|
||||||
|
}
|
||||||
@if (isSuggested)
|
@if (isSuggested)
|
||||||
{
|
{
|
||||||
<p class="pricing-badge">Most Popular</p>
|
<p class="pricing-badge">Most Popular</p>
|
||||||
@ -94,12 +99,17 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<strong>Free</strong>
|
<strong>Free</strong>
|
||||||
<span>trial workspace</span>
|
<span>30-day trial workspace</span>
|
||||||
<small>Assigned when you create an account.</small>
|
<small>No payment details required. Starts immediately after registration.</small>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="pricing-feature-list">
|
<ul class="pricing-feature-list">
|
||||||
|
@if (!plan.RequiresStripeSubscription)
|
||||||
|
{
|
||||||
|
<li>30 days of free access</li>
|
||||||
|
<li>No credit card required</li>
|
||||||
|
}
|
||||||
<li>@FormatLimit(plan.MaxBooks) books</li>
|
<li>@FormatLimit(plan.MaxBooks) books</li>
|
||||||
<li>@FormatLimit(plan.MaxCharactersPerProject) characters per project</li>
|
<li>@FormatLimit(plan.MaxCharactersPerProject) characters per project</li>
|
||||||
<li>@FormatLimit(plan.MaxScenesPerBook) scenes per book</li>
|
<li>@FormatLimit(plan.MaxScenesPerBook) scenes per book</li>
|
||||||
@ -166,7 +176,7 @@
|
|||||||
<div class="faq-grid">
|
<div class="faq-grid">
|
||||||
<article>
|
<article>
|
||||||
<h3>Can I start with a free trial?</h3>
|
<h3>Can I start with a free trial?</h3>
|
||||||
<p>Yes. Creating an account starts you on the Draft Desk evaluation plan, so you can try the workspace before choosing a paid subscription.</p>
|
<p>Yes. Creating an account starts a 30-day Draft Desk trial immediately, so you can try the workspace before choosing a paid subscription.</p>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h3>Can I subscribe without using the trial?</h3>
|
<h3>Can I subscribe without using the trial?</h3>
|
||||||
@ -186,7 +196,11 @@
|
|||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h3>Do I need a credit card for the trial?</h3>
|
<h3>Do I need a credit card for the trial?</h3>
|
||||||
<p>The registration flow creates the evaluation workspace. Payment details are only handled when you choose a paid subscription through Stripe Checkout.</p>
|
<p>No. Payment details are not required for the free trial. Stripe Checkout is only used when you choose a paid subscription.</p>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h3>What happens when the trial expires?</h3>
|
||||||
|
<p>Your trial status will show in your dashboard and subscription pages. When the 30 days are over, choose a paid plan to continue with higher paid-plan access.</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -196,7 +210,7 @@
|
|||||||
<div class="marketing-container final-cta__inner">
|
<div class="marketing-container final-cta__inner">
|
||||||
<p class="marketing-eyebrow">Begin with clarity</p>
|
<p class="marketing-eyebrow">Begin with clarity</p>
|
||||||
<h2>Ready to take control of your story?</h2>
|
<h2>Ready to take control of your story?</h2>
|
||||||
<p>Try PlotDirector free, or choose the paid plan that fits the novel you are building.</p>
|
<p>Try PlotDirector free for 30 days with no payment details, or choose the paid plan that fits the novel you are building.</p>
|
||||||
<div class="marketing-actions">
|
<div class="marketing-actions">
|
||||||
<a class="marketing-btn marketing-btn--primary" asp-controller="Pricing" asp-action="StartTrial">Start Free Trial</a>
|
<a class="marketing-btn marketing-btn--primary" asp-controller="Pricing" asp-action="StartTrial">Start Free Trial</a>
|
||||||
<a class="marketing-btn marketing-btn--secondary" href="#plans">View Plans</a>
|
<a class="marketing-btn marketing-btn--secondary" href="#plans">View Plans</a>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
@model ProjectIndexViewModel
|
@model ProjectIndexViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Projects";
|
ViewData["Title"] = "Projects";
|
||||||
|
var trial = Model.Trial;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="page-heading">
|
<div class="page-heading">
|
||||||
@ -14,6 +15,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (trial is not null)
|
||||||
|
{
|
||||||
|
<section class="trial-dashboard-banner @trial.WarningCssClass"
|
||||||
|
data-trial-banner
|
||||||
|
data-trial-banner-key="projects:@trial.ExpiryDateUtc.ToString("yyyyMMdd")"
|
||||||
|
aria-label="Trial status">
|
||||||
|
<div class="trial-dashboard-banner__body">
|
||||||
|
<p class="eyebrow">Free trial status</p>
|
||||||
|
<h2>@trial.StatusLabel on @trial.PlanName</h2>
|
||||||
|
<p>Your 30-day trial expires on @FormatDate(trial.ExpiryDateUtc). Upgrade whenever you are ready.</p>
|
||||||
|
</div>
|
||||||
|
<div class="trial-dashboard-banner__actions">
|
||||||
|
<a class="btn btn-primary btn-sm" asp-controller="ManageAccount" asp-action="Subscription">Upgrade</a>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="button" data-trial-banner-dismiss>Dismiss</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
@if (TempData["ArchiveMessage"] is string archiveMessage)
|
@if (TempData["ArchiveMessage"] is string archiveMessage)
|
||||||
{
|
{
|
||||||
<div class="alert alert-success">@archiveMessage</div>
|
<div class="alert alert-success">@archiveMessage</div>
|
||||||
@ -140,6 +159,10 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
private static string FormatDate(DateTime value) => value.ToLocalTime().ToString("yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
|
|||||||
@ -558,6 +558,49 @@ a:focus-visible {
|
|||||||
color: var(--plotline-text);
|
color: var(--plotline-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trial-status-panel,
|
||||||
|
.trial-dashboard-banner {
|
||||||
|
border-color: rgba(84, 68, 48, 0.16);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(255, 253, 248, 0.96), rgba(242, 226, 207, 0.56)),
|
||||||
|
var(--plotline-surface);
|
||||||
|
box-shadow: var(--plotline-shadow-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status-panel--signup {
|
||||||
|
border-color: rgba(49, 95, 103, 0.22);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(223, 236, 239, 0.72), rgba(255, 253, 248, 0.92)),
|
||||||
|
var(--plotline-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status-panel h2,
|
||||||
|
.trial-status-panel h3,
|
||||||
|
.trial-dashboard-banner h2 {
|
||||||
|
color: var(--plotline-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-progress {
|
||||||
|
box-shadow: inset 0 1px 2px rgba(62, 45, 28, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-progress__bar {
|
||||||
|
background: linear-gradient(90deg, var(--plotline-secondary), var(--plotline-accent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status--fourteen-days {
|
||||||
|
border-color: rgba(184, 118, 43, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status--seven-days,
|
||||||
|
.trial-status--three-days,
|
||||||
|
.trial-status--one-day {
|
||||||
|
border-color: rgba(184, 118, 43, 0.38);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(251, 236, 211, 0.84), rgba(255, 253, 248, 0.92)),
|
||||||
|
var(--plotline-surface);
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
--bs-table-bg: transparent;
|
--bs-table-bg: transparent;
|
||||||
--bs-table-color: var(--plotline-text);
|
--bs-table-color: var(--plotline-text);
|
||||||
@ -3649,6 +3692,14 @@ a:focus-visible {
|
|||||||
box-shadow: 0 26px 58px rgba(31, 42, 68, 0.13);
|
box-shadow: 0 26px 58px rgba(31, 42, 68, 0.13);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pricing-card--trial {
|
||||||
|
border-color: rgba(95, 120, 108, 0.52);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 253, 249, 0.98), rgba(235, 241, 237, 0.9)),
|
||||||
|
#fff;
|
||||||
|
box-shadow: 0 24px 54px rgba(31, 42, 68, 0.11);
|
||||||
|
}
|
||||||
|
|
||||||
.pricing-badge {
|
.pricing-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: clamp(1.2rem, 2.4vw, 1.65rem);
|
top: clamp(1.2rem, 2.4vw, 1.65rem);
|
||||||
@ -3666,6 +3717,12 @@ a:focus-visible {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pricing-badge--trial {
|
||||||
|
border-color: rgba(95, 120, 108, 0.48);
|
||||||
|
color: #314b42;
|
||||||
|
background: rgba(95, 120, 108, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
.pricing-card__heading {
|
.pricing-card__heading {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|||||||
2
PlotLine/wwwroot/css/plotline-theme.min.css
vendored
2
PlotLine/wwwroot/css/plotline-theme.min.css
vendored
File diff suppressed because one or more lines are too long
@ -169,6 +169,77 @@ td.text-end {
|
|||||||
max-width: 980px;
|
max-width: 980px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trial-status-panel,
|
||||||
|
.trial-dashboard-banner {
|
||||||
|
border: 1px solid var(--plotline-line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
background: var(--plotline-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status-panel--signup {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 980px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status-panel h2,
|
||||||
|
.trial-status-panel h3,
|
||||||
|
.trial-dashboard-banner h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status-panel p,
|
||||||
|
.trial-dashboard-banner p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status-panel__heading,
|
||||||
|
.trial-dashboard-banner {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-dashboard-banner {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-dashboard-banner__body {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-dashboard-banner__actions {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-progress {
|
||||||
|
height: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(84, 68, 48, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-progress__bar {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-status-panel[hidden],
|
||||||
|
.trial-dashboard-banner[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.location-page .page-heading.compact,
|
.location-page .page-heading.compact,
|
||||||
.location-page .edit-panel,
|
.location-page .edit-panel,
|
||||||
.location-page .empty-panel,
|
.location-page .empty-panel,
|
||||||
@ -3974,6 +4045,19 @@ body.dragging-location [data-drag-type="location"] {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trial-status-panel--signup,
|
||||||
|
.trial-status-panel__heading,
|
||||||
|
.trial-dashboard-banner {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trial-dashboard-banner__actions,
|
||||||
|
.trial-dashboard-banner__actions .btn,
|
||||||
|
.trial-status-panel--signup .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.attention-list p {
|
.attention-list p {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
2
PlotLine/wwwroot/css/site.min.css
vendored
2
PlotLine/wwwroot/css/site.min.css
vendored
File diff suppressed because one or more lines are too long
@ -37,6 +37,32 @@
|
|||||||
applyTheme(readTheme() || "light");
|
applyTheme(readTheme() || "light");
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const banner = document.querySelector("[data-trial-banner]");
|
||||||
|
if (!banner) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `plotline.trialBanner.dismissed.${banner.dataset.trialBannerKey || "current"}`;
|
||||||
|
try {
|
||||||
|
if (localStorage.getItem(key) === "true") {
|
||||||
|
banner.hidden = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// The banner can still be dismissed for this page view if storage is unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
banner.querySelector("[data-trial-banner-dismiss]")?.addEventListener("click", () => {
|
||||||
|
banner.hidden = true;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, "true");
|
||||||
|
} catch {
|
||||||
|
// Dismissal persistence is a convenience only.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const modalElement = document.getElementById("plotlineConfirmModal");
|
const modalElement = document.getElementById("plotlineConfirmModal");
|
||||||
const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null;
|
const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null;
|
||||||
|
|||||||
2
PlotLine/wwwroot/js/site.min.js
vendored
2
PlotLine/wwwroot/js/site.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user