Skip to content

Commit ed356bf

Browse files
authored
Merge pull request #957 from synonymdev/codex/paykit-feature-flag-followups
fix: gate paykit ui
2 parents 67755de + 62818d2 commit ed356bf

27 files changed

Lines changed: 1198 additions & 240 deletions

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ android {
164164
buildConfigField("boolean", "TREZOR_BRIDGE", trezorBridgeEnv)
165165
buildConfigField("String", "TREZOR_BRIDGE_URL", "\"$trezorBridgeUrlEnv\"")
166166
buildConfigField("boolean", "GEO", System.getenv("GEO")?.toBoolean()?.toString() ?: "true")
167+
buildConfigField("boolean", "FEATURE_PAYKIT_UI_DISABLED", System.getenv("PAYKIT_UI_DISABLED")?.toBoolean()?.toString() ?: "false")
167168
buildConfigField("String", "LOCALES", "\"${bcp47Locales.joinToString(",")}\"")
168169
}
169170

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ package to.bitkit.data
33
import android.content.Context
44
import androidx.datastore.core.DataStore
55
import androidx.datastore.dataStore
6+
import androidx.datastore.preferences.core.Preferences
7+
import androidx.datastore.preferences.core.booleanPreferencesKey
8+
import androidx.datastore.preferences.core.edit
9+
import androidx.datastore.preferences.preferencesDataStore
610
import dagger.hilt.android.qualifiers.ApplicationContext
711
import kotlinx.coroutines.flow.Flow
12+
import kotlinx.coroutines.flow.map
813
import kotlinx.serialization.Serializable
914
import to.bitkit.data.serializers.SettingsSerializer
1015
import to.bitkit.env.Env
@@ -24,13 +29,17 @@ private val Context.settingsDataStore: DataStore<SettingsData> by dataStore(
2429
serializer = SettingsSerializer,
2530
)
2631

