Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ graph TB
BACKUPS --> WALLET_BACKUP
BACKUPS --> WALLETS
BTC_PRICE --> SETTINGS
ONBOARDING --> BACKUPS
BTCPAY --> BIP85_REGISTRY
BTCPAY --> DETERMINISTIC_WALLETS
BTCPAY --> KEYCHAIN_MANIFEST
Expand Down Expand Up @@ -119,18 +118,21 @@ graph TB
LIGHTNING_ADDRESS --> BIP85_REGISTRY
LIGHTNING_ADDRESS --> BULLNYM
LIGHTNING_ADDRESS --> DETERMINISTIC_WALLETS
LIGHTNING_ADDRESS --> GET_PAID_SETTINGS
LIGHTNING_ADDRESS --> KEYCHAIN_MANIFEST
LIGHTNING_ADDRESS --> NOSTR_IDENTITY
PAYMENT_PAGE --> BULLNYM
PAYMENT_PAGE --> LIGHTNING_ADDRESS
PAYMENT_PAGE --> DETERMINISTIC_WALLETS
PAYMENT_PAGE --> GET_PAID_SETTINGS
PAYMENT_PAGE --> KEYCHAIN_MANIFEST
PAYMENT_PAGE --> BIP85_REGISTRY
PAYMENT_PAGE --> NOSTR_IDENTITY
REMOTE_KEYCHAIN_RECOVERY --> PAYMENT_PAGE
POS --> BULLNYM
POS --> LIGHTNING_ADDRESS
POS --> DETERMINISTIC_WALLETS
POS --> GET_PAID_SETTINGS
POS --> KEYCHAIN_MANIFEST
POS --> BIP85_REGISTRY
POS --> NOSTR_IDENTITY
Expand All @@ -142,19 +144,15 @@ graph TB
GET_PAID_SETTINGS --> CORE
GET_PAID --> AUTOMATIC_FALLBACK
GET_PAID --> BTCPAY
GET_PAID --> BTC_PRICE
GET_PAID --> BULLNYM
GET_PAID --> GET_PAID_SETTINGS
GET_PAID --> INVOICES
GET_PAID --> LIGHTNING_ADDRESS
GET_PAID --> NOSTR_IDENTITY
GET_PAID --> PAYMENT_PAGE
GET_PAID --> POS
GET_PAID --> WALLETS
REMOTE_KEYCHAIN_RECOVERY --> KEYCHAIN_MANIFEST
REMOTE_KEYCHAIN_RECOVERY --> KEYCHAIN_RECOVERY
REMOTE_KEYCHAIN_RECOVERY --> WALLET_METADATA_BACKUP
REMOTE_KEYCHAIN_RECOVERY --> LIGHTNING_ADDRESS
REMOTE_KEYCHAIN_RECOVERY --> PAYMENT_PAGE
REMOTE_KEYCHAIN_RECOVERY --> POS
BUY --> EXCHANGE
BUY --> RECEIVE
Expand All @@ -171,11 +169,6 @@ graph TB
WALLET_BACKUP --> WALLET_METADATA_BACKUP
KEYCHAIN_MANIFEST --> BULLNYM
KEYCHAIN_MANIFEST --> NOSTR_IDENTITY
GET_PAID_SETTINGS --> KEYCHAIN_MANIFEST
REMOTE_KEYCHAIN_RECOVERY --> KEYCHAIN_MANIFEST
REMOTE_KEYCHAIN_RECOVERY --> KEYCHAIN_RECOVERY
REMOTE_KEYCHAIN_RECOVERY --> LIGHTNING_ADDRESS
ONBOARDING --> REMOTE_KEYCHAIN_RECOVERY
WALLET_METADATA_BACKUP --> LABELS
WALLET_METADATA_BACKUP --> WALLETS
WIZARD --> WALLET_METADATA_BACKUP
Expand Down
11 changes: 11 additions & 0 deletions lib/features/bitcoin_price/public/currency_text.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// The narrow public contract for rendering a Bitcoin amount.
///
/// [CurrencyText] is the only thing another feature needs from Bitcoin Price to
/// state an amount in the user's chosen unit while honoring the hide-amounts
/// setting. Publishing it here — rather than moving the widget — keeps this
/// feature's internal layout untouched while giving consumers a surface that is
/// not an internal path.
library;

export 'package:bb_mobile/features/bitcoin_price/ui/currency_text.dart'
show CurrencyText;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/features/get_paid/domain/get_paid_dashboard_snapshot.dart';
import 'package:bb_mobile/features/get_paid/domain/get_paid_product_probe.dart';
import 'package:bb_mobile/features/payment_page/public/payment_page_facade.dart';

