Files
buzz/mobile/test/shared/identity_transfer/native_community_entry_test.dart
klopez4212 f7708d0694 Polish mobile onboarding and profile setup (#8438)
Originally stacked on #8375, now merged. Rebased onto main.

Polishes the onboarding landing page, community-link entry, and recovery
sheets. Profile setup uses a consistent sheet with a larger avatar,
inline display name, and draft photo/emoji/animated editing; iOS uses
native controls and coordinated avatar/sheet transitions.

Validation: mobile analysis, full tests, and normal pre-push checks
passed after rebase. Prior iOS build and device walkthrough passed;
native validation was not repeated after this rebase. Snapshots omitted
as requested.

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Co-authored-by: Tom Brow <tomb@block.xyz>
2026-10-22 17:27:38 +00:00

175 lines
5.4 KiB
Dart

import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:buzz/shared/identity_transfer/native_community_entry.dart';
import 'package:buzz/shared/hosted/device_identity_source.dart';
import 'package:nostr/nostr.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('buzz/community_entry');
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
late Completer<bool> dismissed;
late Completer<Map<dynamic, dynamic>> presented;
setUp(() {
dismissed = Completer<bool>();
presented = Completer<Map<dynamic, dynamic>>();
messenger.setMockMethodCallHandler(channel, (call) async {
presented.complete(call.arguments as Map);
return dismissed.future;
});
});
tearDown(() => messenger.setMockMethodCallHandler(channel, null));
Future<Map<dynamic, dynamic>?> action(
String method,
Map<String, dynamic> args,
) async {
final result = Completer<Map<dynamic, dynamic>?>();
messenger.handlePlatformMessage(
channel.name,
const StandardMethodCodec().encodeMethodCall(MethodCall(method, args)),
(data) => result.complete(
const StandardMethodCodec().decodeEnvelope(data!) as Map?,
),
);
return result.future;
}
for (final code in [
'presentation_failed',
'invalid_arguments',
'already_presented',
]) {
test('native $code preserves the presentation contract', () async {
messenger.setMockMethodCallHandler(channel, (_) async {
throw PlatformException(code: code);
});
final result = NativeCommunityEntry.show(
name: 'Preview',
dark: false,
primaryArgb: 0xFF007AFF,
choices: const [],
connect: (_, _, _, _) async => fail('Must not connect'),
);
if (code == 'presentation_failed') {
expect(await result, isNull);
// A failed presentation must not leave the singleton busy.
messenger.setMockMethodCallHandler(channel, (_) async => true);
expect(
await NativeCommunityEntry.show(
name: 'Preview',
dark: false,
primaryArgb: 0xFF007AFF,
choices: const [],
connect: (_, _, _, _) async {},
),
isTrue,
);
} else {
await expectLater(
result,
throwsA(isA<PlatformException>().having((e) => e.code, 'code', code)),
);
}
});
}
test(
'invite confirmation delegates claiming and preserves retry errors',
() async {
var claims = 0;
final shown = NativeCommunityEntry.show(
name: 'preview.example.com',
dark: false,
primaryArgb: 0xFF000000,
choices: const [],
connect: (_, _, _, _) async => fail('Must use invite claiming'),
confirmInvite: () async =>
++claims == 1 ? {'error': 'Try again'} : {'joined': true},
);
final config = await presented.future;
expect(config['invite'], true);
final args = <String, dynamic>{'session': config['session']};
expect((await action('join', args))?['error'], 'Try again');
expect((await action('join', args))?['joined'], true);
dismissed.complete(true);
expect(await shown, true);
},
);
test(
'native import requires preview and confirmation before connecting',
() async {
final keys = Keys('1'.padLeft(64, '0'));
var joins = 0;
final shown = NativeCommunityEntry.show(
name: 'Preview',
dark: false,
primaryArgb: 0xFF007AFF,
choices: [],
connect: (_, nsec, create, current) async {
expect(nsec, keys.nsec);
expect(create, false);
expect(current(), true);
joins++;
},
);
final config = await presented.future;
final args = <String, dynamic>{
'session': config['session'],
'nsec': keys.nsec,
};
expect((await action('join', args))?['error'], isNotNull);
expect(joins, 0);
expect((await action('preview', args))?['npub'], startsWith('npub1'));
expect(joins, 0);
expect((await action('join', args))?['joined'], true);
expect(joins, 1);
dismissed.complete(true);
expect(await shown, true);
},
);
test(
'returning to choices fences an in-flight join and allows retry',
() async {
final pending = Completer<void>();
final began = Completer<void>();
var calls = 0;
var staleWasCurrent = true;
final shown = NativeCommunityEntry.show(
name: 'Preview',
dark: false,
primaryArgb: 0xFF007AFF,
choices: const [DeviceIdentityChoice('pubkey', 'Saved')],
connect: (_, _, _, current) async {
calls++;
if (calls == 1) {
began.complete();
await pending.future;
staleWasCurrent = current();
}
},
);
final config = await presented.future;
final args = <String, dynamic>{
'session': config['session'],
'pubkey': 'pubkey',
};
final first = action('join', args);
await began.future;
await action('cancel', args);
pending.complete();
expect((await first)?['cancelled'], true);
expect(staleWasCurrent, false);
expect((await action('join', args))?['joined'], true);
expect(calls, 2);
dismissed.complete(true);
expect(await shown, true);
},
);
}