mirror of
https://github.com/block/buzz.git
synced 2026-11-06 17:31:19 +00:00
## Summary After scanning or pasting a community invite in dark mode, the native Join button can display white text on a white background. This fix passes the theme’s foreground color to native confirmation and applies it alongside the button’s background color. ### Related issue Related invite handoff: https://github.com/block/buzz/pull/8601. ### Testing A widget regression checks the foreground and background colors sent to native confirmation. The isolated PR build shows readable black Join text on the white native button in dark mode. ### Screenshots Before (consent-only build `3d566de`):  After (this PR):  Signed-off-by: John Tennant <jtennant@block.xyz> Signed-off-by: John Tennant <jtennant@squareup.com> Co-authored-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz> Co-authored-by: Aaron <f425b86c3b780d43f8d9cf1802429d04dd985bf92eac44fca9468b5ad3887c5e@buzz.block.builderlab.xyz>
132 lines
4.4 KiB
Dart
132 lines
4.4 KiB
Dart
import 'package:flutter/services.dart';
|
|
|
|
import '../hosted/account_identity_store.dart';
|
|
import '../hosted/device_identity_source.dart';
|
|
import '../utils/string_utils.dart';
|
|
import 'nsec_transfer.dart';
|
|
|
|
/// UIKit presentation with identity operations kept in the Dart service layer.
|
|
abstract final class NativeCommunityEntry {
|
|
static const _channel = MethodChannel('buzz/community_entry');
|
|
static bool _presenting = false;
|
|
static int _session = 0;
|
|
|
|
/// Returns null when native presentation is unavailable.
|
|
static Future<bool?> show({
|
|
required String name,
|
|
required bool dark,
|
|
required int primaryArgb,
|
|
int? onPrimaryArgb,
|
|
required List<DeviceIdentityChoice> choices,
|
|
Future<Map<String, dynamic>> Function()? confirmInvite,
|
|
required Future<void> Function(
|
|
String? pubkey,
|
|
String? nsec,
|
|
bool create,
|
|
bool Function() isCurrent,
|
|
)
|
|
connect,
|
|
}) async {
|
|
if (_presenting) return false;
|
|
_presenting = true;
|
|
final session = ++_session;
|
|
var active = true;
|
|
var generation = 0;
|
|
var busy = false;
|
|
String? previewed;
|
|
_channel.setMethodCallHandler((call) async {
|
|
final args = Map<String, dynamic>.from(call.arguments as Map);
|
|
if (!active || args['session'] != session) return {'cancelled': true};
|
|
if (call.method == 'cancel') {
|
|
generation++;
|
|
busy = false;
|
|
previewed = null;
|
|
return null;
|
|
}
|
|
if (busy) return {'cancelled': true};
|
|
final attempt = ++generation;
|
|
bool current() => active && generation == attempt;
|
|
busy = true;
|
|
try {
|
|
switch (call.method) {
|
|
case 'preview':
|
|
case 'file':
|
|
final text = call.method == 'file'
|
|
? await NsecTransfer.pickFile()
|
|
: args['nsec'] as String?;
|
|
if (!current() || text == null) return {'cancelled': true};
|
|
previewed = NsecTransfer.preview(text);
|
|
return {
|
|
'npub': fullNpub(previewed!),
|
|
if (call.method == 'file') 'nsec': text,
|
|
};
|
|
case 'join':
|
|
if (confirmInvite != null) return await confirmInvite();
|
|
final nsec = args['nsec'] as String?;
|
|
final pubkey = args['pubkey'] as String?;
|
|
final create = args['create'] == true;
|
|
if (nsec != null) {
|
|
if (previewed == null ||
|
|
NsecTransfer.preview(nsec) != previewed) {
|
|
throw AccountIdentityFailure.invalidKey;
|
|
}
|
|
} else if (create
|
|
? choices.isNotEmpty
|
|
: !choices.any((choice) => choice.pubkey == pubkey)) {
|
|
return {'error': 'Choose an identity to continue.'};
|
|
}
|
|
await connect(pubkey, nsec, create, current);
|
|
return current() ? {'joined': true} : {'cancelled': true};
|
|
default:
|
|
throw MissingPluginException();
|
|
}
|
|
} catch (error) {
|
|
if (!current()) return {'cancelled': true};
|
|
return {
|
|
'error': switch (error) {
|
|
AccountIdentityFailure.invalidKey =>
|
|
'Enter a valid nsec private key.',
|
|
AccountIdentityFailure.occupied ||
|
|
AccountIdentityFailure.mismatch =>
|
|
'This device already has a different identity. It has not been replaced.',
|
|
_ => 'Could not finish. Try again. Your saved keys have been kept.',
|
|
},
|
|
};
|
|
} finally {
|
|
if (current()) busy = false;
|
|
}
|
|
});
|
|
try {
|
|
return await _channel.invokeMethod<bool>('present', {
|
|
'session': session,
|
|
'name': name,
|
|
'dark': dark,
|
|
'primaryArgb': primaryArgb,
|
|
'onPrimaryArgb': ?onPrimaryArgb,
|
|
'invite': confirmInvite != null,
|
|
'choices': [
|
|
for (final choice in choices)
|
|
{
|
|
'pubkey': choice.pubkey,
|
|
'label': choice.label,
|
|
'npub': fullNpub(choice.pubkey) ?? choice.pubkey,
|
|
},
|
|
],
|
|
}) ??
|
|
false;
|
|
} on MissingPluginException {
|
|
return null;
|
|
} on PlatformException catch (error) {
|
|
// Scene transitions can temporarily leave UIKit without a presenter.
|
|
if (error.code == 'presentation_failed') return null;
|
|
rethrow;
|
|
} finally {
|
|
active = false;
|
|
generation++;
|
|
previewed = null;
|
|
_channel.setMethodCallHandler(null);
|
|
_presenting = false;
|
|
}
|
|
}
|
|
}
|