mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 03:49:22 +00:00
Merge branch 'ui-fixes' into trocador
This commit is contained in:
commit
34f7d80051
10 changed files with 455 additions and 119 deletions
|
@ -14,6 +14,8 @@ import 'package:stackwallet/utilities/show_loading.dart';
|
|||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/basic_dialog.dart';
|
||||
import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
|
@ -36,6 +38,36 @@ class _MyTokenSelectItemState extends ConsumerState<MyTokenSelectItem> {
|
|||
|
||||
late final CachedEthTokenBalance cachedBalance;
|
||||
|
||||
Future<bool> _loadTokenWallet(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
try {
|
||||
await ref.read(tokenServiceProvider)!.initialize();
|
||||
return true;
|
||||
} catch (_) {
|
||||
await showDialog<void>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => BasicDialog(
|
||||
title: "Failed to load token data",
|
||||
desktopHeight: double.infinity,
|
||||
desktopWidth: 450,
|
||||
rightButton: PrimaryButton(
|
||||
label: "OK",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
if (!isDesktop) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _onPressed() async {
|
||||
ref.read(tokenServiceStateProvider.state).state = EthTokenWallet(
|
||||
token: widget.token,
|
||||
|
@ -49,13 +81,17 @@ class _MyTokenSelectItemState extends ConsumerState<MyTokenSelectItem> {
|
|||
),
|
||||
);
|
||||
|
||||
await showLoading<void>(
|
||||
whileFuture: ref.read(tokenServiceProvider)!.initialize(),
|
||||
final success = await showLoading<bool>(
|
||||
whileFuture: _loadTokenWallet(context, ref),
|
||||
context: context,
|
||||
isDesktop: isDesktop,
|
||||
message: "Loading ${widget.token.name}",
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
await Navigator.of(context).pushNamed(
|
||||
isDesktop ? DesktopTokenView.routeName : TokenView.routeName,
|
||||
|
|
|
@ -53,6 +53,7 @@ import 'package:stackwallet/widgets/conditional_parent.dart';
|
|||
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/loading_indicator.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart';
|
||||
|
@ -420,6 +421,33 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
eventBus: null,
|
||||
textColor:
|
||||
Theme.of(context).extension<StackColors>()!.textDark,
|
||||
actionButton: SecondaryButton(
|
||||
label: "Cancel",
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => StackDialog(
|
||||
title: "Warning!",
|
||||
message: "Skipping this process can completely"
|
||||
" break your wallet. It is only meant to be done in"
|
||||
" emergency situations where the migration fails"
|
||||
" and will not let you continue. Still skip?",
|
||||
leftButton: SecondaryButton(
|
||||
label: "Cancel",
|
||||
onPressed:
|
||||
Navigator.of(context, rootNavigator: true).pop,
|
||||
),
|
||||
rightButton: SecondaryButton(
|
||||
label: "Ok",
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop();
|
||||
setState(() => _rescanningOnOpen = false);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -790,7 +818,6 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
|||
label: "Receive",
|
||||
icon: const ReceiveNavIcon(),
|
||||
onTap: () {
|
||||
final coin = ref.read(managerProvider).coin;
|
||||
if (mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context).pushNamed(
|
||||
|
|
|
@ -31,7 +31,11 @@ import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
|
|||
import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
|
||||
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/desktop/secondary_button.dart';
|
||||
import 'package:stackwallet/widgets/hover_text_field.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
|
||||
|
@ -150,6 +154,83 @@ class _DesktopWalletViewState extends ConsumerState<DesktopWalletView> {
|
|||
subMessage: "This only needs to run once per wallet",
|
||||
eventBus: null,
|
||||
textColor: Theme.of(context).extension<StackColors>()!.textDark,
|
||||
actionButton: SecondaryButton(
|
||||
label: "Skip",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => DesktopDialog(
|
||||
maxWidth: 500,
|
||||
maxHeight: double.infinity,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"Warning!",
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
const DesktopDialogCloseButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
"Skipping this process can completely"
|
||||
" break your wallet. It is only meant to be done in"
|
||||
" emergency situations where the migration fails"
|
||||
" and will not let you continue. Still skip?",
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SecondaryButton(
|
||||
label: "Cancel",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: Navigator.of(context,
|
||||
rootNavigator: true)
|
||||
.pop,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: "Ok",
|
||||
buttonHeight: ButtonHeight.l,
|
||||
onPressed: () {
|
||||
Navigator.of(context,
|
||||
rootNavigator: true)
|
||||
.pop();
|
||||
setState(
|
||||
() => _rescanningOnOpen = false);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
|
@ -1949,8 +1949,6 @@ class BitcoinCashWallet extends CoinServiceAPI
|
|||
|
||||
List<Map<String, dynamic>> allTransactions = [];
|
||||
|
||||
final currentHeight = await chainHeight;
|
||||
|
||||
for (final txHash in allTxHashes) {
|
||||
final storedTx = await db
|
||||
.getTransactions(walletId)
|
||||
|
@ -1958,7 +1956,9 @@ class BitcoinCashWallet extends CoinServiceAPI
|
|||
.txidEqualTo(txHash["tx_hash"] as String)
|
||||
.findFirst();
|
||||
|
||||
if (storedTx == null || storedTx.address.value == null
|
||||
if (storedTx == null ||
|
||||
storedTx.address.value == null ||
|
||||
storedTx.height == null
|
||||
// zero conf messes this up
|
||||
// !storedTx.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS)
|
||||
) {
|
||||
|
|
|
@ -253,6 +253,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache {
|
|||
);
|
||||
_credentials = web3dart.EthPrivateKey.fromHex(privateKey);
|
||||
|
||||
try {
|
||||
_deployedContract = web3dart.DeployedContract(
|
||||
ContractAbiExtensions.fromJsonList(
|
||||
jsonList: tokenContract.abi!,
|
||||
|
@ -260,6 +261,9 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache {
|
|||
),
|
||||
contractAddress,
|
||||
);
|
||||
} catch (_) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
try {
|
||||
_sendFunction = _deployedContract.function('transfer');
|
||||
|
@ -328,6 +332,7 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache {
|
|||
//====================================================================
|
||||
}
|
||||
|
||||
try {
|
||||
_deployedContract = web3dart.DeployedContract(
|
||||
ContractAbiExtensions.fromJsonList(
|
||||
jsonList: tokenContract.abi!,
|
||||
|
@ -335,6 +340,9 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache {
|
|||
),
|
||||
contractAddress,
|
||||
);
|
||||
} catch (_) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
_sendFunction = _deployedContract.function('transfer');
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:web3dart/web3dart.dart';
|
||||
|
||||
extension ContractAbiExtensions on ContractAbi {
|
||||
|
@ -7,10 +8,12 @@ extension ContractAbiExtensions on ContractAbi {
|
|||
required String name,
|
||||
required String jsonList,
|
||||
}) {
|
||||
try {
|
||||
final List<ContractFunction> functions = [];
|
||||
final List<ContractEvent> events = [];
|
||||
|
||||
final list = List<Map<String, dynamic>>.from(jsonDecode(jsonList) as List);
|
||||
final list =
|
||||
List<Map<String, dynamic>>.from(jsonDecode(jsonList) as List);
|
||||
|
||||
for (final json in list) {
|
||||
final type = json["type"] as String;
|
||||
|
@ -20,6 +23,7 @@ extension ContractAbiExtensions on ContractAbi {
|
|||
final anonymous = json["anonymous"] as bool? ?? false;
|
||||
final List<EventComponent<dynamic>> components = [];
|
||||
|
||||
if (json["inputs"] is List) {
|
||||
for (final input in json["inputs"] as List) {
|
||||
components.add(
|
||||
EventComponent(
|
||||
|
@ -28,6 +32,7 @@ extension ContractAbiExtensions on ContractAbi {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
events.add(ContractEvent(anonymous, name, components));
|
||||
} else {
|
||||
|
@ -51,6 +56,13 @@ extension ContractAbiExtensions on ContractAbi {
|
|||
}
|
||||
|
||||
return ContractAbi(name, functions, events);
|
||||
} catch (e, s) {
|
||||
Logging.instance.log(
|
||||
"Failed to parse ABI for $name: $e\n$s",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static const Map<String, ContractFunctionType> _functionTypeNames = {
|
||||
|
|
|
@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||
import 'package:stackwallet/widgets/loading_indicator.dart';
|
||||
|
||||
class CustomLoadingOverlay extends ConsumerStatefulWidget {
|
||||
|
@ -14,12 +16,14 @@ class CustomLoadingOverlay extends ConsumerStatefulWidget {
|
|||
this.subMessage,
|
||||
required this.eventBus,
|
||||
this.textColor,
|
||||
this.actionButton,
|
||||
}) : super(key: key);
|
||||
|
||||
final String message;
|
||||
final String? subMessage;
|
||||
final EventBus? eventBus;
|
||||
final Color? textColor;
|
||||
final Widget? actionButton;
|
||||
|
||||
@override
|
||||
ConsumerState<CustomLoadingOverlay> createState() =>
|
||||
|
@ -30,6 +34,7 @@ class _CustomLoadingOverlayState extends ConsumerState<CustomLoadingOverlay> {
|
|||
double _percent = 0;
|
||||
|
||||
late final StreamSubscription<double>? subscription;
|
||||
final bool isDesktop = Util.isDesktop;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -49,14 +54,45 @@ class _CustomLoadingOverlayState extends ConsumerState<CustomLoadingOverlay> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: ConditionalParent(
|
||||
condition: widget.actionButton != null,
|
||||
builder: (child) => Stack(
|
||||
children: [
|
||||
child,
|
||||
if (isDesktop)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
child: widget.actionButton!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!isDesktop)
|
||||
Positioned(
|
||||
bottom: 1,
|
||||
left: 0,
|
||||
right: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: widget.actionButton!),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.message,
|
||||
|
@ -97,10 +133,6 @@ class _CustomLoadingOverlayState extends ConsumerState<CustomLoadingOverlay> {
|
|||
.extension<StackColors>()!
|
||||
.loadingOverlayTextColor,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 64,
|
||||
|
@ -111,6 +143,8 @@ class _CustomLoadingOverlayState extends ConsumerState<CustomLoadingOverlay> {
|
|||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
104
lib/widgets/dialogs/basic_dialog.dart
Normal file
104
lib/widgets/dialogs/basic_dialog.dart
Normal file
|
@ -0,0 +1,104 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:stackwallet/utilities/text_styles.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
|
||||
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
|
||||
import 'package:stackwallet/widgets/stack_dialog.dart';
|
||||
|
||||
class BasicDialog extends StatelessWidget {
|
||||
const BasicDialog({
|
||||
Key? key,
|
||||
this.leftButton,
|
||||
this.rightButton,
|
||||
this.icon,
|
||||
required this.title,
|
||||
this.message,
|
||||
this.desktopHeight = 474,
|
||||
this.desktopWidth = 641,
|
||||
this.canPopWithBackButton = false,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget? leftButton;
|
||||
final Widget? rightButton;
|
||||
|
||||
final Widget? icon;
|
||||
|
||||
final String title;
|
||||
final String? message;
|
||||
|
||||
final double? desktopHeight;
|
||||
final double desktopWidth;
|
||||
|
||||
final bool canPopWithBackButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = Util.isDesktop;
|
||||
|
||||
if (isDesktop) {
|
||||
return DesktopDialog(
|
||||
maxHeight: desktopHeight,
|
||||
maxWidth: desktopWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: STextStyles.desktopH3(context),
|
||||
),
|
||||
const DesktopDialogCloseButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (message != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
message!,
|
||||
style: STextStyles.desktopTextSmall(context),
|
||||
),
|
||||
),
|
||||
if (leftButton != null || rightButton != null)
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
if (leftButton != null || rightButton != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Row(
|
||||
children: [
|
||||
leftButton != null
|
||||
? Expanded(child: leftButton!)
|
||||
: const Spacer(),
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
rightButton != null
|
||||
? Expanded(child: rightButton!)
|
||||
: const Spacer(),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return canPopWithBackButton;
|
||||
},
|
||||
child: StackDialog(
|
||||
title: title,
|
||||
leftButton: leftButton,
|
||||
rightButton: rightButton,
|
||||
icon: icon,
|
||||
message: message,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,8 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|||
import 'package:stackwallet/utilities/show_loading.dart';
|
||||
import 'package:stackwallet/utilities/util.dart';
|
||||
import 'package:stackwallet/widgets/conditional_parent.dart';
|
||||
import 'package:stackwallet/widgets/desktop/primary_button.dart';
|
||||
import 'package:stackwallet/widgets/dialogs/basic_dialog.dart';
|
||||
import 'package:stackwallet/widgets/rounded_white_container.dart';
|
||||
import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
@ -37,7 +39,7 @@ class SimpleWalletCard extends ConsumerWidget {
|
|||
final bool popPrevious;
|
||||
final NavigatorState? desktopNavigatorState;
|
||||
|
||||
Future<void> _loadTokenWallet(
|
||||
Future<bool> _loadTokenWallet(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Manager manager,
|
||||
|
@ -52,7 +54,31 @@ class SimpleWalletCard extends ConsumerWidget {
|
|||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await ref.read(tokenServiceProvider)!.initialize();
|
||||
return true;
|
||||
} catch (_) {
|
||||
await showDialog<void>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => BasicDialog(
|
||||
title: "Failed to load token data",
|
||||
desktopHeight: double.infinity,
|
||||
desktopWidth: 450,
|
||||
rightButton: PrimaryButton(
|
||||
label: "OK",
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
if (desktopNavigatorState == null) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void _openWallet(BuildContext context, WidgetRef ref) async {
|
||||
|
@ -91,13 +117,21 @@ class SimpleWalletCard extends ConsumerWidget {
|
|||
final contract =
|
||||
ref.read(mainDBProvider).getEthContractSync(contractAddress!)!;
|
||||
|
||||
await showLoading<void>(
|
||||
whileFuture: _loadTokenWallet(context, ref, manager, contract),
|
||||
context: context,
|
||||
final success = await showLoading<bool>(
|
||||
whileFuture: _loadTokenWallet(
|
||||
desktopNavigatorState?.context ?? context,
|
||||
ref,
|
||||
manager,
|
||||
contract),
|
||||
context: desktopNavigatorState?.context ?? context,
|
||||
opaqueBG: true,
|
||||
message: "Loading ${contract.name}",
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (desktopNavigatorState == null) {
|
||||
// pop loading
|
||||
nav.pop();
|
||||
|
|
|
@ -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.4+164
|
||||
version: 1.7.4+165
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
|
|
Loading…
Reference in a new issue