feat(sharing): make going online opt-in, and show what it unlocks
Tane dialled its four default relays at launch, before anyone had asked for anything — an F-Droid reviewer spotted it, and they were right. The seed book needs no network at all, so the app should not have one until the person joins the sharing side. - SocialSettings gains a three-state `sharingEnabled`. `null` means "never asked", which is what lets `migrateSharingEnabled` keep an existing install exactly as it was: anyone past the intro was on a build that connected at launch, so they keep messaging, device sync and offer alerts. A fresh install starts fully offline. - bootstrap only starts the shared connection when sharing is on. The inbox/sync/plantaré/alert listeners are untouched: they react to a session, and none arrives. - SharingSwitch is the single place that moves the stored choice, the live connection and the flag the UI listens to, so they cannot drift. - Agreeing to the community rules is the opt-in — one consent surface, reached from the market or from the drawer's invitation. - SocialConnection.start is now idempotent and gains stop(), so turning sharing off goes offline immediately instead of at the next launch. - The social drawer entries stay visible but padlocked while sharing is off; tapping one explains what wakes up and offers to join. Hiding them would have kept the tool a secret. "Coming soon" is gone for good — everything it labelled is built. Covered by tests for the migration in both directions, start/stop lifecycle, the gate turning sharing on, the invitation, and the drawer in all three states (no social layer / off / on).
This commit is contained in:
parent
62123582f5
commit
fed0e8200e
35 changed files with 926 additions and 173 deletions
|
|
@ -2,15 +2,18 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/app.dart';
|
||||
import 'package:tane/i18n/strings.g.dart';
|
||||
import 'package:tane/services/sharing_switch.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
void main() {
|
||||
Widget app(db) => TranslationProvider(
|
||||
/// The everyday case: the social layer exists and sharing is already on.
|
||||
Widget app(db, {SharingSwitch? sharing}) => TranslationProvider(
|
||||
child: TaneApp(
|
||||
repository: newTestRepository(db),
|
||||
species: newTestSpeciesRepository(db),
|
||||
onboarding: newTestOnboardingStore(),
|
||||
sharing: sharing ?? newTestSharingSwitch(),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
@ -24,7 +27,8 @@ void main() {
|
|||
|
||||
expect(find.text('Your inventory'), findsOneWidget);
|
||||
expect(find.text('Market'), findsOneWidget);
|
||||
expect(find.text('COMING SOON'), findsOneWidget); // market is Block 2
|
||||
// "Coming soon" is gone for good: everything on this screen is built.
|
||||
expect(find.textContaining('COMING SOON'), findsNothing);
|
||||
|
||||
// Redesign copy: tagline + the two destination subtitles.
|
||||
expect(find.text('Share and grow local seeds'), findsOneWidget);
|
||||
|
|
@ -49,8 +53,9 @@ void main() {
|
|||
await tester.tap(find.byIcon(Icons.menu)); // hamburger
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Social destinations are shown but disabled.
|
||||
// With sharing on, the social destinations are live.
|
||||
expect(find.text('Your profile'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.lock_outline), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Inventory')); // active drawer item
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -84,6 +89,62 @@ void main() {
|
|||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('sharing off: market stays open, the rest wears a padlock',
|
||||
(tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final db = newTestDatabase();
|
||||
addTearDown(db.close);
|
||||
|
||||
await tester.pumpWidget(
|
||||
app(db, sharing: newTestSharingSwitch(enabled: false)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The market card is the door into sharing, so it must stay reachable.
|
||||
expect(find.byKey(const Key('home.market')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Market live; the social entries are visible but padlocked — and tapping
|
||||
// one invites the person in rather than doing nothing.
|
||||
expect(find.text('Your profile'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.lock_outline), findsWidgets);
|
||||
|
||||
await tester.tap(find.text('Your profile'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('This wakes up when you start sharing'), findsOneWidget);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('no social layer: the market and its friends are not drawn',
|
||||
(tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final db = newTestDatabase();
|
||||
addTearDown(db.close);
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: TaneApp(
|
||||
repository: newTestRepository(db),
|
||||
species: newTestSpeciesRepository(db),
|
||||
onboarding: newTestOnboardingStore(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Nothing here can be switched on, so it is hidden rather than teased.
|
||||
expect(find.byKey(const Key('home.market')), findsNothing);
|
||||
expect(find.text('Your inventory'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.menu));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Your profile'), findsNothing);
|
||||
expect(find.byIcon(Icons.lock_outline), findsNothing);
|
||||
await disposeTree(tester);
|
||||
});
|
||||
|
||||
testWidgets('drawer Settings opens the settings screen', (tester) async {
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue