feat(block2): social settings + offers cubit factory (area, relays)

Persistence + wiring the offers screen needs, per the product decisions:
- SocialSettings (keystore-backed, no plaintext): coarse area geohash +
  configurable relay URLs, both empty by default (no bundled public relays —
  don't leak metadata to third parties). Registered in DI.
- createOffersCubit(): opens a SocialSession on the first configured relay when
  online, else a null-transport cubit that degrades gracefully. OffersCubit now
  closes the owning session on dispose.

4 settings tests green; offers/social suites still green.
This commit is contained in:
vjrj 2026-07-10 02:54:06 +02:00
parent 7fd87e6e62
commit 528b499209
4 changed files with 116 additions and 1 deletions

View file

@ -0,0 +1,41 @@
import '../security/secret_store.dart';
/// User choices for the (Block 2) social layer: the coarse area to publish/
/// browse offers in, and which community relays to talk to. Backed by the OS
/// keystore (via [SecretStore]) to honour "no plaintext at rest" no
/// shared_preferences.
///
/// Both default to "unset": empty area and no relays mean the social layer is
/// simply unavailable, and the app stays fully usable offline.
class SocialSettings {
SocialSettings(this._store);
final SecretStore _store;
static const _areaKey = 'tane.social.area_geohash';
static const _relaysKey = 'tane.social.relays';
/// The user's coarse area as a low-precision geohash (may be empty). Set
/// manually or from device location; the transport coarsens it further.
Future<String> areaGeohash() async => (await _store.read(_areaKey)) ?? '';
Future<void> setAreaGeohash(String geohash) =>
_store.write(_areaKey, geohash.trim().toLowerCase());
/// Configured community relay URLs (empty by default the user adds their
/// own; we bundle none, to avoid leaking metadata to third-party relays).
Future<List<String>> relayUrls() async {
final raw = await _store.read(_relaysKey);
if (raw == null || raw.isEmpty) return const [];
return raw.split('\n').where((s) => s.trim().isNotEmpty).toList();
}
Future<void> setRelayUrls(List<String> urls) => _store.write(
_relaysKey,
urls.map((u) => u.trim()).where((u) => u.isNotEmpty).join('\n'),
);
/// Whether the social layer has enough config to attempt going online.
Future<bool> get isConfigured async =>
(await relayUrls()).isNotEmpty && (await areaGeohash()).isNotEmpty;
}