open-standards/aac/aac-rs/crates/aac-web/src/account.rs
Transaction Science 0af1c8a002 aac-web: optional symbol sets draw only once loaded, and only on a free plan
- Restoring a saved ARASAAC/Sclera choice draws the default set until the
  pack is in the page, then switches without saving (symbols is now derived
  from the person's choice, the pack's state and the plan).
- A download that brings nothing for a minute is stopped and ends in the
  usual message and fallback.
- A later choice overrides a pack still downloading (symbol_pending).
- A skin-tone change keeps the chosen set, even one not drawn for now.
- The offering comes from the signed-in plan at run time (account.rs):
  signed out or free -> FreeNoAds; any other plan -> Paid, which hides the
  optional sets and draws the open sets instead.
- Packs ask for persistent storage; "On this device" only when kept; kept
  packs can be removed, with the usual confirmation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 07:48:05 -04:00

73 lines
2.7 KiB
Rust

//! The account plan this device is signed in to, as far as the app needs
//! it: which symbol sets their licences allow here.
//!
//! The app needs no account. A deployment may offer one (aac.aurasense.ai:
//! AuraSense Account, with a free plan and paid ones). ARASAAC's and
//! Sclera's licences allow their pictograms only where nothing is paid for
//! (aac-core [`Offering`]), so the offering is worked out here, at run time,
//! from the plan:
//!
//! - signed out, or on the free plan: free with no adverts
//! ([`Offering::FreeNoAds`]), every set offered;
//! - on any paid plan: [`Offering::Paid`]. The optional sets are not offered,
//! and the open sets draw in their place (the person's choice is kept on
//! this device, for when they are free again).
//!
//! Signing in records the plan's id on this device ([`KEY_PLAN`]); signing
//! out removes it. An id this app does not know counts as paid: when in
//! doubt, the licences win.
use aac_core::symbols::Offering;
/// Where sign-in records the plan's id on this device (localStorage).
pub const KEY_PLAN: &str = "aac_account_plan";
/// The plan id of the free plan. Any other id is a paid plan.
pub const FREE_PLAN: &str = "free";
/// What the app is, as the symbol sets' licences see it, for a device
/// signed in to `plan` (`None`: signed out).
pub fn offering(plan: Option<&str>) -> Offering {
match plan.map(str::trim) {
None | Some("") | Some(FREE_PLAN) => Offering::FreeNoAds,
Some(_) => Offering::Paid,
}
}
/// The plan id recorded on this device, if signed in: stored as a JSON
/// string (`"personal"`), or as the bare id.
pub fn stored_plan() -> Option<String> {
let raw = crate::persistence::load_raw(KEY_PLAN)?;
let id = serde_json::from_str::<String>(&raw).unwrap_or(raw);
Some(id.trim().to_string()).filter(|id| !id.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signed_out_or_free_is_free_with_no_adverts() {
assert_eq!(offering(None), Offering::FreeNoAds);
assert_eq!(offering(Some("")), Offering::FreeNoAds);
assert_eq!(offering(Some("free")), Offering::FreeNoAds);
}
#[test]
fn every_other_plan_is_paid() {
for plan in ["personal", "professional", "research", "professional-research", "team", "Free ", "unknown"] {
if plan.trim() == FREE_PLAN {
continue;
}
assert_eq!(offering(Some(plan)), Offering::Paid, "{plan}");
}
}
#[test]
fn a_paid_plan_never_offers_a_noncommercial_set() {
for set in aac_core::symbols::SymbolSet::NONCOMMERCIAL {
assert!(!set.offered_in(offering(Some("personal"))));
assert!(set.offered_in(offering(None)));
}
}
}