simple_stripe/
lib.rs

1use anyhow::{Result, anyhow};
2use chrono::Utc;
3use hmac::{Hmac, Mac};
4use serde::Deserialize;
5use serde_json::Value;
6use sha2::Sha256;
7use std::collections::HashMap;
8
9const CREATE_SESSION_URL: &str = "https://api.stripe.com/v1/checkout/sessions";
10
11// signature code from `async-stripe`
12struct Signature<'r> {
13    t: i64,
14    v1: &'r str,
15}
16
17impl<'r> Signature<'r> {
18    fn parse(raw: &'r str) -> Result<Signature<'r>> {
19        let headers: HashMap<&str, &str> = raw
20            .split(',')
21            .map(|header| {
22                let mut key_and_value = header.split('=');
23                let key = key_and_value.next();
24                let value = key_and_value.next();
25                (key, value)
26            })
27            .filter_map(|(key, value)| match (key, value) {
28                (Some(key), Some(value)) => Some((key, value)),
29                _ => None,
30            })
31            .collect();
32        let t = headers
33            .get("t")
34            .and_then(|t| t.parse::<i64>().ok())
35            .ok_or(anyhow!("Bad signature t"))?;
36        let v1 = headers.get("v1").ok_or(anyhow!("Bad signature v1"))?;
37        Ok(Signature { t, v1 })
38    }
39}
40
41#[derive(Deserialize)]
42struct Event {
43    r#type: String,
44    data: Object,
45}
46
47#[derive(Deserialize)]
48struct Object {
49    object: Value,
50}
51
52/// simple event for one-time payment and subscription
53pub enum SimpleEvent {
54    // one-time payment complete: session_id, customer_id
55    PaymentComplete(String, String),
56    // session_id, customer_id
57    SubscriptionCreated(String, String),
58    // customer_id, prices_id
59    SubscriptionPaid(String, Vec<String>),
60    // customer_id, prices_id
61    SubscriptionDeleted(String, Vec<String>),
62    // not support event
63    None(String),
64}
65
66#[derive(Clone, Debug)]
67pub struct Stripe {
68    secret: String,
69    webhook: String,
70    prices: HashMap<String, String>,
71    success_url: Option<String>,
72    cancel_url: Option<String>,
73}
74
75impl Stripe {
76    /// init stripe with secret
77    pub fn init(secret: String, webhook: String) -> Self {
78        Self {
79            secret,
80            webhook,
81            prices: HashMap::new(),
82            success_url: None,
83            cancel_url: None,
84        }
85    }
86
87    /// add one-time payment price
88    pub fn add_payment_price(&mut self, id: &str) {
89        self.prices.insert(id.to_owned(), "payment".to_owned());
90    }
91
92    /// add subscription price
93    pub fn add_subscription_price(&mut self, id: &str) {
94        self.prices.insert(id.to_owned(), "subscription".to_owned());
95    }
96
97    /// set session success and cancel url
98    pub fn set_urls(&mut self, success: &str, cancel: &str) {
99        self.success_url = Some(success.to_owned());
100        self.cancel_url = Some(cancel.to_owned());
101    }
102
103    /// create session, return the session id and session url
104    pub async fn create_session(
105        &self,
106        uuid: &str,
107        email: Option<String>,
108        customer: Option<String>,
109        price: &str,
110        quantity: i32,
111    ) -> Result<(String, String)> {
112        let mode = self.prices.get(price).ok_or(anyhow!("No price"))?.to_owned();
113
114        let mut params = vec![
115            ("mode", mode),
116            ("line_items[0][price]", price.to_owned()),
117            ("line_items[0][quantity]", quantity.to_string()),
118        ];
119
120        if !uuid.is_empty() {
121            params.push(("client_reference_id", uuid.to_owned()));
122        }
123
124        if let Some(customer) = customer && customer.starts_with("cus_") {
125            params.push(("customer", customer));
126        } else if let Some(email) = email {
127            params.push(("customer_email", email));
128        }
129
130        if let Some(url) = &self.success_url {
131            params.push(("success_url", url.to_owned()));
132        }
133        if let Some(url) = &self.cancel_url {
134            params.push(("cancel_url", url.to_owned()));
135        }
136
137        let client = reqwest::Client::new();
138        let resp = client
139            .post(CREATE_SESSION_URL)
140            .basic_auth(self.secret.clone(), Some(""))
141            .form(&params)
142            .send()
143            .await?
144            .json::<Value>()
145            .await?;
146
147        match (resp.get("id"), resp.get("url")) {
148            (Some(id), Some(url)) => {
149                let s_i = id.as_str().unwrap_or_default().to_owned();
150                let s_u = url.as_str().unwrap_or_default().to_owned();
151                Ok((s_i, s_u))
152            }
153            _ => Err(anyhow!("Failed to create session for {uuid}: {}", resp["error"]["message"])),
154        }
155    }
156
157    /// check session is paid, if paid, return customer id, if not, return err
158    pub async fn retrieve(&self, _session: &str) -> Result<String> {
159        todo!()
160    }
161
162    /// verify signature and parse to SimpleEvent
163    pub fn simple_event(&self, body: &str, signature: &str) -> Result<SimpleEvent> {
164        // Get Stripe signature from header
165        let signature = Signature::parse(&signature)?;
166        let signed_payload = format!("{}.{}", signature.t, body);
167
168        // Compute HMAC with the SHA256 hash function, using endpoing secret as key
169        // and signed_payload string as the message.
170        let mut mac = Hmac::<Sha256>::new_from_slice(self.webhook.as_bytes()).map_err(|_| anyhow!("Bad Key"))?;
171        mac.update(signed_payload.as_bytes());
172
173        let sig = hex::decode(signature.v1).map_err(|_| anyhow!("Bad signature hex"))?;
174        mac.verify_slice(sig.as_slice()).map_err(|_| anyhow!("Bad signature verify"))?;
175
176        // Get current timestamp to compare to signature timestamp
177        let current_timestamp = Utc::now().timestamp();
178        if (current_timestamp - signature.t).abs() > 300 {
179            return Err(anyhow!("Bad signature time"));
180        }
181
182        let event: Event = serde_json::from_str(&body)?;
183        let se = match event.r#type.as_str() {
184            "checkout.session.completed" => {
185                // subscription created || one-time paid
186                let session = event.data.object["id"].as_str().unwrap_or_default().to_owned();
187                let customer = event.data.object["customer"].as_str().unwrap_or_default().to_owned();
188                let paid = event.data.object["payment_status"].as_str().unwrap_or_default();
189                let mode = event.data.object["mode"].as_str().unwrap_or_default();
190                if session.is_empty() || paid.is_empty() || mode.is_empty() {
191                    tracing::error!("Stripe checkout.session.completed changed!");
192                }
193                if paid == "paid" {
194                    match mode {
195                        "payment" => SimpleEvent::PaymentComplete(session, customer),
196                        "subscription" => SimpleEvent::SubscriptionCreated(session, customer),
197                        _ => SimpleEvent::None(format!("checkout.session.completed payment mode: {mode}")),
198                    }
199                } else {
200                    SimpleEvent::None(format!("checkout.session.completed payment status: {paid}"))
201                }
202            }
203            "invoice.paid" => {
204                // subscription paid
205                let customer = event.data.object["customer"].as_str().unwrap_or_default().to_owned();
206                let lines = event.data.object["lines"]["data"].as_array();
207
208                let mut prices = vec![];
209                if let Some(lines) = lines {
210                    for line in lines {
211                        if let Some(p) = line.pointer("/pricing/price_details/price") {
212                            prices.push(p.as_str().unwrap_or_default().to_owned());
213                        }
214                    }
215                }
216
217                if customer.is_empty() {
218                    tracing::error!("Stripe invoice.paid changed!");
219                }
220
221                SimpleEvent::SubscriptionPaid(customer, prices)
222            }
223            "invoice.payment_failed" => {
224                // subscription failed
225                let customer = event.data.object["customer"].as_str().unwrap_or_default().to_owned();
226                let lines = event.data.object["lines"]["data"].as_array();
227
228                let mut prices = vec![];
229                if let Some(lines) = lines {
230                    for line in lines {
231                        if let Some(p) = line.pointer("/pricing/price_details/price") {
232                            prices.push(p.as_str().unwrap_or_default().to_owned());
233                        }
234                    }
235                }
236
237                if customer.is_empty() {
238                    tracing::error!("Stripe invoice.payment_failed changed!");
239                }
240
241                SimpleEvent::SubscriptionDeleted(customer, prices)
242            }
243            "customer.subscription.deleted" => {
244                // subscription cancel & deleted
245                let customer = event.data.object["customer"].as_str().unwrap_or_default().to_owned();
246                let lines = event.data.object["lines"]["data"].as_array();
247
248                let mut prices = vec![];
249                if let Some(lines) = lines {
250                    for line in lines {
251                        if let Some(p) = line.pointer("/pricing/price_details/price") {
252                            prices.push(p.as_str().unwrap_or_default().to_owned());
253                        }
254                    }
255                }
256
257                if customer.is_empty() {
258                    tracing::error!("Stripe customer.subscription.deleted changed!");
259                }
260
261                SimpleEvent::SubscriptionDeleted(customer, prices)
262            }
263            _ => SimpleEvent::None(event.r#type)
264        };
265
266        Ok(se)
267    }
268}