use serde::de::DeserializeOwned;
use tauri::{AppHandle, Runtime, plugin::PluginApi};
use crate::models::{
GetProductsResponse, ProductStatus, Purchase, PurchaseRequest, RestorePurchasesResponse,
};
mod validation {
pub fn require_bundle() -> crate::Result<()> {
std::env::current_exe()
.ok()
.and_then(|exe| {
let macos = exe.parent()?;
let contents = macos.parent()?;
let bundle = contents.parent()?;
(macos.ends_with("MacOS")
&& contents.ends_with("Contents")
&& bundle.to_string_lossy().ends_with(".app"))
.then_some(())
})
.ok_or_else(|| {
crate::error::PluginInvokeError::InvokeRejected(crate::error::ErrorResponse {
code: None,
message: Some("IAP requires the app to run from a .app bundle.".to_string()),
data: (),
})
.into()
})
}
}
#[swift_bridge::bridge]
mod ffi {
pub enum FFIResult {
Err(String), }
extern "Rust" {
fn trigger(event: String, payload: String) -> Result<(), FFIResult>;
}
extern "Swift" {
#[swift_bridge(Sendable)]
type IapPlugin;
#[swift_bridge(init, swift_name = "initPlugin")]
fn init_plugin() -> IapPlugin;
async fn getProducts(
&self,
productIds: Vec<String>,
productType: String,
) -> Result<String, FFIResult>;
async fn purchase(
&self,
productId: String,
productType: String,
offerToken: Option<String>,
) -> Result<String, FFIResult>;
async fn restorePurchases(&self, productType: String) -> Result<String, FFIResult>;
async fn getProductStatus(
&self,
productId: String,
productType: String,
) -> Result<String, FFIResult>;
}
}
trait ParseFfiResponse {
fn parse<T: DeserializeOwned>(self) -> crate::Result<T>;
}
impl ParseFfiResponse for Result<String, ffi::FFIResult> {
fn parse<T: DeserializeOwned>(self) -> crate::Result<T> {
match self {
Ok(json) => serde_json::from_str(&json)
.map_err(|e| crate::error::PluginInvokeError::CannotDeserializeResponse(e).into()),
Err(ffi::FFIResult::Err(msg)) => Err(crate::error::PluginInvokeError::InvokeRejected(
crate::error::ErrorResponse {
code: None,
message: Some(msg),
data: (),
},
)
.into()),
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn trigger(event: String, payload: String) -> Result<(), ffi::FFIResult> {
crate::listeners::trigger(&event, &payload)
.map_err(|e| ffi::FFIResult::Err(format!("Failed to trigger event '{event}': {e}")))
}
#[allow(clippy::unnecessary_wraps)]
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
_api: &PluginApi<R, C>,
) -> crate::Result<Iap<R>> {
Ok(Iap {
_app: app.clone(),
plugin: ffi::IapPlugin::init_plugin(),
})
}
pub struct Iap<R: Runtime> {
_app: AppHandle<R>,
plugin: ffi::IapPlugin,
}
impl<R: Runtime> Iap<R> {
pub async fn get_products(
&self,
product_ids: Vec<String>,
product_type: String,
) -> crate::Result<GetProductsResponse> {
validation::require_bundle()?;
self.plugin
.getProducts(product_ids, product_type)
.await
.parse()
}
pub async fn purchase(&self, payload: PurchaseRequest) -> crate::Result<Purchase> {
validation::require_bundle()?;
self.plugin
.purchase(
payload.product_id,
payload.product_type,
payload.options.and_then(|opts| opts.offer_token),
)
.await
.parse()
}
pub async fn restore_purchases(
&self,
product_type: String,
) -> crate::Result<RestorePurchasesResponse> {
validation::require_bundle()?;
self.plugin.restorePurchases(product_type).await.parse()
}
#[allow(clippy::unused_async)]
pub async fn acknowledge_purchase(&self, _purchase_token: String) -> crate::Result<()> {
validation::require_bundle()?;
Ok(())
}
#[allow(clippy::unused_async)]
pub async fn consume_purchase(&self, _purchase_token: String) -> crate::Result<()> {
validation::require_bundle()?;
Ok(())
}
pub async fn get_product_status(
&self,
product_id: String,
product_type: String,
) -> crate::Result<ProductStatus> {
validation::require_bundle()?;
self.plugin
.getProductStatus(product_id, product_type)
.await
.parse()
}
}