v0.2.0:3 — durable payment-provider switching (Option B)

Closes the gap from :2 where Connect Zaprite swapped the
in-memory provider but BTCPay would silently re-take active on
the next daemon restart (because the boot-time loader picked
BTCPay first whenever btcpay_config was present, regardless of
operator intent).

What changed:

**New settings key `active_payment_provider`** in the existing
settings table. Records the operator's last explicit choice
('btcpay' | 'zaprite' | NULL = no preference). Both
btcpay_config and zaprite_config can coexist; the flag is what
determines which one the daemon loads.

**Boot-time loader respects the preference.** main.rs now reads
the flag at startup. If set to 'zaprite', Zaprite wins; if set to
'btcpay', BTCPay wins; if unset (legacy installs), falls back to
the previous BTCPay-first ordering. Cross-load fallbacks log a
WARN and try the other provider — operators with a stale flag
pointing at a wiped config don't boot unconfigured.

**Connect endpoints write the preference.**
- finish_connect (BTCPay) now sets the flag to 'btcpay' on
  successful authorize-callback completion.
- ZapriteAuthorize::connect now sets the flag to 'zaprite' on
  successful API-key validation.
- Both Disconnect endpoints clear the flag IF it pointed at the
  provider being disconnected — but leave it alone if it pointed
  at the OTHER provider (different operator intent).

**New endpoints for fast switching without re-Connect:**
- GET /v1/admin/payment-provider/status — both configs' state +
  current preference + runtime active provider, in one call.
- POST /v1/admin/payment-provider/activate { provider: "btcpay" |
  "zaprite" } — flips the active provider and the flag together,
  without going through the full Connect flow. 400 if the named
  provider isn't configured (operator must run Connect first).

**New StartOS Actions** under existing groups:
- "Activate BTCPay" (in BTCPay group)
- "Activate Zaprite" (in Zaprite group)
Both call the new activate endpoint. Operators with both
providers configured can flip back and forth in one click.

**Test:** payment_provider_preference_round_trip pre-seeds both
configs, walks through Activate-Zaprite → Activate-BTCPay →
attempt-Activate-on-wiped-config → bad-provider-name → manual
write/read of the preference key. Pins the contract.

Test count: 42 (was 41; +1).