32+
private val Context.localSettingsDataStore: DataStore<Preferences> by preferencesDataStore("local_settings")
33+
2734
@Singleton
2835
class SettingsStore @Inject constructor(
2936
@ApplicationContext private val context: Context,
3037
) {
3138
private val store = context.settingsDataStore
39+
private val localStore = context.localSettingsDataStore
3240

3341
val data: Flow<SettingsData> = store.data
42+
val isPaykitEnabled: Flow<Boolean> = localStore.data.map { it[PAYKIT_ENABLED_KEY] ?: false }
3443

3544
@Volatile
3645
var restoredMonitoredTypesFromBackup: Boolean = false
@@ -53,6 +62,10 @@ class SettingsStore @Inject constructor(
5362
store.updateData(transform)
5463
}
5564

65+
suspend fun setIsPaykitEnabled(value: Boolean) {
66+
localStore.edit { it[PAYKIT_ENABLED_KEY] = value }
67+
}
68+
5669
suspend fun addLastUsedTag(newTag: String) {
5770
store.updateData { currentSettings ->
5871
val combinedTags = (listOf(newTag) + currentSettings.lastUsedTags).distinct()
@@ -76,13 +89,15 @@ class SettingsStore @Inject constructor(
7689

7790
suspend fun reset() {
7891
store.updateData { SettingsData() }
92+
localStore.edit { it.clear() }
7993
restoredMonitoredTypesFromBackup = false
8094
Logger.info("Deleted all user settings data.")
8195
}
8296

8397
companion object {
8498
private const val TAG = "SettingsStore"
8599
private const val MAX_LAST_USED_TAGS = 10
100+
private val PAYKIT_ENABLED_KEY = booleanPreferencesKey("paykit_enabled")
86101
}
87102
}
88103

@@ -103,6 +118,7 @@ data class SettingsData(
103118
val hasConfirmedPublicPaykitEndpoints: Boolean = false,
104119
val sharesPublicPaykitEndpoints: Boolean = false,
105120
val sharesPrivatePaykitEndpoints: Boolean = false,
121+
val publicPaykitCleanupPending: Boolean = false,
106122
val publicPaykitLightningEnabled: Boolean = true,
107123
val publicPaykitOnchainEnabled: Boolean = true,
108124
val publicPaykitBolt11: String = "",
@@ -148,3 +164,25 @@ fun SettingsData.resetPin() = this.copy(
148164
isPinForPaymentsEnabled = false,
149165
isBiometricEnabled = false,
150166
)
167+
168+
fun SettingsData.hasPublicPaykitPublicationState(): Boolean =
169+
hasConfirmedPublicPaykitEndpoints ||
170+
sharesPublicPaykitEndpoints ||
171+
publicPaykitCleanupPending ||
172+
publicPaykitBolt11.isNotBlank() ||
173+
publicPaykitBolt11PaymentHash.isNotBlank() ||
174+
publicPaykitBolt11ExpiresAtMillis > 0L
175+
176+
fun SettingsData.hasPaykitState(): Boolean =
177+
hasPublicPaykitPublicationState() ||
178+
sharesPrivatePaykitEndpoints
179+
180+
fun SettingsData.paykitDisabled(markPublicCleanupPending: Boolean = false) = copy(
181+
hasConfirmedPublicPaykitEndpoints = false,
182+
sharesPublicPaykitEndpoints = false,
183+
sharesPrivatePaykitEndpoints = false,
184+
publicPaykitCleanupPending = publicPaykitCleanupPending || markPublicCleanupPending,
185+
publicPaykitBolt11 = "",
186+
publicPaykitBolt11PaymentHash = "",
187+
publicPaykitBolt11ExpiresAtMillis = 0,
188+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package to.bitkit.flags
2+
3+
import to.bitkit.BuildConfig
4+
5+
object PaykitFeatureFlags {
6+
const val isUiAvailable = !BuildConfig.FEATURE_PAYKIT_UI_DISABLED
7+
8+
fun isUiEnabled(localFlagEnabled: Boolean): Boolean {
9+
return isUiAvailable && localFlagEnabled
10+
}
11+
}

app/src/main/java/to/bitkit/models/AddressType.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ fun AddressType.toDerivationPath(
8787
}
8888
}
8989

90+
fun AddressType.toAccountDerivationPath(network: Network = Env.network): String {
91+
val coinType = if (network == Network.BITCOIN) 0 else 1
92+
93+
return when (this) {
94+
AddressType.P2TR -> "m/86'/$coinType'/0'"
95+
AddressType.P2WPKH -> "m/84'/$coinType'/0'"
96+
AddressType.P2SH -> "m/49'/$coinType'/0'"
97+
AddressType.P2PKH -> "m/44'/$coinType'/0'"
98+
else -> ""
99+
}
100+
}
101+
90102
fun AddressType.toSettingsString(): String = when (this) {
91103
AddressType.P2TR -> "taproot"
92104
AddressType.P2WPKH -> "nativeSegwit"

app/src/main/java/to/bitkit/repositories/BackupRepo.kt

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ class BackupRepo @Inject constructor(
591591
}
592592
performRestore(BackupCategory.WALLET) { dataBytes ->
593593
restoreWalletBackup(dataBytes)
594-
}
594+
}.getOrThrow()
595595
performRestore(BackupCategory.BLOCKTANK) { dataBytes ->
596596
val parsed = json.decodeFromString<BlocktankBackupV1>(String(dataBytes))
597597
blocktankRepo.restoreFromBackup(parsed)
@@ -621,20 +621,11 @@ class BackupRepo @Inject constructor(
621621
if (!parsed.privatePaykitHighestReservedReceiveIndexByAddressType.isNullOrEmpty()) {
622622
cacheStore.update { it.copy(onchainAddress = "", bip21 = "") }
623623
}
624-
privatePaykitAddressReservationRepo.get()
625-
.restoreBackup(parsed.privatePaykitHighestReservedReceiveIndexByAddressType)
626-
.onFailure {
627-
Logger.warn("Failed to restore private Paykit reservations", it, context = TAG)
628-
}
629-
privatePaykitRepo.get().restoreBackup(parsed.privatePaykitContactLinks)
630-
.onFailure {
631-
Logger.warn("Failed to restore private Paykit contact links", it, context = TAG)
632-
}
633-
privatePaykitAddressReservationRepo.get()
634-
.reconcileReservedIndexesWithLdk()
635-
.onFailure {
636-
Logger.warn("Failed to reconcile restored private Paykit reservations", it, context = TAG)
637-
}
624+
val addressReservationRepo = privatePaykitAddressReservationRepo.get()
625+
addressReservationRepo.restoreBackup(parsed.privatePaykitHighestReservedReceiveIndexByAddressType).getOrThrow()
626+
val privateRepo = privatePaykitRepo.get()
627+
privateRepo.restoreBackup(parsed.privatePaykitContactLinks).getOrThrow()
628+
addressReservationRepo.reconcileReservedIndexesWithLdk().getOrThrow()
638629
Logger.debug("Restored ${parsed.transfers.size} transfers", context = TAG)
639630
return parsed.createdAt
640631
}

app/src/main/java/to/bitkit/repositories/PubkyRepo.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.StateFlow
2020
import kotlinx.coroutines.flow.asSharedFlow
2121
import kotlinx.coroutines.flow.asStateFlow
2222
import kotlinx.coroutines.flow.combine
23+
import kotlinx.coroutines.flow.first
2324
import kotlinx.coroutines.flow.map
2425
import kotlinx.coroutines.flow.stateIn
2526
import kotlinx.coroutines.flow.update
@@ -30,6 +31,7 @@ import kotlinx.coroutines.withContext
3031
import to.bitkit.data.PrivatePaykitCacheStore
3132
import to.bitkit.data.PubkyStore
3233
import to.bitkit.data.SettingsStore
34+
import to.bitkit.data.hasPublicPaykitPublicationState
3335
import to.bitkit.data.keychain.Keychain
3436
import to.bitkit.di.IoDispatcher
3537
import to.bitkit.env.Env
@@ -983,7 +985,8 @@ class PubkyRepo @Inject constructor(
983985
// region Sign out
984986

985987
suspend fun signOut(): Result<Unit> {
986-
removeBitkitPaymentEndpoints()
988+
val hadPublicPaykitState = settingsStore.data.first().hasPublicPaykitPublicationState()
989+
val endpointCleanupResult = removeBitkitPaymentEndpoints()
987990
.onFailure { Logger.warn("Failed to remove Bitkit payment endpoints", it, context = TAG) }
988991

989992
val result = runCatching {
@@ -993,7 +996,7 @@ class PubkyRepo @Inject constructor(
993996
withContext(ioDispatcher) { pubkyService.forceSignOut() }
994997
}
995998

996-
clearLocalState()
999+
clearLocalState(publicPaykitCleanupPending = endpointCleanupResult.isFailure && hadPublicPaykitState)
9971000
return result
9981001
}
9991002

@@ -1074,23 +1077,24 @@ class PubkyRepo @Inject constructor(
10741077
_contactsLoadVersion.update { it + 1 }
10751078
}
10761079

1077-
private suspend fun clearLocalState() = withContext(ioDispatcher) {
1080+
private suspend fun clearLocalState(publicPaykitCleanupPending: Boolean = false) = withContext(ioDispatcher) {
10781081
runCatching { keychain.delete(Keychain.Key.PAYKIT_SESSION.name) }
10791082
runCatching { keychain.delete(Keychain.Key.PUBKY_SECRET_KEY.name) }
1080-
runCatching { clearPublicPaykitSharingState() }
1083+
runCatching { clearPublicPaykitSharingState(publicPaykitCleanupPending) }
10811084
.onFailure { Logger.warn("Failed to clear public Paykit sharing state", it, context = TAG) }
10821085
notifyBackupStateChanged()
10831086
clearAuthenticatedState()
10841087
}
10851088

1086-
private suspend fun clearPublicPaykitSharingState() {
1089+
private suspend fun clearPublicPaykitSharingState(publicPaykitCleanupPending: Boolean) {
10871090
settingsStore.update {
10881091
it.copy(
10891092
hasConfirmedPublicPaykitEndpoints = false,
10901093
sharesPublicPaykitEndpoints = false,
10911094
publicPaykitBolt11 = "",
10921095
publicPaykitBolt11PaymentHash = "",
10931096
publicPaykitBolt11ExpiresAtMillis = 0,
1097+
publicPaykitCleanupPending = publicPaykitCleanupPending,
10941098
)
10951099
}
10961100
}

app/src/main/java/to/bitkit/repositories/PublicPaykitRepo.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,13 @@ class PublicPaykitRepo @Inject constructor(
179179
runCatching {
180180
if (!publish) {
181181
removePublishedEndpoints()
182+
settingsStore.update { it.copy(publicPaykitCleanupPending = false) }
182183
return@runCatching
183184
}
184185

185186
val desired = buildWalletEndpoints(refresh = true)
186187
applyPublishedEndpoints(desired)
188+
settingsStore.update { it.copy(publicPaykitCleanupPending = false) }
187189
}
188190
}
189191

@@ -198,6 +200,7 @@ class PublicPaykitRepo @Inject constructor(
198200
requireEndpoint = requireEndpoint,
199201
)
200202
applyPublishedEndpoints(desired)
203+
settingsStore.update { it.copy(publicPaykitCleanupPending = false) }
201204
}
202205
}
203206

app/src/main/java/to/bitkit/repositories/WalletRepo.kt

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import to.bitkit.data.CacheStore
2525
import to.bitkit.data.SettingsStore
2626
import to.bitkit.data.keychain.Keychain
2727
import to.bitkit.di.BgDispatcher
28+
import to.bitkit.env.Env
2829
import to.bitkit.ext.filterOpen
2930
import to.bitkit.ext.nowTimestamp
3031
import to.bitkit.ext.toHex
@@ -33,12 +34,15 @@ import to.bitkit.models.AddressModel
3334
import to.bitkit.models.BalanceState
3435
import to.bitkit.models.DEFAULT_ADDRESS_TYPE_STRING
3536
import to.bitkit.models.msatFloorOf
37+
import to.bitkit.models.toAccountDerivationPath
3638
import to.bitkit.models.toDerivationPath
39+
import to.bitkit.services.AddressDerivationInfo
3740
import to.bitkit.services.CoreService
3841
import to.bitkit.usecases.DeriveBalanceStateUseCase
3942
import to.bitkit.usecases.WipeWalletUseCase
4043
import to.bitkit.utils.Bip21Utils
4144
import to.bitkit.utils.Logger
45+
import to.bitkit.utils.ServiceError
4246
import to.bitkit.utils.measured
4347
import javax.inject.Inject
4448
import javax.inject.Singleton
@@ -401,7 +405,14 @@ class WalletRepo @Inject constructor(
401405
isChange = isChange,
402406
startIndex = startIndex,
403407
count = count,
404-
).getOrThrow()
408+
).getOrElse {
409+
deriveAddressInfosFromMnemonic(
410+
addressType = addressType,
411+
isChange = isChange,
412+
startIndex = startIndex,
413+
count = count,
414+
)
415+
}
405416

406417
val addresses = result.map { address ->
407418
AddressModel(
@@ -417,6 +428,33 @@ class WalletRepo @Inject constructor(
417428
}
418429
}
419430

431+
private suspend fun deriveAddressInfosFromMnemonic(
432+
addressType: AddressType,
433+
isChange: Boolean,
434+
startIndex: Int,
435+
count: Int,
436+
): List<AddressDerivationInfo> {
437+
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
438+
?: throw ServiceError.MnemonicNotFound()
439+
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)
440+
val baseDerivationPath = addressType.toAccountDerivationPath()
441+
442+
return coreService.onchain.deriveBitcoinAddresses(
443+
mnemonicPhrase = mnemonic,
444+
derivationPathStr = baseDerivationPath,
445+
network = Env.network,
446+
bip39Passphrase = passphrase,
447+
isChange = isChange,
448+
startIndex = startIndex.toUInt(),
449+
count = count.toUInt(),
450+
).addresses.mapIndexed { offset, address ->
451+
AddressDerivationInfo(
452+
address = address.address,
453+
index = startIndex + offset,
454+
)
455+
}
456+
}
457+
420458
fun getBolt11(): String = _walletState.value.bolt11
421459

422460
suspend fun setBolt11(bolt11: String) {

0 commit comments

Comments
 (0)