mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-22 15:04:50 +01:00
REF: Rewrite widget to kotlin
This commit is contained in:
parent
ff2136e804
commit
e6e3a76edc
13 changed files with 421 additions and 205 deletions
|
@ -94,12 +94,27 @@ android {
|
|||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
buildConfigField "boolean", "USE_MOCK_SERVER", "false"
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"
|
||||
}
|
||||
debug {
|
||||
buildConfigField "boolean", "USE_MOCK_SERVER", "false"
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
normal {
|
||||
dimension "mode"
|
||||
}
|
||||
mock {
|
||||
dimension "mode"
|
||||
buildConfigField "boolean", "USE_MOCK_SERVER", "true"
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "mode"
|
||||
}
|
||||
|
||||
task copyFiatUnits(type: Copy) {
|
||||
|
@ -114,6 +129,7 @@ dependencies {
|
|||
implementation("com.facebook.react:react-android")
|
||||
implementation files("../../node_modules/rn-ldk/android/libs/LDK-release.aar")
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.core:core-remoteviews:1.1.0'
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
|
@ -121,11 +137,23 @@ dependencies {
|
|||
implementation jscFlavor
|
||||
}
|
||||
androidTestImplementation('com.wix:detox:+')
|
||||
androidTestImplementation "androidx.work:work-testing:2.7.1"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
testImplementation 'com.google.guava:guava:30.1.1-android'
|
||||
androidTestImplementation 'com.google.guava:guava:30.1.1-android'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
|
||||
androidTestImplementation 'androidx.test:core:1.4.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.3'
|
||||
implementation 'com.squareup.okhttp3:mockwebserver:4.9.3'
|
||||
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
}
|
||||
apply plugin: 'com.google.gms.google-services' // Google Services plugin
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ package io.bluewallet.bluewallet;
|
|||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class DetoxTest {
|
||||
public class DetoxTest {
|
||||
// Replace 'MainActivity' with the value of android:name entry in
|
||||
// <activity> in AndroidManifest.xml
|
||||
@Rule
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
package io.bluewallet.bluewallet
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.widget.RemoteViews
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import androidx.work.WorkManager
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.json.JSONObject
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MockServerTest {
|
||||
|
||||
private lateinit var mockWebServer: MockWebServer
|
||||
private lateinit var context: Context
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
context = ApplicationProvider.getApplicationContext()
|
||||
mockWebServer = MockWebServer()
|
||||
mockWebServer.start()
|
||||
|
||||
val baseUrl = mockWebServer.url("/").toString()
|
||||
MarketAPI.baseUrl = baseUrl
|
||||
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWidgetUpdate() {
|
||||
// Mock API response
|
||||
val mockResponse = MockResponse()
|
||||
.setBody(createMockJsonResponse("USD", "61500"))
|
||||
.setResponseCode(200)
|
||||
mockWebServer.enqueue(mockResponse)
|
||||
|
||||
// Trigger the widget update
|
||||
WidgetUpdateWorker.scheduleWork(context)
|
||||
|
||||
// Wait for the worker to run
|
||||
val workInfos = WorkManager.getInstance(context).getWorkInfosByTag(WidgetUpdateWorker.WORK_NAME).get()
|
||||
val testDriver = WorkManagerTestInitHelper.getTestDriver()
|
||||
for (workInfo in workInfos) {
|
||||
testDriver?.setAllConstraintsMet(workInfo.id)
|
||||
}
|
||||
Thread.sleep(10000) // Wait for 10 seconds to simulate the update interval
|
||||
|
||||
// Validate the widget updates
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, BitcoinPriceWidget::class.java))
|
||||
|
||||
for (widgetId in widgetIds) {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_layout)
|
||||
// Add your assertions here to validate the widget update
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMockJsonResponse(currency: String, price: String): String {
|
||||
return """
|
||||
{
|
||||
"$currency": {
|
||||
"endPointKey": "$currency",
|
||||
"locale": "en-US",
|
||||
"source": "Kraken",
|
||||
"symbol": "$",
|
||||
"country": "United States (US Dollar)",
|
||||
"price": "$price"
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
|
@ -2,37 +2,27 @@ package io.bluewallet.bluewallet
|
|||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import android.util.Log
|
||||
import androidx.work.WorkManager
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BitcoinPriceWidget : AppWidgetProvider() {
|
||||
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
scheduleWork(context)
|
||||
WidgetUpdateWorker.scheduleWork(context)
|
||||
}
|
||||
|
||||
private fun scheduleWork(context: Context) {
|
||||
val workRequest = PeriodicWorkRequest.Builder(WidgetUpdateWorker::class.java, 15, TimeUnit.MINUTES)
|
||||
.setInitialDelay(15L, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
"UpdateWidgetWork",
|
||||
ExistingPeriodicWorkPolicy.REPLACE,
|
||||
workRequest
|
||||
)
|
||||
override fun onEnabled(context: Context) {
|
||||
super.onEnabled(context)
|
||||
Log.d("BitcoinPriceWidget", "Widget enabled")
|
||||
WidgetUpdateWorker.scheduleWork(context)
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
super.onReceive(context, intent)
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
scheduleWork(context)
|
||||
}
|
||||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
Log.d("BitcoinPriceWidget", "Widget disabled")
|
||||
WorkManager.getInstance(context).cancelAllWorkByTag("widget_update_work")
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package io.bluewallet.bluewallet
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.bugsnag.android.Bugsnag
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
|
@ -13,6 +12,8 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
|
|||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.soloader.SoLoader
|
||||
import com.facebook.react.modules.i18nmanager.I18nUtil
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
|
@ -43,6 +44,10 @@ class MainApplication : Application(), ReactApplication {
|
|||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load()
|
||||
}
|
||||
if (BuildConfig.USE_MOCK_SERVER) {
|
||||
startMockServer()
|
||||
}
|
||||
|
||||
val sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
|
||||
|
||||
// Retrieve the "donottrack" value. Default to "0" if not found.
|
||||
|
@ -54,4 +59,12 @@ class MainApplication : Application(), ReactApplication {
|
|||
Bugsnag.start(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startMockServer() {
|
||||
val mockWebServer = MockWebServer()
|
||||
mockWebServer.start(8080)
|
||||
MarketAPI.baseUrl = mockWebServer.url("/").toString()
|
||||
|
||||
mockWebServer.enqueue(MockResponse().setBody("{\"USD\": {\"price\": \"60000.00\"}}"))
|
||||
}
|
||||
}
|
|
@ -2,26 +2,25 @@ package io.bluewallet.bluewallet
|
|||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
|
||||
object MarketAPI {
|
||||
|
||||
private const val HARD_CODED_JSON = """
|
||||
{
|
||||
"USD": {
|
||||
"endPointKey": "USD",
|
||||
"locale": "en-US",
|
||||
"source": "Kraken",
|
||||
"symbol": "$",
|
||||
"country": "United States (US Dollar)"
|
||||
}
|
||||
}
|
||||
"""
|
||||
private const val TAG = "MarketAPI"
|
||||
private const val HARD_CODED_JSON = "{\n" +
|
||||
" \"USD\": {\n" +
|
||||
" \"endPointKey\": \"USD\",\n" +
|
||||
" \"locale\": \"en-US\",\n" +
|
||||
" \"source\": \"Kraken\",\n" +
|
||||
" \"symbol\": \"$\",\n" +
|
||||
" \"country\": \"United States (US Dollar)\"\n" +
|
||||
" }\n" +
|
||||
"}"
|
||||
|
||||
var baseUrl: String? = null
|
||||
|
||||
fun fetchPrice(context: Context, currency: String): String? {
|
||||
return try {
|
||||
|
@ -31,43 +30,50 @@ object MarketAPI {
|
|||
val endPointKey = currencyInfo.getString("endPointKey")
|
||||
|
||||
val urlString = buildURLString(source, endPointKey)
|
||||
Log.d("MarketAPI", "Fetching price from: $urlString")
|
||||
URI(urlString).toURL().run {
|
||||
(openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connect()
|
||||
}.run {
|
||||
if (responseCode != HttpURLConnection.HTTP_OK) return null
|
||||
Log.d(TAG, "Fetching price from URL: $urlString")
|
||||
|
||||
InputStreamReader(inputStream).use { reader ->
|
||||
val jsonResponse = StringBuilder()
|
||||
val buffer = CharArray(1024)
|
||||
var read: Int
|
||||
while (reader.read(buffer).also { read = it } != -1) {
|
||||
jsonResponse.append(buffer, 0, read)
|
||||
}
|
||||
parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey)
|
||||
}
|
||||
}
|
||||
val url = URL(urlString)
|
||||
val urlConnection = url.openConnection() as HttpURLConnection
|
||||
urlConnection.requestMethod = "GET"
|
||||
urlConnection.connect()
|
||||
|
||||
val responseCode = urlConnection.responseCode
|
||||
if (responseCode != 200) {
|
||||
Log.e(TAG, "Failed to fetch price. Response code: $responseCode")
|
||||
return null
|
||||
}
|
||||
|
||||
val reader = InputStreamReader(urlConnection.inputStream)
|
||||
val jsonResponse = StringBuilder()
|
||||
val buffer = CharArray(1024)
|
||||
var read: Int
|
||||
while (reader.read(buffer).also { read = it } != -1) {
|
||||
jsonResponse.append(buffer, 0, read)
|
||||
}
|
||||
|
||||
parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, "Error fetching price", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildURLString(source: String, endPointKey: String): String {
|
||||
return when (source) {
|
||||
"Yadio" -> "https://api.yadio.io/json/$endPointKey"
|
||||
"YadioConvert" -> "https://api.yadio.io/convert/1/BTC/$endPointKey"
|
||||
"Exir" -> "https://api.exir.io/v1/ticker?symbol=btc-irt"
|
||||
"wazirx" -> "https://api.wazirx.com/api/v2/tickers/btcinr"
|
||||
"Bitstamp" -> "https://www.bitstamp.net/api/v2/ticker/btc${endPointKey.toLowerCase()}"
|
||||
"Coinbase" -> "https://api.coinbase.com/v2/prices/BTC-${endPointKey.toUpperCase()}/buy"
|
||||
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.toLowerCase()}"
|
||||
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
|
||||
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.toUpperCase()}"
|
||||
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
|
||||
return if (baseUrl != null) {
|
||||
baseUrl + endPointKey
|
||||
} else {
|
||||
when (source) {
|
||||
"Yadio" -> "https://api.yadio.io/json/$endPointKey"
|
||||
"YadioConvert" -> "https://api.yadio.io/convert/1/BTC/$endPointKey"
|
||||
"Exir" -> "https://api.exir.io/v1/ticker?symbol=btc-irt"
|
||||
"wazirx" -> "https://api.wazirx.com/api/v2/tickers/btcinr"
|
||||
"Bitstamp" -> "https://www.bitstamp.net/api/v2/ticker/btc${endPointKey.lowercase()}"
|
||||
"Coinbase" -> "https://api.coinbase.com/v2/prices/BTC-${endPointKey.uppercase()}/buy"
|
||||
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
|
||||
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
|
||||
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
|
||||
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,16 +83,17 @@ object MarketAPI {
|
|||
when (source) {
|
||||
"Yadio" -> json.getJSONObject(endPointKey).getString("price")
|
||||
"YadioConvert" -> json.getString("rate")
|
||||
"CoinGecko" -> json.getJSONObject("bitcoin").getString(endPointKey.toLowerCase())
|
||||
"Exir", "Bitstamp" -> json.getString("last")
|
||||
"CoinGecko" -> json.getJSONObject("bitcoin").getString(endPointKey.lowercase())
|
||||
"Exir" -> json.getString("last")
|
||||
"Bitstamp" -> json.getString("last")
|
||||
"wazirx" -> json.getJSONObject("ticker").getString("buy")
|
||||
"Coinbase" -> json.getJSONObject("data").getString("amount")
|
||||
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.toUpperCase()}").getJSONArray("c").getString(0)
|
||||
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e(TAG, "Error parsing price", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,95 +1,72 @@
|
|||
package io.bluewallet.bluewallet
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.*
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
|
||||
class WidgetUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
val context = applicationContext
|
||||
val appWidgetManager = android.appwidget.AppWidgetManager.getInstance(context)
|
||||
val thisWidget = android.content.ComponentName(context, BitcoinPriceWidget::class.java)
|
||||
val allWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
|
||||
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
|
||||
val widgetComponent = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
|
||||
val allWidgetIds = appWidgetManager.getAppWidgetIds(widgetComponent)
|
||||
|
||||
val price = MarketAPI.fetchPrice(applicationContext, "USD")
|
||||
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
|
||||
|
||||
if (price != null) {
|
||||
val formattedPrice = NumberFormat.getCurrencyInstance(Locale.getDefault()).format(price.toDouble())
|
||||
|
||||
views.setTextViewText(R.id.price_value, formattedPrice)
|
||||
views.setViewVisibility(R.id.loading_indicator, View.GONE)
|
||||
views.setViewVisibility(R.id.price_value, View.VISIBLE)
|
||||
views.setViewVisibility(R.id.last_updated, View.VISIBLE)
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val formattedTime = java.text.DateFormat.getTimeInstance().format(currentTime)
|
||||
views.setTextViewText(R.id.last_updated, "Last Updated: $formattedTime")
|
||||
} else {
|
||||
Log.d("WidgetUpdateWorker", "Failed to fetch price")
|
||||
}
|
||||
|
||||
for (widgetId in allWidgetIds) {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_layout)
|
||||
|
||||
// Show loading indicator
|
||||
views.setViewVisibility(R.id.loading_indicator, android.view.View.VISIBLE)
|
||||
views.setViewVisibility(R.id.price_value, android.view.View.GONE)
|
||||
views.setViewVisibility(R.id.last_updated, android.view.View.GONE)
|
||||
views.setViewVisibility(R.id.last_updated_time, android.view.View.GONE)
|
||||
views.setViewVisibility(R.id.price_arrow_container, android.view.View.GONE)
|
||||
|
||||
appWidgetManager.updateAppWidget(widgetId, views)
|
||||
|
||||
val price = MarketAPI.fetchPrice(context, "USD")
|
||||
|
||||
if (price != null) {
|
||||
updateWidgetWithPrice(context, appWidgetManager, widgetId, views, price)
|
||||
} else {
|
||||
handleError(context, appWidgetManager, widgetId, views)
|
||||
}
|
||||
}
|
||||
|
||||
scheduleNextUpdate()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun updateWidgetWithPrice(context: Context, appWidgetManager: android.appwidget.AppWidgetManager, widgetId: Int, views: RemoteViews, price: String) {
|
||||
val sharedPref = context.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
|
||||
val prevPrice = sharedPref.getString("prev_price", null)
|
||||
val editor = sharedPref.edit()
|
||||
private fun scheduleNextUpdate() {
|
||||
val request = OneTimeWorkRequestBuilder<WidgetUpdateWorker>()
|
||||
.setInitialDelay(10, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
Log.d("WidgetUpdateWorker", "Fetch completed with price: $price at ${getCurrentTime()}. Previous price: $prevPrice at ${sharedPref.getString("prev_time", "N/A")}")
|
||||
WorkManager.getInstance(applicationContext).enqueueUniqueWork(
|
||||
"widget_update_work",
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
request
|
||||
)
|
||||
|
||||
val currencyFormat = NumberFormat.getCurrencyInstance(Locale.getDefault())
|
||||
views.setTextViewText(R.id.price_value, currencyFormat.format(price.toDouble()))
|
||||
Log.d("WidgetUpdateWorker", "Scheduled next update for widget")
|
||||
}
|
||||
|
||||
if (prevPrice != null) {
|
||||
val previousPrice = prevPrice.toDouble()
|
||||
val currentPrice = price.toDouble()
|
||||
|
||||
if (currentPrice > previousPrice) {
|
||||
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_up_float)
|
||||
} else if (currentPrice < previousPrice) {
|
||||
views.setImageViewResource(R.id.price_arrow, android.R.drawable.arrow_down_float)
|
||||
} else {
|
||||
views.setImageViewResource(R.id.price_arrow, 0)
|
||||
}
|
||||
|
||||
views.setTextViewText(R.id.previous_price, "from ${currencyFormat.format(previousPrice)}")
|
||||
views.setViewVisibility(R.id.price_arrow_container, android.view.View.VISIBLE)
|
||||
companion object {
|
||||
fun createWorkRequest(): OneTimeWorkRequest {
|
||||
return OneTimeWorkRequestBuilder<WidgetUpdateWorker>()
|
||||
.setInitialDelay(0, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
editor.putString("prev_price", price)
|
||||
editor.putString("prev_time", getCurrentTime())
|
||||
editor.apply()
|
||||
|
||||
views.setTextViewText(R.id.last_updated, "Last Updated")
|
||||
views.setTextViewText(R.id.last_updated_time, getCurrentTime())
|
||||
|
||||
// Hide loading indicator
|
||||
views.setViewVisibility(R.id.loading_indicator, android.view.View.GONE)
|
||||
views.setViewVisibility(R.id.price_value, android.view.View.VISIBLE)
|
||||
views.setViewVisibility(R.id.last_updated, android.view.View.VISIBLE)
|
||||
views.setViewVisibility(R.id.last_updated_time, android.view.View.VISIBLE)
|
||||
|
||||
appWidgetManager.updateAppWidget(widgetId, views)
|
||||
}
|
||||
|
||||
private fun handleError(context: Context, appWidgetManager: android.appwidget.AppWidgetManager, widgetId: Int, views: RemoteViews) {
|
||||
Log.e("WidgetUpdateWorker", "Failed to fetch Bitcoin price")
|
||||
views.setViewVisibility(R.id.loading_indicator, android.view.View.GONE)
|
||||
views.setViewVisibility(R.id.price_value, android.view.View.VISIBLE)
|
||||
appWidgetManager.updateAppWidget(widgetId, views)
|
||||
}
|
||||
|
||||
private fun getCurrentTime(): String {
|
||||
val dateFormatter = java.text.SimpleDateFormat.getTimeInstance(java.text.SimpleDateFormat.SHORT, Locale.getDefault())
|
||||
return dateFormatter.format(Date())
|
||||
fun scheduleWork(context: Context) {
|
||||
WorkManager.getInstance(context).enqueue(createWorkRequest())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:background="@drawable/widget_background"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/last_updated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Last Updated"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_alignParentEnd="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/last_updated_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="12sp"
|
||||
android:layout_below="@id/last_updated"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_alignParentEnd="true"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/price_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_below="@id/last_updated_time"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_alignParentEnd="true"/>
|
||||
android:text="@string/loading"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/text_primary"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/price_arrow_container"
|
||||
<TextView
|
||||
android:id="@+id/last_updated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:text="@string/last_updated"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:layout_below="@id/price_value"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/price_arrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/previous_price"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
</LinearLayout>
|
||||
android:layout_centerHorizontal="true"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="8dp"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/price_arrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/price_value"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/previous_price"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:layout_below="@id/price_arrow"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:visibility="gone"/>
|
||||
</RelativeLayout>
|
|
@ -1,3 +1,6 @@
|
|||
<resources>
|
||||
<string name="app_name">BlueWallet</string>
|
||||
<string name="loading">Loading...</string>
|
||||
<string name="last_updated">Last Updated</string>
|
||||
<string name="from">From</string>
|
||||
</resources>
|
||||
|
|
|
@ -169,8 +169,21 @@
|
|||
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */; };
|
||||
B4D0B2682C1DED67006B6B1B /* ReceiveMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */; };
|
||||
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
|
||||
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
|
||||
B4EFF73C2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
|
||||
B4EFF73D2C3F6C6C0095D655 /* MarketData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E82BCC371A00162242 /* MarketData.swift */; };
|
||||
B4EFF73F2C3F6C870095D655 /* PriceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73E2C3F6C870095D655 /* PriceViewTests.swift */; };
|
||||
B4EFF7402C3F6C870095D655 /* PriceViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73E2C3F6C870095D655 /* PriceViewTests.swift */; };
|
||||
B4EFF7412C3F6C960095D655 /* PriceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA4BC255872E3009312A5 /* PriceWidget.swift */; };
|
||||
B4EFF7422C3F6C990095D655 /* PriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6CA5272558EC52009312A5 /* PriceView.swift */; };
|
||||
B4EFF7432C3F6F650095D655 /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033C92BCC350A00162242 /* Currency.swift */; };
|
||||
B4EFF7442C3F6F6A0095D655 /* FiatUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2AA8072568B8F40090B089 /* FiatUnit.swift */; };
|
||||
B4EFF7452C3F6FF30095D655 /* Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4BFA254FBA0E00E9F9AA /* Placeholders.swift */; };
|
||||
B4EFF7462C3F6FF90095D655 /* WalletData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033E32BCC36FF00162242 /* WalletData.swift */; };
|
||||
B4EFF7472C3F70010095D655 /* LatestTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033DC2BCC36C300162242 /* LatestTransaction.swift */; };
|
||||
B4EFF7482C3F70090095D655 /* BitcoinUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033BE2BCC32F800162242 /* BitcoinUnit.swift */; };
|
||||
C59F90CE0D04D3E4BB39BC5D /* libPods-BlueWalletUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F02C2F7CA3591E4E0B06EBA /* libPods-BlueWalletUITests.a */; };
|
||||
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
|
||||
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -461,6 +474,8 @@
|
|||
B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
|
||||
B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveMethod.swift; sourceTree = "<group>"; };
|
||||
B4D3235A177F4580BA52F2F9 /* libRNCSlider.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCSlider.a; sourceTree = "<group>"; };
|
||||
B4EFF73A2C3F6C5E0095D655 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = "<group>"; };
|
||||
B4EFF73E2C3F6C870095D655 /* PriceViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceViewTests.swift; sourceTree = "<group>"; };
|
||||
B642AFB13483418CAB6FF25E /* libRCTQRCodeLocalImage.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTQRCodeLocalImage.a; sourceTree = "<group>"; };
|
||||
B68F8552DD4428F64B11DCFB /* Pods-BlueWallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BlueWallet.debug.xcconfig"; path = "Target Support Files/Pods-BlueWallet/Pods-BlueWallet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B9D9B3A7B2CB4255876B67AF /* libz.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||
|
@ -485,7 +500,7 @@
|
|||
files = (
|
||||
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
|
||||
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
|
||||
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
|
||||
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
|
||||
773E382FE62E836172AAB98B /* libPods-BlueWallet.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -550,6 +565,8 @@
|
|||
children = (
|
||||
00E356F01AD99517003FC87E /* Supporting Files */,
|
||||
B49038D82B8FBAD300A8164A /* BlueWalletUITest.swift */,
|
||||
B4EFF73A2C3F6C5E0095D655 /* MockData.swift */,
|
||||
B4EFF73E2C3F6C870095D655 /* PriceViewTests.swift */,
|
||||
);
|
||||
path = BlueWalletTests;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1627,10 +1644,12 @@
|
|||
B44033DB2BCC369B00162242 /* Colors.swift in Sources */,
|
||||
B40D4E632258425500428FCC /* ReceiveInterfaceController.swift in Sources */,
|
||||
B43D0378225847C500FBAA95 /* WalletGradient.swift in Sources */,
|
||||
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */,
|
||||
B44033C02BCC32F800162242 /* BitcoinUnit.swift in Sources */,
|
||||
B44033E52BCC36FF00162242 /* WalletData.swift in Sources */,
|
||||
B44033EF2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
|
||||
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */,
|
||||
B4EFF73F2C3F6C870095D655 /* PriceViewTests.swift in Sources */,
|
||||
B44033D02BCC352F00162242 /* UserDefaultsGroup.swift in Sources */,
|
||||
B44033C52BCC332400162242 /* Balance.swift in Sources */,
|
||||
6D4AF18425D215D1009DD853 /* UserDefaultsExtension.swift in Sources */,
|
||||
|
@ -1645,6 +1664,17 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B49038D92B8FBAD300A8164A /* BlueWalletUITest.swift in Sources */,
|
||||
B4EFF7472C3F70010095D655 /* LatestTransaction.swift in Sources */,
|
||||
B4EFF7422C3F6C990095D655 /* PriceView.swift in Sources */,
|
||||
B4EFF7482C3F70090095D655 /* BitcoinUnit.swift in Sources */,
|
||||
B4EFF73D2C3F6C6C0095D655 /* MarketData.swift in Sources */,
|
||||
B4EFF73C2C3F6C5E0095D655 /* MockData.swift in Sources */,
|
||||
B4EFF7432C3F6F650095D655 /* Currency.swift in Sources */,
|
||||
B4EFF7412C3F6C960095D655 /* PriceWidget.swift in Sources */,
|
||||
B4EFF7462C3F6FF90095D655 /* WalletData.swift in Sources */,
|
||||
B4EFF7402C3F6C870095D655 /* PriceViewTests.swift in Sources */,
|
||||
B4EFF7452C3F6FF30095D655 /* Placeholders.swift in Sources */,
|
||||
B4EFF7442C3F6F6A0095D655 /* FiatUnit.swift in Sources */,
|
||||
B47B21EC2B2128B8001F6690 /* BlueWalletUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
15
ios/BlueWalletTests/MockData.swift
Normal file
15
ios/BlueWalletTests/MockData.swift
Normal file
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// MockData.swift
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 7/10/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct MockData {
|
||||
static let currentMarketData = MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2023-01-01T00:00:00+00:00")
|
||||
static let previousMarketData = MarketData(nextBlock: "", sats: "", price: "$9,000", rate: 9000, dateString: "2022-12-31T00:00:00+00:00")
|
||||
static let noChangeMarketData = MarketData(nextBlock: "", sats: "", price: "$10,000", rate: 10000, dateString: "2023-01-01T00:00:00+00:00")
|
||||
}
|
75
ios/BlueWalletTests/PriceViewTests.swift
Normal file
75
ios/BlueWalletTests/PriceViewTests.swift
Normal file
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// PriceViewTests.swift
|
||||
// BlueWallet
|
||||
//
|
||||
// Created by Marcos Rodriguez on 7/10/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
@testable import BlueWallet
|
||||
|
||||
class PriceViewTests: XCTestCase {
|
||||
|
||||
func testAccessoryCircularView() {
|
||||
guard #available(iOS 16.0, *) else { return }
|
||||
let entry = PriceWidgetEntry(date: Date(), family: .accessoryCircular, currentMarketData: MockData.currentMarketData, previousMarketData: MockData.previousMarketData)
|
||||
let view = PriceView(entry: entry)
|
||||
let exp = expectation(description: "Test Circular View")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
XCTAssertNotNil(view.body)
|
||||
exp.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testAccessoryInlineView() {
|
||||
guard #available(iOS 16.0, *) else { return }
|
||||
let entry = PriceWidgetEntry(date: Date(), family: .accessoryInline, currentMarketData: MockData.currentMarketData, previousMarketData: MockData.previousMarketData)
|
||||
let view = PriceView(entry: entry)
|
||||
let exp = expectation(description: "Test Inline View")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
XCTAssertNotNil(view.body)
|
||||
exp.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testAccessoryRectangularView() {
|
||||
guard #available(iOS 16.0, *) else { return }
|
||||
let entry = PriceWidgetEntry(date: Date(), family: .accessoryRectangular, currentMarketData: MockData.currentMarketData, previousMarketData: MockData.previousMarketData)
|
||||
let view = PriceView(entry: entry)
|
||||
let exp = expectation(description: "Test Rectangular View")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
XCTAssertNotNil(view.body)
|
||||
exp.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testDefaultView() {
|
||||
let entry = PriceWidgetEntry(date: Date(), family: .systemSmall, currentMarketData: MockData.currentMarketData, previousMarketData: MockData.previousMarketData)
|
||||
let view = PriceView(entry: entry)
|
||||
let exp = expectation(description: "Test Default View")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
XCTAssertNotNil(view.body)
|
||||
exp.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
func testNoChangeCircularView() {
|
||||
guard #available(iOS 16.0, *) else { return }
|
||||
let entry = PriceWidgetEntry(date: Date(), family: .accessoryCircular, currentMarketData: MockData.noChangeMarketData, previousMarketData: MockData.noChangeMarketData)
|
||||
let view = PriceView(entry: entry)
|
||||
let exp = expectation(description: "Test No Change Circular View")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
XCTAssertNotNil(view.body)
|
||||
exp.fulfill()
|
||||
}
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
}
|
|
@ -15,9 +15,13 @@ struct PriceView: View {
|
|||
var body: some View {
|
||||
switch entry.family {
|
||||
case .accessoryInline, .accessoryCircular, .accessoryRectangular:
|
||||
wrappedView(for: getView(for: entry.family), family: entry.family)
|
||||
if #available(iOSApplicationExtension 16.0, *) {
|
||||
wrappedView(for: getView(for: entry.family), family: entry.family)
|
||||
} else {
|
||||
getView(for: entry.family)
|
||||
}
|
||||
default:
|
||||
defaultView.background(Color.widgetBackground)
|
||||
defaultView.background(Color(UIColor.systemBackground))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,7 +44,7 @@ struct PriceView: View {
|
|||
ZStack {
|
||||
if family == .accessoryRectangular {
|
||||
AccessoryWidgetBackground()
|
||||
.background(Color(.systemBackground))
|
||||
.background(Color(UIColor.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
} else {
|
||||
AccessoryWidgetBackground()
|
||||
|
@ -118,26 +122,26 @@ struct PriceView: View {
|
|||
}
|
||||
.padding(.all, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color(.systemBackground))
|
||||
.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, design: .default)).foregroundColor(.textColorLightGray)
|
||||
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(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01).transition(.opacity)
|
||||
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(.textColor).font(Font.system(size:28, weight: .bold, design: .default)).minimumScaleFactor(0.01).transition(.opacity)
|
||||
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(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01)
|
||||
Text(previousMarketDataPrice).lineLimit(1).foregroundColor(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01)
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
@ -178,7 +182,7 @@ struct PriceView_Previews: PreviewProvider {
|
|||
.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue