iap/
google.rs

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/// See <https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions#SubscriptionPurchase>
11/// and <https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.products#ProductPurchase> for details
12/// on each field.
13#[derive(Default, Debug, Clone, Serialize, Deserialize)]
14pub struct GoogleResponse {
15    /// Time at which the subscription will expire, in milliseconds since the Epoch. Only set when it is a subscription
16    #[serde(rename = "expiryTimeMillis")]
17    pub expiry_time: Option<String>,
18    /// ISO 4217 currency code for the subscription price.
19    #[serde(rename = "priceCurrencyCode")]
20    pub price_currency_code: Option<String>,
21    /// Price of the subscription, not including tax. Price is expressed in micro-units, where 1,000,000 micro-units represents one unit of the currency.
22    #[serde(rename = "priceAmountMicros")]
23    pub price_amount_micros: Option<String>,
24    /// The order id of the latest recurring order associated with the purchase of the subscription.
25    #[serde(rename = "orderId")]
26    pub order_id: String,
27    /// The type of purchase of the subscription. This field is only set if this purchase was not made using the standard in-app billing flow. Possible values are: 0. Test (i.e. purchased from a license testing account) 1. Promo (i.e. purchased using a promo code)
28    #[serde(rename = "purchaseType")]
29    pub purchase_type: Option<i64>,
30    #[serde(rename = "productId")]
31    /// The inapp product SKU.
32    pub product_id: Option<String>,
33    #[serde(rename = "purchaseState")]
34    /// The purchase state of the order. Possible values are: 0. Purchased 1. Canceled 2. Pending
35    pub purchase_state: Option<u32>,
36}
37
38/// Metadata related to the purchase, used to populate the get request to google
39#[derive(Serialize, Deserialize)]
40pub struct GooglePlayData {
41    /// JSON data which contains the url parameters for the get request
42    pub json: String,
43    ///
44    pub signature: String,
45    /// Contains the `SkuType`
46    #[serde(rename = "skuDetails")]
47    pub sku_details: String,
48}
49
50/// enum for differentiating between product purchases and subscriptions
51#[derive(Deserialize)]
52pub enum SkuType {
53    /// Subscription
54    #[serde(rename = "subs")]
55    Subs,
56    /// Product
57    #[serde(rename = "inapp")]
58    Inapp,
59}
60
61impl GooglePlayData {
62    /// Construct the `GooglePlayData` from the `UnityPurchaseReceipt` payload
63    pub fn from(payload: &str) -> Result<Self> {
64        Ok(serde_json::from_str(payload)?)
65    }
66
67    /// Construct the uri for the get request from the parameters in the json field
68    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            &parameters.package_name,
74            &parameters.product_id,
75            &parameters.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    /// Extract the `SkuDetails`
91    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, //0 - unspecified, 1 - purchased, 2 - pending
119}
120
121/// Retrieves the response body from google
122/// # Errors
123/// Will return an error if authentication fails, if there is no response from the endpoint, or if the `payload` in the `UnityPurchaseReceipt` is malformed.
124pub 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
137/// Retrieves the google response with a specific uri, useful for running tests.
138/// # Errors
139/// Will return an error if authentication fails, if there is no response from the endpoint, or if the `payload` in the `UnityPurchaseReceipt` is malformed.
140pub 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
199/// Simply validates based on whether or not the subscription's expiration has passed.
200/// # Errors
201/// Will return an error if the `expiry_time` in the response cannot be parsed as an `i64`
202pub 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]
230/// Simply validates product purchase
231pub 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}