33// Stripe Checkout + webhook via raw curl. No SDK.
44declare (strict_types=1 );
55
6+ // Reject webhooks whose signed timestamp is older than this, to limit replay.
7+ const STRIPE_WEBHOOK_TOLERANCE = 300 ; // 5 minutes
8+
69function stripe_enabled (): bool
710{
811 $ c = config ();
912 return !empty ($ c ['stripe_secret_key ' ]) &&
1013 !empty ($ c ['stripe_price_id ' ]);
1114}
1215
16+ // One call to the Stripe REST API. Secret key is the basic-auth user.
17+ // Returns the decoded response, or null on transport failure.
18+ // $method: 'GET' | 'POST' | 'DELETE'. POST sends $fields as form-encoded body.
19+ function stripe_api (string $ method , string $ path , array $ fields = []): ?array
20+ {
21+ $ opt = [
22+ CURLOPT_RETURNTRANSFER => true ,
23+ CURLOPT_USERPWD => config ()['stripe_secret_key ' ] . ': ' ,
24+ CURLOPT_TIMEOUT => 20 ,
25+ ];
26+ if ($ method === 'POST ' ) {
27+ $ opt [CURLOPT_POST ] = true ;
28+ $ opt [CURLOPT_POSTFIELDS ] = http_build_query ($ fields );
29+ } elseif ($ method === 'DELETE ' ) {
30+ $ opt [CURLOPT_CUSTOMREQUEST ] = 'DELETE ' ;
31+ }
32+ $ ch = curl_init ('https://api.stripe.com/v1/ ' . $ path );
33+ curl_setopt_array ($ ch , $ opt );
34+ $ resp = curl_exec ($ ch );
35+ return $ resp === false ? null : json_decode ($ resp , true );
36+ }
37+
1338// Create a subscription Checkout Session, return its hosted URL.
1439function stripe_create_checkout (array $ user ): ?string
1540{
16- $ c = config ();
17- $ fields = [
41+ $ c = config ();
42+ $ data = stripe_api ( ' POST ' , ' checkout/sessions ' , [
1843 'mode ' => 'subscription ' ,
1944 'success_url ' => $ c ['base_url ' ] . '/app?checkout=success ' ,
2045 'cancel_url ' => $ c ['base_url ' ] . '/app?checkout=cancel ' ,
2146 'customer_email ' => $ user ['email ' ],
2247 'client_reference_id ' => (string )$ user ['id ' ], // maps the webhook back to our user
2348 'line_items[0][price] ' => $ c ['stripe_price_id ' ],
2449 'line_items[0][quantity] ' => 1 ,
25- ];
26- $ ch = curl_init ('https://api.stripe.com/v1/checkout/sessions ' );
27- curl_setopt_array ($ ch , [
28- CURLOPT_RETURNTRANSFER => true ,
29- CURLOPT_POST => true ,
30- CURLOPT_POSTFIELDS => http_build_query ($ fields ),
31- CURLOPT_USERPWD => $ c ['stripe_secret_key ' ] . ': ' , // secret key as basic-auth user
32- CURLOPT_TIMEOUT => 20 ,
3350 ]);
34- $ resp = curl_exec ($ ch );
35- if ($ resp === false ) {
36- return null ;
37- }
38- $ data = json_decode ($ resp , true );
3951 return $ data ['url ' ] ?? null ;
4052}
4153
@@ -56,8 +68,8 @@ function stripe_verify_webhook(string $payload, string $sigHeader, string
5668 if (!$ t || !$ sigs ) {
5769 return false ;
5870 }
59- // Reject anything older than 5 minutes to limit replay.
60- if (abs (time () - (int )$ t ) > 300 ) {
71+ // Reject anything too old to limit replay.
72+ if (abs (time () - (int )$ t ) > STRIPE_WEBHOOK_TOLERANCE ) {
6173 return false ;
6274 }
6375 $ expected = hash_hmac ('sha256 ' , $ t . '. ' . $ payload , $ secret );
@@ -77,56 +89,24 @@ function stripe_portal_url(array $user): ?string
7789 if (empty ($ user ['stripe_id ' ]) || !stripe_enabled ()) {
7890 return null ;
7991 }
80- $ fields = [
92+ $ data = stripe_api ( ' POST ' , ' billing_portal/sessions ' , [
8193 'customer ' => $ user ['stripe_id ' ],
8294 'return_url ' => $ c ['base_url ' ] . '/app ' ,
83- ];
84- $ ch = curl_init ('https://api.stripe.com/v1/billing_portal/sessions ' );
85- curl_setopt_array ($ ch , [
86- CURLOPT_RETURNTRANSFER => true ,
87- CURLOPT_POST => true ,
88- CURLOPT_POSTFIELDS => http_build_query ($ fields ),
89- CURLOPT_USERPWD => $ c ['stripe_secret_key ' ] . ': ' ,
90- CURLOPT_TIMEOUT => 20 ,
9195 ]);
92- $ resp = curl_exec ($ ch );
93- if ($ resp === false ) {
94- return null ;
95- }
96- $ data = json_decode ($ resp , true );
9796 return $ data ['url ' ] ?? null ;
9897}
9998
10099// Cancel any active subscriptions for the user's Stripe customer. No-op when
101100// Stripe is unconfigured or the user has no customer id.
102101function stripe_cancel_subscription (array $ user ): void
103102{
104- $ c = config ();
105103 if (empty ($ user ['stripe_id ' ]) || !stripe_enabled ()) {
106104 return ;
107105 }
108- $ ch = curl_init ('https://api.stripe.com/v1/subscriptions?customer= ' . urlencode ((string )$ user ['stripe_id ' ]) . '&status=active ' );
109- curl_setopt_array ($ ch , [
110- CURLOPT_RETURNTRANSFER => true ,
111- CURLOPT_USERPWD => $ c ['stripe_secret_key ' ] . ': ' ,
112- CURLOPT_TIMEOUT => 20 ,
113- ]);
114- $ resp = curl_exec ($ ch );
115- if ($ resp === false ) {
116- return ;
117- }
118- $ list = json_decode ($ resp , true );
106+ $ list = stripe_api ('GET ' , 'subscriptions?customer= ' . urlencode ((string )$ user ['stripe_id ' ]) . '&status=active ' );
119107 foreach ($ list ['data ' ] ?? [] as $ sub ) {
120- if (empty ($ sub ['id ' ])) {
121- continue ;
108+ if (! empty ($ sub ['id ' ])) {
109+ stripe_api ( ' DELETE ' , ' subscriptions/ ' . $ sub [ ' id ' ]) ;
122110 }
123- $ del = curl_init ('https://api.stripe.com/v1/subscriptions/ ' . $ sub ['id ' ]);
124- curl_setopt_array ($ del , [
125- CURLOPT_RETURNTRANSFER => true ,
126- CURLOPT_CUSTOMREQUEST => 'DELETE ' ,
127- CURLOPT_USERPWD => $ c ['stripe_secret_key ' ] . ': ' ,
128- CURLOPT_TIMEOUT => 20 ,
129- ]);
130- curl_exec ($ del );
131111 }
132112}
0 commit comments