diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index ce868592f..da19a3190 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -25,8 +25,6 @@ jobs: KEY_JKS: ${{ secrets.KEY_JKS }} KEY_PASSWORD: ${{ secrets.ALIAS_PASSWORD }} ALIAS_PASSWORD: ${{ secrets.KEY_PASSWORD }} - MAP_API_KEY: ${{ secrets.MAP_API_KEY }} - RAMP_API_KEY: ${{ secrets.RAMP_API_KEY }} run: echo -n $KEY_JKS | base64 -di > android/key.jks && just docker-build-android-sign - uses: actions/upload-artifact@v2 diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index b8e1ab9a5..f17478669 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -70,11 +70,8 @@ jobs: run: ./build_ffi_ios.sh - name: Build iOS App Store Package - env: - MAP_API_KEY: ${{ secrets.MAP_API_KEY }} - RAMP_API_KEY: ${{ secrets.RAMP_API_KEY }} run: | - flutter build ipa --dart-define=MAP_API_KEY=$MAP_API_KEY --dart-define=RAMP_API_KEY=$RAMP_API_KEY --release --export-options-plist=ios/ExportOptions.plist + flutter build ipa --release --export-options-plist=ios/ExportOptions.plist - uses: actions/upload-artifact@v2 with: diff --git a/justfile b/justfile index ed945d419..29f18fc1d 100644 --- a/justfile +++ b/justfile @@ -11,18 +11,17 @@ docker-build: docker-build-android: docker-build mkdir -p release && \ docker run --mount type=bind,source="$(pwd)"/release,target=/release \ - -e MAP_API_KEY=$MAP_API_KEY -e RAMP_API_KEY=$RAMP_API_KEY\ -t {{docker_image}} /bin/bash \ - -c "flutter build apk --release --dart-define='MAP_API_KEY=$MAP_API_KEY' --dart-define='RAMP_API_KEY=$RAMP_API_KEY' -P nosign && flutter build appbundle --dart-define='MAP_API_KEY=$MAP_API_KEY' --dart-define='RAMP_API_KEY=$RAMP_API_KEY' --release -P nosign \ + -c "flutter build apk --release -P nosign && flutter build appbundle --release -P nosign \ && cp /root/build/app/outputs/flutter-apk/app-release.apk /release \ && cp /root/build/app/outputs/bundle/release/app-release.aab /release" docker-build-android-sign: docker-build mkdir -p release && \ docker run --mount type=bind,source="$(pwd)"/release,target=/release \ - -e ALIAS_PASSWORD=$ALIAS_PASSWORD -e KEY_PASSWORD=$KEY_PASSWORD -e MAP_API_KEY=$MAP_API_KEY -e RAMP_API_KEY=$RAMP_API_KEY \ + -e ALIAS_PASSWORD=$ALIAS_PASSWORD -e KEY_PASSWORD=$KEY_PASSWORD \ -t {{docker_image}} /bin/bash \ - -c "flutter build apk --dart-define='MAP_API_KEY=$MAP_API_KEY' --dart-define='RAMP_API_KEY=$RAMP_API_KEY' --release && flutter build appbundle --dart-define='MAP_API_KEY=$MAP_API_KEY' --dart-define='RAMP_API_KEY=$RAMP_API_KEY' --release \ + -c "flutter build apk --release && flutter build appbundle --release \ && cp /root/build/app/outputs/flutter-apk/app-release.apk /release \ && cp /root/build/app/outputs/bundle/release/app-release.aab /release" diff --git a/lib/business/keys_manager.dart b/lib/business/keys_manager.dart new file mode 100644 index 000000000..35d48c4cc --- /dev/null +++ b/lib/business/keys_manager.dart @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 Foundation Devices Inc. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import 'dart:async'; +import 'package:envoy/util/console.dart'; +import 'package:envoy/util/envoy_storage.dart'; +import 'package:envoy/business/server.dart'; + +class KeysManager { + ApiKeys? keys; + + static final KeysManager _instance = KeysManager._internal(); + + factory KeysManager() { + return _instance; + } + + static Future init() async { + var singleton = KeysManager._instance; + await singleton._initAsync(); + return singleton; + } + + _initAsync() async { + keys = await EnvoyStorage().getApiKeys(); + } + + KeysManager._internal() { + kPrint("Instance of KeysManager created!"); + + // Fetch from the Envoy Server + _fetchKeysFromServer(); + + // Check once an hour + Timer.periodic(const Duration(hours: 1), (_) { + _fetchKeysFromServer(); + }); + } + + void _fetchKeysFromServer() { + Server().fetchApiKeys().then((keys) => _store(keys)).catchError((e) { + kPrint("Couldn't fetch API keys: $e"); + }); + } + + _store(ApiKeys newKeys) async { + if (keys == null || + keys!.mapsKey != newKeys.mapsKey || + keys!.rampKey != newKeys.rampKey) { + await EnvoyStorage().storeApiKeys(newKeys); + keys = newKeys; + } + } +} diff --git a/lib/business/map_data.dart b/lib/business/map_data.dart index b1146b1e5..701e98943 100644 --- a/lib/business/map_data.dart +++ b/lib/business/map_data.dart @@ -10,11 +10,8 @@ import 'package:tor/tor.dart'; import 'package:http_tor/http_tor.dart'; import 'package:envoy/business/scheduler.dart'; import 'dart:core'; -import 'package:envoy/business/coordinates.dart'; class MapData { - static const String mapApiKey = - String.fromEnvironment("MAP_API_KEY", defaultValue: ''); List venues = []; static final MapData _instance = MapData._internal(); @@ -107,42 +104,4 @@ class MapData { "https://coinmap.org/api/v1/venues/$id", ); } - - Future getCoordinates( - String divisionName, String countryName) async { - var response = await HttpTor(Tor.instance, EnvoyScheduler().parallel).get( - "https://api.geoapify.com/v1/geocode/search?text=$divisionName&format=json&apiKey=$mapApiKey", - ); - - var data = jsonDecode(response.body); - if (data['results'] != null && data['results'].isNotEmpty) { - for (var result in data['results']) { - if (result['country'] == countryName) { - // Extract latitude and longitude - double latitude = (result['lat'] as num).toDouble(); - double longitude = (result['lon'] as num).toDouble(); - - return Coordinates(latitude, longitude); - } - } - } - - // If no matching result is found, search by country name - var response2 = await HttpTor(Tor.instance, EnvoyScheduler().parallel).get( - "https://api.geoapify.com/v1/geocode/search?country=$countryName&format=json&apiKey=$mapApiKey", - ); - - var data2 = jsonDecode(response2.body); - if (data2['results'] != null && data2['results'].isNotEmpty) { - var firstResult = data2['results'][0]; - - // Extract latitude and longitude - double latitude = (firstResult['lat'] as num).toDouble(); - double longitude = (firstResult['lon'] as num).toDouble(); - - return Coordinates(latitude, longitude); - } - - return Coordinates(null, null); - } } diff --git a/lib/business/server.dart b/lib/business/server.dart index a64eae6c7..89d96e251 100644 --- a/lib/business/server.dart +++ b/lib/business/server.dart @@ -29,20 +29,39 @@ class Server { throw Exception('Failed to find firmware'); } } + + Future fetchApiKeys() async { + final response = await http!.get('$_serverAddress/keys'); + + if (response.statusCode == 202) { + return ApiKeys.fromJson(jsonDecode(response.body)); + } else { + throw Exception('Failed to fetch API keys'); + } + } } -class User { - final String id; - bool confirmed = false; - String token = ""; +class ApiKeys { + final String mapsKey; + final String rampKey; - User({ - required this.id, - required this.confirmed, + ApiKeys({ + required this.mapsKey, + required this.rampKey, }); - factory User.fromJson(Map json) { - return User(id: json['id'], confirmed: json['confirmed']); + factory ApiKeys.fromJson(Map json) { + final keys = json['keys']; + return ApiKeys(mapsKey: keys['maps_api'], rampKey: keys['ramp_api']); + } + + Map toJson() { + return { + 'keys': { + 'maps_api': mapsKey, + 'ramp_api': rampKey, + } + }; } } diff --git a/lib/main.dart b/lib/main.dart index b3514354d..791bb8416 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:envoy/business/account_manager.dart'; import 'package:envoy/business/connectivity_manager.dart'; import 'package:envoy/business/envoy_seed.dart'; +import 'package:envoy/business/keys_manager.dart'; import 'package:envoy/business/map_data.dart'; import 'package:envoy/business/scheduler.dart'; import 'package:envoy/business/local_storage.dart'; @@ -58,6 +59,7 @@ Future initSingletons() async { await EnvoyStorage().init(); await LocalStorage.init(); EnvoyScheduler.init(); + await KeysManager.init(); EnvoyReport().init(); Settings.restore(); Tor.init(enabled: Settings().torEnabled()); diff --git a/lib/ui/components/map_widget.dart b/lib/ui/components/map_widget.dart index 244f2e44d..6b5dfd780 100644 --- a/lib/ui/components/map_widget.dart +++ b/lib/ui/components/map_widget.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:core'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:envoy/business/keys_manager.dart'; import 'package:envoy/ui/components/pop_up.dart'; import 'package:envoy/ui/theme/envoy_icons.dart'; import 'package:envoy/ui/theme/envoy_spacing.dart'; @@ -186,10 +187,13 @@ class MarkersPageState extends State { } } - String _openStreetMap(int z, int x, int y) { - final url = - "https://maps.geoapify.com/v1/tile/$mapType/$z/$x/$y.png?&apiKey=${MapData.mapApiKey}"; - return url; + String _getTileUrl(int z, int x, int y) { + final mapApiKey = KeysManager().keys?.mapsKey; + if (mapApiKey == null) { + return ""; + } + + return "https://maps.geoapify.com/v1/tile/$mapType/$z/$x/$y.png?&apiKey=$mapApiKey"; } Widget _buildVenueMarkerWidget( @@ -349,7 +353,7 @@ class MarkersPageState extends State { x %= tilesInZoom; y %= tilesInZoom; return CachedNetworkImage( - imageUrl: _openStreetMap(z, x, y), + imageUrl: _getTileUrl(z, x, y), fit: BoxFit.cover, errorListener: (e) { setState(() { @@ -560,10 +564,8 @@ class TriangleShadow extends CustomPainter { center: Alignment.topCenter, focalRadius: 0.5, radius: 0.8, - stops: const [ - 0.0, - 0.9 - ], // Ensure stops are correct for smooth transition + stops: const [0.0, 0.9], + // Ensure stops are correct for smooth transition colors: [ EnvoyColors.border1.withOpacity(0.7), // Adjust colors as needed Colors.transparent diff --git a/lib/ui/components/ramp_widget.dart b/lib/ui/components/ramp_widget.dart index 07f717181..040fb4b13 100644 --- a/lib/ui/components/ramp_widget.dart +++ b/lib/ui/components/ramp_widget.dart @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later import 'dart:convert'; +import 'package:envoy/business/keys_manager.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:http_tor/http_tor.dart'; @@ -20,15 +21,16 @@ import 'package:envoy/ui/shield.dart'; import 'package:envoy/business/scheduler.dart'; class RampWidget { - static const String rampApiKey = - String.fromEnvironment("RAMP_API_KEY", defaultValue: ''); - static void showRamp(BuildContext context, Account account, String address) { + if (KeysManager().keys == null) { + return; + } + final ramp = RampFlutter(); final Configuration configuration = Configuration(); configuration.hostAppName = "Ramp Flutter"; configuration.url = "https://app.ramp.network"; - configuration.hostApiKey = rampApiKey; + configuration.hostApiKey = KeysManager().keys!.rampKey; configuration.userAddress = address; configuration.swapAsset = "BTC_BTC"; ramp.onOnrampPurchaseCreated = (purchase, purchaseViewToken, apiUrl) => diff --git a/lib/ui/storage/coins_repository.dart b/lib/ui/storage/coins_repository.dart index d17ba12a7..a5bf4eb5c 100644 --- a/lib/ui/storage/coins_repository.dart +++ b/lib/ui/storage/coins_repository.dart @@ -11,7 +11,7 @@ import 'package:sembast/sembast.dart'; /// Repository for storing and retrieving coin tag data class CoinRepository { final storage = EnvoyStorage(); - Database db = EnvoyStorage().db; + Database db = EnvoyStorage().db(); static CoinRepository? _instance; diff --git a/lib/util/envoy_storage.dart b/lib/util/envoy_storage.dart index 05f03da0b..82b7e2640 100644 --- a/lib/util/envoy_storage.dart +++ b/lib/util/envoy_storage.dart @@ -25,6 +25,7 @@ import 'package:sembast/utils/sembast_import_export.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:wallet/wallet.dart' as wallet; import 'package:envoy/business/country.dart'; +import 'package:envoy/business/server.dart'; class FirmwareInfo { FirmwareInfo({ @@ -65,6 +66,7 @@ const String blogPostsStoreName = "blog_posts"; const String exchangeRateStoreName = "exchange_rate"; const String locationsStoreName = "locations"; const String selectedCountryStoreName = "countries"; +const String apiKeysStoreName = "api_keys"; ///keeps track of spend input tags, this would be handy to show previously used tags ///for example when user trying RBF. @@ -116,6 +118,8 @@ class EnvoyStorage { StoreRef locationStore = StoreRef(locationsStoreName); + StoreRef apiKeysStore = StoreRef(apiKeysStoreName); + // Store everything except videos, blogs and locations Map storesToBackUp = {}; @@ -236,7 +240,7 @@ class EnvoyStorage { } Future removePromptState(DismissiblePrompt prompt) async { - await dismissedPromptsStore.record(prompt.toString()).delete(db); + await dismissedPromptsStore.record(prompt.toString()).delete(_db); return true; } @@ -254,7 +258,7 @@ class EnvoyStorage { final filter = Finder(filter: Filter.byKey(prompt.toString())); //returns boolean stream that updates when provided key is updated return dismissedPromptsStore - .find(db, finder: filter) + .find(_db, finder: filter) .then((event) => event.isNotEmpty); } @@ -503,7 +507,7 @@ class EnvoyStorage { // Following methods have same signature as shared_preferences // but use the preferences DB store instead Future setNewPreferencesValue(String key, value) async { - await preferencesStore.record(key).put(db, value); + await preferencesStore.record(key).put(_db, value); return true; } @@ -752,5 +756,19 @@ class EnvoyStorage { return null; } - Database get db => _db; + Future storeApiKeys(ApiKeys keys) async { + await apiKeysStore.record(0).put(_db, jsonEncode(keys.toJson())); + return true; + } + + Future getApiKeys() async { + var finder = Finder(filter: Filter.byKey(0)); + var keys = await apiKeysStore.findFirst(_db, finder: finder); + if (keys != null) { + return ApiKeys.fromJson(jsonDecode(keys.value)); + } + return null; + } + + Database db() => _db; }