Remove subscription levels; license valid = subscribed

This commit is contained in:
NeilClack
2026-08-15 00:54:24 -05:00
parent 8ca60de647
commit f14bf3e29a
19 changed files with 136 additions and 1534 deletions
+6 -24
View File
@@ -189,27 +189,9 @@ bool parseLicenseJson(const std::string& body, LicenseInfo& out) {
out.key = root.stringField("key");
out.email_address = root.stringField("email_address");
// subscription_level is an object {id,name,description} in the updated API
// and a bare level-name string in the current one. Tolerate both, plus a
// flat {subscription_level, subscription_level_id, subscription_level_description}.
const radiantwave::json::JsonValue* level = root.find("subscription_level");
if (level && level->type == radiantwave::json::JsonValue::Type::Object) {
out.subscription_level.id = static_cast<int>(level->numberField("id"));
out.subscription_level.name = level->stringField("name");
out.subscription_level.description = level->stringField("description");
} else if (level && level->type == radiantwave::json::JsonValue::Type::String) {
out.subscription_level.name = level->str;
}
if (out.subscription_level.name.empty()) {
out.subscription_level.name = root.stringField("subscription_level");
}
if (out.subscription_level.id == 0) {
out.subscription_level.id = static_cast<int>(root.numberField("subscription_level_id"));
}
if (out.subscription_level.description.empty()) {
out.subscription_level.description = root.stringField("subscription_level_description");
}
// Any other fields (e.g. the legacy `subscription_level`) are ignored. A
// successful validation itself is the "subscribed" flag; the kiosk treats
// a 2xx response as an active license.
return !out.key.empty();
}
@@ -251,12 +233,12 @@ bool parseCreateAccountJson(const std::string& body, AccountBody& out) {
return true;
}
AccountResult createAccount(const std::string& email, const std::string& subscription_level,
const std::string& name, const std::string& apiKey) {
AccountResult createAccount(const std::string& email, const std::string& name,
const std::string& apiKey) {
AccountResult result;
std::string jsonBody =
"{\"email\":\"" + email + "\",\"subscription_level\":\"" + subscription_level + "\"";
"{\"email\":\"" + email + "\"";
if (!name.empty()) {
jsonBody += ",\"name\":\"" + name + "\"";
}
+3 -12
View File
@@ -79,19 +79,9 @@ bool fetchColorPalettes(std::vector<ColorPalette>& out, const std::string& apiKe
// ─── POST /license ─────────────────────────────────────────────────────
// A subscription level as described by the portal's `SubscriptionLevel`
// table. `name` is always populated; `id`/`description` may be 0/empty when
// the API only reported the level name.
struct SubscriptionLevel {
int id = 0;
std::string name;
std::string description;
};
struct LicenseInfo {
std::string key;
std::string email_address;
SubscriptionLevel subscription_level;
};
// Parse the 200 JSON payload returned by the license endpoint.
@@ -107,6 +97,7 @@ struct LicenseResult {
// Validate a license key / email pair against the portal. Never throws; failures
// (network, HTTP error status) are reported through the returned LicenseResult.
// A 2xx response means the license is valid and the subscription is active.
LicenseResult validateLicense(const std::string& key, const std::string& email,
const std::string& apiKey = "");
@@ -131,7 +122,7 @@ struct AccountResult {
// Creates (or reuses) a user account and issues a license key. The key is emailed
// to the user and is NOT part of the response. `name` may be empty.
AccountResult createAccount(const std::string& email, const std::string& subscription_level,
const std::string& name = "", const std::string& apiKey = "");
AccountResult createAccount(const std::string& email, const std::string& name = "",
const std::string& apiKey = "");
} // namespace radiantwave::api
+4 -47
View File
@@ -2,9 +2,8 @@
#include "db/db_keys.h"
#include "network/api_client.h"
#include <cctype>
#include <cstdlib>
#include <chrono>
#include <cstdlib>
namespace radiantwave::license {
@@ -19,9 +18,7 @@ long long nowEpochSeconds() {
Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
const std::string& apiKey,
std::string* outLevelName,
std::string* outErrorMessage) {
if (outLevelName) outLevelName->clear();
if (outErrorMessage) outErrorMessage->clear();
const std::string email = db.getKV(radiantwave::db::KV_EMAIL_ADDRESS);
@@ -42,21 +39,10 @@ Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
return Result::Invalid;
}
const auto& level = result.info.subscription_level;
int levelId = level.id;
if (levelId <= 0) {
// The portal may only report the level name (e.g. "Elite"). Derive the
// numeric id from the name so the Home footer and proclamation gating
// can rank the level even when the response omits the id.
levelId = subscriptionLevelIdForName(level.name);
}
db.setKV(radiantwave::db::KV_SUBSCRIPTION_LEVEL_ID,
levelId > 0 ? std::to_string(levelId) : std::string());
db.setKV(radiantwave::db::KV_SUBSCRIPTION_LEVEL_NAME, level.name);
db.setKV(radiantwave::db::KV_SUBSCRIPTION_LEVEL_DESCRIPTION, level.description);
// A successful validation is the "subscribed" flag: the license is valid
// and the subscription is active. Only the "last verified" stamp is stored
// (it drives the offline grace window).
markLicenseVerified(db);
if (outLevelName) *outLevelName = level.name;
return Result::Ok;
}
@@ -76,33 +62,4 @@ bool isLicenseWithinGracePeriod(const radiantwave::db::DatabaseManager& db) {
return nowEpochSeconds() - stamp < GRACE_PERIOD_SECONDS;
}
int subscriptionLevelIdForName(const std::string& name) {
std::string lower;
lower.reserve(name.size());
for (char c : name) {
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
}
if (lower == "basic") return 1;
if (lower == "advanced") return 2;
if (lower == "elite") return 3;
return 0;
}
int getStoredSubscriptionLevelId(const radiantwave::db::DatabaseManager& db) {
const std::string raw = db.getKV(radiantwave::db::KV_SUBSCRIPTION_LEVEL_ID);
if (raw.empty()) return 0;
char* end = nullptr;
const long id = std::strtol(raw.c_str(), &end, 10);
return (end && *end == '\0' && id > 0) ? static_cast<int>(id) : 0;
}
std::string subscriptionLevelName(int levelId) {
switch (levelId) {
case 1: return "Basic";
case 2: return "Advanced";
case 3: return "Elite";
default: return "Subscription Level " + std::to_string(levelId);
}
}
} // namespace radiantwave::license
+4 -20
View File
@@ -5,11 +5,8 @@
namespace radiantwave::license {
// NOTE: the system_kv keys where the validated subscription level is persisted
// live in db/db_keys.h (KV_SUBSCRIPTION_LEVEL_*).
enum class Result {
Ok, // license valid; subscription level persisted to the DB
Ok, // license valid; subscription active
NotConfigured, // email and/or license key missing from the DB
Invalid, // API rejected the pair (bad key, wrong user, inactive, ...)
NetworkError, // the request never completed
@@ -22,29 +19,16 @@ enum class Result {
constexpr long long GRACE_PERIOD_SECONDS = 7LL * 24 * 60 * 60;
// Validates the stored license key against the stored email address via the
// portal API. On success the subscription level (id/name/description) is
// written to the DB; on failure the stored level is left untouched so a
// portal API. A successful validation is the device's "subscribed" flag — no
// separate subscription state is tracked. On failure nothing is written, so a
// previous good value survives a transient outage. Returns NotConfigured
// without touching the network when either value is missing. Never throws.
//
// `outLevelName` / `outErrorMessage` are optional out-params.
// `outErrorMessage` is an optional out-param.
Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
const std::string& apiKey = "",
std::string* outLevelName = nullptr,
std::string* outErrorMessage = nullptr);
// Returns the subscription level id currently stored after a successful
// license validation. Returns 0 when no validated license is on record.
int getStoredSubscriptionLevelId(const radiantwave::db::DatabaseManager& db);
// Human-readable name for a subscription level id (Basic / Advanced / Elite).
// Falls back to a generic label for unknown ids.
std::string subscriptionLevelName(int levelId);
// Numeric id for a subscription level name (case-insensitive match against
// Basic / Advanced / Elite). Returns 0 for an unknown or empty name.
int subscriptionLevelIdForName(const std::string& name);
// Records the current time as the "last verified" stamp of the license. Called
// after every successful server validation so the offline grace window starts
// (or is extended) from the most recent confirmation.
-2
View File
@@ -67,8 +67,6 @@ bool parseProclamationsJson(const std::string& body,
rec.content = stripHtml(entry.stringField("content"));
rec.category_id = static_cast<int>(entry.numberField("categoryId"));
rec.solfeggio_frequency_id = static_cast<int>(entry.numberField("solfeggioFrequencyId"));
rec.required_level_id = static_cast<int>(entry.numberField("subscriptionLevelId"));
if (rec.required_level_id <= 0) rec.required_level_id = 1;
out.push_back(std::move(rec));
}
return true;