use std::collections::HashSet;
use bevy::prelude::Resource;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProductInfo {
pub id: String,
pub display_name: String,
pub display_price: String,
pub description: String,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ProductsState {
#[default]
Loading,
Ready,
Failed,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EntitlementsState {
#[default]
Checking,
Ready,
Failed,
}
#[derive(Resource, Default)]
pub struct StoreProducts {
pub state: ProductsState,
pub items: Vec<ProductInfo>,
}
impl StoreProducts {
pub fn get(&self, id: &str) -> Option<&ProductInfo> {
self.items.iter().find(|product| product.id == id)
}
}
#[derive(Resource, Default)]
pub struct Entitlements {
state: EntitlementsState,
owned: HashSet<String>,
}
impl Entitlements {
pub const fn state(&self) -> EntitlementsState {
self.state
}
pub const fn is_ready(&self) -> bool {
matches!(self.state, EntitlementsState::Ready)
}
pub fn owns(&self, id: &str) -> bool {
self.owned.contains(id)
}
pub fn iter(&self) -> impl Iterator<Item = &String> {
self.owned.iter()
}
pub(crate) fn apply(&mut self, snapshot: EntitlementSnapshot) -> bool {
let previous_state = self.state;
let previous_owned = self.owned.clone();
self.state = snapshot.state;
if snapshot.state != EntitlementsState::Checking {
self.owned = snapshot.product_ids.into_iter().collect();
}
self.state != previous_state || self.owned != previous_owned
}
pub(crate) fn fail(&mut self) -> bool {
if self.state == EntitlementsState::Failed {
return false;
}
self.state = EntitlementsState::Failed;
true
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct EntitlementSnapshot {
state: EntitlementsState,
product_ids: Vec<String>,
}
pub(crate) fn decode_entitlement_snapshot(
json: &str,
) -> Result<EntitlementSnapshot, serde_json::Error> {
serde_json::from_str(json)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_empty_snapshot_is_authoritative() {
let mut entitlements = Entitlements::default();
let snapshot =
decode_entitlement_snapshot(r#"{"state":"ready","product_ids":[]}"#).unwrap();
assert!(entitlements.apply(snapshot));
assert_eq!(entitlements.state(), EntitlementsState::Ready);
assert!(entitlements.is_ready());
assert!(!entitlements.owns("com.example.supporter"));
}
#[test]
fn revocation_replaces_owned_snapshot_with_empty() {
let mut entitlements = Entitlements::default();
let owned = decode_entitlement_snapshot(
r#"{"state":"ready","product_ids":["com.example.supporter"]}"#,
)
.unwrap();
let revoked = decode_entitlement_snapshot(r#"{"state":"ready","product_ids":[]}"#).unwrap();
assert!(entitlements.apply(owned));
assert!(entitlements.owns("com.example.supporter"));
assert!(entitlements.apply(revoked));
assert!(!entitlements.owns("com.example.supporter"));
}
#[test]
fn failed_refresh_retains_last_verified_ownership() {
let mut entitlements = Entitlements::default();
let owned = decode_entitlement_snapshot(
r#"{"state":"ready","product_ids":["com.example.supporter"]}"#,
)
.unwrap();
entitlements.apply(owned);
let failed = decode_entitlement_snapshot(
r#"{"state":"failed","product_ids":["com.example.supporter"]}"#,
)
.unwrap();
assert!(entitlements.apply(failed));
assert_eq!(entitlements.state(), EntitlementsState::Failed);
assert!(entitlements.owns("com.example.supporter"));
}
#[test]
fn malformed_snapshot_is_not_an_empty_entitlement() {
assert!(decode_entitlement_snapshot("[]").is_err());
assert!(
decode_entitlement_snapshot(r#"{"state":"ready","product_ids":"not-an-array"}"#)
.is_err()
);
}
}