#![allow(clippy::module_name_repetitions)]
use super::{error, error::Result, PurchaseResponse, UnityPurchaseReceipt};
use chrono::{DateTime, Utc};
use hyper::{body, Body, Client, Request};
use hyper_tls::HttpsConnector;
use serde::{de::Error, Deserialize, Serialize};
use yup_oauth2::{ServiceAccountAuthenticator, ServiceAccountKey};
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct GoogleResponse {
#[serde(rename = "expiryTimeMillis")]
pub expiry_time: Option<String>,
#[serde(rename = "priceCurrencyCode")]
pub price_currency_code: Option<String>,
#[serde(rename = "priceAmountMicros")]
pub price_amount_micros: Option<String>,
#[serde(rename = "orderId")]
pub order_id: String,
#[serde(rename = "purchaseType")]
pub purchase_type: Option<i64>,
#[serde(rename = "productId")]
pub product_id: Option<String>,
#[serde(rename = "purchaseState")]
pub purchase_state: Option<u32>,
}
#[derive(Serialize, Deserialize)]
pub struct GooglePlayData {
pub json: String,
pub signature: String,
#[serde(rename = "skuDetails")]
pub sku_details: String,
}
#[derive(Deserialize)]
pub enum SkuType {
#[serde(rename = "subs")]
Subs,
#[serde(rename = "inapp")]
Inapp,
}
impl GooglePlayData {
pub fn from(payload: &str) -> Result<Self> {
Ok(serde_json::from_str(payload)?)
}
pub fn get_uri(&self, sku_type: &SkuType) -> Result<String> {
let parameters: GooglePlayDataJson = serde_json::from_str(&self.json)?;
tracing::debug!(
"google purchase/receipt params, package: {}, productId: {}, token: {}",
¶meters.package_name,
¶meters.product_id,
¶meters.token,
);
match sku_type {
SkuType::Subs => Ok(format!(
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{}/purchases/subscriptions/{}/tokens/{}",
parameters.package_name, parameters.product_id, parameters.token
)),
SkuType::Inapp => Ok(format!(
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{}/purchases/products/{}/tokens/{}",
parameters.package_name, parameters.product_id, parameters.token
))
}
}
pub fn get_sku_details(&self) -> Result<SkuDetails> {
Ok(serde_json::from_str(&self.sku_details)?)
}
}
#[derive(Deserialize)]
pub struct SkuDetails {
#[serde(rename = "type")]
pub sku_type: SkuType,
}
#[derive(Serialize, Deserialize)]
pub struct GooglePlayDataJson {
#[serde(rename = "packageName")]
pub package_name: String,
#[serde(rename = "productId")]
pub product_id: String,
#[serde(rename = "purchaseToken")]
pub token: String,
pub acknowledged: bool,
#[serde(rename = "autoRenewing")]
pub auto_renewing: Option<bool>,
#[serde(rename = "purchaseTime")]
pub purchase_time: i64,
#[serde(rename = "orderId")]
pub order_id: String,
#[serde(rename = "purchaseState")]
pub purchase_state: i64, }
pub async fn fetch_google_receipt_data<S: AsRef<[u8]> + Send>(
receipt: &UnityPurchaseReceipt,
secret: S,
) -> Result<GoogleResponse> {
let data = GooglePlayData::from(&receipt.payload)?;
let sku_details = data.get_sku_details()?;
let uri = data.get_uri(&sku_details.sku_type)?;
let service_account_key = get_service_account_key(secret)?;
fetch_google_receipt_data_with_uri(Some(&service_account_key), uri, Some(data)).await
}
pub async fn fetch_google_receipt_data_with_uri(
service_account_key: Option<&ServiceAccountKey>,
uri: String,
data: Option<GooglePlayData>,
) -> Result<GoogleResponse> {
let https = HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);
tracing::debug!(
"validate google parameters, service_account_key: {}, uri: {}",
service_account_key.map_or(&"key not set".to_string(), |key| &key.client_email),
uri.clone()
);
let req = if let Some(key) = service_account_key {
let authenticator = ServiceAccountAuthenticator::builder(key.clone())
.build()
.await?;
let scopes = &["https://www.googleapis.com/auth/androidpublisher"];
let auth_token = authenticator.token(scopes).await?;
Request::builder()
.method("GET")
.header(
"Authorization",
format!("Bearer {}", auth_token.as_str()).as_str(),
)
.uri(uri)
.body(Body::empty())
} else {
Request::builder()
.method("GET")
.uri(format!("{}/test", uri).as_str())
.body(Body::empty())
}?;
let response = client.request(req).await?;
let buf = body::to_bytes(response).await?;
let string = String::from_utf8(buf.to_vec())?.replace('\n', "");
tracing::debug!("Google response: {}", &string);
let mut response: GoogleResponse = serde_json::from_slice(&buf).map_err(|err| {
error::Error::SerdeError(serde_json::Error::custom(format!(
"Failed to deserialize google response. Was the service account key set? Error message: {}", err)
))
})?;
if response.product_id.is_none() {
if let Some(data) = data {
tracing::info!("Product id was not set in the response, getting from unity metadata");
let parameters: GooglePlayDataJson = serde_json::from_str(&data.json)?;
response.product_id = Some(parameters.product_id);
}
}
Ok(response)
}
pub fn validate_google_subscription(
response: &GoogleResponse,
now: DateTime<Utc>,
) -> Result<PurchaseResponse> {
let expiry_time = response
.expiry_time
.clone()
.unwrap_or_default()
.parse::<i64>()?;
let now = now.timestamp_millis();
let valid = expiry_time > now;
tracing::info!("google receipt verification, valid: {}, now: {}, order_id: {}, expiry_time: {:?}, price_currency_code: {:?}, price_amount_micros: {:?}",
valid,
now,
response.order_id,
response.expiry_time,
response.price_currency_code,
response.price_amount_micros
);
Ok(PurchaseResponse {
valid,
product_id: response.product_id.clone(),
})
}
#[must_use]
pub fn validate_google_package(response: &GoogleResponse) -> PurchaseResponse {
let valid = response.purchase_state.filter(|i| *i == 0).is_some();
tracing::info!(
"google receipt verification, valid: {}, order_id: {}",
valid,
response.order_id,
);
PurchaseResponse {
valid,
product_id: response.product_id.clone(),
}
}
pub fn get_service_account_key<S: AsRef<[u8]>>(secret: S) -> Result<ServiceAccountKey> {
Ok(serde_json::from_slice(secret.as_ref())?)
}