feat(block2): offline outbox — share now, publish when connected
Sender-side offline delivery hardening.
- OfferOutbox: a keystore-backed (no plaintext) queue of lot ids to publish
later. Stores ids, not offers, so on flush each offer is rebuilt from the
lot's CURRENT state — an offline edit is respected, a deleted lot drops out.
- flushOutbox(): on reconnect, republishes queued lots and clears them (no-op
offline; drops ids whose lot is gone).
- Market screen: sharing offline parks the lots ('we'll share these when you're
connected'); opening the market online auto-flushes the queue first.
- Wired via DI + TaneApp(outbox); i18n en/es/pt.
Tests: 4 outbox + 3 flush + market UI, 20 green. Analyzer clean for new code.
This commit is contained in:
parent
2a6b4b0947
commit
cf5d0243ee
15 changed files with 222 additions and 8 deletions
|
|
@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||
import '../data/variety_repository.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/coarse_location.dart';
|
||||
import '../services/offer_outbox.dart';
|
||||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
import '../state/offers_cubit.dart';
|
||||
|
|
@ -18,6 +19,7 @@ class MarketScreen extends StatefulWidget {
|
|||
required this.social,
|
||||
required this.settings,
|
||||
this.location,
|
||||
this.outbox,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -28,6 +30,10 @@ class MarketScreen extends StatefulWidget {
|
|||
/// null (tests, or a platform without it) the shortcut is hidden.
|
||||
final CoarseLocationProvider? location;
|
||||
|
||||
/// Optional offline outbox: parks shares attempted with no network and flushes
|
||||
/// them on reconnect. Null in tests → sharing offline is simply a no-op.
|
||||
final OfferOutbox? outbox;
|
||||
|
||||
@override
|
||||
State<MarketScreen> createState() => _MarketScreenState();
|
||||
}
|
||||
|
|
@ -43,6 +49,10 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
// Only needed to flush the outbox; read here (while mounted) to avoid using
|
||||
// context across the awaits below. Null when there's no outbox (e.g. tests).
|
||||
final repo =
|
||||
widget.outbox != null ? context.read<VarietyRepository>() : null;
|
||||
setState(() => _loading = true);
|
||||
final cubit = await createOffersCubit(widget.social, widget.settings);
|
||||
final area = await widget.settings.areaGeohash();
|
||||
|
|
@ -56,7 +66,20 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
_loading = false;
|
||||
});
|
||||
await previous?.close();
|
||||
if (area.isNotEmpty && cubit.isOnline) await cubit.discover(area);
|
||||
if (area.isNotEmpty && cubit.isOnline) {
|
||||
// Flush anything parked while offline, then show what's out there.
|
||||
final outbox = widget.outbox;
|
||||
if (outbox != null && repo != null) {
|
||||
await flushOutbox(
|
||||
outbox: outbox,
|
||||
cubit: cubit,
|
||||
shareableLots: await repo.shareableLots(),
|
||||
authorPubkeyHex: widget.social.publicKeyHex,
|
||||
areaGeohash: area,
|
||||
);
|
||||
}
|
||||
if (mounted) await cubit.discover(area);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openConfig() async {
|
||||
|
|
@ -73,7 +96,7 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
/// coarse area. Prompts for setup when the area isn't set.
|
||||
Future<void> _shareMine() async {
|
||||
final cubit = _cubit;
|
||||
if (cubit == null || !cubit.isOnline) return;
|
||||
if (cubit == null) return;
|
||||
final repo = context.read<VarietyRepository>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final t = context.t;
|
||||
|
|
@ -89,12 +112,23 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
messenger.showSnackBar(SnackBar(content: Text(t.market.nothingToShare)));
|
||||
return;
|
||||
}
|
||||
final ids = lots.map((l) => l.lotId).toList();
|
||||
|
||||
// Offline: park it and tell the person we'll share it later.
|
||||
if (!cubit.isOnline) {
|
||||
await widget.outbox?.enqueue(ids);
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(SnackBar(content: Text(t.market.queued)));
|
||||
return;
|
||||
}
|
||||
|
||||
final count = await cubit.publishLots(
|
||||
lots,
|
||||
authorPubkeyHex: widget.social.publicKeyHex,
|
||||
areaGeohash: area,
|
||||
);
|
||||
if (!mounted) return;
|
||||
await widget.outbox?.remove(ids); // published now, so unpark any duplicates
|
||||
messenger.showSnackBar(SnackBar(content: Text(t.market.sharedCount(n: count))));
|
||||
await cubit.discover(area); // refresh — now including the just-shared lots
|
||||
}
|
||||
|
|
@ -113,7 +147,7 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
appBar: AppBar(
|
||||
title: Text(t.market.title),
|
||||
actions: [
|
||||
if (cubit != null && cubit.isOnline)
|
||||
if (cubit != null && (cubit.isOnline || widget.outbox != null))
|
||||
IconButton(
|
||||
key: const Key('market.shareMine'),
|
||||
icon: const Icon(Icons.ios_share_outlined),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue