11use 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} ;
88use 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