Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions lib/db/db_version_migration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import '../services/wallets_service.dart';
import '../utilities/amount/amount.dart';
import '../utilities/constants.dart';
import '../utilities/default_eth_tokens.dart';
import '../utilities/default_nodes.dart';
import '../utilities/flutter_secure_storage_interface.dart';
import '../utilities/logger.dart';
import '../utilities/prefs.dart';
Expand Down Expand Up @@ -423,6 +424,16 @@ class DbVersionMigrator with WalletDB {
);
return await migrate(17, secureStore: secureStore);

case 17:
await _v17();

await DB.instance.put<dynamic>(
boxName: DB.boxNameDBInfo,
key: "hive_data_version",
value: 18,
);
return await migrate(18, secureStore: secureStore);

default:
// finally return
return;
Expand Down Expand Up @@ -711,4 +722,37 @@ class DbVersionMigrator with WalletDB {

await primariesBox.deleteFromDisk();
}

Future<void> _v17() async {
final nodesBox = await DB.instance.hive.openBox<NodeModel>(
DB.boxNameNodeModels,
);
final id = DefaultNodes.buildId(Fact0rn(CryptoCurrencyNetwork.main));
final node = nodesBox.get(id);

// The Fact0rn default node used to ship clearnet only. Only touch a node
// that still matches everything the user could have edited on it, so a
// customised node keeps its settings.
final untouched =
node != null &&
node.host == "electrumx2.projectfactor.io" &&
node.port == 50002 &&
node.name == DefaultNodes.defaultName &&
node.useSSL &&
node.isFailover &&
node.loginName == null &&
!node.torEnabled &&
node.clearnetEnabled;

if (untouched) {
await nodesBox.put(
id,
node.copyWith(
loginName: node.loginName,
trusted: node.trusted,
torEnabled: true,
),
);
}
}
}
138 changes: 138 additions & 0 deletions lib/networking/tor_aware_network_image.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';

import '../app_config.dart';
import '../services/tor_service.dart';
import '../utilities/prefs.dart';
import 'http.dart';

/// Fetches [url] through the app's [HTTP] client, routing over Tor when it is
/// enabled. Throws if Tor is enabled but not connected, so nothing falls back
/// to clearnet.
Future<Uint8List> fetchTorAwareBytes(
String url, {
Map<String, String>? headers,
}) async {
final response = await const HTTP().get(
url: Uri.parse(url),
headers: headers,
proxyInfo: !AppConfig.hasFeature(AppFeature.tor)
? null
: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);

if (response.code != 200) {
throw Exception("Failed to load $url: status=${response.code}");
}

return Uint8List.fromList(response.bodyBytes);
}

/// Drop-in replacement for [NetworkImage] that respects the Tor preference.
/// Use with [Image] instead of [Image.network], which always goes clearnet.
@immutable
class TorAwareNetworkImage extends ImageProvider<TorAwareNetworkImage> {
const TorAwareNetworkImage(this.url, {this.headers, this.cacheScope});

final String url;

/// Sent with the request but not part of the cache key, so e.g. a refreshed
/// auth token doesn't cause a refetch.
final Map<String, String>? headers;

/// Part of the cache key, unlike [headers]. Set it to a stable identity
/// (e.g. a customer key) when the same [url] can serve different content
/// depending on who is asking, so one identity never sees another's image.
final String? cacheScope;

@override
Future<TorAwareNetworkImage> obtainKey(ImageConfiguration configuration) =>
SynchronousFuture<TorAwareNetworkImage>(this);

@override
ImageStreamCompleter loadImage(
TorAwareNetworkImage key,
ImageDecoderCallback decode,
) {
final chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
codec: _load(key, decode, chunkEvents),
chunkEvents: chunkEvents.stream,
scale: 1.0,
debugLabel: key.url,
);
}

Future<ui.Codec> _load(
TorAwareNetworkImage key,
ImageDecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) async {
try {
// Lets loadingBuilder show its indicator while the request runs.
chunkEvents.add(
const ImageChunkEvent(
cumulativeBytesLoaded: 0,
expectedTotalBytes: null,
),
);
final bytes = await fetchTorAwareBytes(key.url, headers: key.headers);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes);
return await decode(buffer);
} catch (_) {
// Don't keep a failed load in the cache.
scheduleMicrotask(() {
PaintingBinding.instance.imageCache.evict(key);
});
rethrow;
} finally {
unawaited(chunkEvents.close());
}
}

@override
bool operator ==(Object other) =>
other is TorAwareNetworkImage &&
other.url == url &&
other.cacheScope == cacheScope;

@override
int get hashCode => Object.hash(url, cacheScope);

@override
String toString() =>
"${objectRuntimeType(this, "TorAwareNetworkImage")}"
"(\"$url\")";
}

