From e8bf5a618d6a1733449e2930c4997646e3d71e97 Mon Sep 17 00:00:00 2001 From: ILIYANGERMANOV Date: Thu, 31 Aug 2023 23:03:17 +0300 Subject: [PATCH] Bugfixes: GitHub auto-backup & other reported bugs (#2521) * Update the GitHub Backups tutorial video to the one by KLAUS * Fix an UI bug in the "GitHub Backups" card in Settings * Remove the "Request a feature" in GitHub feature * Make the Activity edge-to-edge * Fix the ItemStatisticScreen * Migrate GitHub PAT from `EncryptedSharedPrefs` to regular `DataStore` * Refactor and make the backups every 12 hours * Bump version to "4.4.1" (141) * Fix the PR template --- .github/PULL_REQUEST_TEMPLATE.md | 3 +- app/build.gradle.kts | 2 + .../ivy/wallet/android/billing/IvyBilling.kt | 19 ++- .../backup/github/GitHubAutoBackupManager.kt | 4 +- .../ivy/wallet/backup/github/GitHubBackup.kt | 4 +- .../ivy/wallet/backup/github/GitHubClient.kt | 25 ++-- .../backup/github/GitHubCredentialsManager.kt | 31 +---- .../backup/github/ui/GitHubBackupCard.kt | 9 +- .../backup/github/ui/GitHubBackupScreen.kt | 2 +- .../ivy/wallet/data/EncryptedSharedPrefs.kt | 1 + .../com/ivy/wallet/data/IvyWalletDatastore.kt | 1 + .../com/ivy/wallet/io/network/RestClient.kt | 23 ---- .../io/network/service/GithubService.kt | 29 ----- .../com/ivy/wallet/migrations/Migration.kt | 7 ++ .../wallet/migrations/MigrationsManager.kt | 54 +++++++++ .../migrations/impl/GitHubPATMigration.kt | 40 +++++++ .../java/com/ivy/wallet/ui/RootActivity.kt | 2 + .../java/com/ivy/wallet/ui/RootViewModel.kt | 25 ++-- .../ivy/wallet/ui/settings/SettingsScreen.kt | 28 +---- .../wallet/ui/settings/SettingsViewModel.kt | 32 ----- .../statistic/level2/ItemStatisticScreen.kt | 4 +- .../ui/theme/modal/RequestFeatureModal.kt | 113 ------------------ .../com/ivy/wallet/utils/ActivityResultExt.kt | 6 +- .../com/ivy/wallet/buildsrc/dependencies.kt | 4 +- gradle/libs.versions.toml | 14 ++- .../java/com/ivy/design/l0_system/IvyTheme.kt | 3 +- 26 files changed, 193 insertions(+), 292 deletions(-) delete mode 100644 app/src/main/java/com/ivy/wallet/io/network/service/GithubService.kt create mode 100644 app/src/main/java/com/ivy/wallet/migrations/Migration.kt create mode 100644 app/src/main/java/com/ivy/wallet/migrations/MigrationsManager.kt create mode 100644 app/src/main/java/com/ivy/wallet/migrations/impl/GitHubPATMigration.kt delete mode 100644 app/src/main/java/com/ivy/wallet/ui/theme/modal/RequestFeatureModal.kt diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9bd4479963..35aac67364 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,7 @@ ## Pull Request (PR) Checklist Please check if your pull request fulfills the following requirements: - [ ] The PR is submitted to the `main` branch. -- [ ] I've read the - **[Contribution Guidelines](https://github.com/Ivy-Apps/ivy-wallet/blob/main/CONTRIBUTING.md)**. +- [ ] I've read the **[Contribution Guidelines](https://github.com/Ivy-Apps/ivy-wallet/blob/main/CONTRIBUTING.md)**. - [ ] The code builds and is tested on an actual Android device. - [ ] I confirm that I've run the code locally and everything works as expected. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01aaa31c44..2e46e81002 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -149,8 +149,10 @@ dependencies { implementation(libs.bundles.arrow) implementation(libs.bundles.compose) implementation(libs.bundles.glance) + implementation(libs.bundles.activity) implementation(libs.datastore) implementation(libs.androidx.security) + implementation(libs.androidx.biometrics) testImplementation(libs.bundles.kotest) testImplementation(libs.bundles.kotlin.test) diff --git a/app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt b/app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt index 7d69127f8a..9a9e0e96d1 100644 --- a/app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt +++ b/app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt @@ -1,7 +1,18 @@ package com.ivy.wallet.android.billing -import androidx.appcompat.app.AppCompatActivity -import com.android.billingclient.api.* +import android.app.Activity +import com.android.billingclient.api.AcknowledgePurchaseParams +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.SkuDetails +import com.android.billingclient.api.SkuDetailsParams +import com.android.billingclient.api.acknowledgePurchase +import com.android.billingclient.api.queryPurchasesAsync +import com.android.billingclient.api.querySkuDetails import com.ivy.wallet.utils.ioThread import com.ivy.wallet.utils.sendToCrashlytics import timber.log.Timber @@ -44,7 +55,7 @@ class IvyBilling( private lateinit var billingClient: BillingClient fun init( - activity: AppCompatActivity, + activity: Activity, onReady: () -> Unit, onPurchases: (List) -> Unit, onError: (code: Int, msg: String) -> Unit, @@ -166,7 +177,7 @@ class IvyBilling( } fun buy( - activity: AppCompatActivity, + activity: Activity, skuToBuy: SkuDetails, oldSubscriptionPurchaseToken: String? ) { diff --git a/app/src/main/java/com/ivy/wallet/backup/github/GitHubAutoBackupManager.kt b/app/src/main/java/com/ivy/wallet/backup/github/GitHubAutoBackupManager.kt index 2ad26cb966..0db87090cb 100644 --- a/app/src/main/java/com/ivy/wallet/backup/github/GitHubAutoBackupManager.kt +++ b/app/src/main/java/com/ivy/wallet/backup/github/GitHubAutoBackupManager.kt @@ -28,7 +28,7 @@ class GitHubAutoBackupManager @Inject constructor( val initialDelay = calculateInitialDelayMillis() val dailyWorkRequest = PeriodicWorkRequestBuilder( - 24, TimeUnit.HOURS + 12, TimeUnit.HOURS ).setInitialDelay(initialDelay, TimeUnit.MILLISECONDS) .setConstraints( Constraints.Builder() @@ -86,7 +86,7 @@ class GitHubBackupWorker @AssistedInject constructor( override suspend fun doWork(): Result { return gitHubBackup.backupData( - isAutomatic = true + commitMsg = "Automatic Ivy Wallet data backup" ).fold( ifLeft = { if (runAttemptCount <= MAX_RETRIES) { diff --git a/app/src/main/java/com/ivy/wallet/backup/github/GitHubBackup.kt b/app/src/main/java/com/ivy/wallet/backup/github/GitHubBackup.kt index 49bd83e8fa..9cbe42d9c9 100644 --- a/app/src/main/java/com/ivy/wallet/backup/github/GitHubBackup.kt +++ b/app/src/main/java/com/ivy/wallet/backup/github/GitHubBackup.kt @@ -76,7 +76,7 @@ class GitHubBackup @Inject constructor( } suspend fun backupData( - isAutomatic: Boolean = false, + commitMsg: String = "Manual Ivy Wallet data backup", ): Either = withContext(Dispatchers.IO) { either { val credentials = credentialsManager.getCredentials() @@ -87,7 +87,7 @@ class GitHubBackup @Inject constructor( credentials = credentials, path = BACKUP_FILE_PATH, content = json, - isAutomatic = isAutomatic, + commitMsg = commitMsg, ).mapLeft(Error::Commit).bind() appContext.dataStore.edit { diff --git a/app/src/main/java/com/ivy/wallet/backup/github/GitHubClient.kt b/app/src/main/java/com/ivy/wallet/backup/github/GitHubClient.kt index 8def71700a..7ade0f67ca 100644 --- a/app/src/main/java/com/ivy/wallet/backup/github/GitHubClient.kt +++ b/app/src/main/java/com/ivy/wallet/backup/github/GitHubClient.kt @@ -52,7 +52,7 @@ class GitHubClient @Inject constructor( credentials: GitHubCredentials, path: String, content: String, - isAutomatic: Boolean = false, + commitMsg: String, ): Either = either { val repoUrl = repoUrl(credentials, path) val sha = getExistingFile(credentials, repoUrl)?.sha @@ -61,11 +61,7 @@ class GitHubClient @Inject constructor( val requestBody = GitHubFileContent( content = encodedContent, - message = if (isAutomatic) { - "Automatic Ivy Wallet data backup" - } else { - "Manual Ivy Wallet data backup" - }, + message = commitMsg, committer = Committer( name = "Ivy Wallet", email = "automation@ivywallet.com" @@ -73,13 +69,17 @@ class GitHubClient @Inject constructor( sha = sha, ) - val response = httpClient.get().put(repoUrl) { - headers { - githubToken(credentials) - contentType(ContentType.Application.Json) - acceptsUtf16() + val response = try { + httpClient.get().put(repoUrl) { + headers { + githubToken(credentials) + contentType(ContentType.Application.Json) + acceptsUtf16() + } + setBody(requestBody) } - setBody(requestBody) + } catch (e: Exception) { + return Either.Left("HttpException: ${e.message}") } ensure(response.status.isSuccess()) { when (response.status.value) { @@ -88,7 +88,6 @@ class GitHubClient @Inject constructor( else -> "Unsuccessful response: '${response.status}' $response." } } - return Either.Right(Unit) } suspend fun readFileContent( diff --git a/app/src/main/java/com/ivy/wallet/backup/github/GitHubCredentialsManager.kt b/app/src/main/java/com/ivy/wallet/backup/github/GitHubCredentialsManager.kt index 46fed52c7a..92209fde81 100644 --- a/app/src/main/java/com/ivy/wallet/backup/github/GitHubCredentialsManager.kt +++ b/app/src/main/java/com/ivy/wallet/backup/github/GitHubCredentialsManager.kt @@ -1,16 +1,12 @@ package com.ivy.wallet.backup.github import android.content.Context -import android.content.SharedPreferences import androidx.datastore.preferences.core.edit import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensureNotNull import com.ivy.wallet.data.DatastoreKeys -import com.ivy.wallet.data.EncryptedPrefsKeys -import com.ivy.wallet.data.EncryptedSharedPrefs import com.ivy.wallet.data.dataStore -import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull @@ -18,22 +14,20 @@ import kotlinx.coroutines.withContext import javax.inject.Inject class GitHubCredentialsManager @Inject constructor( - @EncryptedSharedPrefs - private val encryptedSharedPrefs: Lazy, @ApplicationContext private val appContext: Context, ) { suspend fun getCredentials(): Either = withContext(Dispatchers.IO) { either { - val token = getGitHubPAT() - ensureNotNull(token) { - "GitHub PAT (Personal Access Token) isn't configured." - } val data = appContext.dataStore.data.firstOrNull() ensureNotNull(data) { "Error: Datastore data is null!" } + val token = data[DatastoreKeys.GITHUB_PAT] + ensureNotNull(token) { + "GitHub PAT (Personal Access Token) isn't configured." + } val owner = data[DatastoreKeys.GITHUB_OWNER] ensureNotNull(owner) { "GitHub owner isn't configured." @@ -60,30 +54,17 @@ class GitHubCredentialsManager @Inject constructor( appContext.dataStore.edit { it[DatastoreKeys.GITHUB_OWNER] = owner it[DatastoreKeys.GITHUB_REPO] = repo + it[DatastoreKeys.GITHUB_PAT] = gitHubPAT } - encryptedSharedPrefs.get().edit() - .putString(EncryptedPrefsKeys.BACKUP_GITHUB_PAT, gitHubPAT) - .apply() } } suspend fun removeSaved(): Unit = withContext(Dispatchers.IO) { - encryptedSharedPrefs.get().edit().remove(EncryptedPrefsKeys.BACKUP_GITHUB_PAT).apply() appContext.dataStore.edit { it.remove(DatastoreKeys.GITHUB_OWNER) it.remove(DatastoreKeys.GITHUB_REPO) + it.remove(DatastoreKeys.GITHUB_PAT) } } - - - private fun getGitHubPAT(): String? { - return encryptedSharedPrefs.get() - .getString(EncryptedPrefsKeys.BACKUP_GITHUB_PAT, null) - } - - private data class ParsedUrl( - val owner: String, - val repo: String, - ) } diff --git a/app/src/main/java/com/ivy/wallet/backup/github/ui/GitHubBackupCard.kt b/app/src/main/java/com/ivy/wallet/backup/github/ui/GitHubBackupCard.kt index 353b3b3231..224c7f0298 100644 --- a/app/src/main/java/com/ivy/wallet/backup/github/ui/GitHubBackupCard.kt +++ b/app/src/main/java/com/ivy/wallet/backup/github/ui/GitHubBackupCard.kt @@ -152,7 +152,12 @@ private fun LastBackup( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = if (backup.indicateDanger) "⚠\uFE0F " else "" + "Last Backup: ${backup.time}", + text = buildString { + if (backup.indicateDanger) { + append("⚠\uFE0F ") + } + append("Last Backup: ${backup.time}") + }, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Start, @@ -171,7 +176,7 @@ private fun LastBackup( } } else { Text( - text = "⚠\uFE0F No backup detected!", + text = "⚠\uFE0F No backup detected!", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Start, diff --git a/app/src/main/java/com/ivy/wallet/backup/github/ui/GitHubBackupScreen.kt b/app/src/main/java/com/ivy/wallet/backup/github/ui/GitHubBackupScreen.kt index c38c1127d6..ab36abb404 100644 --- a/app/src/main/java/com/ivy/wallet/backup/github/ui/GitHubBackupScreen.kt +++ b/app/src/main/java/com/ivy/wallet/backup/github/ui/GitHubBackupScreen.kt @@ -49,7 +49,7 @@ private const val GITHUB_PAT_INFO_URL = "https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token" private const val VIDEO_TUTORIAL_URL = - "https://www.youtube.com/watch?v=wcgORjVFy4I" + "https://www.youtube.com/watch?v=sDmZxXlXsCM" object GitHubBackupScreen : Screen diff --git a/app/src/main/java/com/ivy/wallet/data/EncryptedSharedPrefs.kt b/app/src/main/java/com/ivy/wallet/data/EncryptedSharedPrefs.kt index e32d968b1e..9d102ea7e3 100644 --- a/app/src/main/java/com/ivy/wallet/data/EncryptedSharedPrefs.kt +++ b/app/src/main/java/com/ivy/wallet/data/EncryptedSharedPrefs.kt @@ -34,5 +34,6 @@ object EncryptedSharedPrefsModule { } object EncryptedPrefsKeys { + @Deprecated("Use DataStoreKeys.GITHUB_PAT instead") const val BACKUP_GITHUB_PAT = "github_backup_pat" } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/data/IvyWalletDatastore.kt b/app/src/main/java/com/ivy/wallet/data/IvyWalletDatastore.kt index 080f46927e..923eaec36e 100644 --- a/app/src/main/java/com/ivy/wallet/data/IvyWalletDatastore.kt +++ b/app/src/main/java/com/ivy/wallet/data/IvyWalletDatastore.kt @@ -14,6 +14,7 @@ val Context.dataStore: DataStore by preferencesDataStore( object DatastoreKeys { val GITHUB_OWNER = stringPreferencesKey("github_backup_owner") val GITHUB_REPO = stringPreferencesKey("github_backup_repo") + val GITHUB_PAT = stringPreferencesKey("github_backup_pat") val GITHUB_LAST_BACKUP_EPOCH_SEC = longPreferencesKey("github_backup_last_backup_time_epoch_sec") } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/io/network/RestClient.kt b/app/src/main/java/com/ivy/wallet/io/network/RestClient.kt index bc3e7a13b7..79039640c3 100644 --- a/app/src/main/java/com/ivy/wallet/io/network/RestClient.kt +++ b/app/src/main/java/com/ivy/wallet/io/network/RestClient.kt @@ -11,7 +11,6 @@ import com.ivy.wallet.io.network.error.ErrorCode import com.ivy.wallet.io.network.error.NetworkError import com.ivy.wallet.io.network.error.RestError import com.ivy.wallet.io.network.service.* -import okhttp3.Credentials import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor @@ -102,27 +101,6 @@ class RestClient private constructor( response }) - //Github Rest API interceptor (not the best solution) - httpClientBuilder.addInterceptor(Interceptor { chain -> - val request = chain.request() - val finalRequest = - if (request.url.toUrl().toString().startsWith(GithubService.BASE_URL)) { - val credentials = Credentials.basic( - GithubService.GITHUB_SERVICE_ACC_USERNAME, - GithubService.GITHUB_SERVICE_ACC_ACCESS_TOKEN_PART_1 + - GithubService.GITHUB_SERVICE_ACC_ACCESS_TOKEN_PART_2 - ) - - request.newBuilder() - .header("Authorization", credentials) - .build() - } else { - request - } - - chain.proceed(request = finalRequest) - }) - trustAllSSLCertificates(httpClientBuilder) return Retrofit.Builder() @@ -207,6 +185,5 @@ class RestClient private constructor( } val analyticsService: AnalyticsService by lazy { retrofit.create(AnalyticsService::class.java) } val exchangeRatesService: ExchangeRatesService by lazy { retrofit.create(ExchangeRatesService::class.java) } - val githubService: GithubService by lazy { retrofit.create(GithubService::class.java) } val nukeService: NukeService by lazy { retrofit.create(NukeService::class.java) } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/io/network/service/GithubService.kt b/app/src/main/java/com/ivy/wallet/io/network/service/GithubService.kt deleted file mode 100644 index e72e07bbe8..0000000000 --- a/app/src/main/java/com/ivy/wallet/io/network/service/GithubService.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.ivy.wallet.io.network.service - -import androidx.annotation.Keep -import com.ivy.wallet.io.network.request.github.OpenIssueRequest -import com.ivy.wallet.io.network.request.github.OpenIssueResponse -import retrofit2.http.Body -import retrofit2.http.Header -import retrofit2.http.POST - -@Keep -interface GithubService { - companion object { - const val BASE_URL = "https://api.github.com" - const val OPEN_ISSUE_URL = "$BASE_URL/repos/ILIYANGERMANOV/ivy-wallet/issues" - - const val GITHUB_SERVICE_ACC_USERNAME = "ivywallet" - - //Split Github Access token in two parts so Github doesn't delete it - //because "Personal access token was found in commit." - const val GITHUB_SERVICE_ACC_ACCESS_TOKEN_PART_1 = "ghp_MuvrbtIH897" - const val GITHUB_SERVICE_ACC_ACCESS_TOKEN_PART_2 = "JASL6i8mBvXJ3aM7DLk4U9Gwq" - } - - @POST(OPEN_ISSUE_URL) - suspend fun openIssue( - @Header("Accept") accept: String = "application/vnd.github.v3+json", - @Body request: OpenIssueRequest - ): OpenIssueResponse -} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/migrations/Migration.kt b/app/src/main/java/com/ivy/wallet/migrations/Migration.kt new file mode 100644 index 0000000000..a3f66db5a1 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/migrations/Migration.kt @@ -0,0 +1,7 @@ +package com.ivy.wallet.migrations + +interface Migration { + val key: String + + suspend fun migrate() +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/migrations/MigrationsManager.kt b/app/src/main/java/com/ivy/wallet/migrations/MigrationsManager.kt new file mode 100644 index 0000000000..0466c0942d --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/migrations/MigrationsManager.kt @@ -0,0 +1,54 @@ +package com.ivy.wallet.migrations + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import com.ivy.wallet.data.dataStore +import com.ivy.wallet.migrations.impl.GitHubPATMigration +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +class MigrationsManager @Inject constructor( + @ApplicationContext + private val context: Context, + private val gitHubPATMigration: Lazy +) { + private val migrations by lazy { + listOf( + gitHubPATMigration.get() + ) + } + + suspend fun executeMigrations() { + delay(2_000L) // to not the make app slower + + val data = context.dataStore.data.firstOrNull() + + for (migration in migrations) { + val key = booleanPreferencesKey("migration_${migration.key}") + val isExecuted = data?.get(key) ?: false + if (!isExecuted) { + Timber.i("[MIGRATION] Executing '${migration.key}' migration...") + try { + withContext(Dispatchers.IO) { + migration.migrate() + } + } catch (e: Exception) { + e.printStackTrace() + } + + // Mark the migration as executed + context.dataStore.edit { + it[key] = true + } + Timber.i("[MIGRATION] Finished '${migration.key}' migration.") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/migrations/impl/GitHubPATMigration.kt b/app/src/main/java/com/ivy/wallet/migrations/impl/GitHubPATMigration.kt new file mode 100644 index 0000000000..52ca5893d9 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/migrations/impl/GitHubPATMigration.kt @@ -0,0 +1,40 @@ +package com.ivy.wallet.migrations.impl + +import android.content.Context +import android.content.SharedPreferences +import androidx.datastore.preferences.core.edit +import com.ivy.wallet.backup.github.GitHubBackup +import com.ivy.wallet.data.DatastoreKeys +import com.ivy.wallet.data.EncryptedPrefsKeys +import com.ivy.wallet.data.EncryptedSharedPrefs +import com.ivy.wallet.data.dataStore +import com.ivy.wallet.migrations.Migration +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class GitHubPATMigration @Inject constructor( + @EncryptedSharedPrefs + private val encryptedSharedPreferences: SharedPreferences, + @ApplicationContext + private val context: Context, + private val gitHubBackup: GitHubBackup +) : Migration { + override val key: String + get() = "github_pat_v1" + + override suspend fun migrate() { + val oldGitHubPAT = encryptedSharedPreferences.getString( + EncryptedPrefsKeys.BACKUP_GITHUB_PAT, + null + ) + + if (oldGitHubPAT != null) { + context.dataStore.edit { + it[DatastoreKeys.GITHUB_PAT] = oldGitHubPAT + } + gitHubBackup.backupData( + commitMsg = "[MIGRATION] Ivy Wallet data backup" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt index acc3590740..e7462b121c 100644 --- a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt @@ -15,6 +15,7 @@ import android.text.format.DateFormat import android.view.WindowManager import android.widget.Toast import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResultLauncher import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -132,6 +133,7 @@ class RootActivity : AppCompatActivity() { ExperimentalFoundationApi::class ) override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) setupActivityForResultLaunchers() diff --git a/app/src/main/java/com/ivy/wallet/ui/RootViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/RootViewModel.kt index d48e634883..e13f40e939 100644 --- a/app/src/main/java/com/ivy/wallet/ui/RootViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/RootViewModel.kt @@ -1,7 +1,7 @@ package com.ivy.wallet.ui +import android.app.Activity import android.content.Intent -import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricPrompt import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -18,15 +18,19 @@ import com.ivy.wallet.io.network.IvyAnalytics import com.ivy.wallet.io.network.IvySession import com.ivy.wallet.io.persistence.SharedPrefs import com.ivy.wallet.io.persistence.dao.SettingsDao +import com.ivy.wallet.migrations.MigrationsManager import com.ivy.wallet.stringRes import com.ivy.wallet.utils.ioThread import com.ivy.wallet.utils.readOnly import com.ivy.wallet.utils.sendToCrashlytics import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch import timber.log.Timber -import java.lang.IllegalArgumentException import java.util.concurrent.atomic.AtomicLong import javax.inject.Inject @@ -40,7 +44,8 @@ class RootViewModel @Inject constructor( private val ivySession: IvySession, private val ivyBilling: IvyBilling, private val paywallLogic: PaywallLogic, - private val transactionReminderLogic: TransactionReminderLogic + private val transactionReminderLogic: TransactionReminderLogic, + private val migrationsManager: MigrationsManager, ) : ViewModel() { companion object { @@ -89,6 +94,10 @@ class RootViewModel @Inject constructor( TestIdlingResource.decrement() } + + viewModelScope.launch { + migrationsManager.executeMigrations() + } } private fun navigateOnboardedUser(intent: Intent) { @@ -100,9 +109,9 @@ class RootViewModel @Inject constructor( private fun handleSpecialStart(intent: Intent): Boolean { val addTrnType: TransactionType? = try { - intent.getSerializableExtra(EXTRA_ADD_TRANSACTION_TYPE) as? TransactionType ?: - TransactionType.valueOf(intent.getStringExtra(EXTRA_ADD_TRANSACTION_TYPE) ?: "") - } catch (e: IllegalArgumentException){ + intent.getSerializableExtra(EXTRA_ADD_TRANSACTION_TYPE) as? TransactionType + ?: TransactionType.valueOf(intent.getStringExtra(EXTRA_ADD_TRANSACTION_TYPE) ?: "") + } catch (e: IllegalArgumentException) { null } @@ -141,7 +150,7 @@ class RootViewModel @Inject constructor( } } - fun initBilling(activity: AppCompatActivity) { + fun initBilling(activity: Activity) { ivyBilling.init( activity = activity, onReady = { diff --git a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt index 7b309d9234..2a6b54317b 100644 --- a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt @@ -90,7 +90,6 @@ import com.ivy.wallet.ui.theme.modal.CurrencyModal import com.ivy.wallet.ui.theme.modal.DeleteModal import com.ivy.wallet.ui.theme.modal.NameModal import com.ivy.wallet.ui.theme.modal.ProgressModal -import com.ivy.wallet.ui.theme.modal.RequestFeatureModal import com.ivy.wallet.utils.OpResult import com.ivy.wallet.utils.clickableNoIndication import com.ivy.wallet.utils.drawColoredShadow @@ -120,7 +119,6 @@ fun BoxWithConstraintsScope.SettingsScreen(screen: Settings) { viewModel.start() } - val ivyActivity = LocalContext.current as RootActivity val context = LocalContext.current UI( user = user, @@ -134,8 +132,6 @@ fun BoxWithConstraintsScope.SettingsScreen(screen: Settings) { nameLocalAccount = nameLocalAccount, startDateOfMonth = startDateOfMonth, - opFetchTrns = opFetchTrns, - onSetCurrency = viewModel::setCurrency, onSetName = viewModel::setName, @@ -154,16 +150,8 @@ fun BoxWithConstraintsScope.SettingsScreen(screen: Settings) { onSetHideCurrentBalance = viewModel::setHideCurrentBalance, onSetStartDateOfMonth = viewModel::setStartDateOfMonth, onSetTreatTransfersAsIncExp = viewModel::setTransfersAsIncomeExpense, - onRequestFeature = { title, body -> - viewModel.requestFeature( - rootActivity = ivyActivity, - title = title, - body = body - ) - }, onDeleteAllUserData = viewModel::deleteAllUserData, onDeleteCloudUserData = viewModel::deleteCloudUserData, - onFetchMissingTransactions = viewModel::fetchMissingTransactions ) } @@ -183,8 +171,6 @@ private fun BoxWithConstraintsScope.UI( nameLocalAccount: String?, startDateOfMonth: Int = 1, - opFetchTrns: OpResult? = null, - onSetCurrency: (String) -> Unit, onSetName: (String) -> Unit = {}, @@ -199,16 +185,13 @@ private fun BoxWithConstraintsScope.UI( onSetTreatTransfersAsIncExp: (Boolean) -> Unit = {}, onSetHideCurrentBalance: (Boolean) -> Unit = {}, onSetStartDateOfMonth: (Int) -> Unit = {}, - onRequestFeature: (String, String) -> Unit = { _, _ -> }, onDeleteAllUserData: () -> Unit = {}, onDeleteCloudUserData: () -> Unit = {}, - onFetchMissingTransactions: () -> Unit = {}, ) { var currencyModalVisible by remember { mutableStateOf(false) } var nameModalVisible by remember { mutableStateOf(false) } var chooseStartDateOfMonthVisible by remember { mutableStateOf(false) } - var requestFeatureModalVisible by remember { mutableStateOf(false) } var deleteCloudDataModalVisible by remember { mutableStateOf(false) } var deleteAllDataModalVisible by remember { mutableStateOf(false) } var deleteAllDataModalFinalVisible by remember { mutableStateOf(false) } @@ -449,8 +432,9 @@ private fun BoxWithConstraintsScope.UI( Spacer(Modifier.height(12.dp)) + val rootActivity = rootActivity() RequestFeature { - requestFeatureModalVisible = true + rootActivity.openUrlInBrowser(Constants.URL_IVY_TELEGRAM_INVITE) } Spacer(Modifier.height(12.dp)) @@ -525,14 +509,6 @@ private fun BoxWithConstraintsScope.UI( onSetStartDateOfMonth(it) } - RequestFeatureModal( - visible = requestFeatureModalVisible, - dismiss = { - requestFeatureModalVisible = false - }, - onSubmit = onRequestFeature - ) - DeleteModal( title = stringResource(R.string.delete_all_user_data_question), description = stringResource( diff --git a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt index 670b6522ca..3340b23a1a 100644 --- a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt @@ -23,7 +23,6 @@ import com.ivy.wallet.io.network.IvyAnalytics import com.ivy.wallet.io.network.IvySession import com.ivy.wallet.io.network.RestClient import com.ivy.wallet.io.network.request.auth.GoogleSignInRequest -import com.ivy.wallet.io.network.request.github.OpenIssueRequest import com.ivy.wallet.io.persistence.SharedPrefs import com.ivy.wallet.io.persistence.dao.SettingsDao import com.ivy.wallet.io.persistence.dao.UserDao @@ -370,37 +369,6 @@ class SettingsViewModel @Inject constructor( } } - fun requestFeature( - rootActivity: RootActivity, - title: String, - body: String - ) { - viewModelScope.launch { - TestIdlingResource.increment() - - try { - val response = restClient.githubService.openIssue( - request = OpenIssueRequest( - title = title, - body = body, - ) - ) - - //Returned: https://api.github.com/repos/octocat/Hello-World/issues/1347 - //Should open: https://github.com/octocat/Hello-World/issues/1347 - val issueUrl = response.url - .replace("api.github.com", "github.com") - .replace("/repos", "") - - rootActivity.openUrlInBrowser(issueUrl) - } catch (e: Exception) { - e.printStackTrace() - } - - TestIdlingResource.decrement() - } - } - fun deleteAllUserData() { viewModelScope.launch { try { diff --git a/app/src/main/java/com/ivy/wallet/ui/statistic/level2/ItemStatisticScreen.kt b/app/src/main/java/com/ivy/wallet/ui/statistic/level2/ItemStatisticScreen.kt index 4dce37ae79..1728244060 100644 --- a/app/src/main/java/com/ivy/wallet/ui/statistic/level2/ItemStatisticScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/statistic/level2/ItemStatisticScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState @@ -292,7 +292,7 @@ private fun BoxWithConstraintsScope.UI( LazyColumn( modifier = Modifier .fillMaxSize() - .systemBarsPadding() + .statusBarsPadding() .padding(top = 16.dp) .clip(UI.shapes.r1Top) .background(UI.colors.pure) diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/RequestFeatureModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/RequestFeatureModal.kt deleted file mode 100644 index 89dbb3e125..0000000000 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/RequestFeatureModal.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.ivy.wallet.ui.theme.modal - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.ivy.wallet.R -import com.ivy.wallet.ui.IvyWalletPreview -import com.ivy.wallet.ui.theme.Gray -import com.ivy.wallet.ui.theme.components.IvyDescriptionTextField -import com.ivy.wallet.utils.selectEndTextFieldValue -import java.util.* - -@Composable -fun BoxWithConstraintsScope.RequestFeatureModal( - id: UUID = UUID.randomUUID(), - visible: Boolean, - - dismiss: () -> Unit, - onSubmit: (title: String, body: String) -> Unit -) { - var title by remember(id) { - mutableStateOf(selectEndTextFieldValue("")) - } - var body by remember(id) { - mutableStateOf(selectEndTextFieldValue("")) - } - - - IvyModal( - id = id, - visible = visible, - dismiss = dismiss, - PrimaryAction = { - ModalSet( - label = stringResource(R.string.submit), - enabled = title.text.isNotBlank() - ) { - onSubmit( - title.text, - body.text - ) - dismiss() - } - } - ) { - Spacer(Modifier.height(32.dp)) - - ModalTitle(text = stringResource(R.string.request_a_feature)) - - Spacer(Modifier.height(24.dp)) - - ModalNameInput( - hint = stringResource(R.string.what_do_you_need), - autoFocusKeyboard = true, - textFieldValue = title, - setTextFieldValue = { - title = it - } - ) - - Spacer(Modifier.height(16.dp)) - - IvyDescriptionTextField( - modifier = Modifier - .padding(horizontal = 32.dp) - .fillMaxWidth(), - keyboardOptions = KeyboardOptions( - autoCorrect = true, - capitalization = KeyboardCapitalization.Sentences, - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default - ), - keyboardActions = KeyboardActions( - onAny = { - body = body.copy( - text = StringBuilder(body.text) - .insert(body.selection.end, "\n") - .toString(), - selection = TextRange(body.selection.end + 1) - ) - } - ), - hint = stringResource(R.string.explain_it_in_one_sentence), - hintColor = Gray, - value = body, - ) { - body = it - } - - Spacer(Modifier.height(24.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - IvyWalletPreview { - RequestFeatureModal( - visible = true, - dismiss = {}, - onSubmit = { _, _ -> } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/utils/ActivityResultExt.kt b/app/src/main/java/com/ivy/wallet/utils/ActivityResultExt.kt index a16310cb6b..3395709bd2 100644 --- a/app/src/main/java/com/ivy/wallet/utils/ActivityResultExt.kt +++ b/app/src/main/java/com/ivy/wallet/utils/ActivityResultExt.kt @@ -2,12 +2,12 @@ package com.ivy.wallet.utils import android.content.Context import android.content.Intent +import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContract -import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -fun AppCompatActivity.simpleActivityForResultLauncher( +fun ComponentActivity.simpleActivityForResultLauncher( intent: Intent, onActivityResult: (resultCode: Int, data: Intent?) -> Unit ): ActivityResultLauncher { @@ -27,7 +27,7 @@ fun Fragment.simpleActivityForResultLauncher( ) } -fun AppCompatActivity.activityForResultLauncher( +fun ComponentActivity.activityForResultLauncher( createIntent: (context: Context, input: I) -> Intent, onActivityResult: (resultCode: Int, data: Intent?) -> Unit ): ActivityResultLauncher { diff --git a/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt b/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt index c5e8ca56e9..6414b6ec74 100644 --- a/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt +++ b/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt @@ -21,8 +21,8 @@ import org.gradle.api.artifacts.dsl.DependencyHandler object Project { //Version - const val versionName = "4.4.0" - const val versionCode = 140 + const val versionName = "4.4.1" + const val versionCode = 141 //Compile SDK & Build Tools const val compileSdkVersion = 34 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79536562df..12bd83f5cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ kotlinx-serialization = "1.6.0" arrow = "1.2.0" kotest = "5.6.2" compose = "1.5.0" -compose-compiler = "1.5.2" +compose-compiler = "1.5.2" # It's used - don't delete it! compose-material3 = "1.1.1" compose-activity = "1.7.2" compose-viewmodel = "2.6.1" @@ -14,6 +14,9 @@ coil = "2.4.0" glance = "1.0.0-rc01" datastore = "1.0.0" androidx-security = "1.0.0" +androidx-activity = "1.8.0-alpha07" +appcompat-activity = "1.7.0-alpha03" +androidx-biometrics = "1.2.0-alpha05" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } @@ -52,6 +55,9 @@ glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" } datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx-security = { module = "androidx.security:security-crypto", version.ref = "androidx-security" } +androidx-biometrics = { module = "androidx.biometric:biometric", version.ref = "androidx-biometrics" } +androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } +appcompat-activity = { module = "androidx.appcompat:appcompat", version.ref = "appcompat-activity" } [bundles] kotlin = [ @@ -93,7 +99,11 @@ compose = [ "compose-viewmodel", "compose-tooling", "compose-runtime-livedate-temp", - "compose-coil" + "compose-coil", +] +activity = [ + "androidx-activity", + "appcompat-activity" ] glance = [ "glance", diff --git a/ivy-design/src/main/java/com/ivy/design/l0_system/IvyTheme.kt b/ivy-design/src/main/java/com/ivy/design/l0_system/IvyTheme.kt index 88bdc414fc..3c2cb052e2 100644 --- a/ivy-design/src/main/java/com/ivy/design/l0_system/IvyTheme.kt +++ b/ivy-design/src/main/java/com/ivy/design/l0_system/IvyTheme.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat @@ -61,7 +62,7 @@ fun IvyTheme( if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - window.statusBarColor = colors.pure.toArgb() + window.statusBarColor = Color.Transparent.toArgb() WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = colors.isLight }