Skip to content
This repository has been archived by the owner on Nov 5, 2024. It is now read-only.

Bugfixes: GitHub auto-backup & other reported bugs #2521

Merged
merged 10 commits into from
Aug 31, 2023
3 changes: 1 addition & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 15 additions & 4 deletions app/src/main/java/com/ivy/wallet/android/billing/IvyBilling.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -44,7 +55,7 @@ class IvyBilling(
private lateinit var billingClient: BillingClient

fun init(
activity: AppCompatActivity,
activity: Activity,
onReady: () -> Unit,
onPurchases: (List<Purchase>) -> Unit,
onError: (code: Int, msg: String) -> Unit,
Expand Down Expand Up @@ -166,7 +177,7 @@ class IvyBilling(
}

fun buy(
activity: AppCompatActivity,
activity: Activity,
skuToBuy: SkuDetails,
oldSubscriptionPurchaseToken: String?
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class GitHubAutoBackupManager @Inject constructor(
val initialDelay = calculateInitialDelayMillis()

val dailyWorkRequest = PeriodicWorkRequestBuilder<GitHubBackupWorker>(
24, TimeUnit.HOURS
12, TimeUnit.HOURS
).setInitialDelay(initialDelay, TimeUnit.MILLISECONDS)
.setConstraints(
Constraints.Builder()
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class GitHubBackup @Inject constructor(
}

suspend fun backupData(
isAutomatic: Boolean = false,
commitMsg: String = "Manual Ivy Wallet data backup",
): Either<Error, Unit> = withContext(Dispatchers.IO) {
either {
val credentials = credentialsManager.getCredentials()
Expand All @@ -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 {
Expand Down
25 changes: 12 additions & 13 deletions app/src/main/java/com/ivy/wallet/backup/github/GitHubClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class GitHubClient @Inject constructor(
credentials: GitHubCredentials,
path: String,
content: String,
isAutomatic: Boolean = false,
commitMsg: String,
): Either<String, Unit> = either {
val repoUrl = repoUrl(credentials, path)
val sha = getExistingFile(credentials, repoUrl)?.sha
Expand All @@ -61,25 +61,25 @@ 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 = "[email protected]"
),
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) {
Expand All @@ -88,7 +88,6 @@ class GitHubClient @Inject constructor(
else -> "Unsuccessful response: '${response.status}' $response."
}
}
return Either.Right(Unit)
}

suspend fun readFileContent(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,33 @@
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
import kotlinx.coroutines.withContext
import javax.inject.Inject

class GitHubCredentialsManager @Inject constructor(
@EncryptedSharedPrefs
private val encryptedSharedPrefs: Lazy<SharedPreferences>,
@ApplicationContext
private val appContext: Context,
) {

suspend fun getCredentials(): Either<String, GitHubCredentials> = 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."
Expand All @@ -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,
)
}

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ object EncryptedSharedPrefsModule {
}

object EncryptedPrefsKeys {
@Deprecated("Use DataStoreKeys.GITHUB_PAT instead")
const val BACKUP_GITHUB_PAT = "github_backup_pat"
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ val Context.dataStore: DataStore<Preferences> 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")
}
23 changes: 0 additions & 23 deletions app/src/main/java/com/ivy/wallet/io/network/RestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) }
}

This file was deleted.

7 changes: 7 additions & 0 deletions app/src/main/java/com/ivy/wallet/migrations/Migration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ivy.wallet.migrations

interface Migration {
val key: String

suspend fun migrate()
}
Loading