diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..a319913a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,64 @@ +name: 🐞 Bug Report +description: File a new bug report +title: 'Bug: ' +labels: [Bug] +body: + - type: markdown + attributes: + value: ':stop_sign: _For support questions, please visit the [Discord](https://discord.gg/mRPZuXx3At), [Telegram](https://t.me/stackwallet) or [Reddit](https://www.reddit.com/r/stackwallet/) instead._' + - type: checkboxes + attributes: + label: 'Is there an existing issue for this?' + description: 'Please [search :mag: the issues](https://github.com/cypherstack/stack_wallet/issues) to check if this bug has already been reported.' + options: + - label: 'I have searched the existing issues' + required: true + - type: textarea + attributes: + label: 'Current Behavior' + description: 'Describe the problem you are experiencing. **Please do not paste your logs here.** Screenshots are welcome.' + validations: + required: true + - type: textarea + attributes: + label: 'Expected Behavior' + description: 'Describe what you expect to happen instead.' + validations: + required: true + - type: textarea + attributes: + label: 'Reproduce Steps' + description: | + Please provide a the _smallest, complete steps_ that Stack Wallet's maintainers can run to reproduce the issue ([read more about what this entails](https://stackoverflow.com/help/minimal-reproducible-example)). Failing this, any sort of reproduction steps are better than nothing! + validations: + required: true + - type: textarea + attributes: + label: 'Environment' + description: 'Please provide the following information about your environment.' + value: | + - Operating system and version: + - Device platform and version: + - Real device or emulator/simulator: + validations: + required: true + - type: input + attributes: + label: 'Logs' + description: | + Create a [Gist](https://gist.github.com) which contains your _full_ Stack Wallet logs and link it here. + + :warning: _Remember to redact or remove any sensitive information!_ + placeholder: 'https://gist.github.com/...' + validations: + required: false + - type: textarea + attributes: + label: 'Further Information' + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + validations: + required: false + - type: markdown + attributes: + value: ':stop_sign: _For support questions, please visit the [Discord](https://discord.gg/mRPZuXx3At), [Telegram](https://t.me/stackwallet) or [Reddit](https://www.reddit.com/r/stackwallet/) instead._' \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..c2c87271b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,37 @@ +name: Feature request +description: Suggest an idea for this project +title: 'FR: <title>' +labels: [Feature Request] +body: + - type: textarea + attributes: + label: 'Is Problem' + description: 'Is your feature request related to a problem? Please describe.' + value: | + A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: false + - type: textarea + attributes: + label: 'Solution' + description: 'Describe the solution you'd like.' + value: | + A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: 'Alternatives' + description: 'Describe alternatives you've considered.' + value: | + A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + - type: textarea + attributes: + label: 'Context' + description: 'Additional context.' + value: | + Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/docs/building.md b/docs/building.md index 4409980d7..3f5f4415a 100644 --- a/docs/building.md +++ b/docs/building.md @@ -4,7 +4,7 @@ Here you will find instructions on how to install the necessary tools for buildi ## Prerequisites -- The only OS supported for building is Ubuntu 20.04. Advanced users may also be able to build on other Debian-based distributions like Fedora 37. +- The only OS supported for building is Ubuntu 20.04. Advanced users may also be able to build on other Debian-based distributions like Linux Mint. - Android setup ([Android Studio](https://developer.android.com/studio) and subsequent dependencies) - 100 GB of storage @@ -54,6 +54,7 @@ sudo apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 lib32z1 libbz2- Install [Rust](https://www.rust-lang.org/tools/install) with command: ``` curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source ~/.bashrc rustup install 1.67.1 rustup default 1.67.1 ``` diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index af2e7bf31..4f2f20dc9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -458,7 +458,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 102; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 4DQKUWSG6C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -512,7 +512,7 @@ "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", ); - MARKETING_VERSION = 1.5.28; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -645,7 +645,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 102; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 4DQKUWSG6C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -699,7 +699,7 @@ "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", ); - MARKETING_VERSION = 1.5.28; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -724,7 +724,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 102; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 4DQKUWSG6C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -778,7 +778,7 @@ "$(PROJECT_DIR)/../crypto_plugins/flutter_libmonero/cw_shared_external/ios/External/ios/**", "$(PROJECT_DIR)/../crypto_plugins/flutter_libepiccash/ios/libs", ); - MARKETING_VERSION = 1.5.28; + MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)"; ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackwallet; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index a88674a59..0e9d9efc4 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -2,6 +2,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter_native_splash/cli_commands.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; +import 'package:stackwallet/models/isar/models/block_explorer.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -34,6 +35,7 @@ class MainDB { AddressSchema, AddressLabelSchema, EthContractSchema, + TransactionBlockExplorerSchema, StackThemeSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, @@ -45,6 +47,25 @@ class MainDB { return true; } + // tx block explorers + TransactionBlockExplorer? getTransactionBlockExplorer({required Coin coin}) { + return isar.transactionBlockExplorers + .where() + .tickerEqualTo(coin.ticker) + .findFirstSync(); + } + + Future<int> putTransactionBlockExplorer( + TransactionBlockExplorer explorer) async { + try { + return await isar.writeTxn(() async { + return await isar.transactionBlockExplorers.put(explorer); + }); + } catch (e) { + throw MainDBException("failed putTransactionBlockExplorer: $explorer", e); + } + } + // addresses QueryBuilder<Address, Address, QAfterWhereClause> getAddresses( String walletId) => diff --git a/lib/models/isar/models/block_explorer.dart b/lib/models/isar/models/block_explorer.dart new file mode 100644 index 000000000..cf01fa5e2 --- /dev/null +++ b/lib/models/isar/models/block_explorer.dart @@ -0,0 +1,35 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +part 'block_explorer.g.dart'; + +@collection +class TransactionBlockExplorer { + TransactionBlockExplorer({ + required this.ticker, + required this.url, + }); + + Id id = Isar.autoIncrement; + + @Index(unique: true, replace: true) + late final String ticker; + + late final String url; + + @ignore + Coin? get coin { + try { + return coinFromTickerCaseInsensitive(ticker); + } catch (_) { + return null; + } + } + + Uri? getUrlFor({required String txid}) => Uri.tryParse( + url.replaceFirst( + "%5BTXID%5D", + txid, + ), + ); +} diff --git a/lib/models/isar/models/block_explorer.g.dart b/lib/models/isar/models/block_explorer.g.dart new file mode 100644 index 000000000..f524392d5 --- /dev/null +++ b/lib/models/isar/models/block_explorer.g.dart @@ -0,0 +1,764 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'block_explorer.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters + +extension GetTransactionBlockExplorerCollection on Isar { + IsarCollection<TransactionBlockExplorer> get transactionBlockExplorers => + this.collection(); +} + +const TransactionBlockExplorerSchema = CollectionSchema( + name: r'TransactionBlockExplorer', + id: 4209077296238413906, + properties: { + r'ticker': PropertySchema( + id: 0, + name: r'ticker', + type: IsarType.string, + ), + r'url': PropertySchema( + id: 1, + name: r'url', + type: IsarType.string, + ) + }, + estimateSize: _transactionBlockExplorerEstimateSize, + serialize: _transactionBlockExplorerSerialize, + deserialize: _transactionBlockExplorerDeserialize, + deserializeProp: _transactionBlockExplorerDeserializeProp, + idName: r'id', + indexes: { + r'ticker': IndexSchema( + id: -8264639257510259247, + name: r'ticker', + unique: true, + replace: true, + properties: [ + IndexPropertySchema( + name: r'ticker', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _transactionBlockExplorerGetId, + getLinks: _transactionBlockExplorerGetLinks, + attach: _transactionBlockExplorerAttach, + version: '3.0.5', +); + +int _transactionBlockExplorerEstimateSize( + TransactionBlockExplorer object, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.ticker.length * 3; + bytesCount += 3 + object.url.length * 3; + return bytesCount; +} + +void _transactionBlockExplorerSerialize( + TransactionBlockExplorer object, + IsarWriter writer, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + writer.writeString(offsets[0], object.ticker); + writer.writeString(offsets[1], object.url); +} + +TransactionBlockExplorer _transactionBlockExplorerDeserialize( + Id id, + IsarReader reader, + List<int> offsets, + Map<Type, List<int>> allOffsets, +) { + final object = TransactionBlockExplorer( + ticker: reader.readString(offsets[0]), + url: reader.readString(offsets[1]), + ); + object.id = id; + return object; +} + +P _transactionBlockExplorerDeserializeProp<P>( + IsarReader reader, + int propertyId, + int offset, + Map<Type, List<int>> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readString(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _transactionBlockExplorerGetId(TransactionBlockExplorer object) { + return object.id; +} + +List<IsarLinkBase<dynamic>> _transactionBlockExplorerGetLinks( + TransactionBlockExplorer object) { + return []; +} + +void _transactionBlockExplorerAttach( + IsarCollection<dynamic> col, Id id, TransactionBlockExplorer object) { + object.id = id; +} + +extension TransactionBlockExplorerByIndex + on IsarCollection<TransactionBlockExplorer> { + Future<TransactionBlockExplorer?> getByTicker(String ticker) { + return getByIndex(r'ticker', [ticker]); + } + + TransactionBlockExplorer? getByTickerSync(String ticker) { + return getByIndexSync(r'ticker', [ticker]); + } + + Future<bool> deleteByTicker(String ticker) { + return deleteByIndex(r'ticker', [ticker]); + } + + bool deleteByTickerSync(String ticker) { + return deleteByIndexSync(r'ticker', [ticker]); + } + + Future<List<TransactionBlockExplorer?>> getAllByTicker( + List<String> tickerValues) { + final values = tickerValues.map((e) => [e]).toList(); + return getAllByIndex(r'ticker', values); + } + + List<TransactionBlockExplorer?> getAllByTickerSync( + List<String> tickerValues) { + final values = tickerValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'ticker', values); + } + + Future<int> deleteAllByTicker(List<String> tickerValues) { + final values = tickerValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'ticker', values); + } + + int deleteAllByTickerSync(List<String> tickerValues) { + final values = tickerValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'ticker', values); + } + + Future<Id> putByTicker(TransactionBlockExplorer object) { + return putByIndex(r'ticker', object); + } + + Id putByTickerSync(TransactionBlockExplorer object, {bool saveLinks = true}) { + return putByIndexSync(r'ticker', object, saveLinks: saveLinks); + } + + Future<List<Id>> putAllByTicker(List<TransactionBlockExplorer> objects) { + return putAllByIndex(r'ticker', objects); + } + + List<Id> putAllByTickerSync(List<TransactionBlockExplorer> objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'ticker', objects, saveLinks: saveLinks); + } +} + +extension TransactionBlockExplorerQueryWhereSort on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QWhere> { + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterWhere> + anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension TransactionBlockExplorerQueryWhere on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QWhereClause> { + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterWhereClause> idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterWhereClause> idNotEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterWhereClause> idGreaterThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterWhereClause> idLessThan(Id id, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterWhereClause> idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterWhereClause> tickerEqualTo(String ticker) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'ticker', + value: [ticker], + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterWhereClause> tickerNotEqualTo(String ticker) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'ticker', + lower: [], + upper: [ticker], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'ticker', + lower: [ticker], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'ticker', + lower: [ticker], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'ticker', + lower: [], + upper: [ticker], + includeUpper: false, + )); + } + }); + } +} + +extension TransactionBlockExplorerQueryFilter on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QFilterCondition> { + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> idEqualTo(Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> tickerEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'ticker', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> tickerGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'ticker', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> tickerLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'ticker', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> tickerBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'ticker', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> tickerStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'ticker', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> tickerEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'ticker', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> + tickerContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'ticker', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> + tickerMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'ticker', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> tickerIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'ticker', + value: '', + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> tickerIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'ticker', + value: '', + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> urlEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'url', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> urlGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'url', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> urlLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'url', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> urlBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'url', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> urlStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'url', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> urlEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'url', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> + urlContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'url', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> + urlMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'url', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> urlIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'url', + value: '', + )); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, + QAfterFilterCondition> urlIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'url', + value: '', + )); + }); + } +} + +extension TransactionBlockExplorerQueryObject on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QFilterCondition> {} + +extension TransactionBlockExplorerQueryLinks on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QFilterCondition> {} + +extension TransactionBlockExplorerQuerySortBy on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QSortBy> { + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + sortByTicker() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'ticker', Sort.asc); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + sortByTickerDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'ticker', Sort.desc); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + sortByUrl() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'url', Sort.asc); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + sortByUrlDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'url', Sort.desc); + }); + } +} + +extension TransactionBlockExplorerQuerySortThenBy on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QSortThenBy> { + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + thenByTicker() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'ticker', Sort.asc); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + thenByTickerDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'ticker', Sort.desc); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + thenByUrl() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'url', Sort.asc); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QAfterSortBy> + thenByUrlDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'url', Sort.desc); + }); + } +} + +extension TransactionBlockExplorerQueryWhereDistinct on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QDistinct> { + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QDistinct> + distinctByTicker({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'ticker', caseSensitive: caseSensitive); + }); + } + + QueryBuilder<TransactionBlockExplorer, TransactionBlockExplorer, QDistinct> + distinctByUrl({bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'url', caseSensitive: caseSensitive); + }); + } +} + +extension TransactionBlockExplorerQueryProperty on QueryBuilder< + TransactionBlockExplorer, TransactionBlockExplorer, QQueryProperty> { + QueryBuilder<TransactionBlockExplorer, int, QQueryOperations> idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder<TransactionBlockExplorer, String, QQueryOperations> + tickerProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'ticker'); + }); + } + + QueryBuilder<TransactionBlockExplorer, String, QQueryOperations> + urlProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'url'); + }); + } +} diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart index f61a880e2..3285d61cf 100644 --- a/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart +++ b/lib/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/choose_coin_view.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:tuple/tuple.dart'; class AdvancedSettingsView extends StatelessWidget { const AdvancedSettingsView({ @@ -221,6 +224,43 @@ class AdvancedSettingsView extends StatelessWidget { }, ), ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + Navigator.of(context).pushNamed(ChooseCoinView.routeName, + arguments: const Tuple3<String, String, String>( + "Manage block explorers", + "block explorer", + ManageExplorerView.routeName)); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 20, + ), + child: Row( + children: [ + Text( + "Change block explorer", + style: STextStyles.titleBold12(context), + textAlign: TextAlign.left, + ), + ], + ), + ), + ), + ), ], ), ), diff --git a/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart b/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart new file mode 100644 index 000000000..6cf02b148 --- /dev/null +++ b/lib/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/utilities/block_explorers.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class ManageExplorerView extends ConsumerStatefulWidget { + const ManageExplorerView({ + Key? key, + required this.coin, + }) : super(key: key); + + static const String routeName = "/manageExplorer"; + + final Coin coin; + + @override + ConsumerState<ManageExplorerView> createState() => _ManageExplorerViewState(); +} + +class _ManageExplorerViewState extends ConsumerState<ManageExplorerView> { + late TextEditingController textEditingController; + + @override + void initState() { + super.initState(); + textEditingController = TextEditingController( + text: + getBlockExplorerTransactionUrlFor(coin: widget.coin, txid: "[TXID]") + .toString() + .replaceAll("%5BTXID%5D", "[TXID]")); + } + + @override + Widget build(BuildContext context) { + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "${widget.coin.prettyName} block explorer", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Expanded( + child: Column( + children: [ + TextField( + controller: textEditingController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Center( + child: Text( + "Edit your block explorer above. Keep in mind that " + "every block explorer has a slightly different URL " + "scheme.\n\nPaste in your block explorer of choice," + " then edit in [TXID] where the transaction ID " + "should go, and Stack Wallet will auto fill the " + "transaction ID in that place of URL.", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + ], + )), + Align( + alignment: Alignment.bottomCenter, + child: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 480, + minHeight: 70, + ), + child: TextButton( + style: Theme.of(context) + .extension<StackColors>()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + textEditingController.text = + textEditingController.text.trim(); + setBlockExplorerForCoin( + coin: widget.coin, + url: Uri.parse(textEditingController.text)) + .then((value) => Navigator.of(context).pop()); + }, + child: Text( + "Save", + style: STextStyles.button(context), + ), + ), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/sub_widgets/transactions_list.dart b/lib/pages/wallet_view/sub_widgets/transactions_list.dart index a15fbbde5..e68b443ed 100644 --- a/lib/pages/wallet_view/sub_widgets/transactions_list.dart +++ b/lib/pages/wallet_view/sub_widgets/transactions_list.dart @@ -94,7 +94,10 @@ class _TransactionsListState extends ConsumerState<TransactionsList> { TransactionCard( // this may mess with combined firo transactions key: isConfirmed - ? Key(tx.txid + tx.type.name + tx.address.value.toString()) + ? Key(tx.txid + + tx.type.name + + tx.address.value.toString() + + tx.height.toString()) : UniqueKey(), // transaction: tx, walletId: widget.walletId, @@ -191,7 +194,10 @@ class _TransactionsListState extends ConsumerState<TransactionsList> { child: TransactionCard( // this may mess with combined firo transactions key: isConfirmed - ? Key(tx.txid + tx.type.name + tx.address.value.toString()) + ? Key(tx.txid + + tx.type.name + + tx.address.value.toString() + + tx.height.toString()) : UniqueKey(), transaction: tx, walletId: widget.walletId, diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 19a9b6776..5b8e04e0e 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -112,10 +112,11 @@ class _TransactionDetailsViewState super.dispose(); } - String whatIsIt(TransactionType type, int height) { + String whatIsIt(Transaction tx, int height) { + final type = tx.type; if (coin == Coin.firo || coin == Coin.firoTestNet) { - if (_transaction.subType == TransactionSubType.mint) { - if (_transaction.isConfirmed(height, coin.requiredConfirmations)) { + if (tx.subType == TransactionSubType.mint) { + if (tx.isConfirmed(height, coin.requiredConfirmations)) { return "Minted"; } else { return "Minting"; @@ -127,13 +128,13 @@ class _TransactionDetailsViewState // if (_transaction.isMinting) { // return "Minting"; // } else - if (_transaction.isConfirmed(height, coin.requiredConfirmations)) { + if (tx.isConfirmed(height, coin.requiredConfirmations)) { return "Received"; } else { return "Receiving"; } } else if (type == TransactionType.outgoing) { - if (_transaction.isConfirmed(height, coin.requiredConfirmations)) { + if (tx.isConfirmed(height, coin.requiredConfirmations)) { return "Sent"; } else { return "Sending"; @@ -428,7 +429,7 @@ class _TransactionDetailsViewState _transaction.isCancelled ? "Cancelled" : whatIsIt( - _transaction.type, + _transaction, currentHeight, ), style: @@ -545,7 +546,7 @@ class _TransactionDetailsViewState _transaction.isCancelled ? "Cancelled" : whatIsIt( - _transaction.type, + _transaction, currentHeight, ), style: isDesktop diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 732d8f9a9..2c079377b 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -63,6 +63,7 @@ import 'package:stackwallet/pages/send_view/token_send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; +import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/manage_explorer_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings/appearance_settings_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings/manage_themes.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/appearance_settings/system_brightness_theme_selection_view.dart'; @@ -100,6 +101,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; +import 'package:stackwallet/widgets/choose_coin_view.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; import 'package:stackwallet/pages/token_view/token_view.dart'; @@ -207,6 +209,36 @@ class RouteGenerator { builder: (_) => const StackPrivacyCalls(isSettings: false), settings: RouteSettings(name: settings.name)); + case ChooseCoinView.routeName: + if (args is Tuple3<String, String, String>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ChooseCoinView( + title: args.item1, + coinAdditional: args.item2, + nextRouteName: args.item3, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case ManageExplorerView.routeName: + if (args is Coin) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ManageExplorerView( + coin: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case WalletsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index b6dbca26d..5820dcc59 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -1960,7 +1960,8 @@ class BitcoinCashWallet extends CoinServiceAPI if (storedTx == null || storedTx.address.value == null || - storedTx.height == null + storedTx.height == null || + (storedTx.height != null && storedTx.height! <= 0) // zero conf messes this up // !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS) ) { diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index c628dbbb4..a5619587e 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -1,6 +1,8 @@ +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/isar/models/block_explorer.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -Uri getBlockExplorerTransactionUrlFor({ +Uri getDefaultBlockExplorerUrlFor({ required Coin coin, required String txid, }) { @@ -41,3 +43,29 @@ Uri getBlockExplorerTransactionUrlFor({ return Uri.parse("https://chainz.cryptoid.info/part/tx.dws?$txid.htm"); } } + +/// returns internal Isar ID for the inserted object/record +Future<int> setBlockExplorerForCoin({ + required Coin coin, + required Uri url, +}) async { + return await MainDB.instance.putTransactionBlockExplorer( + TransactionBlockExplorer( + ticker: coin.ticker, + url: url.toString(), + ), + ); +} + +Uri getBlockExplorerTransactionUrlFor({ + required Coin coin, + required String txid, +}) { + String? url = MainDB.instance.getTransactionBlockExplorer(coin: coin)?.url; + if (url == null) { + return getDefaultBlockExplorerUrlFor(coin: coin, txid: txid); + } else { + url = url.replaceAll("%5BTXID%5D", txid); + return Uri.parse(url); + } +} diff --git a/lib/widgets/choose_coin_view.dart b/lib/widgets/choose_coin_view.dart new file mode 100644 index 000000000..baced09fe --- /dev/null +++ b/lib/widgets/choose_coin_view.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class ChooseCoinView extends ConsumerStatefulWidget { + const ChooseCoinView({ + Key? key, + required this.title, + required this.coinAdditional, + required this.nextRouteName, + }) : super(key: key); + + static const String routeName = "/chooseCoin"; + + final String title; + final String coinAdditional; + final String nextRouteName; + + @override + ConsumerState<ChooseCoinView> createState() => _ChooseCoinViewState(); +} + +class _ChooseCoinViewState extends ConsumerState<ChooseCoinView> { + List<Coin> _coins = [...Coin.values]; + + @override + void initState() { + _coins = _coins.toList(); + _coins.remove(Coin.firoTestNet); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + bool showTestNet = ref.watch( + prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), + ); + + List<Coin> coins = showTestNet + ? _coins + : _coins.sublist(0, _coins.length - kTestNetCoinCount); + + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension<StackColors>()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + widget.title, + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 12, + right: 12, + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...coins.map( + (coin) { + return Padding( + padding: const EdgeInsets.all(4), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: RawMaterialButton( + // splashColor: Theme.of(context).extension<StackColors>()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + onPressed: () { + Navigator.of(context).pushNamed( + widget.nextRouteName, + arguments: coin, + ); + }, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 24, + height: 24, + ), + const SizedBox( + width: 12, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${coin.prettyName} ${widget.coinAdditional}", + style: STextStyles.titleBold12(context), + ), + ], + ) + ], + ), + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9b48c2cf9..7beff062b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.7.6+168 +version: 1.7.7+169 environment: sdk: ">=2.17.0 <3.0.0"