Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Apply existing coupon to upgrade message; School upgrade path #4154

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 99 additions & 34 deletions frontend/apps/crates/entry/user/src/settings/dom/plan_info.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use components::confirm;
use dominator::{clone, html, Dom};
use futures_signals::signal::SignalExt;
use futures_signals::signal::{Mutable, SignalExt};
use shared::domain::billing::{
AmountInCents, AppliedCoupon, BillingInterval, PaymentMethodType, PaymentNetwork, PlanType,
SubscriptionTier,
};
use std::rc::Rc;
use strum::IntoEnumIterator;
use utils::{events, js_object, prelude::plan_type_signal, unwrap::UnwrapJiExt};
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::spawn_local;
Expand Down Expand Up @@ -105,7 +106,7 @@ impl SettingsPage {
state.render_payment_method(&plan_info.payment_method_type),
html!("div", {
.prop("slot", "change-to-annual")
.child_signal(plan_type_signal().map(clone!(state => move |plan_type| {
.child_signal(plan_type_signal().map(clone!(state, plan_info => move |plan_type| {
let frequency = plan_type?.billing_interval();
match frequency {
BillingInterval::Monthly => {
Expand All @@ -116,11 +117,11 @@ impl SettingsPage {
Some(PlanType::SchoolLevel1Monthly) => "Get 1 month FREE by switching to annual billing",
_ => "Get 2 months FREE by switching to annual billing"
})
.event(clone!(state => move|_ :events::Click| {
spawn_local(clone!(state => async move {
.event(clone!(state, plan_info => move|_ :events::Click| {
spawn_local(clone!(state, plan_info => async move {
let plan_type: PlanType = plan_type.unwrap_ji();
let new_plan_type = plan_type.monthly_to_annual();
let new_price = number_as_price(new_plan_type.plan_price());
let new_price = discounted_price(new_plan_type, &plan_info);
let message = format!("You will be charged {new_price} per year. A renewal reminder will be sent 30 days before the end of your subscription.");

let confirmed = confirm::Confirm {
Expand All @@ -139,40 +140,92 @@ impl SettingsPage {
BillingInterval::Annually => None,
}
})))
.child_signal(plan_type_signal().map(clone!(state => move |plan_type| {
.child_signal(plan_type_signal().map(clone!(state, plan_info => move |plan_type| {
let plan_type: PlanType = plan_type?;
let subscription_tier = plan_type.subscription_tier();
match subscription_tier {
SubscriptionTier::Basic => {
let new_plan_type = plan_type.basic_to_pro();
let billing_interval = plan_type.billing_interval();
Some(html!("button-rect", {
.prop("type", "filled")
.prop("color", "blue")
.text(&format!("Switch to Pro {billing_interval}"))
.event(clone!(state => move|_ :events::Click| {
spawn_local(clone!(state => async move {
let charge_interval = match billing_interval {
BillingInterval::Monthly => "month",
BillingInterval::Annually => "year",
};
let new_price = number_as_price(new_plan_type.plan_price());
let message = format!("You will be charged {new_price} per {charge_interval}. A renewal reminder will be sent 30 days before the end of your subscription.");
if plan_type.is_individual_plan() {
match subscription_tier {
SubscriptionTier::Basic => {
let new_plan_type = plan_type.basic_to_pro();
let billing_interval = plan_type.billing_interval();
Some(html!("button-rect", {
.prop("type", "filled")
.prop("color", "blue")
.text(&format!("Switch to Pro {billing_interval}"))
.event(clone!(state, plan_info => move|_ :events::Click| {
spawn_local(clone!(state, plan_info => async move {
let charge_interval = match billing_interval {
BillingInterval::Monthly => "month",
BillingInterval::Annually => "year",
};
let new_price = discounted_price(new_plan_type, &plan_info);
let message = format!("You will be charged {new_price} per {charge_interval}. A renewal reminder will be sent 30 days before the end of your subscription.");

let confirmed = confirm::Confirm {
title: "Switch to the Pro plan".to_string(),
message,
confirm_text: "Confirm".to_string(),
cancel_text: "Cancel".to_string()
}.confirm().await;
if confirmed {
state.change_to(new_plan_type);
let confirmed = confirm::Confirm {
title: "Switch to the Pro plan".to_string(),
message,
confirm_text: "Confirm".to_string(),
cancel_text: "Cancel".to_string()
}.confirm().await;
if confirmed {
state.change_to(new_plan_type);
}
}));
}))
}))
},
SubscriptionTier::Pro => None,
}
} else {
match plan_type {
PlanType::SchoolUnlimitedMonthly | PlanType::SchoolUnlimitedAnnually => None,
_ => {
let plan_type_signal = Mutable::new(None::<PlanType>);
let plan_types = PlanType::iter().filter(|p| {
p.can_upgrade_from_same_interval(&plan_type)
}).map(|p| html!("input-select-option", {
.text(p.user_display_name())
.prop("value", p.as_str())
.prop_signal("selected", plan_type_signal.signal_cloned().map(move |current| match current {
None => false,
Some(current) => current == p,
}))
.event(clone!(state, plan_info, plan_type_signal => move |e: events::CustomSelectedChange| {
if e.selected() {
plan_type_signal.set(Some(p));
spawn_local(clone!(state, plan_info, plan_type_signal => async move {
let billing_interval = plan_type.billing_interval();
let charge_interval = match billing_interval {
BillingInterval::Monthly => "month",
BillingInterval::Annually => "year",
};
let new_price = discounted_price(p, &plan_info);
let message = format!("You will be charged {new_price} per {charge_interval}. A renewal reminder will be sent 30 days before the end of your subscription.");

let confirmed = confirm::Confirm {
title: format!("Upgrade to {}", p.user_display_name()),
message,
confirm_text: "Confirm".to_string(),
cancel_text: "Cancel".to_string()
}.confirm().await;
if confirmed {
state.change_to(p);
} else {
plan_type_signal.set(None);
}
}));
}
}));
}))
})).collect::<Vec<_>>();
Some(html!("input-select", {
.style("min-width", "200rem")
.prop("label", "Upgrade to")
.prop("multiple", false)
.prop_signal("value", plan_type_signal.signal_cloned().map(|p| p.map_or("", |p| p.user_display_name())))
.children(plan_types)
}))
}))
},
SubscriptionTier::Pro => None,
}
}
}
})))
}),
Expand Down Expand Up @@ -269,6 +322,18 @@ fn price_string(
format!("${discounted}{frequency}{coupon}")
}

fn discounted_price(plan_type: PlanType, plan_info: &Rc<PlanSectionInfo>) -> String {
let discount_percent = match plan_info.coupon {
Some(AppliedCoupon { coupon_percent, .. }) => match coupon_percent {
Some(percent) => 1.0 - f64::from(percent),
None => 1.0,
},
None => 1.0,
};
let new_plan_price = (plan_type.plan_price() as f64 * discount_percent) as u32; // This should still be cents
number_as_price(new_plan_price)
}

/// Move to utils?
fn number_as_price(cents: u32) -> String {
let locales = js_sys::Array::of1(&JsValue::from("en-US"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export class _ extends LitElement {
${this.value
? html`<span class="value">${this.value}</span>`
: html`<span class="placeholder"
>${this.placeholder}</span
>${this.placeholder ? this.placeholder : html`&nbsp;`}</span
>`}
</div>
<img-ui
Expand Down
69 changes: 69 additions & 0 deletions shared/rust/src/domain/billing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,30 @@ impl PlanType {
}
}

/// Get a user-friendly name
#[must_use]
pub const fn user_display_name(&self) -> &'static str {
match self {
Self::IndividualBasicMonthly | Self::IndividualBasicAnnually => "Basic",
Self::IndividualProMonthly | Self::IndividualProAnnually => "Pro",
Self::SchoolLevel1Monthly | Self::SchoolLevel1Annually => {
formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_1_TEACHER_COUNT)
}
Self::SchoolLevel2Monthly | Self::SchoolLevel2Annually => {
formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_2_TEACHER_COUNT)
}
Self::SchoolLevel3Monthly | Self::SchoolLevel3Annually => {
formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_3_TEACHER_COUNT)
}
Self::SchoolLevel4Monthly | Self::SchoolLevel4Annually => {
formatcp!("Up to {} teachers", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
}
Self::SchoolUnlimitedMonthly | Self::SchoolUnlimitedAnnually => {
formatcp!("More than {} teachers", PLAN_SCHOOL_LEVEL_4_TEACHER_COUNT)
}
}
}

/// `SubscriptionTier` of the current plan
#[must_use]
pub const fn subscription_tier(&self) -> SubscriptionTier {
Expand Down Expand Up @@ -980,6 +1004,51 @@ impl PlanType {
}
}

/// Whether it is possible to upgrade from another plan type to self in the same billing interval
#[must_use]
pub const fn can_upgrade_from_same_interval(&self, from_type: &Self) -> bool {
match self {
Self::IndividualProAnnually => matches!(from_type, Self::IndividualBasicAnnually,),
Self::SchoolLevel1Monthly => false,
Self::SchoolLevel2Monthly => matches!(from_type, Self::SchoolLevel1Monthly),
Self::SchoolLevel3Monthly => matches!(
from_type,
Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly,
),
Self::SchoolLevel4Monthly => matches!(
from_type,
Self::SchoolLevel1Monthly | Self::SchoolLevel2Monthly | Self::SchoolLevel3Monthly,
),
Self::SchoolUnlimitedMonthly => matches!(
from_type,
Self::SchoolLevel1Monthly
| Self::SchoolLevel2Monthly
| Self::SchoolLevel3Monthly
| Self::SchoolLevel4Monthly,
),
Self::SchoolLevel1Annually => false,
Self::SchoolLevel2Annually => matches!(from_type, Self::SchoolLevel1Annually),
Self::SchoolLevel3Annually => matches!(
from_type,
Self::SchoolLevel1Annually | Self::SchoolLevel2Annually
),
Self::SchoolLevel4Annually => matches!(
from_type,
Self::SchoolLevel1Annually
| Self::SchoolLevel2Annually
| Self::SchoolLevel3Annually
),
Self::SchoolUnlimitedAnnually => matches!(
from_type,
Self::SchoolLevel1Annually
| Self::SchoolLevel2Annually
| Self::SchoolLevel3Annually
| Self::SchoolLevel4Annually
),
_ => false,
}
}

/// check if is individual plan
#[must_use]
pub const fn is_individual_plan(&self) -> bool {
Expand Down
Loading