/// Drop-in replacement for the loader behind [SvgPicture.network] that
/// respects the Tor preference.
/// Use as `SvgPicture(TorAwareSvgNetworkLoader(url))`.
@immutable
class TorAwareSvgNetworkLoader extends SvgLoader<Uint8List> {
const TorAwareSvgNetworkLoader(this.url);

final String url;

@override
Future<Uint8List?> prepareMessage(BuildContext? context) =>
fetchTorAwareBytes(url);

@override
String provideSvg(Uint8List? message) =>
utf8.decode(message!, allowMalformed: true);

@override
bool operator ==(Object other) =>
other is TorAwareSvgNetworkLoader && other.url == url;

@override
int get hashCode => url.hashCode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:isar_community/isar.dart';
import '../../../../models/isar/exchange_cache/currency.dart';
import '../../../../models/isar/models/contract.dart';
import '../../../../models/isar/models/ethereum/eth_contract.dart';
import '../../../../networking/tor_aware_network_image.dart';
import '../../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../../services/exchange/exchange_data_loading_service.dart';
import '../../../../themes/stack_colors.dart';
Expand Down Expand Up @@ -101,8 +102,8 @@ class _AddTokenListElementState extends ConsumerState<AddTokenListElement> {
)
else
currency != null
? SvgPicture.network(
currency!.image,
? SvgPicture(
TorAwareSvgNetworkLoader(currency!.image),
width: iconSize,
height: iconSize,
placeholderBuilder: (_) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import '../../../../models/add_wallet_list_entity/add_wallet_list_entity.dart';
import '../../../../models/add_wallet_list_entity/sub_classes/eth_token_entity.dart';
import '../../../../models/add_wallet_list_entity/sub_classes/sol_token_entity.dart';
import '../../../../models/isar/exchange_cache/currency.dart';
import '../../../../networking/tor_aware_network_image.dart';
import '../../../../providers/providers.dart';
import '../../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../../services/exchange/exchange_data_loading_service.dart';
Expand Down Expand Up @@ -146,15 +147,15 @@ class _CoinSelectItemState extends ConsumerState<CoinSelectItem> {
else
tokenImageUri != null
? tokenImageUri!.toLowerCase().endsWith(".svg")
? SvgPicture.network(
tokenImageUri!,
? SvgPicture(
TorAwareSvgNetworkLoader(tokenImageUri!),
width: 26,
height: 26,
placeholderBuilder: (_) =>
const AppIcon(width: 26, height: 26),
)
: Image.network(
tokenImageUri!,
: Image(
image: TorAwareNetworkImage(tokenImageUri!),
width: 26,
height: 26,
errorBuilder: (_, _, _) => SvgPicture.file(
Expand Down
5 changes: 3 additions & 2 deletions lib/pages/cakepay/cakepay_card_detail_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../networking/tor_aware_network_image.dart';
import '../../providers/global/locale_provider.dart';
import '../../services/cakepay/cakepay_service.dart';
import '../../services/cakepay/src/models/card.dart';
Expand Down Expand Up @@ -340,8 +341,8 @@ class _CardImage extends StatelessWidget {
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
imageUrl,
child: Image(
image: TorAwareNetworkImage(imageUrl),
width: isDesktop ? 300 : null,
fit: isDesktop ? .contain : .fitWidth,
loadingBuilder: (_, child, event) {
Expand Down
49 changes: 23 additions & 26 deletions lib/pages/cakepay/cakepay_vendors_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';

import '../../networking/tor_aware_network_image.dart';
import '../../services/cakepay/cakepay_service.dart';
import '../../services/cakepay/src/models/card.dart';
import '../../themes/stack_colors.dart';
Expand Down Expand Up @@ -87,9 +88,8 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
}

Future<void> _onCardTapped(CakePayCard card) async {
await Navigator.of(
context,
).pushNamed(CakePayCardDetailView.routeName, arguments: card);
await Navigator.of(context)
.pushNamed(CakePayCardDetailView.routeName, arguments: card);
}

@override
Expand Down Expand Up @@ -130,9 +130,9 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
condition: !isDesktop,
builder: (child) => Background(
child: Scaffold(
backgroundColor: Theme.of(
context,
).extension<StackColors>()!.background,
backgroundColor: Theme.of(context)
.extension<StackColors>()!
.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () => Navigator.of(context).pop(),
Expand Down Expand Up @@ -210,9 +210,9 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
child: Text(
"No items",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
),
Expand All @@ -226,9 +226,9 @@ class _CakePayVendorsViewState extends State<CakePayVendorsView> {
Text(
error.toString(),
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textSubtitle1,
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,
),
),
const SizedBox(height: 16),
Expand Down Expand Up @@ -276,9 +276,9 @@ class _SearchField extends StatelessWidget {
focusNode: focusNode,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(
context,
).extension<StackColors>()!.textFieldActiveText,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.field(context),
decoration:
Expand Down Expand Up @@ -327,9 +327,8 @@ class _CountryDropdown extends StatelessWidget {
);

final itemStyle = isDesktop
? STextStyles.desktopTextExtraSmall(
context,
).copyWith(color: colors.textFieldActiveText)
? STextStyles.desktopTextExtraSmall(context)
.copyWith(color: colors.textFieldActiveText)
: STextStyles.w500_14(context);

return ClipRRect(
Expand All @@ -341,9 +340,8 @@ class _CountryDropdown extends StatelessWidget {
hint: Text(
"All countries",
style: isDesktop
? STextStyles.desktopTextExtraSmall(
context,
).copyWith(color: colors.textFieldDefaultSearchIconLeft)
? STextStyles.desktopTextExtraSmall(context)
.copyWith(color: colors.textFieldDefaultSearchIconLeft)
: STextStyles.fieldLabel(context),
),
items: [
Expand Down Expand Up @@ -446,8 +444,8 @@ class _CardTile extends StatelessWidget {
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: card.cardImageUrl != null
? Image.network(
card.cardImageUrl!,
? Image(
image: TorAwareNetworkImage(card.cardImageUrl!),
width: isDesktop ? 60 : 48,
height: isDesktop ? 40 : 32,
fit: BoxFit.cover,
Expand Down Expand Up @@ -483,9 +481,8 @@ class _CardTile extends StatelessWidget {
].join(' '),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.itemSubtitle12(
context,
).copyWith(color: colors.textSubtitle1),
: STextStyles.itemSubtitle12(context)
.copyWith(color: colors.textSubtitle1),
),
],
),
Expand Down
Loading
Loading