mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 15:04:50 +01:00
ADD: Market Price intent
This commit is contained in:
parent
fda04c2b02
commit
ee739d347d
14 changed files with 760 additions and 522 deletions
|
@ -125,6 +125,18 @@
|
|||
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 */; };
|
||||
B4AB225D2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
|
||||
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
|
||||
|
@ -355,6 +367,11 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
|
@ -527,6 +544,10 @@
|
|||
6D6CA4BB255872E3009312A5 /* PriceWidget */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B48630DF2CCEE7C800A8425C /* PriceWidgetEntryView.swift */,
|
||||
B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */,
|
||||
B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */,
|
||||
B48630D02CCEE3B300A8425C /* PriceIntent.swift */,
|
||||
6D6CA4BC255872E3009312A5 /* PriceWidget.swift */,
|
||||
);
|
||||
path = PriceWidget;
|
||||
|
@ -551,6 +572,7 @@
|
|||
6DD4109F266CADF10087DE03 /* Widgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */,
|
||||
6DD410C3266CCB780087DE03 /* WidgetsExtension.entitlements */,
|
||||
6DD410A0266CADF10087DE03 /* Widgets.swift */,
|
||||
6DD410A4266CADF40087DE03 /* Info.plist */,
|
||||
|
@ -1181,20 +1203,26 @@
|
|||
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 */,
|
||||
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
|
||||
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
|
||||
B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */,
|
||||
B48630E72CCEE91900A8425C /* PriceWidgetProvider.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 */,
|
||||
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 */,
|
||||
|
@ -1202,6 +1230,7 @@
|
|||
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,8 +1240,10 @@
|
|||
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 */,
|
||||
6DD410AC266CAE470087DE03 /* PriceWidget.swift in Sources */,
|
||||
|
@ -1220,8 +1251,10 @@
|
|||
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 */,
|
||||
6DD410B3266CAF5C0087DE03 /* Colors.swift in Sources */,
|
||||
B44033C12BCC32F800162242 /* BitcoinUnit.swift in Sources */,
|
||||
6DD410BB266CAF5C0087DE03 /* MarketView.swift in Sources */,
|
||||
|
@ -1241,6 +1274,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 +1614,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 +1670,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,78 @@
|
|||
{
|
||||
"sourceLanguage" : "en_US",
|
||||
"strings" : {
|
||||
"at %@" : {
|
||||
|
||||
},
|
||||
"Balance" : {
|
||||
|
||||
},
|
||||
"Bitcoin (%@)" : {
|
||||
|
||||
},
|
||||
"BTC" : {
|
||||
|
||||
},
|
||||
"Checked at %@" : {
|
||||
|
||||
},
|
||||
"Current Bitcoin Market Rate" : {
|
||||
|
||||
},
|
||||
"Current Bitcoin Price: %@" : {
|
||||
|
||||
},
|
||||
"from" : {
|
||||
|
||||
},
|
||||
"From %@" : {
|
||||
|
||||
},
|
||||
"Last Updated" : {
|
||||
|
||||
},
|
||||
"Latest transaction" : {
|
||||
|
||||
},
|
||||
"Market" : {
|
||||
|
||||
},
|
||||
"Market Rate" : {
|
||||
|
||||
},
|
||||
"Next Block" : {
|
||||
|
||||
},
|
||||
"Price" : {
|
||||
|
||||
},
|
||||
"receive" : {
|
||||
|
||||
},
|
||||
"Sats/%@" : {
|
||||
|
||||
},
|
||||
"send" : {
|
||||
|
||||
},
|
||||
"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 +128,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,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct MarketData:Codable {
|
||||
public struct MarketData:Codable {
|
||||
var nextBlock: String
|
||||
var sats: String
|
||||
var price: String
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
75
ios/Widgets/PriceWidget/PriceIntent.swift
Normal file
75
ios/Widgets/PriceWidget/PriceIntent.swift
Normal file
|
@ -0,0 +1,75 @@
|
|||
import AppIntents
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct PriceIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Market Rate"
|
||||
static var description = IntentDescription("View the current Bitcoin market rate.")
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("View the current Bitcoin market rate.")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<String> & ProvidesDialog & ShowsSnippetView {
|
||||
let userPreferredCurrency = Currency.getUserPreferredCurrency()
|
||||
if userPreferredCurrency != Currency.getLastSelectedCurrency() {
|
||||
Currency.saveNewSelectedCurrency()
|
||||
}
|
||||
|
||||
var resultText = "--"
|
||||
var lastUpdated = "--"
|
||||
|
||||
do {
|
||||
if let data = try await MarketAPI.fetchPrice(currency: userPreferredCurrency),
|
||||
let formattedRate = data.formattedRate {
|
||||
resultText = formattedRate
|
||||
let currentMarketData = MarketData(
|
||||
nextBlock: "",
|
||||
sats: "",
|
||||
price: formattedRate,
|
||||
rate: data.rateDouble,
|
||||
dateString: data.lastUpdate
|
||||
)
|
||||
lastUpdated = formattedDate(from: data.lastUpdate)
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
let view = CompactPriceView(price: resultText, lastUpdated: lastUpdated)
|
||||
|
||||
return .result(
|
||||
value: resultText,
|
||||
dialog: "Current Bitcoin Market Rate",
|
||||
view: view
|
||||
)
|
||||
}
|
||||
|
||||
private func formattedDate(from isoString: String) -> String {
|
||||
let isoFormatter = ISO8601DateFormatter()
|
||||
if let date = isoFormatter.date(from: isoString) {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .none
|
||||
formatter.timeStyle = .short
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
return "--"
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
struct CompactPriceView: View {
|
||||
let price: String
|
||||
let lastUpdated: String
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(price)
|
||||
.font(.title)
|
||||
Text("at \(lastUpdated)")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.frame(maxWidth: 200)
|
||||
.padding()
|
||||
}
|
||||
}
|
|
@ -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 14.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)
|
||||
}
|
||||
}
|
|
@ -9,180 +9,134 @@
|
|||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
struct PriceView: View {
|
||||
var entry: PriceWidgetEntry
|
||||
|
||||
var body: some View {
|
||||
switch entry.family {
|
||||
case .accessoryInline, .accessoryCircular, .accessoryRectangular:
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
wrappedView(for: getView(for: entry.family), family: entry.family)
|
||||
} else {
|
||||
getView(for: entry.family)
|
||||
}
|
||||
default:
|
||||
defaultView.background(Color(UIColor.systemBackground))
|
||||
}
|
||||
}
|
||||
|
||||
private func getView(for family: WidgetFamily) -> some View {
|
||||
switch family {
|
||||
case .accessoryCircular:
|
||||
return AnyView(accessoryCircularView)
|
||||
case .accessoryInline:
|
||||
return AnyView(accessoryInlineView)
|
||||
case .accessoryRectangular:
|
||||
return AnyView(accessoryRectangularView)
|
||||
default:
|
||||
return AnyView(defaultView)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func wrappedView<Content: View>(for content: Content, family: WidgetFamily) -> some View {
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
ZStack {
|
||||
if family == .accessoryRectangular {
|
||||
AccessoryWidgetBackground()
|
||||
.background(Color(UIColor.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
} else {
|
||||
AccessoryWidgetBackground()
|
||||
}
|
||||
content
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
private var accessoryCircularView: some View {
|
||||
let priceString = formattedPriceString(from: entry.currentMarketData?.rate)
|
||||
let priceChangePercentage = formattedPriceChangePercentage(currentRate: entry.currentMarketData?.rate, previousRate: entry.previousMarketData?.rate)
|
||||
|
||||
return VStack(alignment: .center, spacing: 4) {
|
||||
Text("BTC")
|
||||
.font(.caption)
|
||||
.minimumScaleFactor(0.1)
|
||||
Text(priceString)
|
||||
.font(.body)
|
||||
.minimumScaleFactor(0.1)
|
||||
.lineLimit(1)
|
||||
if let priceChangePercentage = priceChangePercentage {
|
||||
Text(priceChangePercentage)
|
||||
.font(.caption2)
|
||||
.foregroundColor(priceChangePercentage.contains("-") ? .red : .green)
|
||||
}
|
||||
}
|
||||
.widgetURL(URL(string: "bluewallet://marketprice"))
|
||||
}
|
||||
|
||||
private var accessoryInlineView: some View {
|
||||
let priceString = formattedCurrencyString(from: entry.currentMarketData?.rate)
|
||||
let priceChangePercentage = formattedPriceChangePercentage(currentRate: entry.currentMarketData?.rate, previousRate: entry.previousMarketData?.rate)
|
||||
|
||||
return HStack {
|
||||
Text(priceString)
|
||||
.font(.body)
|
||||
.minimumScaleFactor(0.1)
|
||||
if let priceChangePercentage = priceChangePercentage {
|
||||
Image(systemName: priceChangePercentage.contains("-") ? "arrow.down" : "arrow.up")
|
||||
.foregroundColor(priceChangePercentage.contains("-") ? .red : .green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var accessoryRectangularView: some View {
|
||||
let currentPrice = formattedCurrencyString(from: entry.currentMarketData?.rate)
|
||||
|
||||
return VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Bitcoin (\(Currency.getUserPreferredCurrency()))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
HStack {
|
||||
Text(currentPrice)
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
if let currentMarketDataRate = entry.currentMarketData?.rate,
|
||||
let previousMarketDataRate = entry.previousMarketData?.rate,
|
||||
currentMarketDataRate != previousMarketDataRate {
|
||||
Image(systemName: currentMarketDataRate > previousMarketDataRate ? "arrow.up" : "arrow.down")
|
||||
}
|
||||
}
|
||||
|
||||
if let previousMarketDataPrice = entry.previousMarketData?.price, Int(entry.currentMarketData?.rate ?? 0) != Int(entry.previousMarketData?.rate ?? 0) {
|
||||
Text("From \(previousMarketDataPrice)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("at \(entry.currentMarketData?.formattedDate ?? "--")")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.all, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(UIColor.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
private var defaultView: some View {
|
||||
VStack(alignment: .trailing, spacing: nil, content: {
|
||||
Text("Last Updated").font(Font.system(size: 11, weight: .regular)).foregroundColor(Color(UIColor.lightGray))
|
||||
HStack(alignment: .lastTextBaseline, spacing: nil, content: {
|
||||
Text(entry.currentMarketData?.formattedDate ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01).transition(.opacity)
|
||||
})
|
||||
Spacer()
|
||||
VStack(alignment: .trailing, spacing: 16, content: {
|
||||
HStack(alignment: .lastTextBaseline, spacing: nil, content: {
|
||||
Text(entry.currentMarketData?.price ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 28, weight: .bold)).minimumScaleFactor(0.01).transition(.opacity)
|
||||
})
|
||||
if let previousMarketDataPrice = entry.previousMarketData?.price, let currentMarketDataRate = entry.currentMarketData?.rate, let previousMarketDataRate = entry.previousMarketData?.rate, previousMarketDataRate > 0, currentMarketDataRate != previousMarketDataRate {
|
||||
HStack(alignment: .lastTextBaseline, spacing: nil, content: {
|
||||
Image(systemName: currentMarketDataRate > previousMarketDataRate ? "arrow.up" : "arrow.down")
|
||||
Text("from").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01)
|
||||
Text(previousMarketDataPrice).lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01)
|
||||
}).transition(.slide)
|
||||
}
|
||||
})
|
||||
}).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .trailing).padding()
|
||||
}
|
||||
|
||||
private func formattedPriceString(from rate: Double?) -> String {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .decimal
|
||||
numberFormatter.maximumFractionDigits = 0
|
||||
return numberFormatter.string(from: NSNumber(value: rate ?? 0)) ?? "--"
|
||||
}
|
||||
|
||||
private func formattedCurrencyString(from rate: Double?) -> String {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.maximumFractionDigits = 0
|
||||
numberFormatter.numberStyle = .currency
|
||||
numberFormatter.currencySymbol = fiatUnit(currency: Currency.getUserPreferredCurrency())?.symbol
|
||||
return numberFormatter.string(from: NSNumber(value: rate ?? 0)) ?? "--"
|
||||
}
|
||||
|
||||
private func formattedPriceChangePercentage(currentRate: Double?, previousRate: Double?) -> String? {
|
||||
guard let currentRate = currentRate, let previousRate = previousRate, previousRate > 0 else { return nil }
|
||||
let change = ((currentRate - previousRate) / previousRate) * 100
|
||||
return change == 0 ? nil : String(format: "%+.1f%%", change)
|
||||
}
|
||||
}
|
||||
var entry: PriceWidgetEntry
|
||||
|
||||
struct PriceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
PriceView(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)).padding()
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
PriceView(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))
|
||||
PriceView(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))
|
||||
PriceView(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))
|
||||
}
|
||||
var body: some View {
|
||||
switch entry.family {
|
||||
case .accessoryInline, .accessoryCircular, .accessoryRectangular:
|
||||
getView(for: entry.family)
|
||||
default:
|
||||
defaultView.background(Color(UIColor.systemBackground))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func getView(for family: WidgetFamily) -> some View {
|
||||
switch family {
|
||||
case .accessoryCircular:
|
||||
accessoryCircularView
|
||||
case .accessoryInline:
|
||||
accessoryInlineView
|
||||
case .accessoryRectangular:
|
||||
accessoryRectangularView
|
||||
default:
|
||||
defaultView
|
||||
}
|
||||
}
|
||||
|
||||
private var accessoryCircularView: some View {
|
||||
let priceString = (entry.currentMarketData?.rate ?? 0).formattedPriceString()
|
||||
let priceChangePercentage = formattedPriceChangePercentage(currentRate: entry.currentMarketData?.rate, previousRate: entry.previousMarketData?.rate)
|
||||
|
||||
return VStack(alignment: .center, spacing: 4) {
|
||||
Text("BTC")
|
||||
.font(.caption)
|
||||
.minimumScaleFactor(0.1)
|
||||
Text(priceString)
|
||||
.font(.body)
|
||||
.minimumScaleFactor(0.1)
|
||||
.lineLimit(1)
|
||||
if let priceChangePercentage = priceChangePercentage {
|
||||
Text(priceChangePercentage)
|
||||
.font(.caption2)
|
||||
.foregroundColor(priceChangePercentage.contains("-") ? .red : .green)
|
||||
}
|
||||
}
|
||||
.widgetURL(URL(string: "bluewallet://marketprice"))
|
||||
}
|
||||
|
||||
private var accessoryInlineView: some View {
|
||||
let priceString = (entry.currentMarketData?.rate ?? 0).formattedCurrencyString()
|
||||
let priceChangePercentage = formattedPriceChangePercentage(currentRate: entry.currentMarketData?.rate, previousRate: entry.previousMarketData?.rate)
|
||||
|
||||
return HStack {
|
||||
Text(priceString)
|
||||
.font(.body)
|
||||
.minimumScaleFactor(0.1)
|
||||
if let priceChangePercentage = priceChangePercentage {
|
||||
Image(systemName: priceChangePercentage.contains("-") ? "arrow.down" : "arrow.up")
|
||||
.foregroundColor(priceChangePercentage.contains("-") ? .red : .green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var accessoryRectangularView: some View {
|
||||
let currentPrice = (entry.currentMarketData?.rate ?? 0).formattedCurrencyString()
|
||||
|
||||
return VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Bitcoin (\(Currency.getUserPreferredCurrency()))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
HStack {
|
||||
Text(currentPrice)
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
if let currentRate = entry.currentMarketData?.rate,
|
||||
let previousRate = entry.previousMarketData?.rate,
|
||||
currentRate != previousRate {
|
||||
Image(systemName: currentRate > previousRate ? "arrow.up" : "arrow.down")
|
||||
}
|
||||
}
|
||||
|
||||
if let previousPrice = entry.previousMarketData?.price,
|
||||
let currentRate = entry.currentMarketData?.rate,
|
||||
let previousRate = entry.previousMarketData?.rate,
|
||||
currentRate != previousRate {
|
||||
Text("From \(previousPrice)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("at \(entry.currentMarketData?.formattedDate ?? "--")")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.all, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(UIColor.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
private var defaultView: some View {
|
||||
VStack(alignment: .trailing, spacing: nil) {
|
||||
Text("Last Updated").font(Font.system(size: 11, weight: .regular)).foregroundColor(Color(UIColor.lightGray))
|
||||
HStack(alignment: .lastTextBaseline, spacing: nil) {
|
||||
Text(entry.currentMarketData?.formattedDate ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01).transition(.opacity)
|
||||
}
|
||||
Spacer()
|
||||
VStack(alignment: .trailing, spacing: 16) {
|
||||
HStack(alignment: .lastTextBaseline, spacing: nil) {
|
||||
Text(entry.currentMarketData?.price ?? "").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 28, weight: .bold)).minimumScaleFactor(0.01).transition(.opacity)
|
||||
}
|
||||
if let previousPrice = entry.previousMarketData?.price,
|
||||
let currentRate = entry.currentMarketData?.rate,
|
||||
let previousRate = entry.previousMarketData?.rate,
|
||||
previousRate > 0, currentRate != previousRate {
|
||||
HStack(alignment: .lastTextBaseline, spacing: nil) {
|
||||
Image(systemName: currentRate > previousRate ? "arrow.up" : "arrow.down")
|
||||
Text("from").lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01)
|
||||
Text(previousPrice).lineLimit(1).foregroundColor(.primary).font(Font.system(size: 13, weight: .regular)).minimumScaleFactor(0.01)
|
||||
}.transition(.slide)
|
||||
}
|
||||
}
|
||||
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing).padding()
|
||||
}
|
||||
|
||||
private func formattedPriceChangePercentage(currentRate: Double?, previousRate: Double?) -> String? {
|
||||
guard let currentRate = currentRate, let previousRate = previousRate, previousRate > 0 else { return nil }
|
||||
let change = ((currentRate - previousRate) / previousRate) * 100
|
||||
return change == 0 ? nil : String(format: "%+.1f%%", change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
ios/Widgets/WalletAppShortcuts.swift
Normal file
27
ios/Widgets/WalletAppShortcuts.swift
Normal file
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// WalletAppShortcuts.swift
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 10/27/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
import AppIntents
|
||||
@available(iOS 16.4, *)
|
||||
struct WalletAppShortcuts: AppShortcutsProvider {
|
||||
|
||||
@AppShortcutsBuilder
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
AppShortcut(
|
||||
intent: PriceIntent(),
|
||||
phrases: [
|
||||
"Market Rate \(.applicationName)",
|
||||
"Get the current Bitcoin market rate in \(.applicationName)"
|
||||
],
|
||||
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…
Add table
Reference in a new issue