mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 01:40:12 +01:00
Merge branch 'master' into totlview
This commit is contained in:
commit
d8e6425565
@ -93,7 +93,6 @@
|
||||
B44033E42BCC36FF00162242 /* WalletData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E32BCC36FF00162242 /* WalletData.swift */; };
|
||||
B44033E52BCC36FF00162242 /* WalletData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E32BCC36FF00162242 /* WalletData.swift */; };
|
||||
B44033E62BCC36FF00162242 /* WalletData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E32BCC36FF00162242 /* WalletData.swift */; };
|
||||
B44033E92BCC371A00162242 /* MarketData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E82BCC371A00162242 /* MarketData.swift */; };
|
||||
B44033EA2BCC371A00162242 /* MarketData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E82BCC371A00162242 /* MarketData.swift */; };
|
||||
B44033EB2BCC371A00162242 /* MarketData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E82BCC371A00162242 /* MarketData.swift */; };
|
||||
B44033EE2BCC374500162242 /* Numeric+abbreviated.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033ED2BCC374500162242 /* Numeric+abbreviated.swift */; };
|
||||
@ -125,7 +124,26 @@
|
||||
B4742E992CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
|
||||
B4742E9A2CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
|
||||
B4742E9B2CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
|
||||
B48630D62CCEE67100A8425C /* PriceWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */; };
|
||||
B48630DD2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */; };
|
||||
B48630DE2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */; };
|
||||
B48630E02CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630DF2CCEE7C800A8425C /* PriceWidgetEntryView.swift */; };
|
||||
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630DF2CCEE7C800A8425C /* PriceWidgetEntryView.swift */; };
|
||||
B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5272558EC52009312A5 /* PriceView.swift */; };
|
||||
B48630E72CCEE91900A8425C /* PriceWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */; };
|
||||
B48630E82CCEE92400A8425C /* PriceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA4BC255872E3009312A5 /* PriceWidget.swift */; };
|
||||
B48630EA2CCEED8400A8425C /* PriceIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630D02CCEE3B300A8425C /* PriceIntent.swift */; };
|
||||
B48630EC2CCEEEA700A8425C /* WalletAppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */; };
|
||||
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */; };
|
||||
B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630D02CCEE3B300A8425C /* PriceIntent.swift */; };
|
||||
B48A6A292C1DF01000030AB9 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B48A6A282C1DF01000030AB9 /* KeychainSwift */; };
|
||||
B49A28BB2CD18999006B08E4 /* CompactPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BA2CD18999006B08E4 /* CompactPriceView.swift */; };
|
||||
B49A28BC2CD18999006B08E4 /* CompactPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BA2CD18999006B08E4 /* CompactPriceView.swift */; };
|
||||
B49A28BE2CD189B0006B08E4 /* FiatUnitEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BD2CD189B0006B08E4 /* FiatUnitEnum.swift */; };
|
||||
B49A28BF2CD18A9A006B08E4 /* FiatUnitEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BD2CD189B0006B08E4 /* FiatUnitEnum.swift */; };
|
||||
B49A28C02CD199C7006B08E4 /* MarketAPI+Electrum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5142558EBA3009312A5 /* MarketAPI+Electrum.swift */; };
|
||||
B49A28C12CD199FC006B08E4 /* SwiftTCPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40FC3F829CCD1AC0007EBAC /* SwiftTCPClient.swift */; };
|
||||
B49A28C52CD1A894006B08E4 /* MarketData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E82BCC371A00162242 /* MarketData.swift */; };
|
||||
B4AB225D2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
|
||||
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
|
||||
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
|
||||
@ -355,7 +373,14 @@
|
||||
B4742E962CCDBE8300380EEE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||
B4742E9C2CCDC31300380EEE /* en_US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en_US; path = en_US.lproj/Interface.strings; sourceTree = "<group>"; };
|
||||
B47B21EB2B2128B8001F6690 /* BlueWalletUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITests.swift; sourceTree = "<group>"; };
|
||||
B48630D02CCEE3B300A8425C /* PriceIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceIntent.swift; sourceTree = "<group>"; };
|
||||
B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceWidgetProvider.swift; sourceTree = "<group>"; };
|
||||
B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceWidgetEntry.swift; sourceTree = "<group>"; };
|
||||
B48630DF2CCEE7C800A8425C /* PriceWidgetEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceWidgetEntryView.swift; sourceTree = "<group>"; };
|
||||
B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAppShortcuts.swift; sourceTree = "<group>"; };
|
||||
B49038D82B8FBAD300A8164A /* BlueWalletUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITest.swift; sourceTree = "<group>"; };
|
||||
B49A28BA2CD18999006B08E4 /* CompactPriceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactPriceView.swift; sourceTree = "<group>"; };
|
||||
B49A28BD2CD189B0006B08E4 /* FiatUnitEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiatUnitEnum.swift; sourceTree = "<group>"; };
|
||||
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
|
||||
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = "<group>"; };
|
||||
B4B31A352C77BBA000663334 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Interface.strings; sourceTree = "<group>"; };
|
||||
@ -527,6 +552,11 @@
|
||||
6D6CA4BB255872E3009312A5 /* PriceWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B49A28BA2CD18999006B08E4 /* CompactPriceView.swift */,
|
||||
B48630DF2CCEE7C800A8425C /* PriceWidgetEntryView.swift */,
|
||||
B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */,
|
||||
B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */,
|
||||
B48630D02CCEE3B300A8425C /* PriceIntent.swift */,
|
||||
6D6CA4BC255872E3009312A5 /* PriceWidget.swift */,
|
||||
);
|
||||
path = PriceWidget;
|
||||
@ -551,6 +581,7 @@
|
||||
6DD4109F266CADF10087DE03 /* Widgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */,
|
||||
6DD410C3266CCB780087DE03 /* WidgetsExtension.entitlements */,
|
||||
6DD410A0266CADF10087DE03 /* Widgets.swift */,
|
||||
6DD410A4266CADF40087DE03 /* Info.plist */,
|
||||
@ -576,6 +607,7 @@
|
||||
6DEB4BC1254FB98300E9F9AA /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B49A28BD2CD189B0006B08E4 /* FiatUnitEnum.swift */,
|
||||
6DEB4DD82552260200E9F9AA /* Views */,
|
||||
6D4AF18225D215D0009DD853 /* BlueWalletWatch-Bridging-Header.h */,
|
||||
B40FC3F829CCD1AC0007EBAC /* SwiftTCPClient.swift */,
|
||||
@ -1178,30 +1210,41 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B44033E92BCC371A00162242 /* MarketData.swift in Sources */,
|
||||
B44033CA2BCC350A00162242 /* Currency.swift in Sources */,
|
||||
B44033EE2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
|
||||
B48630E82CCEE92400A8425C /* PriceWidget.swift in Sources */,
|
||||
B44033DD2BCC36C300162242 /* LatestTransaction.swift in Sources */,
|
||||
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */,
|
||||
B49A28C12CD199FC006B08E4 /* SwiftTCPClient.swift in Sources */,
|
||||
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
|
||||
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
|
||||
B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */,
|
||||
B48630E72CCEE91900A8425C /* PriceWidgetProvider.swift in Sources */,
|
||||
B49A28C02CD199C7006B08E4 /* MarketAPI+Electrum.swift in Sources */,
|
||||
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */,
|
||||
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */,
|
||||
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
|
||||
B44033F42BCC377F00162242 /* WidgetData.swift in Sources */,
|
||||
B49A28C52CD1A894006B08E4 /* MarketData.swift in Sources */,
|
||||
B49A28BF2CD18A9A006B08E4 /* FiatUnitEnum.swift in Sources */,
|
||||
B44033C42BCC332400162242 /* Balance.swift in Sources */,
|
||||
B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */,
|
||||
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
|
||||
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
|
||||
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
|
||||
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */,
|
||||
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
|
||||
B44033DA2BCC369A00162242 /* Colors.swift in Sources */,
|
||||
B44033D32BCC368800162242 /* UserDefaultsGroupKey.swift in Sources */,
|
||||
32B5A32A2334450100F8D608 /* Bridge.swift in Sources */,
|
||||
B49A28BC2CD18999006B08E4 /* CompactPriceView.swift in Sources */,
|
||||
B44033D82BCC369500162242 /* UserDefaultsExtension.swift in Sources */,
|
||||
B44033E42BCC36FF00162242 /* WalletData.swift in Sources */,
|
||||
B44033BF2BCC32F800162242 /* BitcoinUnit.swift in Sources */,
|
||||
B44034052BCC389200162242 /* XMLParserDelegate.swift in Sources */,
|
||||
B48630DD2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */,
|
||||
B44033F92BCC379200162242 /* WidgetDataStore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -1211,17 +1254,23 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6DD410BE266CAF5C0087DE03 /* SendReceiveButtons.swift in Sources */,
|
||||
B48630E02CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
|
||||
6DD410B4266CAF5C0087DE03 /* MarketAPI.swift in Sources */,
|
||||
B40FC3FA29CCD1D00007EBAC /* SwiftTCPClient.swift in Sources */,
|
||||
B48630EC2CCEEEA700A8425C /* WalletAppShortcuts.swift in Sources */,
|
||||
6DD410A1266CADF10087DE03 /* Widgets.swift in Sources */,
|
||||
B450109D2C0FCD9F00619044 /* Utilities.swift in Sources */,
|
||||
B49A28BE2CD189B0006B08E4 /* FiatUnitEnum.swift in Sources */,
|
||||
6DD410AC266CAE470087DE03 /* PriceWidget.swift in Sources */,
|
||||
B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */,
|
||||
B44033D52BCC368800162242 /* UserDefaultsGroupKey.swift in Sources */,
|
||||
6DD410B2266CAF5C0087DE03 /* WalletInformationView.swift in Sources */,
|
||||
B44034022BCC37F800162242 /* Bundle+decode.swift in Sources */,
|
||||
B48630D62CCEE67100A8425C /* PriceWidgetProvider.swift in Sources */,
|
||||
B44033CC2BCC350A00162242 /* Currency.swift in Sources */,
|
||||
6DD410B6266CAF5C0087DE03 /* PriceView.swift in Sources */,
|
||||
B48630DE2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */,
|
||||
B49A28BB2CD18999006B08E4 /* CompactPriceView.swift in Sources */,
|
||||
6DD410B3266CAF5C0087DE03 /* Colors.swift in Sources */,
|
||||
B44033C12BCC32F800162242 /* BitcoinUnit.swift in Sources */,
|
||||
6DD410BB266CAF5C0087DE03 /* MarketView.swift in Sources */,
|
||||
@ -1241,6 +1290,7 @@
|
||||
6DD410B1266CAF5C0087DE03 /* MarketAPI+Electrum.swift in Sources */,
|
||||
6DD410B9266CAF5C0087DE03 /* UserDefaultsGroup.swift in Sources */,
|
||||
6DD410B8266CAF5C0087DE03 /* UserDefaultsExtension.swift in Sources */,
|
||||
B48630EA2CCEED8400A8425C /* PriceIntent.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -1580,7 +1630,7 @@
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -1636,7 +1686,7 @@
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -206,7 +206,18 @@
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<string>LaunchScreen</string>
|
||||
<key>NSAppIntents</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentClassName</key>
|
||||
<string>PriceView</string>
|
||||
<key>IntentName</key>
|
||||
<string>Bitcoin Price</string>
|
||||
<key>IntentDescription</key>
|
||||
<string>Quickly view the current Bitcoin market rate.</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
|
@ -1,6 +1,289 @@
|
||||
{
|
||||
"sourceLanguage" : "en_US",
|
||||
"strings" : {
|
||||
"%@" : {
|
||||
|
||||
},
|
||||
"Argentina (Argentine Peso)" : {
|
||||
|
||||
},
|
||||
"Aruba (Aruban Florin)" : {
|
||||
|
||||
},
|
||||
"at %@" : {
|
||||
|
||||
},
|
||||
"Australia (Australian Dollar)" : {
|
||||
|
||||
},
|
||||
"Bahrain (Bahraini Dinar)" : {
|
||||
|
||||
},
|
||||
"Balance" : {
|
||||
|
||||
},
|
||||
"Bitcoin (%@)" : {
|
||||
|
||||
},
|
||||
"Bitcoin price: %@" : {
|
||||
|
||||
},
|
||||
"Brazil (Brazilian Real)" : {
|
||||
|
||||
},
|
||||
"BTC" : {
|
||||
|
||||
},
|
||||
"Canada (Canadian Dollar)" : {
|
||||
|
||||
},
|
||||
"Central African Republic (Central African Franc)" : {
|
||||
|
||||
},
|
||||
"Checked at %@" : {
|
||||
|
||||
},
|
||||
"Chile (Chilean Peso)" : {
|
||||
|
||||
},
|
||||
"China (Chinese Yuan)" : {
|
||||
|
||||
},
|
||||
"Choose your preferred fiat currency." : {
|
||||
|
||||
},
|
||||
"Colombia (Colombian Peso)" : {
|
||||
|
||||
},
|
||||
"Croatia (Croatian Kuna)" : {
|
||||
|
||||
},
|
||||
"Currency" : {
|
||||
|
||||
},
|
||||
"Currency: %@" : {
|
||||
|
||||
},
|
||||
"Current Bitcoin Market Rate" : {
|
||||
|
||||
},
|
||||
"Current Bitcoin Price: %@" : {
|
||||
|
||||
},
|
||||
"Czech Republic (Czech Koruna)" : {
|
||||
|
||||
},
|
||||
"Denmark (Danish Krone)" : {
|
||||
|
||||
},
|
||||
"European Union (Euro)" : {
|
||||
|
||||
},
|
||||
"Failed to retrieve the Bitcoin market rate." : {
|
||||
|
||||
},
|
||||
"Fiat Currency" : {
|
||||
|
||||
},
|
||||
"from" : {
|
||||
|
||||
},
|
||||
"From %@" : {
|
||||
|
||||
},
|
||||
"Ghana (Ghanaian Cedi)" : {
|
||||
|
||||
},
|
||||
"Hungary (Hungarian Forint)" : {
|
||||
|
||||
},
|
||||
"Iceland (Icelandic Króna)" : {
|
||||
|
||||
},
|
||||
"India (Indian Rupee)" : {
|
||||
|
||||
},
|
||||
"Indonesia (Indonesian Rupiah)" : {
|
||||
|
||||
},
|
||||
"Iran (Iranian Rial)" : {
|
||||
|
||||
},
|
||||
"Iran (Iranian Toman)" : {
|
||||
|
||||
},
|
||||
"Israel (Israeli New Shekel)" : {
|
||||
|
||||
},
|
||||
"Japan (Japanese Yen)" : {
|
||||
|
||||
},
|
||||
"Kenya (Kenyan Shilling)" : {
|
||||
|
||||
},
|
||||
"Kuwait (Kuwaiti Dinar)" : {
|
||||
|
||||
},
|
||||
"Last Updated" : {
|
||||
|
||||
},
|
||||
"Last updated %@ from %@" : {
|
||||
"localizations" : {
|
||||
"en_US" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Last updated %1$@ from %2$@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Latest transaction" : {
|
||||
|
||||
},
|
||||
"Lebanon (Lebanese Pound)" : {
|
||||
|
||||
},
|
||||
"Malaysia (Malaysian Ringgit)" : {
|
||||
|
||||
},
|
||||
"Market" : {
|
||||
|
||||
},
|
||||
"Market Rate" : {
|
||||
|
||||
},
|
||||
"Mexico (Mexican Peso)" : {
|
||||
|
||||
},
|
||||
"Mozambique (Mozambican Metical)" : {
|
||||
|
||||
},
|
||||
"New Zealand (New Zealand Dollar)" : {
|
||||
|
||||
},
|
||||
"Next Block" : {
|
||||
|
||||
},
|
||||
"Nigeria (Nigerian Naira)" : {
|
||||
|
||||
},
|
||||
"Norway (Norwegian Krone)" : {
|
||||
|
||||
},
|
||||
"Oman (Omani Rial)" : {
|
||||
|
||||
},
|
||||
"Philippines (Philippine Peso)" : {
|
||||
|
||||
},
|
||||
"Poland (Polish Zloty)" : {
|
||||
|
||||
},
|
||||
"Price" : {
|
||||
|
||||
},
|
||||
"Qatar (Qatari Riyal)" : {
|
||||
|
||||
},
|
||||
"receive" : {
|
||||
|
||||
},
|
||||
"Romania (Romanian Leu)" : {
|
||||
|
||||
},
|
||||
"Russia (Russian Ruble)" : {
|
||||
|
||||
},
|
||||
"Sats/%@" : {
|
||||
|
||||
},
|
||||
"Saudi Arabia (Saudi Riyal)" : {
|
||||
|
||||
},
|
||||
"send" : {
|
||||
|
||||
},
|
||||
"Singapore (Singapore Dollar)" : {
|
||||
|
||||
},
|
||||
"Source: %@" : {
|
||||
|
||||
},
|
||||
"South Africa (South African Rand)" : {
|
||||
|
||||
},
|
||||
"South Korea (South Korean Won)" : {
|
||||
|
||||
},
|
||||
"Sri Lanka (Sri Lankan Rupee)" : {
|
||||
|
||||
},
|
||||
"Sweden (Swedish Krona)" : {
|
||||
|
||||
},
|
||||
"Switzerland (Swiss Franc)" : {
|
||||
|
||||
},
|
||||
"Taiwan (New Taiwan Dollar)" : {
|
||||
|
||||
},
|
||||
"Tanzania (Tanzanian Shilling)" : {
|
||||
|
||||
},
|
||||
"Thailand (Thai Baht)" : {
|
||||
|
||||
},
|
||||
"Turkey (Turkish Lira)" : {
|
||||
|
||||
},
|
||||
"Uganda (Ugandan Shilling)" : {
|
||||
|
||||
},
|
||||
"Ukraine (Ukrainian Hryvnia)" : {
|
||||
|
||||
},
|
||||
"United Arab Emirates (UAE Dirham)" : {
|
||||
|
||||
},
|
||||
"United Kingdom (British Pound)" : {
|
||||
|
||||
},
|
||||
"United States of America (US Dollar)" : {
|
||||
|
||||
},
|
||||
"Updated: %@" : {
|
||||
|
||||
},
|
||||
"Uruguay (Uruguayan Peso)" : {
|
||||
|
||||
},
|
||||
"Venezuela (Venezuelan Bolívar Fuerte)" : {
|
||||
|
||||
},
|
||||
"Venezuela (Venezuelan Bolívar Soberano)" : {
|
||||
|
||||
},
|
||||
"View the current Bitcoin market rate in your preferred fiat currency." : {
|
||||
|
||||
},
|
||||
"View the current Bitcoin market rate." : {
|
||||
|
||||
},
|
||||
"View the current market information." : {
|
||||
|
||||
},
|
||||
"View the current price of Bitcoin" : {
|
||||
|
||||
},
|
||||
"View the current price of Bitcoin." : {
|
||||
|
||||
},
|
||||
"View your accumulated balance." : {
|
||||
|
||||
},
|
||||
"View your total wallet balance and network prices." : {
|
||||
|
||||
},
|
||||
"VIEW_ADDRESS_TRANSACTIONS_TITLE" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@ -56,6 +339,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Wallet and Market" : {
|
||||
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
|
@ -2,230 +2,205 @@
|
||||
// MarketAPI.swift
|
||||
//
|
||||
// Created by Marcos Rodriguez on 11/2/19.
|
||||
//
|
||||
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
var numberFormatter: NumberFormatter {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = 0
|
||||
formatter.locale = Locale.current
|
||||
return formatter
|
||||
}
|
||||
|
||||
class MarketAPI {
|
||||
|
||||
private static func buildURLString(source: String, endPointKey: String) -> String {
|
||||
switch source {
|
||||
case "Yadio":
|
||||
return "https://api.yadio.io/json/\(endPointKey)"
|
||||
case "YadioConvert":
|
||||
return "https://api.yadio.io/convert/1/BTC/\(endPointKey)"
|
||||
case "Exir":
|
||||
return "https://api.exir.io/v1/ticker?symbol=btc-irt"
|
||||
case "coinpaprika":
|
||||
return "https://api.coinpaprika.com/v1/tickers/btc-bitcoin?quotes=INR"
|
||||
case "Bitstamp":
|
||||
return "https://www.bitstamp.net/api/v2/ticker/btc\(endPointKey.lowercased())"
|
||||
case "Coinbase":
|
||||
return "https://api.coinbase.com/v2/prices/BTC-\(endPointKey.uppercased())/buy"
|
||||
case "CoinGecko":
|
||||
return "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=\(endPointKey.lowercased())"
|
||||
case "BNR":
|
||||
return "https://www.bnr.ro/nbrfxrates.xml"
|
||||
case "Kraken":
|
||||
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
|
||||
default:
|
||||
return "https://api.coindesk.com/v1/bpi/currentprice/\(endPointKey).json"
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleDefaultData(data: Data, source: String, endPointKey: String, completion: @escaping ((WidgetDataStore?, Error?) -> Void)) {
|
||||
guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? Dictionary<String, Any> else {
|
||||
completion(nil, CurrencyError(errorDescription: "JSON parsing error."))
|
||||
return
|
||||
}
|
||||
|
||||
parseJSONBasedOnSource(json: json, source: source, endPointKey: endPointKey, completion: completion)
|
||||
|
||||
private static func buildURLString(source: String, endPointKey: String) -> String {
|
||||
switch source {
|
||||
case "Yadio":
|
||||
return "https://api.yadio.io/json/\(endPointKey)"
|
||||
case "YadioConvert":
|
||||
return "https://api.yadio.io/convert/1/BTC/\(endPointKey)"
|
||||
case "Exir":
|
||||
return "https://api.exir.io/v1/ticker?symbol=btc-irt"
|
||||
case "coinpaprika":
|
||||
return "https://api.coinpaprika.com/v1/tickers/btc-bitcoin?quotes=INR"
|
||||
case "Bitstamp":
|
||||
return "https://www.bitstamp.net/api/v2/ticker/btc\(endPointKey.lowercased())"
|
||||
case "Coinbase":
|
||||
return "https://api.coinbase.com/v2/prices/BTC-\(endPointKey.uppercased())/buy"
|
||||
case "CoinGecko":
|
||||
return "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=\(endPointKey.lowercased())"
|
||||
case "BNR":
|
||||
return "https://www.bnr.ro/nbrfxrates.xml"
|
||||
case "Kraken":
|
||||
return "https://api.kraken.com/0/public/Ticker?pair=XXBTZ\(endPointKey.uppercased())"
|
||||
default:
|
||||
return "https://api.coindesk.com/v1/bpi/currentprice/\(endPointKey).json"
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseJSONBasedOnSource(json: Dictionary<String, Any>, source: String, endPointKey: String, completion: @escaping ((WidgetDataStore?, Error?) -> Void)) {
|
||||
var latestRateDataStore: WidgetDataStore?
|
||||
|
||||
switch source {
|
||||
case "Yadio":
|
||||
if let rateDict = json[endPointKey] as? [String: Any],
|
||||
let rateDouble = rateDict["price"] as? Double,
|
||||
let lastUpdated = rateDict["timestamp"] as? Int {
|
||||
let unix = Double(lastUpdated / 1_000)
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date(timeIntervalSince1970: unix))
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
completion(latestRateDataStore, nil)
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "Data formatting error for source: \(source)"))
|
||||
}
|
||||
case "YadioConvert":
|
||||
guard let rateDouble = json["rate"] as? Double,
|
||||
let lastUpdated = json["timestamp"] as? Int
|
||||
else { break }
|
||||
let unix = Double(lastUpdated / 1_000)
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date(timeIntervalSince1970: unix))
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
completion(latestRateDataStore, nil)
|
||||
case "CoinGecko":
|
||||
if let bitcoinDict = json["bitcoin"] as? [String: Any],
|
||||
let rateDouble = bitcoinDict[endPointKey.lowercased()] as? Double {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
completion(latestRateDataStore, nil)
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "Data formatting error for source: \(source)"))
|
||||
}
|
||||
|
||||
case "Exir":
|
||||
if let rateDouble = json["last"] as? Double {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
completion(latestRateDataStore, nil)
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "Data formatting error for source: \(source)"))
|
||||
}
|
||||
|
||||
case "Bitstamp":
|
||||
if let rateString = json["last"] as? String, let rateDouble = Double(rateString) {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
completion(latestRateDataStore, nil)
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "Data formatting error for source: \(source)"))
|
||||
}
|
||||
|
||||
case "coinpaprika":
|
||||
if let quotesDict = json["quotes"] as? [String: Any],
|
||||
let inrDict = quotesDict["INR"] as? [String: Any],
|
||||
let rateDouble = inrDict["price"] as? Double {
|
||||
|
||||
private static func handleDefaultData(data: Data, source: String, endPointKey: String) throws -> WidgetDataStore? {
|
||||
guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
|
||||
throw CurrencyError(errorDescription: "JSON parsing error.")
|
||||
}
|
||||
|
||||
let rateString = String(rateDouble)
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
|
||||
latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
completion(latestRateDataStore, nil)
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "Data formatting error for source: \(source)"))
|
||||
return try parseJSONBasedOnSource(json: json, source: source, endPointKey: endPointKey)
|
||||
}
|
||||
|
||||
private static func parseJSONBasedOnSource(json: [String: Any], source: String, endPointKey: String) throws -> WidgetDataStore? {
|
||||
var latestRateDataStore: WidgetDataStore?
|
||||
|
||||
switch source {
|
||||
case "Yadio":
|
||||
if let rateDict = json[endPointKey] as? [String: Any],
|
||||
let rateDouble = rateDict["price"] as? Double,
|
||||
let lastUpdated = rateDict["timestamp"] as? Int {
|
||||
let unix = Double(lastUpdated / 1_000)
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date(timeIntervalSince1970: unix))
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
case "YadioConvert":
|
||||
guard let rateDouble = json["rate"] as? Double,
|
||||
let lastUpdated = json["timestamp"] as? Int else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
let unix = Double(lastUpdated / 1_000)
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date(timeIntervalSince1970: unix))
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
case "CoinGecko":
|
||||
if let bitcoinDict = json["bitcoin"] as? [String: Any],
|
||||
let rateDouble = bitcoinDict[endPointKey.lowercased()] as? Double {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
case "Exir":
|
||||
if let rateDouble = json["last"] as? Double {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: String(rateDouble), lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
case "Bitstamp":
|
||||
if let rateString = json["last"] as? String, let rateDouble = Double(rateString) {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
case "coinpaprika":
|
||||
if let quotesDict = json["quotes"] as? [String: Any],
|
||||
let currencyDict = quotesDict[endPointKey.uppercased()] as? [String: Any],
|
||||
let rateDouble = currencyDict["price"] as? Double {
|
||||
let rateString = String(rateDouble)
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
case "Coinbase":
|
||||
if let data = json["data"] as? [String: Any],
|
||||
let rateString = data["amount"] as? String,
|
||||
let rateDouble = Double(rateString) {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
case "BNR":
|
||||
throw CurrencyError(errorDescription: "BNR data source is not yet implemented")
|
||||
case "Kraken":
|
||||
if let result = json["result"] as? [String: Any],
|
||||
let tickerData = result["XXBTZ\(endPointKey.uppercased())"] as? [String: Any],
|
||||
let c = tickerData["c"] as? [String],
|
||||
let rateString = c.first,
|
||||
let rateDouble = Double(rateString) {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
if let errorMessage = json["error"] as? [String] {
|
||||
throw CurrencyError(errorDescription: "Kraken API error: \(errorMessage.joined(separator: ", "))")
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "Data formatting error for source: \(source)")
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw CurrencyError(errorDescription: "Unsupported data source \(source)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleBNRData(data: Data) async throws -> WidgetDataStore? {
|
||||
let parser = XMLParser(data: data)
|
||||
let delegate = BNRXMLParserDelegate()
|
||||
parser.delegate = delegate
|
||||
if parser.parse(), let usdToRonRate = delegate.usdRate {
|
||||
let coinGeckoUrl = URL(string: "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd")!
|
||||
let (data, _) = try await URLSession.shared.data(from: coinGeckoUrl)
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||
let bitcoinDict = json["bitcoin"] as? [String: Double],
|
||||
let btcToUsdRate = bitcoinDict["usd"] {
|
||||
let btcToRonRate = btcToUsdRate * usdToRonRate
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
let latestRateDataStore = WidgetDataStore(rate: String(btcToRonRate), lastUpdate: lastUpdatedString, rateDouble: btcToRonRate)
|
||||
return latestRateDataStore
|
||||
} else {
|
||||
throw CurrencyError()
|
||||
}
|
||||
} else {
|
||||
throw CurrencyError(errorDescription: "XML parsing error.")
|
||||
}
|
||||
}
|
||||
case "Coinbase":
|
||||
if let data = json["data"] as? [String: Any],
|
||||
let rateString = data["amount"] as? String,
|
||||
let rateDouble = Double(rateString) {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
completion(latestRateDataStore, nil)
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "Data formatting error for source: \(source)"))
|
||||
}
|
||||
|
||||
case "BNR":
|
||||
completion(nil, CurrencyError(errorDescription: "BNR data source is not yet implemented"))
|
||||
|
||||
case "Kraken":
|
||||
if let result = json["result"] as? [String: Any],
|
||||
let tickerData = result["XXBTZ\(endPointKey.uppercased())"] as? [String: Any],
|
||||
let c = tickerData["c"] as? [String],
|
||||
let rateString = c.first,
|
||||
let rateDouble = Double(rateString) {
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
latestRateDataStore = WidgetDataStore(rate: rateString, lastUpdate: lastUpdatedString, rateDouble: rateDouble)
|
||||
completion(latestRateDataStore, nil)
|
||||
} else {
|
||||
if let errorMessage = json["error"] as? [String] {
|
||||
completion(nil, CurrencyError(errorDescription: "Kraken API error: \(errorMessage.joined(separator: ", "))"))
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "Data formatting error for source: \(source)"))
|
||||
}
|
||||
}
|
||||
|
||||
static func fetchPrice(currency: String) async throws -> WidgetDataStore? {
|
||||
let currencyToFiatUnit = fiatUnit(currency: currency)
|
||||
guard let source = currencyToFiatUnit?.source, let endPointKey = currencyToFiatUnit?.endPointKey else {
|
||||
throw CurrencyError(errorDescription: "Invalid currency unit or endpoint.")
|
||||
}
|
||||
|
||||
let urlString = buildURLString(source: source, endPointKey: endPointKey)
|
||||
guard let url = URL(string: urlString) else {
|
||||
throw CurrencyError(errorDescription: "Invalid URL.")
|
||||
}
|
||||
|
||||
default:
|
||||
completion(nil, CurrencyError(errorDescription: "Unsupported data source \(source)"))
|
||||
}
|
||||
}
|
||||
|
||||
private static func handleBNRData(data: Data, completion: @escaping ((WidgetDataStore?, Error?) -> Void)) {
|
||||
let parser = XMLParser(data: data)
|
||||
let delegate = BNRXMLParserDelegate()
|
||||
parser.delegate = delegate
|
||||
if parser.parse(), let usdToRonRate = delegate.usdRate {
|
||||
let coinGeckoUrl = URL(string: "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd")!
|
||||
URLSession.shared.dataTask(with: coinGeckoUrl) { data, _, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(nil, error ?? CurrencyError())
|
||||
return
|
||||
}
|
||||
return try await fetchData(url: url, source: source, endPointKey: endPointKey)
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||
let bitcoinDict = json["bitcoin"] as? [String: Double],
|
||||
let btcToUsdRate = bitcoinDict["usd"] {
|
||||
let btcToRonRate = btcToUsdRate * usdToRonRate
|
||||
let lastUpdatedString = ISO8601DateFormatter().string(from: Date())
|
||||
let latestRateDataStore = WidgetDataStore(rate: String(btcToRonRate), lastUpdate: lastUpdatedString, rateDouble: btcToRonRate)
|
||||
completion(latestRateDataStore, nil)
|
||||
} else {
|
||||
completion(nil, CurrencyError())
|
||||
}
|
||||
} catch {
|
||||
completion(nil, error)
|
||||
}
|
||||
}.resume()
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "XML parsing error."))
|
||||
}
|
||||
}
|
||||
|
||||
static func fetchPrice(currency: String, completion: @escaping ((WidgetDataStore?, Error?) -> Void)) {
|
||||
let currencyToFiatUnit = fiatUnit(currency: currency)
|
||||
guard let source = currencyToFiatUnit?.source, let endPointKey = currencyToFiatUnit?.endPointKey else {
|
||||
completion(nil, CurrencyError(errorDescription: "Invalid currency unit or endpoint."))
|
||||
return
|
||||
}
|
||||
|
||||
let urlString = buildURLString(source: source, endPointKey: endPointKey)
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion(nil, CurrencyError(errorDescription: "Invalid URL."))
|
||||
return
|
||||
}
|
||||
|
||||
fetchData(url: url, source: source, endPointKey: endPointKey, completion: completion)
|
||||
}
|
||||
|
||||
private static func fetchData(url: URL, source: String, endPointKey: String, retries: Int = 3, completion: @escaping ((WidgetDataStore?, Error?) -> Void)) {
|
||||
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
if retries > 0 {
|
||||
fetchData(url: url, source: source, endPointKey: endPointKey, retries: retries - 1, completion: completion)
|
||||
} else {
|
||||
completion(nil, error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data else {
|
||||
if retries > 0 {
|
||||
fetchData(url: url, source: source, endPointKey: endPointKey, retries: retries - 1, completion: completion)
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "Data not found."))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if source == "BNR" {
|
||||
handleBNRData(data: data, completion: completion)
|
||||
} else {
|
||||
handleDefaultData(data: data, source: source, endPointKey: endPointKey, completion: completion)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
private static func fetchData(url: URL, source: String, endPointKey: String, retries: Int = 3) async throws -> WidgetDataStore? {
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
if source == "BNR" {
|
||||
return try await handleBNRData(data: data)
|
||||
} else {
|
||||
return try handleDefaultData(data: data, source: source, endPointKey: endPointKey)
|
||||
}
|
||||
} catch {
|
||||
if retries > 0 {
|
||||
return try await fetchData(url: url, source: source, endPointKey: endPointKey, retries: retries - 1)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func fetchPrice(currency: String, completion: @escaping ((WidgetDataStore?, Error?) -> Void)) {
|
||||
Task {
|
||||
do {
|
||||
if let dataStore = try await fetchPrice(currency: currency) {
|
||||
completion(dataStore, nil)
|
||||
} else {
|
||||
completion(nil, CurrencyError(errorDescription: "No data received."))
|
||||
}
|
||||
} catch {
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,11 +8,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MarketData:Codable {
|
||||
var nextBlock: String
|
||||
var sats: String
|
||||
var price: String
|
||||
var rate: Double
|
||||
public struct MarketData:Codable {
|
||||
public var nextBlock: String
|
||||
public var sats: String
|
||||
public var price: String
|
||||
public var rate: Double
|
||||
|
||||
var formattedNextBlock: String {
|
||||
if nextBlock == "..." {
|
||||
return "..."
|
||||
|
@ -51,3 +51,38 @@ func calcEstimateFeeFromFeeHistogram(numberOfBlocks: Int, feeHistogram: [[Double
|
||||
|
||||
return max(2, percentile(histogramFlat, p: 0.5))
|
||||
}
|
||||
|
||||
|
||||
var numberFormatter: NumberFormatter {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = 0
|
||||
formatter.locale = Locale.current
|
||||
return formatter
|
||||
}
|
||||
|
||||
extension Double {
|
||||
func formattedPriceString() -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = 0
|
||||
return formatter.string(from: NSNumber(value: self)) ?? "--"
|
||||
}
|
||||
|
||||
func formattedCurrencyString() -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .currency
|
||||
formatter.maximumFractionDigits = 0
|
||||
formatter.currencySymbol = fiatUnit(currency: Currency.getUserPreferredCurrency())?.symbol
|
||||
return formatter.string(from: NSNumber(value: self)) ?? "--"
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
var formattedDate: String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: self)
|
||||
}
|
||||
}
|
||||
|
32
ios/Widgets/PriceWidget/CompactPriceView.swift
Normal file
32
ios/Widgets/PriceWidget/CompactPriceView.swift
Normal file
@ -0,0 +1,32 @@
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
struct CompactPriceView: View {
|
||||
let price: String
|
||||
let lastUpdated: String
|
||||
let code: String
|
||||
let dataSource: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
Text(price)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.multilineTextAlignment(.center)
|
||||
.dynamicTypeSize(.large ... .accessibility5)
|
||||
.accessibilityLabel("Bitcoin price: \(price)")
|
||||
|
||||
VStack(alignment: .center, spacing: 8) {
|
||||
Text("\(code)")
|
||||
Text("\(lastUpdated)")
|
||||
Text("\(dataSource)")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
161
ios/Widgets/PriceWidget/PriceIntent.swift
Normal file
161
ios/Widgets/PriceWidget/PriceIntent.swift
Normal file
@ -0,0 +1,161 @@
|
||||
//
|
||||
// PriceIntent.swift
|
||||
// BlueWallet
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct PriceIntent: AppIntent {
|
||||
// MARK: - Intent Metadata
|
||||
|
||||
static var title: LocalizedStringResource = "Market Rate"
|
||||
static var description = IntentDescription("View the current Bitcoin market rate in your preferred currency.")
|
||||
static var openAppWhenRun: Bool { false }
|
||||
|
||||
// MARK: - Parameters
|
||||
|
||||
@Parameter(
|
||||
title: "Currency",
|
||||
description: "Choose your preferred currency."
|
||||
)
|
||||
var fiatCurrency: FiatUnitEnum?
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<Double> & ProvidesDialog & ShowsSnippetView {
|
||||
if let fiat = fiatCurrency {
|
||||
print("Received fiatCurrency parameter: \(fiat.rawValue)")
|
||||
} else {
|
||||
print("fiatCurrency parameter not provided. Proceeding with fallback logic.")
|
||||
}
|
||||
|
||||
// Determine the fiat currency to use:
|
||||
// 1. Use the fiatCurrency parameter if provided
|
||||
// 2. Fallback to Shared Group UserDefaults
|
||||
// 3. Fallback to Device's preferred currency
|
||||
// 4. Default to USD
|
||||
let selectedFiatCurrency: FiatUnitEnum
|
||||
|
||||
if let fiat = fiatCurrency {
|
||||
selectedFiatCurrency = fiat
|
||||
print("Using fiatCurrency parameter: \(selectedFiatCurrency.rawValue)")
|
||||
} else if let sharedCurrencyCode = getSharedCurrencyCode(),
|
||||
let fiat = FiatUnitEnum(rawValue: sharedCurrencyCode.uppercased()) {
|
||||
selectedFiatCurrency = fiat
|
||||
print("Using shared user default currency: \(selectedFiatCurrency.rawValue)")
|
||||
} else if let deviceCurrencyCode = Locale.current.currencyCode,
|
||||
let fiat = FiatUnitEnum(rawValue: deviceCurrencyCode.uppercased()) {
|
||||
selectedFiatCurrency = fiat
|
||||
print("Using device's currency: \(selectedFiatCurrency.rawValue)")
|
||||
} else {
|
||||
selectedFiatCurrency = .USD
|
||||
print("Defaulting to USD.")
|
||||
}
|
||||
|
||||
let dataSource = selectedFiatCurrency.source
|
||||
print("Data Source: \(dataSource)")
|
||||
|
||||
var lastUpdated = "--"
|
||||
var priceDouble: Double = 0.0
|
||||
|
||||
do {
|
||||
guard let fetchedData = try await MarketAPI.fetchPrice(currency: selectedFiatCurrency.rawValue) else {
|
||||
print("Failed to fetch price data.")
|
||||
throw NSError(
|
||||
domain: "PriceIntentErrorDomain",
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Failed to fetch price data."]
|
||||
)
|
||||
}
|
||||
|
||||
priceDouble = fetchedData.rateDouble
|
||||
lastUpdated = formattedDate(from: fetchedData.lastUpdate)
|
||||
print("Fetched Price: \(priceDouble)")
|
||||
print("Last Updated: \(lastUpdated)")
|
||||
|
||||
} catch {
|
||||
print("Error fetching price data: \(error.localizedDescription)")
|
||||
let errorView = CompactPriceView(
|
||||
price: "N/A",
|
||||
lastUpdated: "--",
|
||||
code: selectedFiatCurrency.rawValue,
|
||||
dataSource: "Error fetching data"
|
||||
)
|
||||
|
||||
return .result(
|
||||
value: 0.0,
|
||||
dialog: "Failed to retrieve the Bitcoin market rate.",
|
||||
view: errorView
|
||||
)
|
||||
}
|
||||
|
||||
let formattedPrice = formatPrice(priceDouble, currencyCode: selectedFiatCurrency.rawValue)
|
||||
let currencySymbol = getCurrencySymbol(for: selectedFiatCurrency.rawValue)
|
||||
|
||||
let view = CompactPriceView(
|
||||
price: formattedPrice,
|
||||
lastUpdated: lastUpdated,
|
||||
code: selectedFiatCurrency.rawValue,
|
||||
dataSource: dataSource
|
||||
)
|
||||
|
||||
print("Formatted Price: \(formattedPrice)")
|
||||
print("Currency Symbol: \(currencySymbol)")
|
||||
|
||||
return .result(
|
||||
value: priceDouble,
|
||||
dialog: "Current Bitcoin Market Rate",
|
||||
view: view
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
private func formattedDate(from isoString: String?) -> String {
|
||||
guard let isoString = isoString else { return "--" }
|
||||
let isoFormatter = ISO8601DateFormatter()
|
||||
if let date = isoFormatter.date(from: isoString) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
return "--"
|
||||
}
|
||||
|
||||
private func formatPrice(_ price: Double, currencyCode: String) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .currency
|
||||
formatter.locale = Locale.current // Use device's current locale
|
||||
formatter.currencyCode = currencyCode
|
||||
|
||||
// Omit cents if price is a whole number
|
||||
if price.truncatingRemainder(dividingBy: 1) == 0 {
|
||||
formatter.maximumFractionDigits = 0
|
||||
formatter.minimumFractionDigits = 0
|
||||
} else {
|
||||
formatter.maximumFractionDigits = 2
|
||||
formatter.minimumFractionDigits = 2
|
||||
}
|
||||
|
||||
guard let formattedNumber = formatter.string(from: NSNumber(value: price)) else {
|
||||
return "\(price)"
|
||||
}
|
||||
|
||||
return formattedNumber
|
||||
}
|
||||
|
||||
private func getCurrencySymbol(for currencyCode: String) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .currency
|
||||
formatter.locale = Locale.current // Use device's current locale
|
||||
formatter.currencyCode = currencyCode
|
||||
return formatter.currencySymbol
|
||||
}
|
||||
|
||||
private func getSharedCurrencyCode() -> String? {
|
||||
let sharedDefaults = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
|
||||
return sharedDefaults?.string(forKey: UserDefaultsGroupKey.PreferredCurrency.rawValue)
|
||||
}
|
||||
}
|
@ -9,129 +9,73 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct PriceWidgetProvider: TimelineProvider {
|
||||
typealias Entry = PriceWidgetEntry
|
||||
static var lastSuccessfulEntry: PriceWidgetEntry?
|
||||
|
||||
func placeholder(in context: Context) -> PriceWidgetEntry {
|
||||
return PriceWidgetEntry(date: Date(), family: context.family, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (PriceWidgetEntry) -> ()) {
|
||||
let entry: PriceWidgetEntry
|
||||
if context.isPreview {
|
||||
entry = PriceWidgetEntry(date: Date(), family: context.family, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData)
|
||||
} else {
|
||||
entry = PriceWidgetEntry(date: Date(), family: context.family, currentMarketData: emptyMarketData, previousMarketData: emptyMarketData)
|
||||
}
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
var entries: [PriceWidgetEntry] = []
|
||||
|
||||
if context.isPreview {
|
||||
let entry = PriceWidgetEntry(date: Date(), family: context.family, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData)
|
||||
entries.append(entry)
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
} else {
|
||||
let userPreferredCurrency = Currency.getUserPreferredCurrency()
|
||||
if userPreferredCurrency != Currency.getLastSelectedCurrency() {
|
||||
Currency.saveNewSelectedCurrency()
|
||||
}
|
||||
|
||||
MarketAPI.fetchPrice(currency: userPreferredCurrency) { (data, error) in
|
||||
if let data = data, let formattedRate = data.formattedRate {
|
||||
let currentMarketData = MarketData(nextBlock: "", sats: "", price: formattedRate, rate: data.rateDouble, dateString: data.lastUpdate)
|
||||
|
||||
// Retrieve previous market data from the last entry, if available
|
||||
let previousEntry = PriceWidgetProvider.lastSuccessfulEntry
|
||||
let previousMarketData = previousEntry?.currentMarketData
|
||||
|
||||
// Check if the new fetched price is the same as the current price
|
||||
if let previousMarketData = previousMarketData, previousMarketData.rate == currentMarketData.rate {
|
||||
// If the new price is the same, only update the date
|
||||
let updatedEntry = PriceWidgetEntry(date: Date(), family: context.family, currentMarketData: previousMarketData, previousMarketData: previousEntry?.previousMarketData ?? emptyMarketData)
|
||||
PriceWidgetProvider.lastSuccessfulEntry = updatedEntry
|
||||
entries.append(updatedEntry)
|
||||
} else {
|
||||
// If the new price is different, update the data
|
||||
let entry = PriceWidgetEntry(date: Date(), family: context.family, currentMarketData: currentMarketData, previousMarketData: previousMarketData ?? emptyMarketData)
|
||||
PriceWidgetProvider.lastSuccessfulEntry = entry
|
||||
entries.append(entry)
|
||||
}
|
||||
} else {
|
||||
// Use the last successful entry if available
|
||||
if let lastEntry = PriceWidgetProvider.lastSuccessfulEntry {
|
||||
entries.append(lastEntry)
|
||||
} else {
|
||||
// Fallback to a default entry if no successful entry is available
|
||||
let entry = PriceWidgetEntry(date: Date(), family: context.family, currentMarketData: emptyMarketData, previousMarketData: emptyMarketData)
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PriceWidgetEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let family: WidgetFamily
|
||||
let currentMarketData: MarketData?
|
||||
let previousMarketData: MarketData?
|
||||
}
|
||||
|
||||
struct PriceWidgetEntryView : View {
|
||||
let entry: PriceWidgetEntry
|
||||
|
||||
@ViewBuilder
|
||||
var body: some View {
|
||||
PriceView(entry: entry)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct PriceWidget: Widget {
|
||||
let kind: String = "PriceWidget"
|
||||
|
||||
let kind: String = "PriceWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
return StaticConfiguration(kind: kind, provider: PriceWidgetProvider()) { entry in
|
||||
PriceWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Price")
|
||||
.description("View the current price of Bitcoin.")
|
||||
.supportedFamilies([.systemSmall, .accessoryCircular, .accessoryInline, .accessoryRectangular])
|
||||
.contentMarginsDisabledIfAvailable()
|
||||
} else {
|
||||
return StaticConfiguration(kind: kind, provider: PriceWidgetProvider()) { entry in
|
||||
PriceWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Price")
|
||||
.description("View the current price of Bitcoin.")
|
||||
.supportedFamilies([.systemSmall])
|
||||
.contentMarginsDisabledIfAvailable()
|
||||
StaticConfiguration(kind: kind, provider: PriceWidgetProvider()) { entry in
|
||||
PriceWidgetEntryView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Price")
|
||||
.description("View the current price of Bitcoin.")
|
||||
.supportedFamilies(supportedFamilies)
|
||||
.contentMarginsDisabledIfAvailable()
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
private var supportedFamilies: [WidgetFamily] {
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
return [.systemSmall, .accessoryCircular, .accessoryInline, .accessoryRectangular]
|
||||
} else {
|
||||
return [.systemSmall]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct PriceWidget_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
PriceWidgetEntryView(entry: PriceWidgetEntry(date: Date(), family: .systemSmall, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData))
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
PriceWidgetEntryView(entry: PriceWidgetEntry(date: Date(), family: .accessoryCircular, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData))
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
PriceWidgetEntryView(entry: PriceWidgetEntry(date: Date(), family: .accessoryInline, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData))
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryInline))
|
||||
PriceWidgetEntryView(entry: PriceWidgetEntry(date: Date(), family: .accessoryRectangular, currentMarketData: MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00"), previousMarketData: emptyMarketData))
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
|
||||
}
|
||||
Group {
|
||||
PriceWidgetEntryView(entry: PreviewData.entry)
|
||||
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
PriceWidgetEntryView(entry: PreviewData.entry)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
|
||||
PriceWidgetEntryView(entry: PreviewData.entry)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryInline))
|
||||
PriceWidgetEntryView(entry: PreviewData.entry)
|
||||
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let previewMarketData = MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2019-09-18T17:27:00+00:00")
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PreviewData {
|
||||
static let entry = PriceWidgetEntry(
|
||||
date: Date(),
|
||||
family: .systemSmall,
|
||||
currentMarketData: previewMarketData,
|
||||
previousMarketData: emptyMarketData
|
||||
)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension WidgetConfiguration
|
||||
{
|
||||
@available(iOS 15.0, *)
|
||||
func contentMarginsDisabledIfAvailable() -> some WidgetConfiguration
|
||||
{
|
||||
if #available(iOSApplicationExtension 17.0, *)
|
||||
{
|
||||
return self.contentMarginsDisabled()
|
||||
}
|
||||
else
|
||||
{
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
25
ios/Widgets/PriceWidget/PriceWidgetEntry.swift
Normal file
25
ios/Widgets/PriceWidget/PriceWidgetEntry.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// PriceWidgetEntry.swift
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 10/27/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import WidgetKit
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
public struct PriceWidgetEntry: TimelineEntry {
|
||||
public let date: Date
|
||||
public let family: WidgetFamily
|
||||
public let currentMarketData: MarketData?
|
||||
public let previousMarketData: MarketData?
|
||||
|
||||
public init(date: Date, family: WidgetFamily, currentMarketData: MarketData?, previousMarketData: MarketData?) {
|
||||
self.date = date
|
||||
self.family = family
|
||||
self.currentMarketData = currentMarketData
|
||||
self.previousMarketData = previousMarketData
|
||||
}
|
||||
}
|
19
ios/Widgets/PriceWidget/PriceWidgetEntryView.swift
Normal file
19
ios/Widgets/PriceWidget/PriceWidgetEntryView.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// PriceWidgetEntryView.swift
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 10/27/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUICore
|
||||
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct PriceWidgetEntryView: View {
|
||||
let entry: PriceWidgetEntry
|
||||
|
||||
var body: some View {
|
||||
PriceView(entry: entry)
|
||||
}
|
||||
}
|
79
ios/Widgets/PriceWidget/PriceWidgetProvider.swift
Normal file
79
ios/Widgets/PriceWidget/PriceWidgetProvider.swift
Normal file
@ -0,0 +1,79 @@
|
||||
//
|
||||
// PriceWidgetProvider.swift
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 10/27/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct PriceWidgetProvider: TimelineProvider {
|
||||
typealias Entry = PriceWidgetEntry
|
||||
static var lastSuccessfulEntry: PriceWidgetEntry?
|
||||
|
||||
func placeholder(in context: Context) -> PriceWidgetEntry {
|
||||
createEntry(date: Date(), family: context.family, currentMarketData: previewMarketData)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (PriceWidgetEntry) -> Void) {
|
||||
let entry: PriceWidgetEntry
|
||||
if context.isPreview {
|
||||
entry = createEntry(date: Date(), family: context.family, currentMarketData: previewMarketData)
|
||||
} else {
|
||||
entry = createEntry(date: Date(), family: context.family, currentMarketData: emptyMarketData)
|
||||
}
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<PriceWidgetEntry>) -> Void) {
|
||||
var entries: [PriceWidgetEntry] = []
|
||||
|
||||
let userPreferredCurrency = Currency.getUserPreferredCurrency()
|
||||
if userPreferredCurrency != Currency.getLastSelectedCurrency() {
|
||||
Currency.saveNewSelectedCurrency()
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
if let data = try await MarketAPI.fetchPrice(currency: userPreferredCurrency), let formattedRate = data.formattedRate {
|
||||
let currentMarketData = MarketData(nextBlock: "", sats: "", price: formattedRate, rate: data.rateDouble, dateString: data.lastUpdate)
|
||||
let previousMarketData = PriceWidgetProvider.lastSuccessfulEntry?.currentMarketData
|
||||
|
||||
let entry = createEntry(
|
||||
date: Date(),
|
||||
family: context.family,
|
||||
currentMarketData: currentMarketData,
|
||||
previousMarketData: previousMarketData ?? emptyMarketData
|
||||
)
|
||||
PriceWidgetProvider.lastSuccessfulEntry = entry
|
||||
entries.append(entry)
|
||||
} else {
|
||||
if let lastEntry = PriceWidgetProvider.lastSuccessfulEntry {
|
||||
entries.append(lastEntry)
|
||||
} else {
|
||||
let entry = createEntry(date: Date(), family: context.family, currentMarketData: emptyMarketData)
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if let lastEntry = PriceWidgetProvider.lastSuccessfulEntry {
|
||||
entries.append(lastEntry)
|
||||
} else {
|
||||
let entry = createEntry(date: Date(), family: context.family, currentMarketData: emptyMarketData)
|
||||
entries.append(entry)
|
||||
}
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
|
||||
private func createEntry(date: Date, family: WidgetFamily, currentMarketData: MarketData, previousMarketData: MarketData = emptyMarketData) -> PriceWidgetEntry {
|
||||
PriceWidgetEntry(date: date, family: family, currentMarketData: currentMarketData, previousMarketData: previousMarketData)
|
||||
}
|
||||
}
|
258
ios/Widgets/Shared/FiatUnitEnum.swift
Normal file
258
ios/Widgets/Shared/FiatUnitEnum.swift
Normal file
@ -0,0 +1,258 @@
|
||||
|
||||
// Hardcoding values for simplicity; AppIntents are unnecessarily complex
|
||||
|
||||
import AppIntents
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
enum FiatUnitEnum: String, AppEnum, CaseIterable, Identifiable, Codable {
|
||||
|
||||
var id: String { self.rawValue }
|
||||
|
||||
case AED
|
||||
case ARS
|
||||
case AUD
|
||||
case AWG
|
||||
case BHD
|
||||
case BRL
|
||||
case CAD
|
||||
case CHF
|
||||
case CLP
|
||||
case CNY
|
||||
case COP
|
||||
case CZK
|
||||
case DKK
|
||||
case EUR
|
||||
case GBP
|
||||
case HRK
|
||||
case HUF
|
||||
case IDR
|
||||
case ILS
|
||||
case INR
|
||||
case IRR
|
||||
case IRT
|
||||
case ISK
|
||||
case JPY
|
||||
case KES
|
||||
case KRW
|
||||
case KWD
|
||||
case LBP
|
||||
case LKR
|
||||
case MXN
|
||||
case MYR
|
||||
case MZN
|
||||
case NGN
|
||||
case NOK
|
||||
case NZD
|
||||
case OMR
|
||||
case PHP
|
||||
case PLN
|
||||
case QAR
|
||||
case RON
|
||||
case RUB
|
||||
case SAR
|
||||
case SEK
|
||||
case SGD
|
||||
case THB
|
||||
case TRY
|
||||
case TWD
|
||||
case TZS
|
||||
case UAH
|
||||
case UGX
|
||||
case USD
|
||||
case UYU
|
||||
case VEF
|
||||
case VES
|
||||
case XAF
|
||||
case ZAR
|
||||
case GHS
|
||||
|
||||
var code: String {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
var source: String {
|
||||
switch self {
|
||||
case .AED:
|
||||
return "CoinGecko"
|
||||
case .ARS:
|
||||
return "Yadio"
|
||||
case .AUD:
|
||||
return "CoinGecko"
|
||||
case .AWG:
|
||||
return "CoinDesk"
|
||||
case .BHD:
|
||||
return "CoinGecko"
|
||||
case .BRL:
|
||||
return "CoinGecko"
|
||||
case .CAD:
|
||||
return "CoinGecko"
|
||||
case .CHF:
|
||||
return "CoinGecko"
|
||||
case .CLP:
|
||||
return "Yadio"
|
||||
case .CNY:
|
||||
return "Coinbase"
|
||||
case .COP:
|
||||
return "CoinDesk"
|
||||
case .CZK:
|
||||
return "CoinGecko"
|
||||
case .DKK:
|
||||
return "CoinGecko"
|
||||
case .EUR:
|
||||
return "Kraken"
|
||||
case .GBP:
|
||||
return "Kraken"
|
||||
case .HRK:
|
||||
return "CoinDesk"
|
||||
case .HUF:
|
||||
return "CoinGecko"
|
||||
case .IDR:
|
||||
return "CoinGecko"
|
||||
case .ILS:
|
||||
return "CoinGecko"
|
||||
case .INR:
|
||||
return "coinpaprika"
|
||||
case .IRR:
|
||||
return "Exir"
|
||||
case .IRT:
|
||||
return "Exir"
|
||||
case .ISK:
|
||||
return "CoinDesk"
|
||||
case .JPY:
|
||||
return "CoinGecko"
|
||||
case .KES:
|
||||
return "CoinDesk"
|
||||
case .KRW:
|
||||
return "CoinGecko"
|
||||
case .KWD:
|
||||
return "CoinGecko"
|
||||
case .LBP:
|
||||
return "YadioConvert"
|
||||
case .LKR:
|
||||
return "CoinGecko"
|
||||
case .MXN:
|
||||
return "CoinGecko"
|
||||
case .MYR:
|
||||
return "CoinGecko"
|
||||
case .MZN:
|
||||
return "CoinDesk"
|
||||
case .NGN:
|
||||
return "CoinGecko"
|
||||
case .NOK:
|
||||
return "CoinGecko"
|
||||
case .NZD:
|
||||
return "CoinGecko"
|
||||
case .OMR:
|
||||
return "CoinDesk"
|
||||
case .PHP:
|
||||
return "CoinGecko"
|
||||
case .PLN:
|
||||
return "CoinGecko"
|
||||
case .QAR:
|
||||
return "CoinDesk"
|
||||
case .RON:
|
||||
return "BNR"
|
||||
case .RUB:
|
||||
return "CoinGecko"
|
||||
case .SAR:
|
||||
return "CoinGecko"
|
||||
case .SEK:
|
||||
return "CoinGecko"
|
||||
case .SGD:
|
||||
return "CoinGecko"
|
||||
case .THB:
|
||||
return "CoinGecko"
|
||||
case .TRY:
|
||||
return "CoinGecko"
|
||||
case .TWD:
|
||||
return "CoinGecko"
|
||||
case .TZS:
|
||||
return "CoinDesk"
|
||||
case .UAH:
|
||||
return "CoinGecko"
|
||||
case .UGX:
|
||||
return "CoinDesk"
|
||||
case .USD:
|
||||
return "Kraken"
|
||||
case .UYU:
|
||||
return "CoinDesk"
|
||||
case .VEF:
|
||||
return "CoinGecko"
|
||||
case .VES:
|
||||
return "Yadio"
|
||||
case .XAF:
|
||||
return "CoinDesk"
|
||||
case .ZAR:
|
||||
return "CoinGecko"
|
||||
case .GHS:
|
||||
return "CoinDesk"
|
||||
}
|
||||
}
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation {
|
||||
TypeDisplayRepresentation(stringLiteral: "Currency")
|
||||
}
|
||||
|
||||
static var caseDisplayRepresentations: [FiatUnitEnum: DisplayRepresentation] {
|
||||
return [
|
||||
.AED: DisplayRepresentation(stringLiteral: "United Arab Emirates (UAE Dirham)"),
|
||||
.ARS: DisplayRepresentation(stringLiteral: "Argentina (Argentine Peso)"),
|
||||
.AUD: DisplayRepresentation(stringLiteral: "Australia (Australian Dollar)"),
|
||||
.AWG: DisplayRepresentation(stringLiteral: "Aruba (Aruban Florin)"),
|
||||
.BHD: DisplayRepresentation(stringLiteral: "Bahrain (Bahraini Dinar)"),
|
||||
.BRL: DisplayRepresentation(stringLiteral: "Brazil (Brazilian Real)"),
|
||||
.CAD: DisplayRepresentation(stringLiteral: "Canada (Canadian Dollar)"),
|
||||
.CHF: DisplayRepresentation(stringLiteral: "Switzerland (Swiss Franc)"),
|
||||
.CLP: DisplayRepresentation(stringLiteral: "Chile (Chilean Peso)"),
|
||||
.CNY: DisplayRepresentation(stringLiteral: "China (Chinese Yuan)"),
|
||||
.COP: DisplayRepresentation(stringLiteral: "Colombia (Colombian Peso)"),
|
||||
.CZK: DisplayRepresentation(stringLiteral: "Czech Republic (Czech Koruna)"),
|
||||
.DKK: DisplayRepresentation(stringLiteral: "Denmark (Danish Krone)"),
|
||||
.EUR: DisplayRepresentation(stringLiteral: "European Union (Euro)"),
|
||||
.GBP: DisplayRepresentation(stringLiteral: "United Kingdom (British Pound)"),
|
||||
.HRK: DisplayRepresentation(stringLiteral: "Croatia (Croatian Kuna)"),
|
||||
.HUF: DisplayRepresentation(stringLiteral: "Hungary (Hungarian Forint)"),
|
||||
.IDR: DisplayRepresentation(stringLiteral: "Indonesia (Indonesian Rupiah)"),
|
||||
.ILS: DisplayRepresentation(stringLiteral: "Israel (Israeli New Shekel)"),
|
||||
.INR: DisplayRepresentation(stringLiteral: "India (Indian Rupee)"),
|
||||
.IRR: DisplayRepresentation(stringLiteral: "Iran (Iranian Rial)"),
|
||||
.IRT: DisplayRepresentation(stringLiteral: "Iran (Iranian Toman)"),
|
||||
.ISK: DisplayRepresentation(stringLiteral: "Iceland (Icelandic Króna)"),
|
||||
.JPY: DisplayRepresentation(stringLiteral: "Japan (Japanese Yen)"),
|
||||
.KES: DisplayRepresentation(stringLiteral: "Kenya (Kenyan Shilling)"),
|
||||
.KRW: DisplayRepresentation(stringLiteral: "South Korea (South Korean Won)"),
|
||||
.KWD: DisplayRepresentation(stringLiteral: "Kuwait (Kuwaiti Dinar)"),
|
||||
.LBP: DisplayRepresentation(stringLiteral: "Lebanon (Lebanese Pound)"),
|
||||
.LKR: DisplayRepresentation(stringLiteral: "Sri Lanka (Sri Lankan Rupee)"),
|
||||
.MXN: DisplayRepresentation(stringLiteral: "Mexico (Mexican Peso)"),
|
||||
.MYR: DisplayRepresentation(stringLiteral: "Malaysia (Malaysian Ringgit)"),
|
||||
.MZN: DisplayRepresentation(stringLiteral: "Mozambique (Mozambican Metical)"),
|
||||
.NGN: DisplayRepresentation(stringLiteral: "Nigeria (Nigerian Naira)"),
|
||||
.NOK: DisplayRepresentation(stringLiteral: "Norway (Norwegian Krone)"),
|
||||
.NZD: DisplayRepresentation(stringLiteral: "New Zealand (New Zealand Dollar)"),
|
||||
.OMR: DisplayRepresentation(stringLiteral: "Oman (Omani Rial)"),
|
||||
.PHP: DisplayRepresentation(stringLiteral: "Philippines (Philippine Peso)"),
|
||||
.PLN: DisplayRepresentation(stringLiteral: "Poland (Polish Zloty)"),
|
||||
.QAR: DisplayRepresentation(stringLiteral: "Qatar (Qatari Riyal)"),
|
||||
.RON: DisplayRepresentation(stringLiteral: "Romania (Romanian Leu)"),
|
||||
.RUB: DisplayRepresentation(stringLiteral: "Russia (Russian Ruble)"),
|
||||
.SAR: DisplayRepresentation(stringLiteral: "Saudi Arabia (Saudi Riyal)"),
|
||||
.SEK: DisplayRepresentation(stringLiteral: "Sweden (Swedish Krona)"),
|
||||
.SGD: DisplayRepresentation(stringLiteral: "Singapore (Singapore Dollar)"),
|
||||
.THB: DisplayRepresentation(stringLiteral: "Thailand (Thai Baht)"),
|
||||
.TRY: DisplayRepresentation(stringLiteral: "Turkey (Turkish Lira)"),
|
||||
.TWD: DisplayRepresentation(stringLiteral: "Taiwan (New Taiwan Dollar)"),
|
||||
.TZS: DisplayRepresentation(stringLiteral: "Tanzania (Tanzanian Shilling)"),
|
||||
.UAH: DisplayRepresentation(stringLiteral: "Ukraine (Ukrainian Hryvnia)"),
|
||||
.UGX: DisplayRepresentation(stringLiteral: "Uganda (Ugandan Shilling)"),
|
||||
.USD: DisplayRepresentation(stringLiteral: "United States of America (US Dollar)"),
|
||||
.UYU: DisplayRepresentation(stringLiteral: "Uruguay (Uruguayan Peso)"),
|
||||
.VEF: DisplayRepresentation(stringLiteral: "Venezuela (Venezuelan Bolívar Fuerte)"),
|
||||
.VES: DisplayRepresentation(stringLiteral: "Venezuela (Venezuelan Bolívar Soberano)"),
|
||||
.XAF: DisplayRepresentation(stringLiteral: "Central African Republic (Central African Franc)"),
|
||||
.ZAR: DisplayRepresentation(stringLiteral: "South Africa (South African Rand)"),
|
||||
.GHS: DisplayRepresentation(stringLiteral: "Ghana (Ghanaian Cedi)"),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct PriceView: View {
|
||||
var entry: PriceWidgetEntry
|
||||
|
||||
@ -170,6 +171,7 @@ struct PriceView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct PriceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
|
27
ios/Widgets/WalletAppShortcuts.swift
Normal file
27
ios/Widgets/WalletAppShortcuts.swift
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// WalletAppShortcuts.swift
|
||||
// BlueWallet
|
||||
|
||||
|
||||
import AppIntents
|
||||
|
||||
@available(iOS 16.4, *)
|
||||
struct WalletAppShortcuts: AppShortcutsProvider {
|
||||
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
AppShortcut(
|
||||
intent: PriceIntent(),
|
||||
phrases: [
|
||||
AppShortcutPhrase<PriceIntent>("Market rate for Bitcoin in \(\.$fiatCurrency) using BlueWallet"),
|
||||
AppShortcutPhrase<PriceIntent>("Get the current Bitcoin market rate in \(\.$fiatCurrency) with BlueWallet"),
|
||||
AppShortcutPhrase<PriceIntent>("What's the current Bitcoin rate in \(\.$fiatCurrency) using BlueWallet?"),
|
||||
AppShortcutPhrase<PriceIntent>("Show me the current Bitcoin price in \(\.$fiatCurrency) via BlueWallet"),
|
||||
AppShortcutPhrase<PriceIntent>("Retrieve Bitcoin rate in \(\.$fiatCurrency) from BlueWallet")
|
||||
],
|
||||
shortTitle: "Market Rate",
|
||||
systemImageName: "bitcoinsign.circle"
|
||||
)
|
||||
|
||||
}
|
||||
}
|
@ -19,18 +19,3 @@ struct Widgets: WidgetBundle {
|
||||
WalletInformationAndMarketWidget()
|
||||
}
|
||||
}
|
||||
|
||||
extension WidgetConfiguration
|
||||
{
|
||||
func contentMarginsDisabledIfAvailable() -> some WidgetConfiguration
|
||||
{
|
||||
if #available(iOSApplicationExtension 17.0, *)
|
||||
{
|
||||
return self.contentMarginsDisabled()
|
||||
}
|
||||
else
|
||||
{
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user