<NME> StakingHandler.kt
<BEF> /**
 * BreadWallet
 *
 * Created by Drew Carlson <drew.carlson@breadwallet.com> on 10/30/20.
 * Copyright (c) 2020 breadwallet LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.breadwallet.ui.uistaking

import android.content.Context
import com.breadwallet.breadbox.BreadBox
import com.breadwallet.breadbox.addressFor
import com.breadwallet.breadbox.estimateFee
import com.breadwallet.breadbox.toBigDecimal
import com.breadwallet.crypto.Amount
import com.breadwallet.crypto.TransferFeeBasis
import com.breadwallet.crypto.TransferState.Type.CREATED
import com.breadwallet.crypto.TransferState.Type.DELETED
import com.breadwallet.crypto.TransferState.Type.FAILED
import com.breadwallet.crypto.TransferState.Type.INCLUDED
import com.breadwallet.crypto.TransferState.Type.PENDING
import com.breadwallet.crypto.TransferState.Type.SIGNED
import com.breadwallet.crypto.TransferState.Type.SUBMITTED
import com.breadwallet.crypto.WalletManagerState
import com.breadwallet.crypto.errors.FeeEstimationError
import com.breadwallet.crypto.errors.TransferSubmitError
import com.breadwallet.logger.logError
import com.breadwallet.tools.manager.BRClipboardManager
import com.breadwallet.tools.manager.BRSharedPrefs
import com.breadwallet.tools.security.BrdUserManager
import com.breadwallet.tools.security.isFingerPrintAvailableAndSetup
import com.breadwallet.ui.uistaking.Staking.E
import com.breadwallet.ui.uistaking.Staking.F
import com.breadwallet.ui.uistaking.Staking.M
import com.breadwallet.ui.uistaking.Staking.M.ViewValidator.State.PENDING_STAKE
import com.breadwallet.ui.uistaking.Staking.M.ViewValidator.State.PENDING_UNSTAKE
import com.breadwallet.ui.uistaking.Staking.M.ViewValidator.State.STAKED
import drewcarlson.mobius.flow.flowTransformer
import drewcarlson.mobius.flow.subtypeEffectHandler
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.single
import java.util.Date
import kotlin.time.milliseconds

private const val DELEGATE = "Delegate"
private const val UNSTAKE_KEY = "Type"
private const val UNSTAKE_VALUE = "Delegation"
private const val DELEGATION_OP = "DelegationOp"
private const val DELEGATION_OP_VALUE = "1"
private val WALLET_UPDATE_DEBOUNCE = 250L.milliseconds

fun createStakingHandler(
    context: Context,
    currencyId: String,
    breadBox: BreadBox,
    userManager: BrdUserManager
) = subtypeEffectHandler<F, E> {
    addTransformer(handleLoadAccount(currencyId, breadBox))
    addFunction<F.ValidateAddress> { (address) ->
        val wallet = breadBox.wallet(currencyId).first()
        E.OnAddressValidated(
            isValid = wallet.addressFor(address) != null
        )
    }
    addFunction<F.Stake> { (address, feeBasis) ->
        breadBox.changeValidator(
            currencyId,
            targetAddress = address,
            feeEstimate = feeBasis,
            phrase = checkNotNull(userManager.getPhrase())
        )
    }
    addFunction<F.Unstake> { (feeBasis) ->
        breadBox.changeValidator(
            currencyId,
            targetAddress = null,
            feeEstimate = feeBasis,
            phrase = checkNotNull(userManager.getPhrase())
        )
    }
    addLatestValueCollector<F.PasteFromClipboard> { effect ->
        val wallet = breadBox.wallet(currencyId).first()
        val text = BRClipboardManager.getClipboard()
        if (wallet.addressFor(text) == null) {
            emit(E.OnAddressValidated(isValid = false))
        } else if (text != effect.currentDelegateAddress) {
            emit(E.OnAddressChanged(text))
        }
    }
    addFunction<F.EstimateFee> { (address) ->
        val wallet = breadBox.wallet(currencyId).first()
        val attrs = wallet.transferAttributes
            .filter { it.key == DELEGATION_OP }
            .onEach { it.setValue(DELEGATION_OP_VALUE) }
            .toSet()
        val networkFee = wallet.walletManager.network.fees.minByOrNull {
            it.confirmationTimeInMilliseconds.toLong()
        }.run(::checkNotNull)
        try {
            val amount = Amount.create(0, wallet.unitForFee)
            val coreAddress = if (address.isNullOrBlank()) {
                wallet.target
            } else {
                checkNotNull(wallet.addressFor(address))
            }
            val feeBasis = wallet.estimateFee(coreAddress, amount, networkFee, attrs = attrs)
            E.OnFeeUpdated(address, feeBasis, wallet.balance.toBigDecimal())
        } catch (e: FeeEstimationError) {
            E.OnTransferFailed(M.TransactionError.FeeEstimateFailed)
        }
    }
    addFunctionSync<F.LoadAuthenticationSettings> {
        val isEnabled = isFingerPrintAvailableAndSetup(context) && BRSharedPrefs.sendMoneyWithFingerprint
        E.OnAuthenticationSettingsUpdated(isEnabled)
    }
}

private val stakedTransferStates = listOf(SUBMITTED, INCLUDED, PENDING, CREATED, SIGNED)

private fun handleLoadAccount(
    currencyId: String,
    breadBox: BreadBox
) = flowTransformer<F.LoadAccount, E> { effects ->
    effects
        .flatMapLatest { breadBox.wallet(currencyId) }
        .dropWhile { it.walletManager.state != WalletManagerState.CONNECTED() }
        .debounce(WALLET_UPDATE_DEBOUNCE)
        .mapLatest { wallet ->
            val currencyCode = wallet.currency.code
            val transfer = wallet.transfers
                .filter { stakedTransferStates.contains(it.state.type) }
                .sortedByDescending { it.confirmation.orNull()?.confirmationTime ?: Date() }
                .firstOrNull { transfer ->
                    transfer.attributes.any {
                        it.key.equals(DELEGATION_OP, true) ||
                        it.key.equals(DELEGATE, true) ||
                            (it.key.equals(UNSTAKE_KEY, true) &&
                                it.value.or("").equals(UNSTAKE_VALUE, true))
                    } && (transfer.confirmation.orNull()?.success ?: true)
                }
            val balance = wallet.balance.toBigDecimal()

            when (transfer) {
                null -> E.AccountUpdated.Unstaked(currencyCode, balance)
                else -> {
                    val address = transfer.target.orNull()?.toString() ?: ""
                    val isConfirmed = transfer.confirmation.orNull()?.success ?: false
                    val delegateAddress = transfer.attributes.find { it.key.equals(DELEGATE, true) }?.value?.or(address)
                    val isStaked = delegateAddress != null
                    if (isConfirmed) {
                        if (isStaked) {
                            E.AccountUpdated.Staked(currencyCode, delegateAddress!!, STAKED, balance)
                        } else E.AccountUpdated.Unstaked(currencyCode, balance)
                    } else {
                        val state = if (isStaked) PENDING_STAKE else PENDING_UNSTAKE
                        E.AccountUpdated.Staked(currencyCode, address, state, balance)
                    }
                }
            }
        }
        .distinctUntilChanged()
}

private suspend fun BreadBox.changeValidator(
    currencyId: String,
    targetAddress: String? = null,
    feeEstimate: TransferFeeBasis,
    phrase: ByteArray
): E {
    val wallet = wallet(currencyId).first()
    val amount = Amount.create(0, wallet.unit)
    val attrs = wallet.transferAttributes
        .filter { it.key.equals(DELEGATION_OP, true) }
        .onEach { it.setValue(DELEGATION_OP_VALUE) }
        .toSet()

    return try {
        // Estimate and submit transfer
        val address = if (targetAddress == null) {
            wallet.target // unstake
        } else {
            checkNotNull(wallet.addressFor(targetAddress))
        }
        val transfer = wallet.createTransfer(address, amount, feeEstimate, attrs).orNull()
        checkNotNull(transfer) { "Failed to create transfer." }
        wallet.walletManager.submit(transfer, phrase)
        val transferHash = checkNotNull(transfer.hash.orNull()).toString()
        // Await a result for the transfer
        walletTransfer(wallet.currency.code, transferHash)
            .mapNotNull { tx ->
                when (checkNotNull(tx.state.type)) {
                    // Ignore pre-submit states
                    CREATED, SIGNED -> null
                    INCLUDED, PENDING, SUBMITTED -> {
                        val target = checkNotNull(tx.target.orNull()).toString()
                        val balance = wallet.balance.toBigDecimal()
                        E.AccountUpdated.Staked(wallet.currency.code, target, PENDING_STAKE, balance)
                    }
                    DELETED, FAILED -> {
                        logError("Failed to submit transfer ${tx.state.failedError.orNull()}")
                        E.OnTransferFailed(M.TransactionError.Unknown)
                    }
                }
            }.first()
    } catch (e: TransferSubmitError) {
        logError("Failed to submit transfer", e)
        E.OnTransferFailed(M.TransactionError.TransferFailed)
    } catch (e: IllegalStateException) {
        logError("Unexpected error when submitting transfer", e)
        E.OnTransferFailed(M.TransactionError.Unknown)
    }
}

<MSG> Merge branch 'drew/DROID-1925' into 'develop'

DROID-1925, DROID-1851: Track gifting and staking events

Closes DROID-1925

See merge request breadwallet/breadwallet-android!1441
<DFF> @@ -46,6 +46,7 @@ import com.breadwallet.tools.manager.BRClipboardManager
 import com.breadwallet.tools.manager.BRSharedPrefs
 import com.breadwallet.tools.security.BrdUserManager
 import com.breadwallet.tools.security.isFingerPrintAvailableAndSetup
+import com.breadwallet.tools.util.EventUtils
 import com.breadwallet.ui.uistaking.Staking.E
 import com.breadwallet.ui.uistaking.Staking.F
 import com.breadwallet.ui.uistaking.Staking.M
@@ -61,7 +62,6 @@ import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.single
 import java.util.Date
 import kotlin.time.milliseconds
 
@@ -199,9 +199,12 @@ private suspend fun BreadBox.changeValidator(
 
     return try {
         // Estimate and submit transfer
+        val event: String
         val address = if (targetAddress == null) {
+            event = EventUtils.EVENT_WALLET_UNSTAKE
             wallet.target // unstake
         } else {
+            event = EventUtils.EVENT_WALLET_STAKE
             checkNotNull(wallet.addressFor(targetAddress))
         }
         val transfer = wallet.createTransfer(address, amount, feeEstimate, attrs).orNull()
@@ -217,6 +220,7 @@ private suspend fun BreadBox.changeValidator(
                     INCLUDED, PENDING, SUBMITTED -> {
                         val target = checkNotNull(tx.target.orNull()).toString()
                         val balance = wallet.balance.toBigDecimal()
+                        EventUtils.pushEvent(event)
                         E.AccountUpdated.Staked(wallet.currency.code, target, PENDING_STAKE, balance)
                     }
                     DELETED, FAILED -> {

{'commit': 'diff --git a/app/src/main/java/com/breadwallet/tools/util/EventUtils.java b/app-core/src/main/java/com/breadwallet/tools/util/EventUtils.java\nsimilarity index 97%\nrename from app/src/main/java/com/breadwallet/tools/util/EventUtils.java\nrename to app-core/src/main/java/com/breadwallet/tools/util/EventUtils.java\nindex c94d87430..d98333fd0 100644\n--- a/app/src/main/java/com/breadwallet/tools/util/EventUtils.java\n+++ b/app-core/src/main/java/com/breadwallet/tools/util/EventUtils.java\n@@ -29,7 +29,7 @@ import android.content.Context;\n import android.text.format.DateUtils;\n import android.util.Log;\n \n-import com.breadwallet.BuildConfig;\n+import com.breadwallet.appcore.BuildConfig;\n import com.breadwallet.tools.manager.BRReportsManager;\n import com.platform.APIClient;\n \n@@ -161,6 +161,15 @@ public final class EventUtils {\n     public static final String PROMPT_RECOMMEND_RESCAN = EventUtils.EVENT_PROMPT_PREFIX + "recommendRescanPrompt";\n     public static final String PROMPT_EMAIL = EventUtils.EVENT_PROMPT_PREFIX + "emailPrompt";\n     public static final String PROMPT_RATE_APP = EventUtils.EVENT_PROMPT_PREFIX + "rateAppPrompt";\n+    // Gift events\n+    public static final String EVENT_GIFT_SEND = "gift.send";\n+    public static final String EVENT_GIFT_REDEEM = "gift.redeem";\n+    public static final String EVENT_GIFT_REDEEM_LINK = "gift.redeem.link";\n+    public static final String EVENT_GIFT_REDEEM_SCAN = "gift.redeem.scan";\n+    public static final String EVENT_GIFT_REDEEM_RECLAIM = "gift.redeem.reclaim";\n+    // Staking\n+    public static final String EVENT_WALLET_STAKE = "wallet.xtz.stake";\n+    public static final String EVENT_WALLET_UNSTAKE = "wallet.xtz.unstake";\n     private static final String TAG = EventUtils.class.getName();\n     private static final String EVENTS_PATH = "/events";\n     private static final String SESSION_ID = "sessionId";\ndiff --git a/app/build.gradle.kts b/app/build.gradle.kts\nindex ff7cd2543..51be5915f 100644\n--- a/app/build.gradle.kts\n+++ b/app/build.gradle.kts\n@@ -124,6 +124,7 @@ android {\n     }\n     kotlinOptions {\n         jvmTarget = "1.8"\n+        languageVersion = "1.4"\n         freeCompilerArgs += listOf(\n             "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",\n             "-Xopt-in=kotlinx.coroutines.FlowPreview",\ndiff --git a/app/src/main/java/com/breadwallet/tools/util/LinkExtensions.kt b/app/src/main/java/com/breadwallet/tools/util/LinkExtensions.kt\nindex 0b477be27..29f037c75 100644\n--- a/app/src/main/java/com/breadwallet/tools/util/LinkExtensions.kt\n+++ b/app/src/main/java/com/breadwallet/tools/util/LinkExtensions.kt\n@@ -57,7 +57,8 @@ const val GIFT_BASE_URL = "$BRD_PROTOCOL://$BRD_HOST$GIFT_PATH_PREFIX"\n @Suppress("ComplexMethod")\n suspend fun String.asLink(\n     breadBox: BreadBox,\n-    uriParser: CryptoUriParser\n+    uriParser: CryptoUriParser,\n+    scanned: Boolean = false\n ): Link? {\n     if (isBlank()) return null\n     val uri = Uri.parse(this)\n@@ -81,7 +82,7 @@ suspend fun String.asLink(\n             )\n         uriParser.isCryptoUrl(this) ->\n             uriParser.parseRequest(this)?.asCryptoRequestUrl()\n-        isGiftUrl(uri) -> uri.asGiftUrl()\n+        isGiftUrl(uri) -> uri.asGiftUrl(scanned = scanned)\n         isPlatformUrl(uri) -> uri.asPlatformUrl()\n         isPlatformDebugUrl(uri) -> uri.asPlatformDebugUrl()\n         isWalletPairUrl(uri) -> Link.WalletPairUrl(PairingMetaData(this))\n@@ -110,9 +111,14 @@ fun CryptoRequest.asCryptoRequestUrl() =\n         destinationTag = destinationTag\n     )\n \n-private fun Uri.asGiftUrl(): Link.ImportWallet {\n+private fun Uri.asGiftUrl(scanned: Boolean): Link.ImportWallet {\n     val key = Base64.decode(lastPathSegment ?: "", Base64.DEFAULT)\n-    return Link.ImportWallet(key.toString(Charsets.UTF_8), false)\n+    return Link.ImportWallet(\n+        key.toString(Charsets.UTF_8),\n+        passwordProtected = false,\n+        gift = true,\n+        scanned = scanned\n+    )\n }\n \n private fun Uri.asPlatformUrl(): Link.PlatformUrl {\ndiff --git a/app/src/main/java/com/breadwallet/ui/importwallet/Import.kt b/app/src/main/java/com/breadwallet/ui/importwallet/Import.kt\nindex d578b77f3..14477cb97 100644\n--- a/app/src/main/java/com/breadwallet/ui/importwallet/Import.kt\n+++ b/app/src/main/java/com/breadwallet/ui/importwallet/Import.kt\n@@ -46,7 +46,9 @@ object Import {\n         val isKeyValid: Boolean = false,\n         val loadingState: LoadingState = LoadingState.IDLE,\n         val currencyCode: CurrencyCode? = null,\n-        val reclaimGiftHash: String? = null\n+        val reclaimGiftHash: String? = null,\n+        val scanned: Boolean = false,\n+        val gift: Boolean = false\n     ) {\n         enum class LoadingState {\n             IDLE, VALIDATING, ESTIMATING, SUBMITTING\n@@ -67,11 +69,15 @@ object Import {\n             fun createDefault(\n                 privateKey: String? = null,\n                 isPasswordProtected: Boolean = false,\n-                reclaimGiftHash: String? = null\n+                reclaimGiftHash: String? = null,\n+                scanned: Boolean = false,\n+                gift: Boolean = false\n             ): M = M(\n                 privateKey = privateKey,\n                 keyRequiresPassword = isPasswordProtected,\n                 reclaimGiftHash = reclaimGiftHash,\n+                scanned = scanned,\n+                gift = gift,\n                 loadingState = if (privateKey == null) {\n                     LoadingState.IDLE\n                 } else {\n@@ -208,7 +214,11 @@ object Import {\n             @Redacted val privateKey: String,\n             @Redacted val password: String?,\n             val currencyCode: CurrencyCode,\n-            val reclaimGiftHash: String?\n+            val reclaimGiftHash: String?,\n+        ) : F()\n+\n+        data class TrackEvent(\n+            val eventString: String,\n         ) : F()\n \n         sealed class Nav(\ndiff --git a/app/src/main/java/com/breadwallet/ui/importwallet/ImportController.kt b/app/src/main/java/com/breadwallet/ui/importwallet/ImportController.kt\nindex a2a57e8bf..50a4ed15f 100644\n--- a/app/src/main/java/com/breadwallet/ui/importwallet/ImportController.kt\n+++ b/app/src/main/java/com/breadwallet/ui/importwallet/ImportController.kt\n@@ -55,9 +55,11 @@ import org.kodein.di.erased.bind\n import org.kodein.di.erased.instance\n import org.kodein.di.erased.singleton\n \n-private const val PRIVATE_KEY = "private_key"\n-private const val PASSWORD_PROTECTED = "password_protected"\n-private const val RECLAIMING_GIFT = "reclaim_gift_hash"\n+private const val PRIVATE_KEY = "ImportController.private_key"\n+private const val PASSWORD_PROTECTED = "ImportController.password_protected"\n+private const val RECLAIMING_GIFT = "ImportController.reclaim_gift_hash"\n+private const val SCANNED = "ImportController.scanned"\n+private const val GIFT = "ImportController.gift"\n \n @Suppress("TooManyFunctions")\n class ImportController(\n@@ -70,7 +72,9 @@ class ImportController(\n     constructor(\n         privateKey: String,\n         isPasswordProtected: Boolean,\n-        reclaimingGift: String? = null\n+        reclaimingGift: String? = null,\n+        scanned: Boolean,\n+        gift: Boolean,\n     ) : this(\n         bundleOf(\n             PRIVATE_KEY to privateKey,\n@@ -86,7 +90,8 @@ class ImportController(\n     override val defaultModel = M.createDefault(\n         privateKey = argOptional(PRIVATE_KEY),\n         isPasswordProtected = arg(PASSWORD_PROTECTED, false),\n-        reclaimGiftHash = argOptional(RECLAIMING_GIFT)\n+        reclaimGiftHash = argOptional(RECLAIMING_GIFT),\n+        scanned = arg(GIFT)\n     )\n \n     override val kodein by Kodein.lazy {\ndiff --git a/app/src/main/java/com/breadwallet/ui/importwallet/ImportHandler.kt b/app/src/main/java/com/breadwallet/ui/importwallet/ImportHandler.kt\nindex 91c1cf286..4d46e1eef 100644\n--- a/app/src/main/java/com/breadwallet/ui/importwallet/ImportHandler.kt\n+++ b/app/src/main/java/com/breadwallet/ui/importwallet/ImportHandler.kt\n@@ -33,6 +33,7 @@ import com.breadwallet.breadbox.toBigDecimal\n import com.breadwallet.crypto.Key\n import com.breadwallet.crypto.Wallet\n import com.breadwallet.crypto.WalletManagerState\n+import com.breadwallet.tools.util.EventUtils\n import drewcarlson.mobius.flow.subtypeEffectHandler\n import kotlinx.coroutines.flow.asFlow\n import kotlinx.coroutines.flow.filterIsInstance\n@@ -49,6 +50,9 @@ fun createImportHandler(\n     addFunction(handleValidateKey(breadBox))\n     addFunction(handleEstimateImport(breadBox, walletImporter))\n     addFunction(handleSubmitTransfer(walletImporter, giftTracker))\n+    addConsumer<Import.F.TrackEvent> { (event) ->\n+        EventUtils.pushEvent(event)\n+    }\n }\n \n private val filterBtcLike: (Wallet) -> Boolean = {\ndiff --git a/app/src/main/java/com/breadwallet/ui/importwallet/ImportUpdate.kt b/app/src/main/java/com/breadwallet/ui/importwallet/ImportUpdate.kt\nindex 7d77e78f0..406ae7f70 100644\n--- a/app/src/main/java/com/breadwallet/ui/importwallet/ImportUpdate.kt\n+++ b/app/src/main/java/com/breadwallet/ui/importwallet/ImportUpdate.kt\n@@ -25,6 +25,7 @@\n package com.breadwallet.ui.importwallet\n \n import com.breadwallet.breadbox.toBigDecimal\n+import com.breadwallet.tools.util.EventUtils\n import com.breadwallet.ui.importwallet.Import.E\n import com.breadwallet.ui.importwallet.Import.F\n import com.breadwallet.ui.importwallet.Import.M\n@@ -140,8 +141,25 @@ val ImportUpdate = Update<M, E, F> { model, event ->\n             next(model.reset(), setOf(F.ShowBalanceTooLow))\n         E.Estimate.NoBalance ->\n             next(model.reset(), setOf(F.ShowNoBalance))\n-        is E.Transfer.OnSuccess ->\n-            next(model.reset(), setOf(F.ShowImportSuccess))\n+        is E.Transfer.OnSuccess -> {\n+            val effects = mutableSetOf<F>(F.ShowImportSuccess)\n+            if (model.gift) {\n+                effects.add(F.TrackEvent(EventUtils.EVENT_GIFT_REDEEM))\n+                if (model.scanned) {\n+                    effects.add(F.TrackEvent(EventUtils.EVENT_GIFT_REDEEM_SCAN))\n+                } else {\n+                    effects.add(F.TrackEvent(EventUtils.EVENT_GIFT_REDEEM_LINK))\n+                }\n+            } else if (!model.reclaimGiftHash.isNullOrBlank()) {\n+                effects.add(F.TrackEvent(EventUtils.EVENT_GIFT_REDEEM))\n+                effects.add(F.TrackEvent(EventUtils.EVENT_GIFT_REDEEM_RECLAIM))\n+            }\n+\n+            next(\n+                model.reset(),\n+                effects\n+            )\n+        }\n         E.Transfer.OnFailed ->\n             next(model.reset(), setOf(F.ShowImportFailed))\n         E.OnImportCancel -> next(model.reset())\ndiff --git a/app/src/main/java/com/breadwallet/ui/navigation/RouterNavigator.kt b/app/src/main/java/com/breadwallet/ui/navigation/RouterNavigator.kt\nindex e35c2ba49..50ca579d2 100644\n--- a/app/src/main/java/com/breadwallet/ui/navigation/RouterNavigator.kt\n+++ b/app/src/main/java/com/breadwallet/ui/navigation/RouterNavigator.kt\n@@ -420,7 +420,13 @@ class RouterNavigator(\n         val controller = if (privateKey.isNullOrBlank()) {\n             ImportController()\n         } else {\n-            ImportController(privateKey, effect.isPasswordProtected, effect.reclaimingGift)\n+            ImportController(\n+                privateKey,\n+                effect.isPasswordProtected,\n+                effect.reclaimingGift,\n+                effect.scanned,\n+                effect.gift\n+            )\n         }\n         router.pushController(\n             controller.asTransaction(\n@@ -557,7 +563,9 @@ class RouterNavigator(\n             is Link.ImportWallet -> {\n                 val controller = ImportController(\n                     privateKey = link.privateKey,\n-                    isPasswordProtected = link.passwordProtected\n+                    isPasswordProtected = link.passwordProtected,\n+                    scanned = false,\n+                    gift = false\n                 ).asTransaction()\n                 router.pushWithStackIfEmpty(controller, effect.authenticated) {\n                     listOf(\ndiff --git a/app/src/main/java/com/breadwallet/ui/scanner/ScannerController.kt b/app/src/main/java/com/breadwallet/ui/scanner/ScannerController.kt\nindex 0852623d5..d3a78cf45 100644\n--- a/app/src/main/java/com/breadwallet/ui/scanner/ScannerController.kt\n+++ b/app/src/main/java/com/breadwallet/ui/scanner/ScannerController.kt\n@@ -98,7 +98,7 @@ class ScannerController(\n     private fun startScanner(breadBox: BreadBox, uriParser: CryptoUriParser) {\n         qrdecoderview\n             .scannedText(true)\n-            .mapLatest { text -> text to text.asLink(breadBox, uriParser) }\n+            .mapLatest { text -> text to text.asLink(breadBox, uriParser, scanned = true) }\n             .flowOn(Default)\n             .transformLatest { (text, link) ->\n                 if (link == null) {\ndiff --git a/ui/ui-gift/src/main/java/CreateGiftHandler.kt b/ui/ui-gift/src/main/java/CreateGiftHandler.kt\nindex 024dbb53e..807d395d7 100644\n--- a/ui/ui-gift/src/main/java/CreateGiftHandler.kt\n+++ b/ui/ui-gift/src/main/java/CreateGiftHandler.kt\n@@ -53,6 +53,7 @@ import com.breadwallet.platform.interfaces.AccountMetaDataProvider\n import com.breadwallet.repository.RatesRepository\n import com.breadwallet.tools.manager.BRSharedPrefs\n import com.breadwallet.tools.security.BrdUserManager\n+import com.breadwallet.tools.util.EventUtils\n import com.breadwallet.ui.uigift.CreateGift.E\n import com.breadwallet.ui.uigift.CreateGift.F\n import com.breadwallet.ui.uigift.CreateGift.PaperWalletError\n@@ -174,6 +175,7 @@ fun createCreateGiftingHandler(\n                             when (checkNotNull(tx.state.type)) {\n                                 TransferState.Type.CREATED, TransferState.Type.SIGNED -> null\n                                 TransferState.Type.INCLUDED, TransferState.Type.PENDING, TransferState.Type.SUBMITTED -> {\n+                                    EventUtils.pushEvent(EventUtils.EVENT_GIFT_SEND)\n                                     E.OnTransactionSent(transferHash)\n                                 }\n                                 TransferState.Type.DELETED, TransferState.Type.FAILED -> {\ndiff --git a/ui/ui-navigation/src/main/java/com/breadwallet/tools/util/Link.kt b/ui/ui-navigation/src/main/java/com/breadwallet/tools/util/Link.kt\nindex cd5434c9f..d71c27948 100644\n--- a/ui/ui-navigation/src/main/java/com/breadwallet/tools/util/Link.kt\n+++ b/ui/ui-navigation/src/main/java/com/breadwallet/tools/util/Link.kt\n@@ -66,7 +66,9 @@ sealed class Link {\n \n     data class ImportWallet(\n         @Redacted val privateKey: String,\n-        val passwordProtected: Boolean\n+        val passwordProtected: Boolean,\n+        val gift: Boolean = false,\n+        val scanned: Boolean = false\n     ) : Link()\n \n     sealed class BreadUrl : Link() {\ndiff --git a/ui/ui-navigation/src/main/java/com/breadwallet/ui/navigation/NavigationTarget.kt b/ui/ui-navigation/src/main/java/com/breadwallet/ui/navigation/NavigationTarget.kt\nindex a21f5f15e..af76a385e 100644\n--- a/ui/ui-navigation/src/main/java/com/breadwallet/ui/navigation/NavigationTarget.kt\n+++ b/ui/ui-navigation/src/main/java/com/breadwallet/ui/navigation/NavigationTarget.kt\n@@ -124,7 +124,9 @@ sealed class NavigationTarget : INavigationTarget {\n     data class ImportWallet(\n         val privateKey: String? = null,\n         val isPasswordProtected: Boolean = false,\n-        val reclaimingGift: String? = null\n+        val reclaimingGift: String? = null,\n+        val scanned: Boolean = false,\n+        val gift: Boolean = false,\n     ) : NavigationTarget()\n     object BitcoinNodeSelector : NavigationTarget()\n     object EnableSegWit : NavigationTarget()\ndiff --git a/ui/ui-staking/src/main/java/StakingHandler.kt b/ui/ui-staking/src/main/java/StakingHandler.kt\nindex 0019468b6..599ad7270 100644\n--- a/ui/ui-staking/src/main/java/StakingHandler.kt\n+++ b/ui/ui-staking/src/main/java/StakingHandler.kt\n@@ -46,6 +46,7 @@ import com.breadwallet.tools.manager.BRClipboardManager\n import com.breadwallet.tools.manager.BRSharedPrefs\n import com.breadwallet.tools.security.BrdUserManager\n import com.breadwallet.tools.security.isFingerPrintAvailableAndSetup\n+import com.breadwallet.tools.util.EventUtils\n import com.breadwallet.ui.uistaking.Staking.E\n import com.breadwallet.ui.uistaking.Staking.F\n import com.breadwallet.ui.uistaking.Staking.M\n@@ -61,7 +62,6 @@ import kotlinx.coroutines.flow.first\n import kotlinx.coroutines.flow.flatMapLatest\n import kotlinx.coroutines.flow.mapLatest\n import kotlinx.coroutines.flow.mapNotNull\n-import kotlinx.coroutines.flow.single\n import java.util.Date\n import kotlin.time.milliseconds\n \n@@ -199,9 +199,12 @@ private suspend fun BreadBox.changeValidator(\n \n     return try {\n         // Estimate and submit transfer\n+        val event: String\n         val address = if (targetAddress == null) {\n+            event = EventUtils.EVENT_WALLET_UNSTAKE\n             wallet.target // unstake\n         } else {\n+            event = EventUtils.EVENT_WALLET_STAKE\n             checkNotNull(wallet.addressFor(targetAddress))\n         }\n         val transfer = wallet.createTransfer(address, amount, feeEstimate, attrs).orNull()\n@@ -217,6 +220,7 @@ private suspend fun BreadBox.changeValidator(\n                     INCLUDED, PENDING, SUBMITTED -> {\n                         val target = checkNotNull(tx.target.orNull()).toString()\n                         val balance = wallet.balance.toBigDecimal()\n+                        EventUtils.pushEvent(event)\n                         E.AccountUpdated.Staked(wallet.currency.code, target, PENDING_STAKE, balance)\n                     }\n                     DELETED, FAILED -> {\n', 'commit_subject': "Merge branch 'drew/DROID-1925' into 'develop'", 'repo_name': 'breadwallet/breadwallet-android', 'committer': {'name': 'Drew Carlson', 'email': '98e34cb345eb537e565243a679c3e011566996b4@pm.me', 'time_sec': '1611178330', 'tz_offset': '0', 'date': {'seconds': '1611178330'}}, 'commit_tree': 'fa4f6cae77a943d7026ca76aed04210eb6da3b68', 'commit_message': "Merge branch 'drew/DROID-1925' into 'develop'\n\nDROID-1925, DROID-1851: Track gifting and staking events\n\nCloses DROID-1925\n\nSee merge request breadwallet/breadwallet-android!1441", 'language_name': 'Java', 'license': 'mit', 'hunks': '@@ -46,6 +46,7 @@ import com.breadwallet.tools.manager.BRClipboardManager\n import com.breadwallet.tools.manager.BRSharedPrefs\n import com.breadwallet.tools.security.BrdUserManager\n import com.breadwallet.tools.security.isFingerPrintAvailableAndSetup\n+import com.breadwallet.tools.util.EventUtils\n import com.breadwallet.ui.uistaking.Staking.E\n import com.breadwallet.ui.uistaking.Staking.F\n import com.breadwallet.ui.uistaking.Staking.M\n@@ -61,7 +62,6 @@ import kotlinx.coroutines.flow.first\n import kotlinx.coroutines.flow.flatMapLatest\n import kotlinx.coroutines.flow.mapLatest\n import kotlinx.coroutines.flow.mapNotNull\n-import kotlinx.coroutines.flow.single\n import java.util.Date\n import kotlin.time.milliseconds\n \n@@ -199,9 +199,12 @@ private suspend fun BreadBox.changeValidator(\n \n     return try {\n         // Estimate and submit transfer\n+        val event: String\n         val address = if (targetAddress == null) {\n+            event = EventUtils.EVENT_WALLET_UNSTAKE\n             wallet.target // unstake\n         } else {\n+            event = EventUtils.EVENT_WALLET_STAKE\n             checkNotNull(wallet.addressFor(targetAddress))\n         }\n         val transfer = wallet.createTransfer(address, amount, feeEstimate, attrs).orNull()\n@@ -217,6 +220,7 @@ private suspend fun BreadBox.changeValidator(\n                     INCLUDED, PENDING, SUBMITTED -> {\n                         val target = checkNotNull(tx.target.orNull()).toString()\n                         val balance = wallet.balance.toBigDecimal()\n+                        EventUtils.pushEvent(event)\n                         E.AccountUpdated.Staked(wallet.currency.code, target, PENDING_STAKE, balance)\n                     }\n                     DELETED, FAILED -> {\n', 'addition_count': 5, 'deletion_count': 1, 'src_file': 'a/ui/ui-staking/src/main/java/StakingHandler.kt', 'tgt_file': 'b/ui/ui-staking/src/main/java/StakingHandler.kt', 'file_extension': '.kt', 'before_file': '/**\n * BreadWallet\n *\n * Created by Drew Carlson <drew.carlson@breadwallet.com> on 10/30/20.\n * Copyright (c) 2020 breadwallet LLC\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the "Software"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n * THE SOFTWARE.\n */\npackage com.breadwallet.ui.uistaking\n\nimport android.content.Context\nimport com.breadwallet.breadbox.BreadBox\nimport com.breadwallet.breadbox.addressFor\nimport com.breadwallet.breadbox.estimateFee\nimport com.breadwallet.breadbox.toBigDecimal\nimport com.breadwallet.crypto.Amount\nimport com.breadwallet.crypto.TransferFeeBasis\nimport com.breadwallet.crypto.TransferState.Type.CREATED\nimport com.breadwallet.crypto.TransferState.Type.DELETED\nimport com.breadwallet.crypto.TransferState.Type.FAILED\nimport com.breadwallet.crypto.TransferState.Type.INCLUDED\nimport com.breadwallet.crypto.TransferState.Type.PENDING\nimport com.breadwallet.crypto.TransferState.Type.SIGNED\nimport com.breadwallet.crypto.TransferState.Type.SUBMITTED\nimport com.breadwallet.crypto.WalletManagerState\nimport com.breadwallet.crypto.errors.FeeEstimationError\nimport com.breadwallet.crypto.errors.TransferSubmitError\nimport com.breadwallet.logger.logError\nimport com.breadwallet.tools.manager.BRClipboardManager\nimport com.breadwallet.tools.manager.BRSharedPrefs\nimport com.breadwallet.tools.security.BrdUserManager\nimport com.breadwallet.tools.security.isFingerPrintAvailableAndSetup\nimport com.breadwallet.ui.uistaking.Staking.E\nimport com.breadwallet.ui.uistaking.Staking.F\nimport com.breadwallet.ui.uistaking.Staking.M\nimport com.breadwallet.ui.uistaking.Staking.M.ViewValidator.State.PENDING_STAKE\nimport com.breadwallet.ui.uistaking.Staking.M.ViewValidator.State.PENDING_UNSTAKE\nimport com.breadwallet.ui.uistaking.Staking.M.ViewValidator.State.STAKED\nimport drewcarlson.mobius.flow.flowTransformer\nimport drewcarlson.mobius.flow.subtypeEffectHandler\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.dropWhile\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.mapLatest\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.single\nimport java.util.Date\nimport kotlin.time.milliseconds\n\nprivate const val DELEGATE = "Delegate"\nprivate const val UNSTAKE_KEY = "Type"\nprivate const val UNSTAKE_VALUE = "Delegation"\nprivate const val DELEGATION_OP = "DelegationOp"\nprivate const val DELEGATION_OP_VALUE = "1"\nprivate val WALLET_UPDATE_DEBOUNCE = 250L.milliseconds\n\nfun createStakingHandler(\n    context: Context,\n    currencyId: String,\n    breadBox: BreadBox,\n    userManager: BrdUserManager\n) = subtypeEffectHandler<F, E> {\n    addTransformer(handleLoadAccount(currencyId, breadBox))\n    addFunction<F.ValidateAddress> { (address) ->\n        val wallet = breadBox.wallet(currencyId).first()\n        E.OnAddressValidated(\n            isValid = wallet.addressFor(address) != null\n        )\n    }\n    addFunction<F.Stake> { (address, feeBasis) ->\n        breadBox.changeValidator(\n            currencyId,\n            targetAddress = address,\n            feeEstimate = feeBasis,\n            phrase = checkNotNull(userManager.getPhrase())\n        )\n    }\n    addFunction<F.Unstake> { (feeBasis) ->\n        breadBox.changeValidator(\n            currencyId,\n            targetAddress = null,\n            feeEstimate = feeBasis,\n            phrase = checkNotNull(userManager.getPhrase())\n        )\n    }\n    addLatestValueCollector<F.PasteFromClipboard> { effect ->\n        val wallet = breadBox.wallet(currencyId).first()\n        val text = BRClipboardManager.getClipboard()\n        if (wallet.addressFor(text) == null) {\n            emit(E.OnAddressValidated(isValid = false))\n        } else if (text != effect.currentDelegateAddress) {\n            emit(E.OnAddressChanged(text))\n        }\n    }\n    addFunction<F.EstimateFee> { (address) ->\n        val wallet = breadBox.wallet(currencyId).first()\n        val attrs = wallet.transferAttributes\n            .filter { it.key == DELEGATION_OP }\n            .onEach { it.setValue(DELEGATION_OP_VALUE) }\n            .toSet()\n        val networkFee = wallet.walletManager.network.fees.minByOrNull {\n            it.confirmationTimeInMilliseconds.toLong()\n        }.run(::checkNotNull)\n        try {\n            val amount = Amount.create(0, wallet.unitForFee)\n            val coreAddress = if (address.isNullOrBlank()) {\n                wallet.target\n            } else {\n                checkNotNull(wallet.addressFor(address))\n            }\n            val feeBasis = wallet.estimateFee(coreAddress, amount, networkFee, attrs = attrs)\n            E.OnFeeUpdated(address, feeBasis, wallet.balance.toBigDecimal())\n        } catch (e: FeeEstimationError) {\n            E.OnTransferFailed(M.TransactionError.FeeEstimateFailed)\n        }\n    }\n    addFunctionSync<F.LoadAuthenticationSettings> {\n        val isEnabled = isFingerPrintAvailableAndSetup(context) && BRSharedPrefs.sendMoneyWithFingerprint\n        E.OnAuthenticationSettingsUpdated(isEnabled)\n    }\n}\n\nprivate val stakedTransferStates = listOf(SUBMITTED, INCLUDED, PENDING, CREATED, SIGNED)\n\nprivate fun handleLoadAccount(\n    currencyId: String,\n    breadBox: BreadBox\n) = flowTransformer<F.LoadAccount, E> { effects ->\n    effects\n        .flatMapLatest { breadBox.wallet(currencyId) }\n        .dropWhile { it.walletManager.state != WalletManagerState.CONNECTED() }\n        .debounce(WALLET_UPDATE_DEBOUNCE)\n        .mapLatest { wallet ->\n            val currencyCode = wallet.currency.code\n            val transfer = wallet.transfers\n                .filter { stakedTransferStates.contains(it.state.type) }\n                .sortedByDescending { it.confirmation.orNull()?.confirmationTime ?: Date() }\n                .firstOrNull { transfer ->\n                    transfer.attributes.any {\n                        it.key.equals(DELEGATION_OP, true) ||\n                        it.key.equals(DELEGATE, true) ||\n                            (it.key.equals(UNSTAKE_KEY, true) &&\n                                it.value.or("").equals(UNSTAKE_VALUE, true))\n                    } && (transfer.confirmation.orNull()?.success ?: true)\n                }\n            val balance = wallet.balance.toBigDecimal()\n\n            when (transfer) {\n                null -> E.AccountUpdated.Unstaked(currencyCode, balance)\n                else -> {\n                    val address = transfer.target.orNull()?.toString() ?: ""\n                    val isConfirmed = transfer.confirmation.orNull()?.success ?: false\n                    val delegateAddress = transfer.attributes.find { it.key.equals(DELEGATE, true) }?.value?.or(address)\n                    val isStaked = delegateAddress != null\n                    if (isConfirmed) {\n                        if (isStaked) {\n                            E.AccountUpdated.Staked(currencyCode, delegateAddress!!, STAKED, balance)\n                        } else E.AccountUpdated.Unstaked(currencyCode, balance)\n                    } else {\n                        val state = if (isStaked) PENDING_STAKE else PENDING_UNSTAKE\n                        E.AccountUpdated.Staked(currencyCode, address, state, balance)\n                    }\n                }\n            }\n        }\n        .distinctUntilChanged()\n}\n\nprivate suspend fun BreadBox.changeValidator(\n    currencyId: String,\n    targetAddress: String? = null,\n    feeEstimate: TransferFeeBasis,\n    phrase: ByteArray\n): E {\n    val wallet = wallet(currencyId).first()\n    val amount = Amount.create(0, wallet.unit)\n    val attrs = wallet.transferAttributes\n        .filter { it.key.equals(DELEGATION_OP, true) }\n        .onEach { it.setValue(DELEGATION_OP_VALUE) }\n        .toSet()\n\n    return try {\n        // Estimate and submit transfer\n        val address = if (targetAddress == null) {\n            wallet.target // unstake\n        } else {\n            checkNotNull(wallet.addressFor(targetAddress))\n        }\n        val transfer = wallet.createTransfer(address, amount, feeEstimate, attrs).orNull()\n        checkNotNull(transfer) { "Failed to create transfer." }\n        wallet.walletManager.submit(transfer, phrase)\n        val transferHash = checkNotNull(transfer.hash.orNull()).toString()\n        // Await a result for the transfer\n        walletTransfer(wallet.currency.code, transferHash)\n            .mapNotNull { tx ->\n                when (checkNotNull(tx.state.type)) {\n                    // Ignore pre-submit states\n                    CREATED, SIGNED -> null\n                    INCLUDED, PENDING, SUBMITTED -> {\n                        val target = checkNotNull(tx.target.orNull()).toString()\n                        val balance = wallet.balance.toBigDecimal()\n                        E.AccountUpdated.Staked(wallet.currency.code, target, PENDING_STAKE, balance)\n                    }\n                    DELETED, FAILED -> {\n                        logError("Failed to submit transfer ${tx.state.failedError.orNull()}")\n                        E.OnTransferFailed(M.TransactionError.Unknown)\n                    }\n                }\n            }.first()\n    } catch (e: TransferSubmitError) {\n        logError("Failed to submit transfer", e)\n        E.OnTransferFailed(M.TransactionError.TransferFailed)\n    } catch (e: IllegalStateException) {\n        logError("Unexpected error when submitting transfer", e)\n        E.OnTransferFailed(M.TransactionError.Unknown)\n    }\n}\n', 'message': "Merge branch 'drew/DROID-1925' into 'develop'\n\nDROID-1925, DROID-1851: Track gifting and staking events\n\nCloses DROID-1925\n\nSee merge request breadwallet/breadwallet-android!1441", 'author_name': 'Drew Carlson', 'lang': 'kt'}