feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
||||
import '../../shared/theme/theme.dart';
|
||||
import '../../shared/l10n/l10n.dart';
|
||||
import '../../shared/widgets/buzz_loading_indicator.dart';
|
||||
import '../../shared/widgets/tappable_flapping_bee.dart';
|
||||
import 'pairing_provider.dart';
|
||||
import 'pairing_qr_scanner.dart';
|
||||
|
||||
part 'pairing_page/onboarding_background.dart';
|
||||
part 'pairing_page/pairing_welcome_view.dart';
|
||||
|
||||
const _onboardingChartreuse = Color(0xFFD7D72E);
|
||||
const _onboardingShellBottom = Color(0xFFD7E7F6);
|
||||
const _onboardingCtaLabel = Color(0xFFD7E6F0);
|
||||
const _onboardingInk = Color(0xFF111111);
|
||||
const _onboardingMutedInk = Color(0xB3111111);
|
||||
|
||||
class PairingPage extends HookConsumerWidget {
|
||||
/// When true, the pairing page is being used to add a new community
|
||||
/// (user is already authenticated with at least one community).
|
||||
final bool addingCommunity;
|
||||
final bool identityRecoveryOnly;
|
||||
|
||||
const PairingPage({
|
||||
super.key,
|
||||
this.addingCommunity = false,
|
||||
this.identityRecoveryOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final pairingState = ref.watch(pairingProvider);
|
||||
final codeController = useTextEditingController();
|
||||
final fallbackScannerVisible = useState(false);
|
||||
final pairingCodeExpanded = useState(false);
|
||||
final isBusy =
|
||||
pairingState.status == PairingStatus.connecting ||
|
||||
pairingState.status == PairingStatus.transferring ||
|
||||
pairingState.status == PairingStatus.storing;
|
||||
|
||||
// When adding a community and pairing succeeds, pop back.
|
||||
if (addingCommunity && pairingState.status == PairingStatus.success) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
ref.read(pairingProvider.notifier).reset();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> handleScannerResult(String? code) async {
|
||||
if (code != null && context.mounted) {
|
||||
if (identityRecoveryOnly &&
|
||||
Uri.tryParse(code)?.queryParameters['mode'] != 'recover') {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.pairingScanDesktopCode)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await ref.read(pairingProvider.notifier).pair(code);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openScanner() async {
|
||||
final usesDynamicIslandPortal = await usesDynamicIslandQrScannerPortal();
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!usesDynamicIslandPortal) {
|
||||
fallbackScannerVisible.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
final code = await showDynamicIslandPairingQrScanner(context);
|
||||
await handleScannerResult(code);
|
||||
}
|
||||
|
||||
final isVerifyingSas = pairingState.status == PairingStatus.confirmingSas;
|
||||
final themedSystemOverlayStyle =
|
||||
(context.theme.brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark)
|
||||
.copyWith(statusBarColor: Colors.transparent);
|
||||
final pairingAppBar = addingCommunity
|
||||
? AppBar(
|
||||
foregroundColor: isVerifyingSas
|
||||
? context.colors.onSurface
|
||||
: _onboardingInk,
|
||||
systemOverlayStyle: isVerifyingSas
|
||||
? themedSystemOverlayStyle
|
||||
: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: const Icon(LucideIcons.arrowLeft),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
title: Text(
|
||||
identityRecoveryOnly
|
||||
? context.l10n.pairingSendToDesktop
|
||||
: context.l10n.pairingAddCommunity,
|
||||
style: isVerifyingSas
|
||||
? null
|
||||
: context.textTheme.titleMedium?.copyWith(
|
||||
color: _onboardingInk,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
final pairingScaffold = isVerifyingSas
|
||||
? AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
key: const Key('pairing-sas-system-overlay'),
|
||||
value: themedSystemOverlayStyle,
|
||||
child: Scaffold(
|
||||
backgroundColor: context.colors.surface,
|
||||
appBar: pairingAppBar,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Grid.sm),
|
||||
child: _SasVerificationView(
|
||||
sasCode: pairingState.sasCode ?? '------',
|
||||
confirmed: pairingState.userConfirmedSas,
|
||||
sendsIdentityToDesktop: pairingState.sendsIdentityToDesktop,
|
||||
onConfirm: () =>
|
||||
ref.read(pairingProvider.notifier).confirmSas(),
|
||||
onDeny: () => ref.read(pairingProvider.notifier).denySas(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
key: const Key('pairing-onboarding-system-overlay'),
|
||||
value: SystemUiOverlayStyle.dark.copyWith(
|
||||
statusBarColor: Colors.transparent,
|
||||
),
|
||||
child: _OnboardingBackground(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: pairingAppBar,
|
||||
body: SafeArea(
|
||||
child: _PairingWelcomeView(
|
||||
codeController: codeController,
|
||||
isBusy: isBusy,
|
||||
pairingCodeExpanded: pairingCodeExpanded.value,
|
||||
errorMessage: pairingState.status == PairingStatus.error
|
||||
? _localizedPairingError(
|
||||
context,
|
||||
pairingState.errorMessage,
|
||||
)
|
||||
: null,
|
||||
onScan: openScanner,
|
||||
onTogglePairingCode: () {
|
||||
pairingCodeExpanded.value = !pairingCodeExpanded.value;
|
||||
},
|
||||
onConnect: () {
|
||||
final code = codeController.text.trim();
|
||||
if (code.isNotEmpty) {
|
||||
unawaited(handleScannerResult(code));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final appSurface = PopScope(
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) {
|
||||
ref.read(pairingProvider.notifier).reset();
|
||||
}
|
||||
},
|
||||
child: pairingScaffold,
|
||||
);
|
||||
|
||||
if (!fallbackScannerVisible.value) {
|
||||
return appSurface;
|
||||
}
|
||||
|
||||
return FallbackPairingQrScanner(
|
||||
appSurface: appSurface,
|
||||
onClosed: (code) {
|
||||
fallbackScannerVisible.value = false;
|
||||
unawaited(handleScannerResult(code));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? _localizedPairingError(BuildContext context, String? error) {
|
||||
if (error == null || Localizations.localeOf(context).languageCode == 'en') {
|
||||
return error;
|
||||
}
|
||||
if (error.startsWith('SAS code mismatch')) {
|
||||
return context.l10n.pairingSasMismatch;
|
||||
}
|
||||
if (error.startsWith('Pairing session timed out')) {
|
||||
return context.l10n.pairingSessionTimedOut;
|
||||
}
|
||||
if (error.startsWith('Invalid pairing code')) {
|
||||
return context.l10n.pairingInvalidCode;
|
||||
}
|
||||
if (error.startsWith('Could not reach the pairing relay')) {
|
||||
return context.l10n.pairingRelayUnreachable;
|
||||
}
|
||||
if (error.startsWith('The pairing relay rejected authentication')) {
|
||||
return context.l10n.pairingAuthenticationRejected;
|
||||
}
|
||||
if (error.startsWith('Pairing stopped because of an internal error')) {
|
||||
return context.l10n.pairingInternalError;
|
||||
}
|
||||
if (error.startsWith('Secure connection failed')) {
|
||||
return context.l10n.pairingSecureConnectionFailed;
|
||||
}
|
||||
if (error.startsWith('Connection timed out')) {
|
||||
return context.l10n.pairingConnectionTimedOut;
|
||||
}
|
||||
if (error.startsWith('Security verification failed')) {
|
||||
return context.l10n.pairingSecurityVerificationFailed;
|
||||
}
|
||||
if (error.startsWith('No identity is available')) {
|
||||
return context.l10n.pairingNoIdentity;
|
||||
}
|
||||
if (error.startsWith('Received empty payload')) {
|
||||
return context.l10n.pairingEmptyPayload;
|
||||
}
|
||||
if (error.startsWith('Desktop could not store')) {
|
||||
return context.l10n.pairingDesktopStoreFailed;
|
||||
}
|
||||
if (error.startsWith('Source device aborted pairing')) {
|
||||
return context.l10n.pairingSourceAborted;
|
||||
}
|
||||
if (error.startsWith('Failed to import credentials')) {
|
||||
return context.l10n.pairingImportFailed;
|
||||
}
|
||||
if (error.startsWith('Lost connection to pairing relay')) {
|
||||
return context.l10n.pairingLostConnection;
|
||||
}
|
||||
if (error.startsWith('Could not connect to relay')) {
|
||||
return context.l10n.pairingRelayRejectedCode;
|
||||
}
|
||||
if (error.startsWith('Connection failed')) {
|
||||
return context.l10n.pairingConnectionFailed;
|
||||
}
|
||||
return context.l10n.pairingConnectionFailed;
|
||||
}
|
||||
|
||||
/// SAS verification screen shown during NIP-AB pairing.
|
||||
class _SasVerificationView extends StatelessWidget {
|
||||
final String sasCode;
|
||||
final bool confirmed;
|
||||
final bool sendsIdentityToDesktop;
|
||||
final VoidCallback onConfirm;
|
||||
final VoidCallback onDeny;
|
||||
|
||||
const _SasVerificationView({
|
||||
required this.sasCode,
|
||||
required this.confirmed,
|
||||
required this.sendsIdentityToDesktop,
|
||||
required this.onConfirm,
|
||||
required this.onDeny,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(flex: 2),
|
||||
|
||||
Icon(LucideIcons.shieldCheck, size: 56, color: context.colors.primary),
|
||||
const SizedBox(height: Grid.sm),
|
||||
|
||||
Text(context.l10n.sasTitle, style: context.textTheme.headlineSmall),
|
||||
const SizedBox(height: Grid.xs),
|
||||
|
||||
Text(
|
||||
confirmed
|
||||
? context.l10n.sasWaitingForDesktop
|
||||
: context.l10n.sasDesktopPrompt,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: Grid.lg),
|
||||
|
||||
// Large SAS code display
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colors.primaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: context.colors.primary.withValues(alpha: 0.3),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'${sasCode.substring(0, 3)} ${sasCode.substring(3)}',
|
||||
style: context.textTheme.displayMedium?.copyWith(
|
||||
fontFamily: 'GeistMono',
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 8,
|
||||
color: context.colors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: Grid.lg),
|
||||
|
||||
Text(
|
||||
sendsIdentityToDesktop
|
||||
? context.l10n.sasTransferFull
|
||||
: context.l10n.sasTransferToDevice,
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: Grid.lg),
|
||||
|
||||
// Confirm / Deny buttons
|
||||
if (confirmed)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
BuzzLoadingIndicator(
|
||||
size: 24,
|
||||
color: context.colors.primary,
|
||||
semanticLabel: context.l10n.connecting,
|
||||
),
|
||||
const SizedBox(width: Grid.twelve),
|
||||
Text(
|
||||
context.l10n.sasConfirmedWaiting,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onDeny,
|
||||
icon: const Icon(LucideIcons.x),
|
||||
label: Text(context.l10n.cancel),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Grid.sm),
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: onConfirm,
|
||||
icon: const Icon(LucideIcons.check),
|
||||
label: Text(context.l10n.codesMatch),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(flex: 3),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user