Skip to content

Commit 4d8229c

Browse files
m13vGemini
andcommitted
Add Stripe redirect endpoint to handle custom URL schemes
Co-Authored-By: Gemini <noreply@google.com>
1 parent 1989a81 commit 4d8229c

1 file changed

Lines changed: 62 additions & 7 deletions

File tree

Backend/src/routes/stripe.rs

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use axum::{
22
body::Bytes,
3-
extract::Extension,
3+
extract::{Extension, Query},
44
http::{HeaderMap, StatusCode},
5-
response::IntoResponse,
5+
response::{IntoResponse, Redirect},
66
Json,
77
};
88
use serde::{Deserialize, Serialize};
@@ -44,12 +44,14 @@ pub async fn create_checkout_session(
4444
}
4545

4646
let firebase_uid = auth.firebase_uid.unwrap_or_default();
47-
let success_url = body
48-
.success_url
49-
.unwrap_or_else(|| "fazm://subscription/success".to_string());
47+
48+
// Stripe Checkout requires https:// URLs. Use backend redirect endpoints
49+
// that will forward to the app's fazm:// custom URL scheme.
50+
let backend_base = &config.vertex_issuer; // reuse VERTEX_ISSUER as backend base URL
51+
let success_url = format!("{backend_base}/api/stripe/redirect?to=fazm://subscription/success");
5052
let cancel_url = body
5153
.cancel_url
52-
.unwrap_or_else(|| "fazm://subscription/cancel".to_string());
54+
.unwrap_or_else(|| format!("{backend_base}/api/stripe/redirect?to=fazm://subscription/cancel"));
5355

5456
let client = reqwest::Client::new();
5557

@@ -65,6 +67,7 @@ pub async fn create_checkout_session(
6567
("customer", customer_id.clone()),
6668
("success_url", success_url),
6769
("cancel_url", cancel_url),
70+
("payment_method_types[0]", "card".to_string()),
6871
("line_items[0][price]", config.stripe_price_id.clone()),
6972
("line_items[0][quantity]", "1".to_string()),
7073
("subscription_data[metadata][firebase_uid]", firebase_uid),
@@ -115,6 +118,58 @@ pub async fn create_checkout_session(
115118
}))
116119
}
117120

121+
// ---------- Redirect ----------
122+
123+
#[derive(Deserialize)]
124+
pub struct RedirectQuery {
125+
pub to: String,
126+
}
127+
128+
/// GET /api/stripe/redirect?to=fazm://subscription/success
129+
/// Stripe Checkout requires https:// success/cancel URLs. This endpoint
130+
/// serves as a trampoline: Stripe redirects here, and we redirect the
131+
/// browser to the app's custom URL scheme (fazm://).
132+
pub async fn checkout_redirect(
133+
Query(query): Query<RedirectQuery>,
134+
) -> impl IntoResponse {
135+
// Only allow redirects to the fazm:// scheme
136+
if !query.to.starts_with("fazm://") {
137+
return Redirect::temporary("https://fazm.ai").into_response();
138+
}
139+
140+
// Return an HTML page that redirects to the custom URL scheme.
141+
// Custom URL schemes don't work with HTTP 302 redirects in all browsers,
142+
// so we use JavaScript + meta refresh as fallback.
143+
let html = format!(
144+
r#"<!DOCTYPE html>
145+
<html>
146+
<head>
147+
<meta charset="utf-8">
148+
<meta http-equiv="refresh" content="1;url={url}">
149+
<title>Redirecting to Fazm...</title>
150+
<style>
151+
body {{ background: #0F0F0F; color: #E5E5E5; font-family: -apple-system, system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }}
152+
.container {{ text-align: center; }}
153+
h1 {{ color: #8B5CF6; font-size: 24px; }}
154+
p {{ color: #B0B0B0; }}
155+
a {{ color: #8B5CF6; }}
156+
</style>
157+
</head>
158+
<body>
159+
<div class="container">
160+
<h1>✓ Payment Successful!</h1>
161+
<p>Redirecting you back to Fazm...</p>
162+
<p><a href="{url}">Click here if not redirected automatically</a></p>
163+
</div>
164+
<script>window.location.href = "{url}";</script>
165+
</body>
166+
</html>"#,
167+
url = query.to
168+
);
169+
170+
axum::response::Html(html).into_response()
171+
}
172+
118173
// ---------- Subscription Status ----------
119174

120175
#[derive(Serialize)]
@@ -201,7 +256,7 @@ pub async fn webhook(
201256
let stripe_secret = &config.stripe_webhook_secret;
202257

203258
// Verify webhook signature if secret is configured
204-
if !stripe_secret.is_empty() {
259+
if !stripe_secret.is_empty() && stripe_secret != "placeholder" {
205260
let sig = headers
206261
.get("stripe-signature")
207262
.and_then(|v| v.to_str().ok())

0 commit comments

Comments
 (0)