/// Get Paid's narrow wrapper over the Donation Page boundary: probe the page for
/// a nym and classify the read as found / confirmed-absent / unavailable. A
/// failure never reads as absent, so the hub cannot show "not configured" for a
/// page it simply could not reach.
class FindGetPaidPaymentPageUsecase {
final PaymentPageFacade _paymentPage;

const FindGetPaidPaymentPageUsecase({required this._paymentPage});

Future<GetPaidProductProbe<GetPaidPaymentPageSnapshot>> execute({
required String nym,
}) async {
try {
final page = await _paymentPage.find(nym: nym);
return page == null
? const GetPaidProductAbsent()
: GetPaidProductFound(
GetPaidPaymentPageSnapshot(
publicUrl: page.publicUrl,
isArchived: page.isArchived,
),
);
} on Exception catch (error, trace) {
log.warning(
'Get Paid Donation Page lookup failed',
error: error,
trace: trace,
);
return const GetPaidProductUnavailable();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/features/get_paid/domain/get_paid_dashboard_snapshot.dart';
import 'package:bb_mobile/features/get_paid/domain/get_paid_product_probe.dart';
import 'package:bb_mobile/features/pos/public/pos_facade.dart';

/// Get Paid's narrow wrapper over the Point of Sale boundary: probe the terminal
/// for a nym and classify the read as found / confirmed-absent / unavailable. A
/// failure never reads as absent, so the hub cannot show "not configured" for a
/// terminal it simply could not reach.
class FindGetPaidPosTerminalUsecase {
final PosFacade _pos;

const FindGetPaidPosTerminalUsecase({required this._pos});

Future<GetPaidProductProbe<GetPaidPosTerminalSnapshot>> execute({
required String nym,
}) async {
try {
final terminal = await _pos.find(nym: nym);
return terminal == null
? const GetPaidProductAbsent()
: GetPaidProductFound(
GetPaidPosTerminalSnapshot(
terminalUrl: terminal.terminalUrl,
isArchived: terminal.isArchived,
),
);
} on Exception catch (error, trace) {
log.warning(
'Get Paid Point of Sale lookup failed',
error: error,
trace: trace,
);
return const GetPaidProductUnavailable();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/utils/result.dart';
import 'package:bb_mobile/features/btcpay/public/btcpay_facade.dart';
import 'package:bb_mobile/features/get_paid/domain/get_paid_dashboard_snapshot.dart';
import 'package:bb_mobile/features/get_paid/domain/get_paid_product_probe.dart';

/// Get Paid's narrow wrapper over the BTCPay boundary: read the current paired
/// connection for the hub card. The BTCPay failure family stays behind the
/// boundary — presentation only learns whether the connection is present,
/// confirmed absent, or unknown.
class GetGetPaidBtcpayConnectionUsecase {
final BtcpayFacade _btcpay;

const GetGetPaidBtcpayConnectionUsecase({required this._btcpay});

Future<GetPaidProductProbe<GetPaidBtcpayConnectionSnapshot>> execute() async {
try {
return switch (await _btcpay.connection()) {
Ok(:final value) =>
value == null
? const GetPaidProductAbsent()
: GetPaidProductFound(
GetPaidBtcpayConnectionSnapshot(serverUrl: value.serverUrl),
),
Err(:final failure) => _reportUnavailable(failure),
};
} on Exception catch (error, trace) {
log.warning(
'Get Paid BTCPay connection lookup failed',
error: error,
trace: trace,
);
return const GetPaidProductUnavailable();
}
}

GetPaidProductProbe<GetPaidBtcpayConnectionSnapshot> _reportUnavailable(
Object failure,
) {
log.warning(
'Get Paid could not load the BTCPay connection',
error: failure,
);
return const GetPaidProductUnavailable();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import 'package:bb_mobile/core/settings/domain/get_settings_usecase.dart';
import 'package:bb_mobile/core/settings/domain/settings_entity.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/core/utils/result.dart';
import 'package:bb_mobile/features/fiat_settlement/public/fiat_settlement_facade.dart';
import 'package:bb_mobile/features/get_paid/domain/get_paid_dashboard_snapshot.dart';

/// The hub's read-only fiat-settlement summary: either a server-confirmed
/// configuration for every product, or [unavailable].
///
/// There is no third "assume Bitcoin-only" state — a failed read must render as
/// unavailable, never as a guessed settlement split.
class GetPaidFiatSettlementSummary {
/// Server-confirmed configuration per product. Empty when [isUnavailable].
final Map<GetPaidDashboardSettlementProduct, GetPaidDashboardSettlementConfig>
configs;

/// True when a mainnet read was attempted and failed.
final bool isUnavailable;

const GetPaidFiatSettlementSummary.confirmed(this.configs)
: isUnavailable = false;

const GetPaidFiatSettlementSummary.unavailable()
: configs = const {},
isUnavailable = true;
}

/// Get Paid's narrow wrapper over the fiat-settlement boundary for the hub
/// badges. It owns the mainnet-only gate and the per-product projection, and
/// keeps the settlement failure family behind the boundary.
///
/// Returns null when settlement does not apply (non-mainnet), which leaves the
/// slots without any settlement badge at all.
class GetGetPaidFiatSettlementSummaryUsecase {
final FiatSettlementFacade _fiatSettlement;
final GetSettingsUsecase _getSettings;

const GetGetPaidFiatSettlementSummaryUsecase({
required this._fiatSettlement,
required this._getSettings,
});

Future<GetPaidFiatSettlementSummary?> execute() async {
try {
final settings = await _getSettings.execute();
if (settings.environment != Environment.mainnet) return null;
return switch (await _fiatSettlement.configuration()) {
Ok(:final value) => GetPaidFiatSettlementSummary.confirmed({
for (final product in GetPaidDashboardSettlementProduct.values)
product: _mapConfig(value.configFor(_foreignProduct(product))),
}),
Err(:final failure) => _reportUnavailable(failure),
};
} on Exception catch (error, trace) {
log.warning(
'Get Paid fiat-settlement summary lookup failed',
error: error,
trace: trace,
);
return const GetPaidFiatSettlementSummary.unavailable();
}
}

GetPaidFiatSettlementSummary _reportUnavailable(Object failure) {
log.warning(
'Get Paid could not load the fiat-settlement summary',
error: failure,
);
return const GetPaidFiatSettlementSummary.unavailable();
}

FiatSettlementProduct _foreignProduct(
GetPaidDashboardSettlementProduct product,
) => switch (product) {
GetPaidDashboardSettlementProduct.lightningAddress =>
FiatSettlementProduct.lightningAddress,
GetPaidDashboardSettlementProduct.paymentPage =>
FiatSettlementProduct.paymentPage,
GetPaidDashboardSettlementProduct.pos => FiatSettlementProduct.pos,
};

GetPaidDashboardSettlementConfig _mapConfig(
FiatSettlementProductConfig config,
) => GetPaidDashboardSettlementConfig(
fiatPercentage: config.fiatPercentage,
currencyCode: config.currency?.code,
);
}
54 changes: 54 additions & 0 deletions lib/features/get_paid/domain/get_paid_dashboard_snapshot.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/// The only Donation Page facts the Get Paid hub renders.
class GetPaidPaymentPageSnapshot {
final String publicUrl;
final bool isArchived;

const GetPaidPaymentPageSnapshot({
required this.publicUrl,
required this.isArchived,
});
}

/// The only Point of Sale facts the Get Paid hub renders.
class GetPaidPosTerminalSnapshot {
final String terminalUrl;
final bool isArchived;

const GetPaidPosTerminalSnapshot({
required this.terminalUrl,
required this.isArchived,
});
}

/// The only BTCPay fact the Get Paid hub renders.
class GetPaidBtcpayConnectionSnapshot {
final String serverUrl;

const GetPaidBtcpayConnectionSnapshot({required this.serverUrl});
}

/// Fiat-settlement slots owned by the Get Paid dashboard contract.
enum GetPaidDashboardSettlementProduct { lightningAddress, paymentPage, pos }

enum GetPaidDashboardSettlementMode { bitcoinOnly, mixed, fiatOnly }

/// The presentation-safe portion of one product's settlement configuration.
class GetPaidDashboardSettlementConfig {
final int fiatPercentage;
final String? currencyCode;

const GetPaidDashboardSettlementConfig({
required this.fiatPercentage,
required this.currencyCode,
});

GetPaidDashboardSettlementMode get mode {
if (fiatPercentage <= 0) {
return GetPaidDashboardSettlementMode.bitcoinOnly;
}
if (fiatPercentage >= 100) {
return GetPaidDashboardSettlementMode.fiatOnly;
}
return GetPaidDashboardSettlementMode.mixed;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import 'package:bb_mobile/core/utils/result.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/features/invoices/public/invoices_facade.dart';

sealed class GetPaidFallbackAttentionResult {
const GetPaidFallbackAttentionResult();
}

final class GetPaidFallbackAttentionKnown
extends GetPaidFallbackAttentionResult {
final int count;

const GetPaidFallbackAttentionKnown(this.count);
}

final class GetPaidFallbackAttentionUnavailable
extends GetPaidFallbackAttentionResult {
const GetPaidFallbackAttentionUnavailable();
}

/// Get Paid's narrow wrapper over the invoices public boundary. Presentation
/// receives only an attention count; authenticated rows and protocol details
/// remain owned by the invoices feature.
Expand All @@ -9,10 +26,27 @@ class GetPaidFallbackAttentionUsecase {

const GetPaidFallbackAttentionUsecase({required this._invoices});

Future<int?> execute() async {
return switch (await _invoices.fallbackSupervision()) {
Ok(:final value) => value.attentionCount,
Err() => null,
};
Future<GetPaidFallbackAttentionResult> execute() async {
try {
return switch (await _invoices.fallbackSupervision()) {
Ok(:final value) => GetPaidFallbackAttentionKnown(value.attentionCount),
Err(:final failure) => _reportUnavailable(failure),
};
} on Exception catch (error, trace) {
log.warning(
'Get Paid fallback attention lookup failed',
error: error,
trace: trace,
);
return const GetPaidFallbackAttentionUnavailable();
}
}

GetPaidFallbackAttentionResult _reportUnavailable(Object failure) {
log.warning(
'Get Paid fallback attention lookup was rejected',
error: failure,
);
return const GetPaidFallbackAttentionUnavailable();
}
}
Loading