Updated on 2026-08-14
This commit is contained in:
commit
22fc180d9e
17 changed files with 142 additions and 28 deletions
|
|
@ -261,7 +261,10 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.Onboarding.Mode.UpgradeHotWallet ->
|
||||
OnboardingEntryComponent.Mode.UpgradeHotWallet(mode.userWalletId)
|
||||
is AppRoute.Onboarding.Mode.AddressSync ->
|
||||
OnboardingEntryComponent.Mode.AddressSync(mode.userWalletId)
|
||||
OnboardingEntryComponent.Mode.AddressSync(
|
||||
mode.userWalletId,
|
||||
mode.isWalletStarted,
|
||||
)
|
||||
},
|
||||
),
|
||||
componentFactory = onboardingEntryComponentFactory,
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
data object RecreateWalletTwin : Mode() // reset twins
|
||||
data object ContinueFinalize : Mode() // continue finalize process (unfinished backup dialog)
|
||||
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode() // upgrade hot wallet
|
||||
data class AddressSync(val userWalletId: UserWalletId) : Mode()
|
||||
data class AddressSync(val userWalletId: UserWalletId, val isWalletStarted: Boolean) : Mode()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -239,7 +239,10 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
val route = if (onboardingV2FeatureToggles.isAddressSyncEnabled) {
|
||||
AppRoute.Onboarding(
|
||||
scanResponse = scanResponse,
|
||||
mode = AppRoute.Onboarding.Mode.AddressSync(userWalletId = userWallet.walletId),
|
||||
mode = AppRoute.Onboarding.Mode.AddressSync(
|
||||
userWalletId = userWallet.walletId,
|
||||
isWalletStarted = false,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
AppRoute.Wallet
|
||||
|
|
|
|||
|
|
@ -538,6 +538,7 @@ internal class CreateWalletStartModelTest {
|
|||
scanResponse = testScanResponse,
|
||||
mode = AppRoute.Onboarding.Mode.AddressSync(
|
||||
userWalletId = testUserWalletId,
|
||||
isWalletStarted = false,
|
||||
),
|
||||
)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ dependencies {
|
|||
implementation(projects.features.tester.api)
|
||||
implementation(projects.features.createWalletSelection.api)
|
||||
implementation(projects.features.tangempay.details.api)
|
||||
implementation(projects.features.onboardingV2.api)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.details.impl.R
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
|
@ -38,6 +39,7 @@ internal class UserWalletSaver @Inject constructor(
|
|||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val router: Router,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
) {
|
||||
|
||||
suspend fun scanAndSaveUserWallet(scope: CoroutineScope) {
|
||||
|
|
@ -55,7 +57,7 @@ internal class UserWalletSaver @Inject constructor(
|
|||
block = {
|
||||
scanResponse ?: return
|
||||
val userWallet = createUserWallet(scanResponse)
|
||||
saveWallet(userWallet)
|
||||
saveWallet(userWallet, scanResponse)
|
||||
},
|
||||
recover = {
|
||||
val message = it.message
|
||||
|
|
@ -69,7 +71,7 @@ internal class UserWalletSaver @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun Raise<Error>.saveWallet(userWallet: UserWallet) {
|
||||
private suspend fun Raise<Error>.saveWallet(userWallet: UserWallet, scanResponse: ScanResponse) {
|
||||
fold(
|
||||
block = {
|
||||
saveWalletUseCase(
|
||||
|
|
@ -92,7 +94,19 @@ internal class UserWalletSaver @Inject constructor(
|
|||
}
|
||||
},
|
||||
transform = {
|
||||
router.popTo<AppRoute.Wallet>()
|
||||
if (onboardingV2FeatureToggles.isAddressSyncEnabled) {
|
||||
router.push(
|
||||
AppRoute.Onboarding(
|
||||
scanResponse = scanResponse,
|
||||
mode = AppRoute.Onboarding.Mode.AddressSync(
|
||||
userWalletId = userWallet.walletId,
|
||||
isWalletStarted = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
router.popTo<AppRoute.Wallet>()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ interface OnboardingEntryComponent : ComposableContentComponent {
|
|||
data object RecreateWalletTwin : Mode()
|
||||
data object ContinueFinalize : Mode()
|
||||
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
|
||||
data class AddressSync(val userWalletId: UserWalletId) : Mode()
|
||||
data class AddressSync(val userWalletId: UserWalletId, val isWalletStarted: Boolean) : Mode()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnboardingEntryComponent>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.staking)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.hot.core)
|
||||
|
|
|
|||
|
|
@ -2,4 +2,6 @@ package com.tangem.features.onboarding.v2.addresssync
|
|||
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface AddressSyncComponent : ComposableContentComponent
|
||||
interface AddressSyncComponent : ComposableContentComponent {
|
||||
data class Params(val isWalletStarted: Boolean)
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
|||
internal class DefaultAddressSyncComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: MultiWalletChildParams,
|
||||
private val addressSyncParams: AddressSyncComponent.Params,
|
||||
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
|
||||
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
|
||||
) : AppComponentContext by appComponentContext, AddressSyncComponent {
|
||||
|
|
@ -83,7 +84,11 @@ internal class DefaultAddressSyncComponent(
|
|||
},
|
||||
)
|
||||
AddressSyncState.Exit -> LaunchedEffect(Unit) {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
if (addressSyncParams.isWalletStarted) {
|
||||
router.popTo(AppRoute.Wallet)
|
||||
} else {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,18 +8,24 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.settings.ShouldAskPermissionUseCase
|
||||
import com.tangem.domain.settings.ShouldShowAskBiometryUseCase
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.domain.tokens.MultiWalletAccountListFetcher
|
||||
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
|
||||
import com.tangem.features.onboarding.v2.addresssync.navigation.AddressSyncStep
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.MultiWalletInnerNavigationState
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -34,6 +40,9 @@ internal class AddressSyncModel @Inject constructor(
|
|||
private val multiWalletAccountListFetcher: MultiWalletAccountListFetcher,
|
||||
private val multiAccountListSupplier: MultiAccountListSupplier,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
||||
private val stakingIdFactory: StakingIdFactory,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -46,12 +55,10 @@ internal class AddressSyncModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
params.innerNavigation.update { innerNavigationState ->
|
||||
innerNavigationState.copy(
|
||||
stackSize = AddressSyncStep.ASK_BIOMETRY.pageNumber,
|
||||
stackMaxSize = ADDRESS_SYNC_MAX_STEPS,
|
||||
)
|
||||
}
|
||||
params.innerNavigation.value = MultiWalletInnerNavigationState(
|
||||
stackSize = AddressSyncStep.ASK_BIOMETRY.pageNumber,
|
||||
stackMaxSize = ADDRESS_SYNC_MAX_STEPS,
|
||||
)
|
||||
modelScope.launch {
|
||||
trySkippingScreen(AddressSyncStep.ASK_BIOMETRY)
|
||||
}
|
||||
|
|
@ -152,11 +159,41 @@ internal class AddressSyncModel @Inject constructor(
|
|||
)
|
||||
TangemLogger.e("Failed to derive public keys", throwable)
|
||||
},
|
||||
ifRight = { state.value = AddressSyncState.Exit },
|
||||
ifRight = {
|
||||
listOf(
|
||||
launch { fetchNetworks(cryptoCurrencies) },
|
||||
launch { fetchStaking(cryptoCurrencies) },
|
||||
).joinAll()
|
||||
state.value = AddressSyncState.Exit
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNetworks(cryptoCurrencies: List<CryptoCurrency>) {
|
||||
multiNetworkStatusFetcher.invoke(
|
||||
MultiNetworkStatusFetcher.Params(
|
||||
userWalletId = walletId,
|
||||
networks = cryptoCurrencies.map(CryptoCurrency::network).toSet(),
|
||||
),
|
||||
)
|
||||
.onLeft { TangemLogger.e("Unable to fetch networks: $it") }
|
||||
}
|
||||
|
||||
private suspend fun fetchStaking(cryptoCurrencies: List<CryptoCurrency>) {
|
||||
val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) {
|
||||
stakingIdFactory.create(userWalletId = walletId, cryptoCurrency = it).getOrNull()
|
||||
}
|
||||
|
||||
multiStakingBalanceFetcher(
|
||||
params = MultiStakingBalanceFetcher.Params(
|
||||
userWalletId = walletId,
|
||||
stakingIds = stakingIds,
|
||||
),
|
||||
)
|
||||
.onLeft { TangemLogger.e("Unable to fetch yield balances: $it") }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ADDRESS_SYNC_MAX_STEPS = 3
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,10 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
is Mode.UpgradeHotWallet -> OnboardingMultiWalletComponent.Mode.UpgradeHotWallet(
|
||||
userWalletId = mode.userWalletId,
|
||||
)
|
||||
is Mode.AddressSync -> OnboardingMultiWalletComponent.Mode.AddressSync(mode.userWalletId)
|
||||
is Mode.AddressSync -> OnboardingMultiWalletComponent.Mode.AddressSync(
|
||||
mode.userWalletId,
|
||||
mode.isWalletStarted,
|
||||
)
|
||||
else -> error("Incorrect onboarding type")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavi
|
|||
data object AddBackup : Mode()
|
||||
data object ContinueFinalize : Mode()
|
||||
data class UpgradeHotWallet(val userWalletId: UserWalletId) : Mode()
|
||||
data class AddressSync(val userWalletId: UserWalletId) : Mode()
|
||||
data class AddressSync(val userWalletId: UserWalletId, val isWalletStarted: Boolean) : Mode()
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnboardingMultiWalletComponent>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation
|
|||
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.onboarding.v2.addresssync.AddressSyncComponent
|
||||
import com.tangem.features.onboarding.v2.addresssync.DefaultAddressSyncComponent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
|
|
@ -192,12 +193,18 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
|
|||
onBack = { model.onBack() },
|
||||
onEvent = ::handleFinalizeComponentEvent,
|
||||
)
|
||||
AddressSync -> DefaultAddressSyncComponent(
|
||||
appComponentContext = childContext,
|
||||
params = childParams,
|
||||
askBiometryComponentFactory = askBiometryComponentFactory,
|
||||
pushNotificationsComponentFactory = pushNotificationsComponentFactory,
|
||||
)
|
||||
AddressSync -> {
|
||||
val mode = params.mode as OnboardingMultiWalletComponent.Mode.AddressSync
|
||||
DefaultAddressSyncComponent(
|
||||
appComponentContext = childContext,
|
||||
params = childParams,
|
||||
addressSyncParams = AddressSyncComponent.Params(
|
||||
isWalletStarted = mode.isWalletStarted,
|
||||
),
|
||||
askBiometryComponentFactory = askBiometryComponentFactory,
|
||||
pushNotificationsComponentFactory = pushNotificationsComponentFactory,
|
||||
)
|
||||
}
|
||||
Done -> error("Unexpected Done state")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@ import com.tangem.domain.account.models.AccountList
|
|||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.settings.ShouldAskPermissionUseCase
|
||||
import com.tangem.domain.settings.ShouldShowAskBiometryUseCase
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||
import com.tangem.domain.tokens.MultiWalletAccountListFetcher
|
||||
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
|
||||
import com.tangem.features.onboarding.v2.TitleProvider
|
||||
|
|
@ -42,6 +45,9 @@ internal class AddressSyncModelTest {
|
|||
private val multiAccountListSupplier: MultiAccountListSupplier = mockk()
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase = mockk()
|
||||
private val paramsContainer: ParamsContainer = mockk()
|
||||
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk()
|
||||
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk()
|
||||
private val stakingIdFactory: StakingIdFactory = mockk()
|
||||
private val testInnerNavigation = MutableStateFlow(
|
||||
value = MultiWalletInnerNavigationState(
|
||||
stackSize = 0,
|
||||
|
|
@ -54,7 +60,10 @@ internal class AddressSyncModelTest {
|
|||
every { innerNavigation } returns testInnerNavigation
|
||||
every { parentParams } returns mockk {
|
||||
every { titleProvider } returns this@AddressSyncModelTest.titleProvider
|
||||
every { mode } returns OnboardingMultiWalletComponent.Mode.AddressSync(walletId)
|
||||
every { mode } returns OnboardingMultiWalletComponent.Mode.AddressSync(
|
||||
userWalletId = walletId,
|
||||
isWalletStarted = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +245,11 @@ internal class AddressSyncModelTest {
|
|||
|
||||
@Test
|
||||
fun `GIVEN success state WHEN Sync THEN state becomes Exit`() = runTest {
|
||||
val currencies = listOf<CryptoCurrency>(mockk(), mockk(), mockk())
|
||||
val currencies = listOf<CryptoCurrency>(
|
||||
mockk { every { network } returns mockk() },
|
||||
mockk { every { network } returns mockk() },
|
||||
mockk { every { network } returns mockk() },
|
||||
)
|
||||
every { multiAccountListSupplier() } returns flowOf(
|
||||
listOf(
|
||||
AccountList.empty(
|
||||
|
|
@ -246,6 +259,11 @@ internal class AddressSyncModelTest {
|
|||
),
|
||||
)
|
||||
coEvery { derivePublicKeysUseCase(walletId, currencies) } returns Either.Right(Unit)
|
||||
coEvery { multiNetworkStatusFetcher.invoke(any()) } returns Either.Right(Unit)
|
||||
coEvery {
|
||||
stakingIdFactory.create(userWalletId = walletId, cryptoCurrency = any())
|
||||
} returns Either.Right(mockk())
|
||||
coEvery { multiStakingBalanceFetcher(any()) } returns Either.Right(Unit)
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
|
@ -254,6 +272,8 @@ internal class AddressSyncModelTest {
|
|||
advanceUntilIdle()
|
||||
|
||||
coVerify { derivePublicKeysUseCase(walletId, currencies) }
|
||||
coVerify { multiNetworkStatusFetcher.invoke(any()) }
|
||||
coVerify { multiStakingBalanceFetcher(any()) }
|
||||
Assertions.assertEquals(AddressSyncState.Exit, model.state.value)
|
||||
}
|
||||
|
||||
|
|
@ -309,6 +329,9 @@ internal class AddressSyncModelTest {
|
|||
multiWalletAccountListFetcher = multiWalletAccountListFetcher,
|
||||
multiAccountListSupplier = multiAccountListSupplier,
|
||||
derivePublicKeysUseCase = derivePublicKeysUseCase,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
paramsContainer = paramsContainer,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ android {
|
|||
dependencies {
|
||||
implementation(projects.features.welcome.api)
|
||||
implementation(projects.features.wallet.api)
|
||||
implementation(projects.features.onboardingV2.api)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
|||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.NonBiometricUnlockWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.features.wallet.utils.UserWalletsFetcher
|
||||
import com.tangem.features.welcome.impl.ui.state.WelcomeUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -54,9 +55,10 @@ internal class WelcomeModel @Inject constructor(
|
|||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
|
||||
private val hotWalletRestrictionManager: HotWalletRestrictionManager,
|
||||
hotWalletRestrictionManager: HotWalletRestrictionManager,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
val uiState: StateFlow<WelcomeUM>
|
||||
|
|
@ -215,7 +217,18 @@ internal class WelcomeModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
.onRight {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
val route = if (onboardingV2FeatureToggles.isAddressSyncEnabled) {
|
||||
AppRoute.Onboarding(
|
||||
scanResponse = scanResponse,
|
||||
mode = AppRoute.Onboarding.Mode.AddressSync(
|
||||
userWalletId = userWallet.walletId,
|
||||
isWalletStarted = false,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
AppRoute.Wallet
|
||||
}
|
||||
router.replaceAll(route)
|
||||
}
|
||||
},
|
||||
onCancel = {},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue