use std::cell::RefCell;
use std::collections::BTreeSet;
use std::rc::Rc;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Product {
pub id: String,
pub display_price: String,
pub title: String,
pub description: String,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StorePhase {
#[default]
Unavailable,
Connecting,
Ready,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct StoreState {
pub phase: StorePhase,
pub products: Vec<Product>,
pub owned: BTreeSet<String>,
pub error: Option<String>,
pub busy: bool,
}
impl StoreState {
pub fn owns(&self, product_id: &str) -> bool {
self.owned.contains(product_id)
}
pub fn product(&self, product_id: &str) -> Option<&Product> {
self.products.iter().find(|p| p.id == product_id)
}
pub fn display_price(&self, product_id: &str) -> Option<&str> {
self.product(product_id).map(|p| p.display_price.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PurchaseEvent {
Purchased(String),
Cancelled,
Pending,
Failed(String),
Restored {
restored: usize,
},
}
pub trait Purchases {
fn configure(&self, product_ids: &[&str]);
fn state(&self) -> StoreState;
fn purchase(&self, product_id: &str);
fn restore(&self);
fn take_event(&self) -> Option<PurchaseEvent>;
}
pub type PurchasesRef = Rc<dyn Purchases>;
struct NoPurchases;
impl Purchases for NoPurchases {
fn configure(&self, _product_ids: &[&str]) {}
fn state(&self) -> StoreState {
StoreState::default()
}
fn purchase(&self, _product_id: &str) {}
fn restore(&self) {}
fn take_event(&self) -> Option<PurchaseEvent> {
None
}
}
thread_local! {
static PLATFORM_PURCHASES: RefCell<Option<PurchasesRef>> = const { RefCell::new(None) };
}
pub fn set_platform_purchases(purchases: PurchasesRef) {
PLATFORM_PURCHASES.with(|cell| *cell.borrow_mut() = Some(purchases));
}
pub fn clear_platform_purchases() {
PLATFORM_PURCHASES.with(|cell| *cell.borrow_mut() = None);
}
pub fn purchases() -> PurchasesRef {
PLATFORM_PURCHASES
.with(|cell| cell.borrow().clone())
.unwrap_or_else(|| Rc::new(NoPurchases))
}
pub fn store_available() -> bool {
PLATFORM_PURCHASES.with(|cell| cell.borrow().is_some())
}
pub fn configure(product_ids: &[&str]) {
purchases().configure(product_ids);
}
pub fn store_state() -> StoreState {
purchases().state()
}
pub fn purchase(product_id: &str) {
purchases().purchase(product_id);
}
pub fn restore() {
purchases().restore();
}
pub fn take_event() -> Option<PurchaseEvent> {
purchases().take_event()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_backend_sells_nothing_and_owns_nothing() {
clear_platform_purchases();
let state = store_state();
assert_eq!(state.phase, StorePhase::Unavailable);
assert!(state.owned.is_empty());
assert!(!state.owns("com.example.pro"));
assert!(!store_available());
configure(&["com.example.pro"]);
purchase("com.example.pro");
restore();
assert_eq!(take_event(), None);
}
#[test]
fn installed_backend_answers_prices_and_ownership() {
struct Fake;
impl Purchases for Fake {
fn configure(&self, _product_ids: &[&str]) {}
fn state(&self) -> StoreState {
StoreState {
phase: StorePhase::Ready,
products: vec![Product {
id: "com.example.pro".into(),
display_price: "34,99 €".into(),
title: "Pro".into(),
description: "Everything unlocked".into(),
}],
owned: BTreeSet::from(["com.example.pro".to_string()]),
error: None,
busy: false,
}
}
fn purchase(&self, _product_id: &str) {}
fn restore(&self) {}
fn take_event(&self) -> Option<PurchaseEvent> {
Some(PurchaseEvent::Purchased("com.example.pro".into()))
}
}
set_platform_purchases(Rc::new(Fake));
let state = store_state();
assert_eq!(state.phase, StorePhase::Ready);
assert!(state.owns("com.example.pro"));
assert_eq!(state.display_price("com.example.pro"), Some("34,99 €"));
assert_eq!(state.display_price("com.example.nope"), None);
assert!(store_available());
assert_eq!(
take_event(),
Some(PurchaseEvent::Purchased("com.example.pro".into()))
);
clear_platform_purchases();
}
}