Merge branch 'master' into totlview

This commit is contained in:
Marcos Rodriguez Velez 2024-11-08 10:07:31 -04:00
commit d8e6425565
16 changed files with 1245 additions and 355 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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"

View File

@ -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)
}
}
}
}

View File

@ -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 "..."

View File

@ -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)
}
}

View 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)
}
}

View 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)
}
}

View File

@ -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
}
}
}
}

View 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
}
}

View 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)
}
}

View 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)
}
}

View 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)"),
]
}
}

View File

@ -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 {

View 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"
)
}
}

View File

@ -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
}
}
}