1#![allow(clippy::module_name_repetitions)]
2
3use super::{error, error::Result, PurchaseResponse, UnityPurchaseReceipt};
4use chrono::{DateTime, Utc};
5use hyper::{body, Body, Client, Request};
6use hyper_tls::HttpsConnector;
7use serde::{de::Error, Deserialize, Serialize};
8use yup_oauth2::{ServiceAccountAuthenticator, ServiceAccountKey};
9
10#[derive(Default, Debug, Clone, Serialize, Deserialize)]
14pub struct GoogleResponse {
15 #[serde(rename = "expiryTimeMillis")]
17 pub expiry_time: Option<String>,
18 #[serde(rename = "priceCurrencyCode")]
20 pub price_currency_code: Option<String>,
21 #[serde(rename = "priceAmountMicros")]
23 pub price_amount_micros: Option<String>,
24 #[serde(rename = "orderId")]
26 pub order_id: String,
27 #[serde(rename = "purchaseType")]
29 pub purchase_type: Option<i64>,
30 #[serde(rename = "productId")]
31 pub product_id: Option<String>,
33 #[serde(rename = "purchaseState")]
34 pub purchase_state: Option<u32>,
36}
37
38#[derive(Serialize, Deserialize)]
40pub struct GooglePlayData {
41 pub json: String,
43 pub signature: String,
45 #[serde(rename = "skuDetails")]
47 pub sku_details: String,
48}
49
50#[derive(Deserialize)]
52pub enum SkuType {
53 #[serde(rename = "subs")]
55 Subs,
56 #[serde(rename = "inapp")]
58 Inapp,
59}
60
61impl GooglePlayData {
62 pub fn from(payload: &str) -> Result<Self> {
64 Ok(serde_json::from_str(payload)?)
65 }
66
67 pub fn get_uri(&self, sku_type: &SkuType) -> Result<String> {
69 let parameters: GooglePlayDataJson = serde_json::from_str(&self.json)?;
70
71 tracing::debug!(
72 "google purchase/receipt params, package: {}, productId: {}, token: {}",
73 ¶meters.package_name,
74 ¶meters.product_id,
75 ¶meters.token,
76 );
77
78 match sku_type {
79 SkuType::Subs => Ok(format!(
80 "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{}/purchases/subscriptions/{}/tokens/{}",
81 parameters.package_name, parameters.product_id, parameters.token
82 )),
83 SkuType::Inapp => Ok(format!(
84 "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{}/purchases/products/{}/tokens/{}",
85 parameters.package_name, parameters.product_id, parameters.token
86 ))
87 }
88 }
89
90 pub fn get_sku_details(&self) -> Result<SkuDetails> {
92 Ok(serde_json::from_str(&self.sku_details)?)
93 }
94}
95
96#[derive(Deserialize)]
97pub struct SkuDetails {
98 #[serde(rename = "type")]
99 pub sku_type: SkuType,
100}
101
102#[derive(Serialize, Deserialize)]
103pub struct GooglePlayDataJson {
104 #[serde(rename = "packageName")]
105 pub package_name: String,
106 #[serde(rename = "productId")]
107 pub product_id: String,
108 #[serde(rename = "purchaseToken")]
109 pub token: String,
110 pub acknowledged: bool,
111 #[serde(rename = "autoRenewing")]
112 pub auto_renewing: Option<bool>,
113 #[serde(rename = "purchaseTime")]
114 pub purchase_time: i64,
115 #[serde(rename = "orderId")]
116 pub order_id: String,
117 #[serde(rename = "purchaseState")]
118 pub purchase_state: i64, }
120
121pub async fn fetch_google_receipt_data<S: AsRef<[u8]> + Send>(
125 receipt: &UnityPurchaseReceipt,
126 secret: S,
127) -> Result<GoogleResponse> {
128 let data = GooglePlayData::from(&receipt.payload)?;
129 let sku_details = data.get_sku_details()?;
130 let uri = data.get_uri(&sku_details.sku_type)?;
131
132 let service_account_key = get_service_account_key(secret)?;
133
134 fetch_google_receipt_data_with_uri(Some(&service_account_key), uri, Some(data)).await
135}
136
137pub async fn fetch_google_receipt_data_with_uri(
141 service_account_key: Option<&ServiceAccountKey>,
142 uri: String,
143 data: Option<GooglePlayData>,
144) -> Result<GoogleResponse> {
145 let https = HttpsConnector::new();
146 let client = Client::builder().build::<_, hyper::Body>(https);
147
148 tracing::debug!(
149 "validate google parameters, service_account_key: {}, uri: {}",
150 service_account_key.map_or(&"key not set".to_string(), |key| &key.client_email),
151 uri.clone()
152 );
153
154 let req = if let Some(key) = service_account_key {
155 let authenticator = ServiceAccountAuthenticator::builder(key.clone())
156 .build()
157 .await?;
158
159 let scopes = &["https://www.googleapis.com/auth/androidpublisher"];
160 let auth_token = authenticator.token(scopes).await?;
161
162 Request::builder()
163 .method("GET")
164 .header(
165 "Authorization",
166 format!("Bearer {}", auth_token.as_str()).as_str(),
167 )
168 .uri(uri)
169 .body(Body::empty())
170 } else {
171 Request::builder()
172 .method("GET")
173 .uri(format!("{}/test", uri).as_str())
174 .body(Body::empty())
175 }?;
176
177 let response = client.request(req).await?;
178 let buf = body::to_bytes(response).await?;
179 let string = String::from_utf8(buf.to_vec())?.replace('\n', "");
180 tracing::debug!("Google response: {}", &string);
181 let mut response: GoogleResponse = serde_json::from_slice(&buf).map_err(|err| {
182 error::Error::SerdeError(serde_json::Error::custom(format!(
183 "Failed to deserialize google response. Was the service account key set? Error message: {}", err)
184 ))
185 })?;
186
187 if response.product_id.is_none() {
188 if let Some(data) = data {
189 tracing::info!("Product id was not set in the response, getting from unity metadata");
190 let parameters: GooglePlayDataJson = serde_json::from_str(&data.json)?;
191
192 response.product_id = Some(parameters.product_id);
193 }
194 }
195
196 Ok(response)
197}
198
199pub fn validate_google_subscription(
203 response: &GoogleResponse,
204 now: DateTime<Utc>,
205) -> Result<PurchaseResponse> {
206 let expiry_time = response
207 .expiry_time
208 .clone()
209 .unwrap_or_default()
210 .parse::<i64>()?;
211 let now = now.timestamp_millis();
212 let valid = expiry_time > now;
213
214 tracing::info!("google receipt verification, valid: {}, now: {}, order_id: {}, expiry_time: {:?}, price_currency_code: {:?}, price_amount_micros: {:?}",
215 valid,
216 now,
217 response.order_id,
218 response.expiry_time,
219 response.price_currency_code,
220 response.price_amount_micros
221 );
222
223 Ok(PurchaseResponse {
224 valid,
225 product_id: response.product_id.clone(),
226 })
227}
228
229#[must_use]
230pub fn validate_google_package(response: &GoogleResponse) -> PurchaseResponse {
232 let valid = response.purchase_state.filter(|i| *i == 0).is_some();
233 tracing::info!(
234 "google receipt verification, valid: {}, order_id: {}",
235 valid,
236 response.order_id,
237 );
238
239 PurchaseResponse {
240 valid,
241 product_id: response.product_id.clone(),
242 }
243}
244
245pub fn get_service_account_key<S: AsRef<[u8]>>(secret: S) -> Result<ServiceAccountKey> {
246 Ok(serde_json::from_slice(secret.as_ref())?)
247}