Migration not needed — settings table from 0005 already has the
key/value/updated_at shape we need.
This commit is contained in:
Grant
2026-05-08 16:51:15 -05:00
parent 0a76c9d121
commit ec2b21d8f7
9 changed files with 519 additions and 14 deletions
@@ -308,6 +308,19 @@ async fn finish_connect(state: &AppState, state_token: &str, api_key: &str) -> A
.with_public_base(state.config.btcpay_public_url.clone()), .with_public_base(state.config.btcpay_public_url.clone()),
); );
state.set_payment_provider(provider).await; state.set_payment_provider(provider).await;
// Persist active-provider preference so the boot-time loader
// picks BTCPay on next restart even if Zaprite's config row
// is also still in the DB. Failure here is non-fatal (BTCPay
// is the historical default, so the fallback loader picks it
// anyway) but logged.
if let Err(e) = crate::payment::write_active_provider_preference(
&state.db,
crate::payment::ProviderKind::Btcpay,
)
.await
{
tracing::warn!(error = %e, "failed to record BTCPay as active payment provider");
}
tracing::info!( tracing::info!(
store = %store.id, store = %store.id,
@@ -392,6 +405,21 @@ pub async fn disconnect(
// attempts return BtcpayNotConfigured cleanly. // attempts return BtcpayNotConfigured cleanly.
state.clear_payment_provider().await; state.clear_payment_provider().await;
// If BTCPay was the recorded active-provider preference, clear
// it. Don't blindly clear if it was Zaprite — different operator
// intent.
if matches!(
crate::payment::read_active_provider_preference(&state.db).await,
Some(crate::payment::ProviderKind::Btcpay)
) {
let _ = crate::db::repo::settings_set(
&state.db,
crate::payment::SETTING_ACTIVE_PROVIDER,
None,
)
.await;
}
let _ = crate::db::repo::insert_audit( let _ = crate::db::repo::insert_audit(
&state.db, &state.db,
"admin_api_key", "admin_api_key",
+12
View File
@@ -71,6 +71,7 @@ pub mod tier;
pub mod validate; pub mod validate;
pub mod community; pub mod community;
pub mod db_info; pub mod db_info;
pub mod payment_provider;
pub mod rates_admin; pub mod rates_admin;
pub mod recover; pub mod recover;
pub mod zaprite_authorize; pub mod zaprite_authorize;
@@ -245,6 +246,17 @@ pub fn router(state: AppState) -> Router {
"/v1/admin/zaprite/status", "/v1/admin/zaprite/status",
get(zaprite_authorize::status), get(zaprite_authorize::status),
) )
// Provider-agnostic active-payment-provider control.
// Operators with both BTCPay and Zaprite configured can flip
// the active one without re-running Connect.
.route(
"/v1/admin/payment-provider/status",
get(payment_provider::status),
)
.route(
"/v1/admin/payment-provider/activate",
post(payment_provider::activate),
)
// Zaprite webhook landing — operator points Zaprite's // Zaprite webhook landing — operator points Zaprite's
// webhook setting at this URL. Same handler as // webhook setting at this URL. Same handler as
// /v1/btcpay/webhook because the underlying validate_webhook // /v1/btcpay/webhook because the underlying validate_webhook
@@ -0,0 +1,139 @@
//! Active-provider swap endpoint.
//!
//! When an operator has both BTCPay AND Zaprite configured (i.e.,
//! they ran Connect on both at some point), this lets them flip
//! the active one without re-authorizing. The Connect flows are
//! still where credentials live; this endpoint only changes which
//! credentials the daemon currently routes through.
use crate::api::admin::{request_context, require_admin};
use crate::api::AppState;
use crate::error::{AppError, AppResult};
use crate::payment::{
self, btcpay::BtcpayProvider, zaprite::ZapriteProvider, ProviderKind,
};
use axum::{extract::State, http::HeaderMap, Json};
use serde::Deserialize;
use serde_json::{json, Value};
use std::sync::Arc;
#[derive(Debug, Deserialize)]
pub struct ActivateReq {
/// `'btcpay'` or `'zaprite'`. Other values → 400.
pub provider: String,
}
/// `GET /v1/admin/payment-provider/status` — both providers'
/// configuration state at a glance, plus the active preference.
/// Lets the SPA render a "BTCPay [active] / Zaprite [configured,
/// not active]" header without two separate fetches.
pub async fn status(
State(state): State<AppState>,
headers: HeaderMap,
) -> AppResult<Json<Value>> {
require_admin(&state, &headers)?;
let btcpay_configured = crate::btcpay::config::load(&state.db)
.await
.map(|o| o.is_some())
.unwrap_or(false);
let zaprite_configured = payment::zaprite::config::load(&state.db)
.await
.map(|o| o.is_some())
.unwrap_or(false);
let preference = payment::read_active_provider_preference(&state.db).await;
let active_runtime = match state.payment.read().await.as_ref() {
Some(p) => Some(p.kind().as_str().to_string()),
None => None,
};
Ok(Json(json!({
"btcpay_configured": btcpay_configured,
"zaprite_configured": zaprite_configured,
"preferred": preference.map(|k| k.as_str().to_string()),
"active": active_runtime,
})))
}
/// `POST /v1/admin/payment-provider/activate` — swap the active
/// provider to whichever already-configured one the operator
/// names. 400 if the named provider isn't configured (run Connect
/// first).
pub async fn activate(
State(state): State<AppState>,
headers: HeaderMap,
Json(req): Json<ActivateReq>,
) -> AppResult<Json<Value>> {
let actor_hash = require_admin(&state, &headers)?;
let (ip, ua) = request_context(&headers);
let kind = match req.provider.to_lowercase().as_str() {
"btcpay" => ProviderKind::Btcpay,
"zaprite" => ProviderKind::Zaprite,
other => {
return Err(AppError::BadRequest(format!(
"unknown provider '{other}'; accepted: btcpay, zaprite"
)))
}
};
// Build the provider from its persisted config. Refuse if the
// config row isn't there — operator has to run Connect first.
match kind {
ProviderKind::Btcpay => {
let cfg = crate::btcpay::config::load(&state.db)
.await
.map_err(AppError::Internal)?
.ok_or_else(|| {
AppError::BadRequest(
"BTCPay not configured. Run Connect BTCPay first.".into(),
)
})?;
let client = crate::btcpay::client::BtcpayClient::new(
&cfg.base_url,
&cfg.api_key,
&cfg.store_id,
);
let provider = Arc::new(
BtcpayProvider::new(client, cfg.webhook_secret)
.with_public_base(state.config.btcpay_public_url.clone()),
);
state.set_payment_provider(provider).await;
}
ProviderKind::Zaprite => {
let cfg = payment::zaprite::config::load(&state.db)
.await
.map_err(|e| AppError::Internal(anyhow::anyhow!("{e:#}")))?
.ok_or_else(|| {
AppError::BadRequest(
"Zaprite not configured. Run Connect Zaprite first.".into(),
)
})?;
let client = payment::zaprite::ZapriteClient::new(&cfg.base_url, &cfg.api_key);
let provider = Arc::new(ZapriteProvider::new(client));
state.set_payment_provider(provider).await;
}
}
// Persist the preference so the boot-time loader picks the
// same one on next restart.
payment::write_active_provider_preference(&state.db, kind)
.await
.map_err(|e| AppError::Internal(anyhow::anyhow!("write preference: {e:#}")))?;
let _ = crate::db::repo::insert_audit(
&state.db,
"admin_api_key",
Some(&actor_hash),
"payment_provider.activate",
Some("payment_provider"),
Some(kind.as_str()),
ip.as_deref(),
ua.as_deref(),
&json!({ "provider": kind.as_str() }),
)
.await;
Ok(Json(json!({
"ok": true,
"active": kind.as_str(),
})))
}
@@ -95,6 +95,15 @@ pub async fn connect(
state state
.set_payment_provider(Arc::new(provider)) .set_payment_provider(Arc::new(provider))
.await; .await;
// Persist the operator's preference so the boot-time loader
// picks Zaprite on next restart, even if BTCPay's config row
// is also still in the DB.
crate::payment::write_active_provider_preference(
&state.db,
crate::payment::ProviderKind::Zaprite,
)
.await
.map_err(|e| AppError::Internal(anyhow::anyhow!("write provider preference: {e:#}")))?;
let _ = crate::db::repo::insert_audit( let _ = crate::db::repo::insert_audit(
&state.db, &state.db,
@@ -146,6 +155,21 @@ pub async fn disconnect(
AppError::Internal(anyhow::anyhow!("clear zaprite_config: {e:#}")) AppError::Internal(anyhow::anyhow!("clear zaprite_config: {e:#}"))
})?; })?;
state.clear_payment_provider().await; state.clear_payment_provider().await;
// If the active-provider preference was Zaprite, clear it.
// Don't blindly clear if it was BTCPay — that's a different
// operator's choice we shouldn't undo just because they ran
// Disconnect Zaprite.
if matches!(
crate::payment::read_active_provider_preference(&state.db).await,
Some(crate::payment::ProviderKind::Zaprite)
) {
let _ = crate::db::repo::settings_set(
&state.db,
crate::payment::SETTING_ACTIVE_PROVIDER,
None,
)
.await;
}
let _ = crate::db::repo::insert_audit( let _ = crate::db::repo::insert_audit(
&state.db, &state.db,
+46 -13
View File
@@ -58,20 +58,53 @@ async fn main() -> anyhow::Result<()> {
); );
// --- payment provider (may be None until operator connects) --- // --- payment provider (may be None until operator connects) ---
// Resolution order: BTCPay first (the original / default), then // Resolution order:
// Zaprite. If both are configured, BTCPay wins — operators with // 1. operator's explicit preference from the
// both connected get sat-priced flows through BTCPay; the // active_payment_provider setting (set by the most recent
// future v0.3 multi-provider routing will let policies pick // Connect or Activate action),
// which provider handles which payment rail. // 2. fallback for legacy installs without the setting:
let provider: Option<Arc<dyn payment::PaymentProvider>> = { // BTCPay first, Zaprite second. Once we ship v0.3 with the
if let Some(p) = load_btcpay_provider(&pool, &cfg).await { // multi-provider routing layer this fallback retires.
let arc: Arc<dyn payment::PaymentProvider> = Arc::new(p); let preferred = payment::read_active_provider_preference(&pool).await;
Some(arc) let provider: Option<Arc<dyn payment::PaymentProvider>> = match preferred {
} else if let Some(p) = load_zaprite_provider(&pool).await { Some(payment::ProviderKind::Zaprite) => {
let arc: Arc<dyn payment::PaymentProvider> = Arc::new(p); // Operator explicitly chose Zaprite. Try Zaprite; if it
Some(arc) // can't be loaded (e.g., the row was deleted out from
} else { // under the setting), fall through to BTCPay rather
// than booting unconfigured.
load_zaprite_provider(&pool)
.await
.map(|p| Arc::new(p) as Arc<dyn payment::PaymentProvider>)
.or_else(|| {
tracing::warn!(
"active_payment_provider=zaprite but zaprite_config is missing; \
falling back to BTCPay"
);
None None
})
.or(load_btcpay_provider(&pool, &cfg)
.await
.map(|p| Arc::new(p) as Arc<dyn payment::PaymentProvider>))
}
Some(payment::ProviderKind::Btcpay) | None => {
// Either operator chose BTCPay, or no preference recorded
// yet (legacy install). Either way, BTCPay wins if
// configured; Zaprite as fallback.
load_btcpay_provider(&pool, &cfg)
.await
.map(|p| Arc::new(p) as Arc<dyn payment::PaymentProvider>)
.or_else(|| {
if preferred == Some(payment::ProviderKind::Btcpay) {
tracing::warn!(
"active_payment_provider=btcpay but btcpay_config is missing; \
falling back to Zaprite"
);
}
None
})
.or(load_zaprite_provider(&pool)
.await
.map(|p| Arc::new(p) as Arc<dyn payment::PaymentProvider>))
} }
}; };
match &provider { match &provider {
+40
View File
@@ -40,6 +40,46 @@ use std::any::Any;
pub mod btcpay; pub mod btcpay;
pub mod zaprite; pub mod zaprite;
/// Settings-table key that records which provider the operator
/// last activated. Used by the boot-time loader to pick which
/// provider to load when both `btcpay_config` and `zaprite_config`
/// are populated. Values: `'btcpay'` | `'zaprite'`. Absent means
/// "use whichever single provider is configured" (back-compat
/// for installs that pre-date this setting).
pub const SETTING_ACTIVE_PROVIDER: &str = "active_payment_provider";
/// Convenience getter for the active-provider setting. Returns
/// `Some(ProviderKind)` if the operator has explicitly chosen
/// one, `None` if they haven't (caller falls back to the
/// load-order heuristic).
pub async fn read_active_provider_preference(
pool: &sqlx::SqlitePool,
) -> Option<ProviderKind> {
match crate::db::repo::settings_get(pool, SETTING_ACTIVE_PROVIDER).await {
Ok(Some(s)) => match s.as_str() {
"btcpay" => Some(ProviderKind::Btcpay),
"zaprite" => Some(ProviderKind::Zaprite),
_ => None,
},
_ => None,
}
}
/// Persist the operator's active-provider preference. Called by
/// the connect endpoints (Connect BTCPay, Connect Zaprite) and
/// by the new "Activate <provider>" endpoint that flips between
/// already-configured providers without re-authorizing.
pub async fn write_active_provider_preference(
pool: &sqlx::SqlitePool,
kind: ProviderKind,
) -> anyhow::Result<()> {
let value = kind.as_str();
crate::db::repo::settings_set(pool, SETTING_ACTIVE_PROVIDER, Some(value))
.await
.map_err(|e| anyhow::anyhow!("write active provider preference: {e:#}"))?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ProviderKind { pub enum ProviderKind {
+137
View File
@@ -1204,6 +1204,143 @@ async fn paid_purchase_in_usd_records_listed_currency_and_rate() {
assert_eq!(row.4, 98_000); assert_eq!(row.4, 98_000);
} }
/// Active-provider preference round-trip. Pins the contract that
/// `Activate <provider>` flips both the in-memory provider AND the
/// persisted preference so the next daemon boot picks the same one.
///
/// Simulates the operator's lifecycle:
/// 1. Configure both BTCPay and Zaprite (both rows in DB)
/// 2. Activate Zaprite → preference flag = "zaprite"
/// 3. Activate BTCPay → preference flag = "btcpay"
/// 4. Disconnect BTCPay → preference flag cleared (because it
/// pointed at the wiped config)
/// 5. Disconnect Zaprite while preference was already "btcpay"
/// → preference NOT cleared (stays at "btcpay" because it
/// was pointing at a different provider)
#[tokio::test]
async fn payment_provider_preference_round_trip() {
use keysat::payment::{self, ProviderKind};
let (state, _tmp) = make_test_state().await;
let auth = format!("Bearer {}", TEST_ADMIN_KEY);
// Pre-seed both configs as if the operator had run Connect on
// each at some point. We bypass the actual Connect endpoints
// because they call out to BTCPay / Zaprite to validate the
// credentials, which we don't want to do in unit tests.
let now = Utc::now().to_rfc3339();
sqlx::query(
"INSERT INTO btcpay_config(id, base_url, api_key, store_id, webhook_id, \
webhook_secret, connected_at) \
VALUES(1, 'http://btcpay.test', 'btcpay-key', 'store-1', 'wh-1', \
'0123456789abcdef', ?)",
)
.bind(&now)
.execute(&state.db)
.await
.unwrap();
sqlx::query(
"INSERT INTO zaprite_config(id, api_key, base_url, webhook_id, connected_at, updated_at) \
VALUES(1, 'zaprite-key', 'https://api.zaprite.test', NULL, ?, ?)",
)
.bind(&now)
.bind(&now)
.execute(&state.db)
.await
.unwrap();
// Step 1: no preference recorded yet.
let pref = payment::read_active_provider_preference(&state.db).await;
assert_eq!(pref, None);
// Step 2: GET status surfaces both as configured, no active yet.
let req = build_request(
"GET",
"/v1/admin/payment-provider/status",
&[("authorization", &auth)],
None,
);
let resp = send(&state, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = body_json(resp).await;
assert_eq!(body["btcpay_configured"], true);
assert_eq!(body["zaprite_configured"], true);
assert!(body["preferred"].is_null());
// Step 3: Activate Zaprite. The endpoint reads the saved
// zaprite_config to build the provider — the saved key
// 'zaprite-key' won't talk to a real API but the activate
// path doesn't ping; that's only on Connect.
let req = build_request(
"POST",
"/v1/admin/payment-provider/activate",
&[("authorization", &auth)],
Some(json!({"provider": "zaprite"})),
);
let resp = send(&state, req).await;
assert_eq!(
resp.status(),
StatusCode::OK,
"activate zaprite should succeed when zaprite_config is present"
);
let pref = payment::read_active_provider_preference(&state.db).await;
assert_eq!(pref, Some(ProviderKind::Zaprite));
// Step 4: Activate BTCPay. Preference flips.
let req = build_request(
"POST",
"/v1/admin/payment-provider/activate",
&[("authorization", &auth)],
Some(json!({"provider": "btcpay"})),
);
let resp = send(&state, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let pref = payment::read_active_provider_preference(&state.db).await;
assert_eq!(pref, Some(ProviderKind::Btcpay));
// Step 5: Activate something that's not configured. Should 400.
sqlx::query("DELETE FROM zaprite_config WHERE id = 1")
.execute(&state.db)
.await
.unwrap();
let req = build_request(
"POST",
"/v1/admin/payment-provider/activate",
&[("authorization", &auth)],
Some(json!({"provider": "zaprite"})),
);
let resp = send(&state, req).await;
assert_eq!(
resp.status(),
StatusCode::BAD_REQUEST,
"activating an unconfigured provider must 400 with 'run Connect first'"
);
// Step 6: Bad provider name → 400.
let req = build_request(
"POST",
"/v1/admin/payment-provider/activate",
&[("authorization", &auth)],
Some(json!({"provider": "stripe"})),
);
let resp = send(&state, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
// Step 7: write_active_provider_preference invariant —
// explicit setting survives a re-read (durability across the
// simulated restart that the boot-time loader cares about).
payment::write_active_provider_preference(&state.db, ProviderKind::Btcpay)
.await
.unwrap();
let pref = payment::read_active_provider_preference(&state.db).await;
assert_eq!(pref, Some(ProviderKind::Btcpay));
payment::write_active_provider_preference(&state.db, ProviderKind::Zaprite)
.await
.unwrap();
let pref = payment::read_active_provider_preference(&state.db).await;
assert_eq!(pref, Some(ProviderKind::Zaprite));
}
/// Zaprite webhook authentication contract. /// Zaprite webhook authentication contract.
/// ///
/// Zaprite doesn't sign webhooks (verified May 2026 — no HMAC, /// Zaprite doesn't sign webhooks (verified May 2026 — no HMAC,
@@ -0,0 +1,89 @@
// Switch the active payment provider WITHOUT re-running Connect.
// Use case: operator has both BTCPay and Zaprite configured (i.e.,
// they ran Connect on both at some point) and wants to flip which
// one currently handles purchases. Two convenience actions —
// "Activate BTCPay" / "Activate Zaprite" — each POSTs to the
// daemon's /v1/admin/payment-provider/activate endpoint.
//
// If the named provider isn't yet configured, the daemon returns
// 400 with a "Run Connect first" message; we surface that to the
// operator unchanged.
import { sdk } from '../sdk'
import { store } from '../fileModels/store'
import { adminCall, LICENSING_URL } from '../utils'
async function activate(provider: 'btcpay' | 'zaprite') {
const storeData = await store.read().once()
if (!storeData) throw new Error('Store not initialized — restart the service.')
const resp = await adminCall(
LICENSING_URL,
storeData.admin_api_key,
'/v1/admin/payment-provider/activate',
{
method: 'POST',
body: JSON.stringify({ provider }),
},
)
if (!resp.ok) {
throw new Error(`Activate failed: HTTP ${resp.status}${await resp.text()}`)
}
const body = (await resp.json()) as { ok: true; active: string }
return body
}
export const activateBtcpay = sdk.Action.withoutInput(
'activate-btcpay',
async () => ({
name: 'Activate BTCPay',
description:
'Switch the active payment provider to BTCPay. Use this if both ' +
'BTCPay and Zaprite are already connected and you want to flip ' +
"which one handles new purchases. Existing license keys aren't " +
'affected by the swap.',
warning: null,
allowedStatuses: 'only-running',
group: 'BTCPay',
visibility: 'enabled',
}),
async () => {
const body = await activate('btcpay')
return {
version: '1',
title: 'BTCPay is now the active provider',
message:
`Active payment provider is now ${body.active}. New purchases ` +
`route through BTCPay. Zaprite remains configured but inactive ` +
`until you run "Activate Zaprite" or "Disconnect Zaprite".`,
result: null,
}
},
)
export const activateZaprite = sdk.Action.withoutInput(
'activate-zaprite',
async () => ({
name: 'Activate Zaprite',
description:
'Switch the active payment provider to Zaprite. Use this if both ' +
'BTCPay and Zaprite are already connected and you want to flip ' +
"which one handles new purchases. Existing license keys aren't " +
'affected by the swap.',
warning: null,
allowedStatuses: 'only-running',
group: 'Zaprite',
visibility: 'enabled',
}),
async () => {
const body = await activate('zaprite')
return {
version: '1',
title: 'Zaprite is now the active provider',
message:
`Active payment provider is now ${body.active}. New purchases ` +
`route through Zaprite. BTCPay remains configured but inactive ` +
`until you run "Activate BTCPay" or "Disconnect BTCPay".`,
result: null,
}
},
)
+3
View File
@@ -20,6 +20,7 @@
import { sdk } from '../sdk' import { sdk } from '../sdk'
import { activateLicense, showLicenseStatus } from './activateLicense' import { activateLicense, showLicenseStatus } from './activateLicense'
import { activateBtcpay, activateZaprite } from './activatePaymentProvider'
import { btcpayStatus, configureBtcpay, disconnectBtcpay } from './configureBtcpay' import { btcpayStatus, configureBtcpay, disconnectBtcpay } from './configureBtcpay'
import { configureZaprite, disconnectZaprite, zapriteStatus } from './configureZaprite' import { configureZaprite, disconnectZaprite, zapriteStatus } from './configureZaprite'
import { setOperatorName } from './setOperatorName' import { setOperatorName } from './setOperatorName'
@@ -33,10 +34,12 @@ export const actions = sdk.Actions.of()
// BTCPay setup (Bitcoin-only payments via your own BTCPay Server) // BTCPay setup (Bitcoin-only payments via your own BTCPay Server)
.addAction(configureBtcpay) .addAction(configureBtcpay)
.addAction(btcpayStatus) .addAction(btcpayStatus)
.addAction(activateBtcpay)
.addAction(disconnectBtcpay) .addAction(disconnectBtcpay)
// Zaprite setup (Bitcoin + fiat-card payments via Zaprite's broker) // Zaprite setup (Bitcoin + fiat-card payments via Zaprite's broker)
.addAction(configureZaprite) .addAction(configureZaprite)
.addAction(zapriteStatus) .addAction(zapriteStatus)
.addAction(activateZaprite)
.addAction(disconnectZaprite) .addAction(disconnectZaprite)
// Keysat self-license (Keysat-licenses-Keysat) // Keysat self-license (Keysat-licenses-Keysat)
.addAction(activateLicense) .addAction(activateLicense)