collab: Don't use a separate product for Zed Pro trials (#28986)

Marshall Bowers created

This PR removes the separate product used for the Zed Pro trials, in
favor of using Stripe's trial functionality.

Release Notes:

- N/A

Change summary

crates/collab/src/api/billing.rs    | 19 +++++++++++--------
crates/collab/src/lib.rs            |  7 -------
crates/collab/src/stripe_billing.rs | 26 ++++++++++++++++++++++++++
3 files changed, 37 insertions(+), 15 deletions(-)

Detailed changes

crates/collab/src/api/billing.rs 🔗

@@ -321,8 +321,8 @@ async fn create_billing_subscription(
         }
         Some(ProductCode::ZedProTrial) => {
             stripe_billing
-                .checkout_with_price(
-                    app.config.zed_pro_trial_price_id()?,
+                .checkout_with_zed_pro_trial(
+                    app.config.zed_pro_price_id()?,
                     customer_id,
                     &user.github_login,
                     &success_url,
@@ -444,7 +444,6 @@ async fn manage_billing_subscription(
         ManageSubscriptionIntent::ManageSubscription => None,
         ManageSubscriptionIntent::UpgradeToPro => {
             let zed_pro_price_id = app.config.zed_pro_price_id()?;
-            let zed_pro_trial_price_id = app.config.zed_pro_trial_price_id()?;
             let zed_free_price_id = app.config.zed_free_price_id()?;
 
             let stripe_subscription =
@@ -457,7 +456,10 @@ async fn manage_billing_subscription(
                 .find_map(|item| {
                     let price = item.price.as_ref()?;
 
-                    if price.id == zed_free_price_id || price.id == zed_pro_trial_price_id {
+                    if price.id == zed_free_price_id
+                        || (price.id == zed_pro_price_id
+                            && stripe_subscription.status == SubscriptionStatus::Trialing)
+                    {
                         Some(item.id.clone())
                     } else {
                         None
@@ -771,16 +773,17 @@ async fn handle_customer_subscription_event(
 
     let subscription_kind = maybe!({
         let zed_pro_price_id = app.config.zed_pro_price_id().ok()?;
-        let zed_pro_trial_price_id = app.config.zed_pro_trial_price_id().ok()?;
         let zed_free_price_id = app.config.zed_free_price_id().ok()?;
 
         subscription.items.data.iter().find_map(|item| {
             let price = item.price.as_ref()?;
 
             if price.id == zed_pro_price_id {
-                Some(SubscriptionKind::ZedPro)
-            } else if price.id == zed_pro_trial_price_id {
-                Some(SubscriptionKind::ZedProTrial)
+                Some(if subscription.status == SubscriptionStatus::Trialing {
+                    SubscriptionKind::ZedProTrial
+                } else {
+                    SubscriptionKind::ZedPro
+                })
             } else if price.id == zed_free_price_id {
                 Some(SubscriptionKind::ZedFree)
             } else {

crates/collab/src/lib.rs 🔗

@@ -207,13 +207,6 @@ impl Config {
         Self::parse_stripe_price_id("Zed Pro", self.stripe_zed_pro_price_id.as_deref())
     }
 
-    pub fn zed_pro_trial_price_id(&self) -> anyhow::Result<stripe::PriceId> {
-        Self::parse_stripe_price_id(
-            "Zed Pro Trial",
-            self.stripe_zed_pro_trial_price_id.as_deref(),
-        )
-    }
-
     pub fn zed_free_price_id(&self) -> anyhow::Result<stripe::PriceId> {
         Self::parse_stripe_price_id("Zed Free", self.stripe_zed_pro_price_id.as_deref())
     }

crates/collab/src/stripe_billing.rs 🔗

@@ -405,6 +405,32 @@ impl StripeBilling {
         let session = stripe::CheckoutSession::create(&self.client, params).await?;
         Ok(session.url.context("no checkout session URL")?)
     }
+
+    pub async fn checkout_with_zed_pro_trial(
+        &self,
+        zed_pro_price_id: PriceId,
+        customer_id: stripe::CustomerId,
+        github_login: &str,
+        success_url: &str,
+    ) -> Result<String> {
+        let mut params = stripe::CreateCheckoutSession::new();
+        params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
+            trial_period_days: Some(14),
+            ..Default::default()
+        });
+        params.mode = Some(stripe::CheckoutSessionMode::Subscription);
+        params.customer = Some(customer_id);
+        params.client_reference_id = Some(github_login);
+        params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
+            price: Some(zed_pro_price_id.to_string()),
+            quantity: Some(1),
+            ..Default::default()
+        }]);
+        params.success_url = Some(success_url);
+
+        let session = stripe::CheckoutSession::create(&self.client, params).await?;
+        Ok(session.url.context("no checkout session URL")?)
+    }
 }
 
 #[derive(Serialize)]