Files
zapstore/lib/widgets/install_alert_dialog.dart
T
Henrique VellosoandCursor b28ebd5a39 Add "Always download from CDN" option to hide IP from GitHub
Global setting so users can always download from cdn.zapstore.dev instead of
the original URL (e.g. GitHub) to avoid exposing their IP to the host.

- SecureStorageService: getAlwaysUseCdn() / setAlwaysUseCdn() and alwaysUseCdnProvider
- PackageManager: _resolveDownloadUrl() reads the setting and returns CDN or original URL
- InstallAlertDialog: switch "Download from CDN (hides IP)"; relay-signed apps show
  dynamic text (CDN vs original URL) based on switch state
- InstallButton: saves alwaysUseCdn from dialog result when user confirms
- ProfileScreen: "Always download from CDN" toggle in Settings

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 17:36:54 -03:00

205 lines
7.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:zapstore/utils/extensions.dart';
import 'package:zapstore/widgets/author_container.dart';
import 'package:zapstore/widgets/common/base_dialog.dart';
import 'package:zapstore/widgets/download_text_container.dart';
import 'package:zapstore/services/secure_storage_service.dart';
import 'package:zapstore/widgets/relevant_who_follow_container.dart';
import 'package:zapstore/widgets/sign_in_button.dart';
class InstallAlertDialog extends HookConsumerWidget {
const InstallAlertDialog({super.key, required this.app});
final App app;
@override
Widget build(BuildContext context, WidgetRef ref) {
// Query publisher profile (app.pubkey is always present)
final publisherState = ref.watch(
query<Profile>(
authors: {app.pubkey},
source: const LocalAndRemoteSource(
relays: {'social', 'vertex'},
cachedFor: Duration(hours: 2),
),
),
);
final publisher = switch (publisherState) {
StorageData(:final models) => models.firstOrNull,
_ => null,
};
if (publisher == null) {
return BaseDialog(
title: const BaseDialogTitle('Trust this app?'),
content: const BaseDialogContent(
children: [
Center(
child: Padding(
padding: EdgeInsets.all(24),
child: CircularProgressIndicator(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: null, // Disabled while loading
child: const Text('Trust and install app'),
),
],
);
}
final trustedSignerNotifier = useState(false);
final alwaysUseCdnAsync = ref.watch(alwaysUseCdnProvider);
final alwaysUseCdnLocal = useState<bool?>(null);
final alwaysUseCdn =
alwaysUseCdnLocal.value ?? alwaysUseCdnAsync.valueOrNull ?? false;
final signedInPubkey = ref.watch(Signer.activePubkeyProvider);
final activeSigner = ref.watch(Signer.activeSignerProvider);
final canPersistTrust = signedInPubkey != null && activeSigner != null;
final theme = Theme.of(context);
final baseTextSize = theme.textTheme.bodyMedium?.fontSize ?? 14.0;
return BaseDialog(
title: const BaseDialogTitle('Trust this app?'),
content: BaseDialogContent(
children: [
if (app.isRelaySigned) ...[
AuthorContainer(
profile: publisher,
beforeText: 'The',
afterText:
' relay makes the ${app.name ?? app.identifier} app available but did not develop it or sign its APK. ',
oneLine: false,
size: baseTextSize,
),
Gap(10),
alwaysUseCdn
? Text(
'${app.name ?? app.identifier} will be installed from cdn.zapstore.dev.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
)
: DownloadTextContainer(
beforeText:
'${app.name ?? app.identifier} will be installed from its original release location:',
oneLine: false,
showFullUrl: true,
url: app.latestFileMetadata!.urls.first,
size: baseTextSize,
),
] else ...[
canPersistTrust
? RelevantWhoFollowContainer(app: app, size: baseTextSize)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AuthorContainer(
profile: publisher,
beforeText:
'${app.name ?? app.identifier} is published by',
afterText: '.',
oneLine: false,
size: baseTextSize,
),
const Gap(14),
SignInButton(
label:
'Sign in to view the publisher\'s reputable followers',
minimal: true,
requireNip55: true,
),
],
),
// Only show "Always trust" when signed in (requires signer to persist)
if (canPersistTrust) ...[
const Gap(14),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Switch(
value: trustedSignerNotifier.value,
onChanged: (value) {
trustedSignerNotifier.value = value;
},
),
Gap(4),
Expanded(
child: Row(
children: [
Text(
'Always trust',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
Gap(4),
AuthorContainer(
beforeText: '',
profile: publisher,
size: baseTextSize,
),
],
),
),
],
),
],
],
const Gap(14),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Switch(
value: alwaysUseCdn,
onChanged: (value) {
alwaysUseCdnLocal.value = value;
},
),
Gap(4),
Expanded(
child: Text(
'Download from CDN (hides IP)',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
),
],
),
],
),
actions: [
TextButton(
onPressed: () {
// NOTE: can't use context.pop()
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop((
trustPermanently: trustedSignerNotifier.value,
alwaysUseCdn: alwaysUseCdn,
));
},
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: const Text('Trust and install app'),
),
],
);
}
}