From f2fd9db91b6a1d4db82dd9481a1029f2c6aa594f Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 9 Oct 2026 11:21:57 +0800 Subject: [PATCH 1/2] Order swap tokens as 1Click lists them, QTC first and USDC second The swap list no longer asks CoinGecko for a market-cap ranking or for token logos. It is 1Click's own listing, one asset per symbol, with QTC moved to the top and USDC right after it; every other token keeps its place. QTC now appears in the token picker, and picking it for the other side moves QTC there. Token icons are the monograms the picker already fell back to. --- mobile-app/lib/v2/components/token_icon.dart | 24 +--- .../lib/v2/screens/swap/swap_screen.dart | 14 +-- quantus_sdk/lib/src/models/swap_token.dart | 15 --- .../lib/src/services/swap_service.dart | 106 ++---------------- .../test/services/swap_service_test.dart | 54 +++------ 5 files changed, 39 insertions(+), 174 deletions(-) diff --git a/mobile-app/lib/v2/components/token_icon.dart b/mobile-app/lib/v2/components/token_icon.dart index 0753ecad..7a06c404 100644 --- a/mobile-app/lib/v2/components/token_icon.dart +++ b/mobile-app/lib/v2/components/token_icon.dart @@ -15,8 +15,6 @@ class TokenIcon extends StatelessWidget { Widget build(BuildContext context) { final colors = context.colorsV3; final text = context.themeTextV3; - final iconUrl = token.iconUrl; - final networkIconUrl = token.networkIconUrl; if (token.isQuantus) { return Container( @@ -34,28 +32,14 @@ class TokenIcon extends StatelessWidget { child: Stack( clipBehavior: Clip.none, children: [ - Positioned.fill( - child: ClipOval( - child: iconUrl != null - ? Image.network(iconUrl, fit: BoxFit.cover, errorBuilder: (_, _, _) => _fallback(token, colors, text)) - : _fallback(token, colors, text), - ), - ), + Positioned.fill(child: _monogram(token, colors, text)), Positioned( right: -2, bottom: -2, child: SizedBox( width: networkBadgeSize, height: networkBadgeSize, - child: ClipOval( - child: networkIconUrl != null - ? Image.network( - networkIconUrl, - fit: BoxFit.cover, - errorBuilder: (_, _, _) => _networkFallback(token, colors, text), - ) - : _networkFallback(token, colors, text), - ), + child: _networkMonogram(token, colors, text), ), ), ], @@ -63,7 +47,7 @@ class TokenIcon extends StatelessWidget { ); } - Widget _fallback(SwapToken token, AppColorsV3 colors, AppTextThemeV3 text) { + Widget _monogram(SwapToken token, AppColorsV3 colors, AppTextThemeV3 text) { return Container( decoration: BoxDecoration( color: colors.bgSurface2, @@ -81,7 +65,7 @@ class TokenIcon extends StatelessWidget { ); } - Widget _networkFallback(SwapToken token, AppColorsV3 colors, AppTextThemeV3 text) { + Widget _networkMonogram(SwapToken token, AppColorsV3 colors, AppTextThemeV3 text) { return Container( decoration: BoxDecoration( color: colors.bgVoid, diff --git a/mobile-app/lib/v2/screens/swap/swap_screen.dart b/mobile-app/lib/v2/screens/swap/swap_screen.dart index 7180d22e..26abeeac 100644 --- a/mobile-app/lib/v2/screens/swap/swap_screen.dart +++ b/mobile-app/lib/v2/screens/swap/swap_screen.dart @@ -74,7 +74,7 @@ class _SwapScreenState extends ConsumerState { final quantus = await service.getListedQuantusToken(); if (!mounted || load != _loads) return; setState(() { - _foreign ??= tokens.first; + _foreign ??= tokens.firstWhere((t) => !t.isQuantus); _quantus = quantus; }); } catch (e) { @@ -95,18 +95,18 @@ class _SwapScreenState extends ConsumerState { _amountController.clear(); } - /// Picks the external token. Picking from the QTC side moves QTC to the other side. + /// Picks a side's token. QTC heads the list: picking it for the other side + /// moves QTC there, as does picking a foreign token for QTC's side. Future _pickToken(SwapToken tapped) async { final service = ref.read(swapServiceProvider); - final foreign = _foreign!; final token = await showTokenPickerSheet( context, - current: foreign, + current: tapped, loadTokens: ({bool forceRefresh = false}) => service.getFromTokens(limit: 10, forceRefresh: forceRefresh), ); - if (token == null || !mounted) return; - setState(() => _foreign = token); - if (tapped.isQuantus) _setDirection(swapOut: !_swapOut); + if (token == null || !mounted || token == tapped) return; + if (!token.isQuantus) setState(() => _foreign = token); + if (token.isQuantus != tapped.isQuantus) _setDirection(swapOut: !_swapOut); } /// The sheet quotes with the service of the moment, so a config change diff --git a/quantus_sdk/lib/src/models/swap_token.dart b/quantus_sdk/lib/src/models/swap_token.dart index 22849cb9..3c7394ea 100644 --- a/quantus_sdk/lib/src/models/swap_token.dart +++ b/quantus_sdk/lib/src/models/swap_token.dart @@ -33,8 +33,6 @@ class SwapToken { final String network; final int decimals; final double usdPrice; - final String? iconUrl; - final String? networkIconUrl; /// QTC's side of a swap: the token that is QTC, or stands in for it. final bool isQuantus; @@ -45,25 +43,12 @@ class SwapToken { required this.network, required this.decimals, required this.usdPrice, - this.iconUrl, - this.networkIconUrl, this.isQuantus = false, }); /// Human name of [network], e.g. "Ethereum" for ETH; the code itself when unknown. String get networkName => _networkNames[network] ?? network; - SwapToken copyWith({String? iconUrl, String? networkIconUrl}) => SwapToken( - assetId: assetId, - symbol: symbol, - network: network, - decimals: decimals, - usdPrice: usdPrice, - iconUrl: iconUrl ?? this.iconUrl, - networkIconUrl: networkIconUrl ?? this.networkIconUrl, - isQuantus: isQuantus, - ); - @override bool operator ==(Object other) => other is SwapToken && assetId == other.assetId; diff --git a/quantus_sdk/lib/src/services/swap_service.dart b/quantus_sdk/lib/src/services/swap_service.dart index 71352e96..7c342da2 100644 --- a/quantus_sdk/lib/src/services/swap_service.dart +++ b/quantus_sdk/lib/src/services/swap_service.dart @@ -65,18 +65,14 @@ class SwapService { static const statusPollInterval = Duration(seconds: 5); static const _savedAddressesKey = 'swap_saved_addresses'; static const _maxSavedAddresses = 50; - static const _coinGeckoTopUrl = - 'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=150&page=1&sparkline=false'; static const _tokensCacheTtl = Duration(minutes: 10); - final http.Client _client; final OneClickService _api; final String _managerPublicKey; final String? _configuredQuantusAssetId; final bool _preflight; (List, SwapToken?)? _cachedListing; DateTime? _cachedListingAt; - List? _cachedFromTokens; /// [quantusAssetId] names the listing that is QTC when its chain code does /// not; it comes from remote config so a surprising listing needs no update. @@ -89,8 +85,7 @@ class SwapService { String managerPublicKey = AppConstants.oneClickManagerPublicKey, String? quantusAssetId, bool preflight = false, - }) : _client = client ?? http.Client(), - _api = OneClickService(client: client, endpoint: endpoint, apiKey: apiKey), + }) : _api = OneClickService(client: client, endpoint: endpoint, apiKey: apiKey), _managerPublicKey = managerPublicKey, _configuredQuantusAssetId = quantusAssetId, _preflight = preflight; @@ -99,15 +94,16 @@ class SwapService { static Duration depositWindowFor(String network) => _slowNetworks.contains(network) ? _slowDepositWindow : depositWindow; + /// The tokens to swap between: QTC first, USDC second, the rest as 1Click + /// orders them, one asset per symbol. Future> getFromTokens({int limit = 10, bool forceRefresh = false}) async { - final (tokens, _) = await _listing(forceRefresh: forceRefresh); - final ranked = _cachedFromTokens ??= await _rankByCoinGecko(tokens); - return ranked.take(limit).toList(); + final (tokens, quantus) = await _listing(forceRefresh: forceRefresh); + final usdc = tokens.where((t) => t.symbol == 'USDC').firstOrNull; + return [?quantus, ?usdc, ...tokens.where((t) => t != usdc)].take(limit).toList(); } /// QTC as 1Click lists it, with its asset id, decimals and price; null until - /// it is listed with a price, and swaps are unavailable until then. Costs - /// the listing alone, never the CoinGecko ranking. + /// it is listed with a price, and swaps are unavailable until then. Future getListedQuantusToken({bool forceRefresh = false}) async => (await _listing(forceRefresh: forceRefresh)).$2; @@ -118,7 +114,6 @@ class SwapService { final listing = await _fetchIntentsTokens(); _cachedListing = listing; _cachedListingAt = now; - _cachedFromTokens = null; return listing; } @@ -280,8 +275,9 @@ class SwapService { return SwapOrder.fromStatusJson(json as Map, quote: order.quote); } - /// The tokens to swap with, one per symbol, and QTC as listed. A token is on - /// Quantus when 1Click says so or when its asset id is the configured one. + /// The tokens to swap with, one per symbol in 1Click's order, and QTC as + /// listed. A token is on Quantus when 1Click says so or when its asset id is + /// the configured one. Future<(List, SwapToken?)> _fetchIntentsTokens() async { final data = await _api.send('GET', '/v0/tokens') as List; final bySymbol = {}; @@ -297,7 +293,6 @@ class SwapService { network: network, decimals: (item['decimals'] as num).toInt(), usdPrice: (item['price'] as num?)?.toDouble() ?? 0, - networkIconUrl: _networkIconUrl(network), isQuantus: ours, ); if (ours) { @@ -347,41 +342,6 @@ class SwapService { return listed; } - /// Orders [tokens] by CoinGecko market cap and picks up their icons. A - /// CoinGecko failure only costs the ordering, so it is logged, not thrown. - Future> _rankByCoinGecko(List tokens) async { - final rankBySymbol = {}; - final iconBySymbol = {}; - try { - final response = await _client.get(Uri.parse(_coinGeckoTopUrl)); - if (response.statusCode < 200 || response.statusCode >= 300) { - throw SwapApiException(response.statusCode, response.body); - } - final payload = jsonDecode(response.body) as List; - for (var i = 0; i < payload.length; i++) { - final item = payload[i] as Map; - final symbol = (item['symbol'] as String).toUpperCase(); - if (rankBySymbol.containsKey(symbol)) continue; - rankBySymbol[symbol] = i; - final icon = item['image'] as String?; - if (icon != null && icon.isNotEmpty) iconBySymbol[symbol] = icon; - } - } catch (e) { - quantusPrint('CoinGecko ranking failed, sorting swap tokens by price: $e'); - } - final ranked = [ - for (final token in tokens) - token.copyWith(iconUrl: iconBySymbol[token.symbol] ?? _fallbackTokenIconUrl(token.symbol)), - ]; - ranked.sort((a, b) { - final ar = rankBySymbol[a.symbol] ?? 99999; - final br = rankBySymbol[b.symbol] ?? 99999; - if (ar != br) return ar.compareTo(br); - return b.usdPrice.compareTo(a.usdPrice); - }); - return ranked; - } - int _networkPriority(String network) { switch (network) { case 'ETH': @@ -401,52 +361,6 @@ class SwapService { } } - String? _fallbackTokenIconUrl(String symbol) { - switch (symbol) { - case 'USDC': - return 'https://assets.coingecko.com/coins/images/6319/large/usdc.png'; - case 'USDT': - return 'https://assets.coingecko.com/coins/images/325/large/Tether.png'; - case 'ETH': - case 'WETH': - return 'https://assets.coingecko.com/coins/images/279/large/ethereum.png'; - case 'BTC': - case 'WBTC': - case 'XBTC': - return 'https://assets.coingecko.com/coins/images/1/large/bitcoin.png'; - case 'SOL': - return 'https://assets.coingecko.com/coins/images/4128/large/solana.png'; - case 'NEAR': - case 'WNEAR': - return 'https://assets.coingecko.com/coins/images/10365/large/near.jpg'; - default: - return null; - } - } - - String? _networkIconUrl(String network) { - switch (network) { - case 'ETH': - case 'BASE': - case 'ARB': - case 'OP': - case 'GNOSIS': - case 'AVAX': - case 'POL': - case 'MONAD': - case 'BSC': - return 'https://assets.coingecko.com/coins/images/279/large/ethereum.png'; - case 'BTC': - return 'https://assets.coingecko.com/coins/images/1/large/bitcoin.png'; - case 'SOL': - return 'https://assets.coingecko.com/coins/images/4128/large/solana.png'; - case 'NEAR': - return 'https://assets.coingecko.com/coins/images/10365/large/near.jpg'; - default: - return null; - } - } - /// Remembers [address] on [network] for future swaps, most recent first. Future saveAddress(String network, String address) async { final prefs = await SharedPreferences.getInstance(); diff --git a/quantus_sdk/test/services/swap_service_test.dart b/quantus_sdk/test/services/swap_service_test.dart index d304bed7..2bfc3f58 100644 --- a/quantus_sdk/test/services/swap_service_test.dart +++ b/quantus_sdk/test/services/swap_service_test.dart @@ -461,7 +461,7 @@ void main() { isQuantus: true, ); expect(quantus.networkName, 'Quantus'); - expect(quantus.copyWith(iconUrl: 'x').isQuantus, isTrue); + expect(quantus.isQuantus, isTrue); }); }); @@ -596,10 +596,7 @@ void main() { ]; SwapService listing(List> list, {String? quantusAssetId, bool preflight = false}) => _service( - (r) async { - if (r.url.host == 'api.coingecko.com') return http.Response('down', 503); - return http.Response(jsonEncode(list), 200); - }, + (r) async => http.Response(jsonEncode(list), 200), quantusAssetId: quantusAssetId, preflight: preflight, ); @@ -615,24 +612,17 @@ void main() { expect(quantus.networkName, 'Quantus'); expect(quantus.decimals, AppConstants.decimals); expect(quantus.usdPrice, 1.5); - expect((await service.getFromTokens()).map((t) => t.symbol), isNot(contains('QTC'))); + final listed = await service.getFromTokens(); + expect(listed.first, quantus); + expect(listed.where((t) => t.symbol == 'QTC'), [quantus]); }, ); - test('the listed QTC costs the listing alone, never CoinGecko', () async { - final service = _service((r) async { - if (r.url.host == 'api.coingecko.com') fail('CoinGecko was asked for the price of QTC'); - return http.Response(jsonEncode(tokens), 200); - }); - expect((await service.getListedQuantusToken())!.usdPrice, 1.5); - }); - test('revalidates the listing by its ETag and keeps the body on a 304', () async { final sent = []; var price = 1.5; var version = 'W/"v1"'; final service = _service((r) async { - if (r.url.host == 'api.coingecko.com') return http.Response('down', 503); sent.add(r.headers['If-None-Match']); if (r.headers['If-None-Match'] == version) return http.Response('', 304); final body = [ @@ -664,7 +654,9 @@ void main() { expect(quantus.isQuantus, isTrue); expect(quantus.network, SwapToken.quantusNetwork); expect(quantus.usdPrice, 2); - expect((await service.getFromTokens()).map((t) => t.assetId), isNot(contains(listedId))); + final listed = await service.getFromTokens(); + expect(listed.first.assetId, listedId); + expect(listed.skip(1).map((t) => t.assetId), isNot(contains(listedId))); }); test('a configured asset id picks QTC out of several tokens on Quantus', () async { @@ -722,7 +714,9 @@ void main() { expect(standIn.symbol, 'WNEAR'); expect(standIn.network, 'NEAR'); expect(standIn.decimals, 24); - expect((await service.getFromTokens()).map((t) => t.symbol), ['BTC', 'USDC']); + final listed = await service.getFromTokens(); + expect(listed.first, standIn); + expect(listed.map((t) => t.symbol), ['WNEAR', 'USDC', 'BTC']); }); test('a QTC listing without a price leaves swaps unavailable', () async { @@ -732,44 +726,32 @@ void main() { expect(await service.getListedQuantusToken(), isNull); }); - test('keeps one asset per symbol, preferring the main network, ranked by CoinGecko', () async { + test('lists QTC first, USDC second and the rest as 1Click orders them, one asset per symbol', () async { final service = _service((r) async { - if (r.url.host == 'api.coingecko.com') { - return http.Response( - jsonEncode([ - {'symbol': 'btc', 'image': 'https://img/btc.png'}, - {'symbol': 'usdc', 'image': 'https://img/usdc.png'}, - ]), - 200, - ); - } expect(r.url.toString(), 'https://oneclick.test/v0/tokens'); return http.Response(jsonEncode(tokens), 200); }); final result = await service.getFromTokens(); - expect(result.map((t) => t.symbol), ['BTC', 'USDC', 'WNEAR']); + expect(result.map((t) => t.symbol), ['QTC', 'USDC', 'WNEAR', 'BTC']); + expect(result.first.isQuantus, isTrue); final usdc = result[1]; expect(usdc.assetId, _usdcEth.assetId); expect(usdc.network, 'ETH'); expect(usdc.decimals, 6); expect(usdc.usdPrice, 0.99966); - expect(usdc.iconUrl, 'https://img/usdc.png'); - expect(usdc.networkIconUrl, isNotNull); - expect(result[2].iconUrl, contains('near')); }); - test('falls back to price order when CoinGecko is down, and caches', () async { + test("keeps 1Click's order when USDC is not listed, and caches the listing", () async { var tokenCalls = 0; final service = _service((r) async { - if (r.url.host == 'api.coingecko.com') return http.Response('down', 503); tokenCalls++; - return http.Response(jsonEncode(tokens), 200); + return http.Response(jsonEncode(tokens.where((t) => t['symbol'] != 'USDC').toList()), 200); }); - expect((await service.getFromTokens()).map((t) => t.symbol), ['BTC', 'WNEAR', 'USDC']); - expect((await service.getFromTokens(limit: 2)).map((t) => t.symbol), ['BTC', 'WNEAR']); + expect((await service.getFromTokens()).map((t) => t.symbol), ['QTC', 'WNEAR', 'BTC']); + expect((await service.getFromTokens(limit: 2)).map((t) => t.symbol), ['QTC', 'WNEAR']); expect(tokenCalls, 1); await service.getFromTokens(forceRefresh: true); expect(tokenCalls, 2); From d14f9563a575bae27bf5df9ea926ee0cbdb01db0 Mon Sep 17 00:00:00 2001 From: Nikolaus Heger Date: Fri, 9 Oct 2026 11:11:32 +0800 Subject: [PATCH 2/2] feature so we can test swap on testflight and internal testing --- mobile-app/build_defines.sh | 12 ++++++++++++ mobile-app/lib/shared/utils/build_label.dart | 5 +++++ .../v2/screens/settings/about_quantus_screen.dart | 3 ++- .../lib/v2/screens/settings/settings_screen.dart | 3 ++- mobile-app/upload_android.sh | 7 ++++++- mobile-app/upload_ios_build.sh | 3 ++- quantus_sdk/lib/src/constants/app_constants.dart | 8 ++++---- quantus_sdk/lib/src/models/remote_config_model.dart | 7 +++---- 8 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 mobile-app/build_defines.sh create mode 100644 mobile-app/lib/shared/utils/build_label.dart diff --git a/mobile-app/build_defines.sh b/mobile-app/build_defines.sh new file mode 100644 index 00000000..15d4f5dd --- /dev/null +++ b/mobile-app/build_defines.sh @@ -0,0 +1,12 @@ +# Sourced by the upload scripts. --swap-test: internal test build that offers swap regardless of flags and location; +# never ship it to a store's production track. +DEFINES="" +[ $# -le 1 ] || { echo "Too many options: $* (only --swap-test is supported)" >&2; exit 1; } +case "${1:-}" in + "") ;; + --swap-test) + DEFINES="--dart-define=SWAP_ALLOW_OVERRIDE=true" + echo "*** SWAP TEST BUILD: swap is on regardless of remote flags and location. Internal testing only. ***" + ;; + *) echo "Unknown option: $1 (only --swap-test is supported)" >&2; exit 1 ;; +esac diff --git a/mobile-app/lib/shared/utils/build_label.dart b/mobile-app/lib/shared/utils/build_label.dart new file mode 100644 index 00000000..ea2259da --- /dev/null +++ b/mobile-app/lib/shared/utils/build_label.dart @@ -0,0 +1,5 @@ +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/generated/version.g.dart'; + +/// The build number as settings shows it; a swap test build says so. +const shownBuildNumber = AppConstants.swapAllowOverride ? '$appBuildNumber swap-test' : appBuildNumber; diff --git a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart index 4c1ba642..3d25691c 100644 --- a/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart +++ b/mobile-app/lib/v2/screens/settings/about_quantus_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart' hide ScaffoldBase; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/generated/version.g.dart'; +import 'package:resonance_network_wallet/shared/utils/build_label.dart'; import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/utils/open_external_url.dart'; @@ -62,7 +63,7 @@ class AboutQuantusScreenV2 extends ConsumerWidget { Image.asset('assets/v2/quantus_orange_logo.png', height: 40), const SizedBox(height: 14), Text( - l10n.settingsAboutVersion(appVersion, appBuildNumber), + l10n.settingsAboutVersion(appVersion, shownBuildNumber), textAlign: TextAlign.center, style: text.caption.copyWith(color: colors.textMuted), ), diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index f005c637..f75e7bb6 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:quantus_sdk/quantus_sdk.dart' hide ScaffoldBase; import 'package:resonance_network_wallet/v2/components/scaffold_base.dart'; import 'package:resonance_network_wallet/generated/version.g.dart'; +import 'package:resonance_network_wallet/shared/utils/build_label.dart'; import 'package:resonance_network_wallet/l10n/app_localizations.dart'; import 'package:resonance_network_wallet/providers/l10n_provider.dart'; import 'package:resonance_network_wallet/shared/constants/e2e_keys.dart'; @@ -122,7 +123,7 @@ List<_SettingsHubItem> _settingsHubItems(BuildContext context, AppLocalizations ), ), title: l10n.settingsAboutTitle, - subtitle: l10n.settingsAboutHubSubtitle(appVersion, appBuildNumber), + subtitle: l10n.settingsAboutHubSubtitle(appVersion, shownBuildNumber), page: const AboutQuantusScreenV2(), ), ]; diff --git a/mobile-app/upload_android.sh b/mobile-app/upload_android.sh index 3ed54da5..a66501f8 100755 --- a/mobile-app/upload_android.sh +++ b/mobile-app/upload_android.sh @@ -1,5 +1,10 @@ #!/bin/sh -flutter build appbundle +set -eu + +cd "$(dirname "$0")" +. ./build_defines.sh + +flutter build appbundle $DEFINES open build/app/outputs/bundle/release diff --git a/mobile-app/upload_ios_build.sh b/mobile-app/upload_ios_build.sh index df92715e..f2d5f079 100755 --- a/mobile-app/upload_ios_build.sh +++ b/mobile-app/upload_ios_build.sh @@ -5,6 +5,7 @@ set -eu cd "$(dirname "$0")" +. ./build_defines.sh # Ensure Xcode export uses Apple's rsync implementation. # In case user installed custom rsync in homebrew for example @@ -16,7 +17,7 @@ echo "Cleaning build folder" rm -rf build echo "Building the app" -flutter build ipa --release +flutter build ipa --release $DEFINES echo "Opening Transporter" open -a "Transporter" "build/ios/ipa/Quantus.ipa" \ No newline at end of file diff --git a/quantus_sdk/lib/src/constants/app_constants.dart b/quantus_sdk/lib/src/constants/app_constants.dart index ac9a20ca..4881cea5 100644 --- a/quantus_sdk/lib/src/constants/app_constants.dart +++ b/quantus_sdk/lib/src/constants/app_constants.dart @@ -54,10 +54,10 @@ class AppConstants { static const String swapPreflightAddress = String.fromEnvironment('SWAP_PREFLIGHT_ADDRESS'); static const bool swapPreflight = swapPreflightAssetId != ''; - // Debug builds only: skips the NEAR Intents location verdict so swap can be - // tried from anywhere. - // flutter run --dart-define=SWAP_IGNORE_GEO=true - static const bool swapIgnoreGeo = bool.fromEnvironment('SWAP_IGNORE_GEO'); + // Internal test builds only (TestFlight, Play internal track): offers swap + // whatever the remote flags and the location say. Never ship one to a store. + // mobile-app/upload_ios_build.sh --swap-test (or upload_android.sh) + static const bool swapAllowOverride = bool.fromEnvironment('SWAP_ALLOW_OVERRIDE'); // internal group URL is this (note the /c) // https://t.me/c/quantusnetwork/2457 diff --git a/quantus_sdk/lib/src/models/remote_config_model.dart b/quantus_sdk/lib/src/models/remote_config_model.dart index 6e9e5a17..2eb1c2f5 100644 --- a/quantus_sdk/lib/src/models/remote_config_model.dart +++ b/quantus_sdk/lib/src/models/remote_config_model.dart @@ -47,11 +47,10 @@ class RemoteConfigModel { }); /// Swap is offered when the platform's flag is on and the location allows - /// NEAR Intents; a debug build started with [AppConstants.swapIgnoreGeo] - /// skips the location. + /// NEAR Intents, or always in a [AppConstants.swapAllowOverride] build. bool get swapAvailable => - (defaultTargetPlatform == TargetPlatform.android ? enableSwapAndroid : enableSwap) && - (geoNearAllowed || (kDebugMode && AppConstants.swapIgnoreGeo)); + AppConstants.swapAllowOverride || + ((defaultTargetPlatform == TargetPlatform.android ? enableSwapAndroid : enableSwap) && geoNearAllowed); RemoteConfigModel copyWith({bool? geoNearAllowed}) => RemoteConfigModel( enableTestButtons: enableTestButtons,