diff --git a/api/src/main/java/com/getcode/network/repository/PaymentRepository.kt b/api/src/main/java/com/getcode/network/repository/PaymentRepository.kt index c4b8e6264..0a74af9d2 100644 --- a/api/src/main/java/com/getcode/network/repository/PaymentRepository.kt +++ b/api/src/main/java/com/getcode/network/repository/PaymentRepository.kt @@ -59,13 +59,14 @@ class PaymentRepository @Inject constructor( messagingRepository.rejectLogin(rendezvousKey) } - fun attemptRequest(payload: CodePayload): Pair? { + suspend fun attemptRequest(payload: CodePayload): Pair? { val fiat = payload.fiat if (fiat == null) { Timber.d("payload does not contain Fiat value") return null } + exchange.fetchRatesIfNeeded() val rate = exchange.rateFor(fiat.currency) if (rate == null) { Timber.d("Unable to determine rate") diff --git a/app/src/main/java/com/getcode/util/DeeplinkHandler.kt b/app/src/main/java/com/getcode/util/DeeplinkHandler.kt index 0493da840..abd81ac53 100644 --- a/app/src/main/java/com/getcode/util/DeeplinkHandler.kt +++ b/app/src/main/java/com/getcode/util/DeeplinkHandler.kt @@ -36,8 +36,13 @@ class DeeplinkHandler @Inject constructor() { fun checkIntent(intent: Intent): Intent? { Timber.d("checking intent=${intent.data}") - handle(intent) ?: return null - return intent + val uri = intent.data ?: return null + return when (uri.deeplinkType) { + is Type.Cash, + is Type.Login, + is Type.Sdk -> intent + is Type.Unknown -> null + } } fun handle(intent: Intent? = debounceIntent): Pair>? { @@ -48,6 +53,7 @@ class DeeplinkHandler @Inject constructor() { } is Type.Cash -> { + Timber.d("cash=${type.link}") type to listOf(HomeScreen(cashLink = type.link)) } diff --git a/app/src/main/java/com/getcode/view/MainActivity.kt b/app/src/main/java/com/getcode/view/MainActivity.kt index 74dd7d11d..76b1c6729 100644 --- a/app/src/main/java/com/getcode/view/MainActivity.kt +++ b/app/src/main/java/com/getcode/view/MainActivity.kt @@ -84,7 +84,6 @@ class MainActivity : FragmentActivity() { handleUncaughtException() authManager.init(this) setFullscreen() - deeplinkHandler.debounceIntent = deeplinkHandler.checkIntent(intent) setContent { CompositionLocalProvider( @@ -98,6 +97,8 @@ class MainActivity : FragmentActivity() { CodeApp() } } + + deeplinkHandler.debounceIntent = intent } override fun onResume() { diff --git a/app/src/main/java/com/getcode/view/camera/KikCodeScannerView.kt b/app/src/main/java/com/getcode/view/camera/KikCodeScannerView.kt index bc44cf2e2..4d658764d 100644 --- a/app/src/main/java/com/getcode/view/camera/KikCodeScannerView.kt +++ b/app/src/main/java/com/getcode/view/camera/KikCodeScannerView.kt @@ -8,6 +8,7 @@ import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.functions.BiFunction import io.reactivex.rxjava3.processors.BehaviorProcessor +import kotlinx.coroutines.flow.Flow import org.kin.sdk.base.tools.Optional class KikCodeScannerView @JvmOverloads constructor( @@ -16,7 +17,7 @@ class KikCodeScannerView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { - private var previewing = false + var previewing = false private val cameraController: CameraController = LegacyCameraController(context) @@ -44,9 +45,10 @@ class KikCodeScannerView @JvmOverloads constructor( cameraController.startPreview() previewSizeSubscription = Flowable - .combineLatest(cameraController.previewSize(), onLayoutChangeSubject.distinctUntilChanged(), BiFunction { previewSize: Optional, _: Pair -> - previewSize - }) + .combineLatest( + cameraController.previewSize(), + onLayoutChangeSubject.distinctUntilChanged() + ) { previewSize: Optional, _: Pair -> previewSize } .filter { it.isPresent } .observeOn(UiThreadScheduler.uiThread()) .subscribe { diff --git a/app/src/main/java/com/getcode/view/camera/LegacyCameraController.kt b/app/src/main/java/com/getcode/view/camera/LegacyCameraController.kt index 7441231b2..0c1861fb0 100644 --- a/app/src/main/java/com/getcode/view/camera/LegacyCameraController.kt +++ b/app/src/main/java/com/getcode/view/camera/LegacyCameraController.kt @@ -6,7 +6,6 @@ import android.graphics.Rect import android.hardware.Camera import android.media.CamcorderProfile import android.media.MediaRecorder -import android.util.Log import android.view.Surface import android.view.SurfaceHolder import android.view.SurfaceView @@ -21,7 +20,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.kin.sdk.base.tools.Optional import timber.log.Timber -import java.lang.IllegalStateException import java.net.URL import kotlin.math.abs import kotlin.math.max @@ -562,7 +560,7 @@ class LegacyCameraController( return Single.fromCallable { val videoOutputUrl = videoOutputUrl val mediaRecorder = mediaRecorder - val recordingStart = recordingStartSubject.value!!.get()!! + val recordingStart = recordingStartSubject.value!!.get() this@LegacyCameraController.videoOutputUrl = null this@LegacyCameraController.mediaRecorder = null diff --git a/app/src/main/java/com/getcode/view/components/AuthCheck.kt b/app/src/main/java/com/getcode/view/components/AuthCheck.kt index 43cdd5234..fc7621483 100644 --- a/app/src/main/java/com/getcode/view/components/AuthCheck.kt +++ b/app/src/main/java/com/getcode/view/components/AuthCheck.kt @@ -23,9 +23,14 @@ import com.getcode.navigation.screens.LoginScreen import com.getcode.util.DeeplinkHandler import com.getcode.util.getActivity import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -50,23 +55,24 @@ fun AuthCheck( } LaunchedEffect(isAuthenticated) { - Timber.tag(AUTH_NAV).d("authenticated=$isAuthenticated") isAuthenticated?.let { authenticated -> - //Allow the seed input screen to complete and avoid - //premature navigation - if (currentRoute is AccessKeyLoginScreen) { - Timber.tag(AUTH_NAV).d("No navigation within seed input") - return@LaunchedEffect - } - if (currentRoute is LoginGraph) { - Timber.tag(AUTH_NAV).d("No navigation within account creation and onboarding") - } else if (!deeplinkRouted) { - if (authenticated) { - Timber.tag(AUTH_NAV).d("Navigating to home") - onNavigate(listOf(HomeScreen()), false) - } else { - Timber.tag(AUTH_NAV).d("Navigating to login") - onNavigate(listOf(LoginScreen()), false) + if (!deeplinkRouted) { + // Allow the seed input screen to complete and avoid + // premature navigation + if (currentRoute is AccessKeyLoginScreen) { + log("No navigation within seed input") + return@LaunchedEffect + } + if (currentRoute is LoginGraph) { + log("No navigation within account creation and onboarding") + } else { + if (authenticated) { + log("Navigating to home") + onNavigate(listOf(HomeScreen()), false) + } else { + log("Navigating to login") + onNavigate(listOf(LoginScreen()), false) + } } } else { deeplinkRouted = false @@ -81,23 +87,21 @@ fun AuthCheck( val scope = this deeplinkHandler.intent .filterNotNull() - .onEach { - deeplinkRouted = false - Timber.tag(AUTH_NAV).d("intent=${it.data}") - } + .distinctUntilChanged() .mapNotNull { deeplinkHandler.handle() } - .onEach { Timber.d("${it.first}") } - .filter { - if (it.first is DeeplinkHandler.Type.Cash) { - return@filter SessionManager.isAuthenticated() == true + .flatMapLatest { combine(flowOf(it), SessionManager.authState) { a, b -> a to b } } + .filter { (data, authState) -> + if (data.first is DeeplinkHandler.Type.Cash || data.first is DeeplinkHandler.Type.Sdk) { + return@filter authState.isAuthenticated == true } return@filter true } - .mapNotNull { (type, screens) -> + .mapNotNull { (data, auth) -> + val (type, screens) = data if (type is DeeplinkHandler.Type.Login) { - if (SessionManager.isAuthenticated() == true) { + if (auth.isAuthenticated == true) { val entropy = (screens.first() as? LoginScreen)?.seed - Timber.d("showing logout confirm") + log("showing logout confirm") if (entropy != null) { deeplinkRouted = true context.getActivity()?.intent = null @@ -107,7 +111,7 @@ fun AuthCheck( entropyB64 = entropy, onSwitchAccounts = { scope.launch { - delay(300) + delay(300) // wait for dismiss onSwitchAccounts(it) deeplinkRouted = false } @@ -124,15 +128,17 @@ fun AuthCheck( } .onEach { screens -> deeplinkRouted = true + log("navigated") onNavigate(screens, true) deeplinkHandler.debounceIntent = null context.getActivity()?.intent = null - deeplinkRouted = false } .launchIn(this) } } +private fun log(message: String) = Timber.tag(AUTH_NAV).d(message) + private fun showLogoutMessage( context: Context, entropyB64: String, @@ -142,7 +148,6 @@ private fun showLogoutMessage( BottomBarManager.showMessage( BottomBarManager.BottomBarMessage( title = context.getString(R.string.subtitle_logoutAndLoginConfirmation), - subtitle = "", positiveText = context.getString(R.string.action_logIn), negativeText = context.getString(R.string.action_cancel), isDismissible = false, diff --git a/app/src/main/java/com/getcode/view/main/account/AccountHome.kt b/app/src/main/java/com/getcode/view/main/account/AccountHome.kt index 0cf671afa..b28414947 100644 --- a/app/src/main/java/com/getcode/view/main/account/AccountHome.kt +++ b/app/src/main/java/com/getcode/view/main/account/AccountHome.kt @@ -49,6 +49,7 @@ import com.getcode.theme.CodeTheme import com.getcode.theme.White10 import com.getcode.util.getActivity import com.getcode.util.rememberedClickable +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable @@ -80,8 +81,11 @@ fun AccountHome( positiveText = context.getString(R.string.action_logout), negativeText = context.getString(R.string.action_cancel), onPositive = { - context.getActivity()?.let { - viewModel.logout(it) + composeScope.launch { + delay(150) // wait for dismiss + context.getActivity()?.let { + viewModel.logout(it) + } } } ) diff --git a/app/src/main/java/com/getcode/view/main/home/HomeScan.kt b/app/src/main/java/com/getcode/view/main/home/HomeScan.kt index 07a0aae2f..f9193d5de 100644 --- a/app/src/main/java/com/getcode/view/main/home/HomeScan.kt +++ b/app/src/main/java/com/getcode/view/main/home/HomeScan.kt @@ -160,7 +160,6 @@ private fun HomeScan( var kikCodeScannerView: KikCodeScannerView? by remember { mutableStateOf(null) } - val focusManager = LocalFocusManager.current LaunchedEffect(dataState.isCameraScanEnabled) { if (dataState.isCameraScanEnabled) { @@ -168,24 +167,31 @@ private fun HomeScan( } } - LaunchedEffect(homeViewModel) { - if (!deepLink.isNullOrBlank()) { - homeViewModel.onCashLinkGrabStart() - } - if (!deepLink.isNullOrBlank() && !dataState.isDeepLinkHandled) { - homeViewModel.openCashLink(deepLink) + LaunchedEffect(kikCodeScannerView?.previewing, deepLink, requestPayload) { + if (kikCodeScannerView?.previewing == true) { + if (!deepLink.isNullOrBlank()) { + delay(1_000) + homeViewModel.openCashLink(deepLink) + } + + if (!requestPayload.isNullOrBlank()) { + delay(1_000) + homeViewModel.handlePaymentRequest(requestPayload) + } } + } - if (!requestPayload.isNullOrBlank()) { - delay(500) - homeViewModel.handlePaymentRequest(requestPayload) + LaunchedEffect(kikCodeScannerView?.previewing) { + val view = kikCodeScannerView ?: return@LaunchedEffect + if (view.previewing) { // kick off preview scanning once preview established + homeViewModel.startScan(view) } } fun startScanPreview() { val view = kikCodeScannerView ?: return + // establish preview view.startPreview() - homeViewModel.startScan(view) } fun stopScanPreview() { @@ -225,7 +231,7 @@ private fun HomeScan( update = { } ) }, - isCameraReady = dataState.isCameraReady, + isCameraReady = kikCodeScannerView?.previewing == true, showBottomSheet = { showBottomSheet(it) } ) @@ -270,11 +276,6 @@ private fun HomeScan( homeViewModel.stopScan() } } - LaunchedEffect(dataState.isCameraScanEnabled) { - if (dataState.isCameraScanEnabled) { - startScanPreview() - } - } val context = LocalContext.current as Activity LaunchedEffect(dataState.billState.bill) { @@ -301,7 +302,6 @@ private fun BillContainer( val launcher = getPermissionLauncher(onPermissionResult) val context = LocalContext.current as Activity - val composeScope = rememberCoroutineScope() SideEffect { PermissionCheck.requestPermission( @@ -322,13 +322,9 @@ private fun BillContainer( if (dataState.isCameraPermissionGranted == true || dataState.isCameraPermissionGranted == null) { scannerView() - var show by rememberSaveable { - mutableStateOf(true) - } - AnimatedVisibility( modifier = Modifier.fillMaxSize(), - visible = show, + visible = !isCameraReady, enter = fadeIn( animationSpec = tween(AnimationUtils.animationTime) ), @@ -339,11 +335,6 @@ private fun BillContainer( .fillMaxSize() .background(CodeTheme.colors.background) ) - LaunchedEffect(isCameraReady) { - if (isCameraReady) { - show = false - } - } } } else { PermissionsBlockingView( diff --git a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt index 8ef955247..3ac0cb8bd 100644 --- a/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/getcode/view/main/home/HomeViewModel.kt @@ -126,13 +126,10 @@ data class HomeUiModel( val showNetworkOffline: Boolean = false, val giveRequestsEnabled: Boolean = false, val isCameraScanEnabled: Boolean = true, - val isCameraReady: Boolean = false, - val selectedBottomSheet: HomeBottomSheet? = null, val presentationStyle: PresentationStyle = PresentationStyle.Hidden, val billState: BillState = BillState.Default, val restrictionType: RestrictionType? = null, val isRemoteSendLoading: Boolean = false, - val isDeepLinkHandled: Boolean = false, ) sealed interface HomeEvent { @@ -526,8 +523,8 @@ class HomeViewModel @Inject constructor( } } - private fun attemptPayment(payload: CodePayload, request: DeepLinkPaymentRequest? = null) { - val (amount, p) = paymentRepository.attemptRequest(payload) ?: return + private fun attemptPayment(payload: CodePayload, request: DeepLinkPaymentRequest? = null) = viewModelScope.launch { + val (amount, p) = paymentRepository.attemptRequest(payload) ?: return@launch BottomBarManager.clear() presentRequest(amount = amount, payload = p, request = request) @@ -829,10 +826,6 @@ class HomeViewModel @Inject constructor( }) } - fun onCashLinkGrabStart() { - analytics.cashLinkGrabStart() - } - fun startScan(view: KikCodeScannerView) { if (cameraStarted) { return @@ -877,6 +870,9 @@ class HomeViewModel @Inject constructor( view: KikCodeScannerView, scanner: KikCodeScanner ): Flowable { + if (!view.previewing) { + view.startPreview() + } return Single.defer { view.previewSize() .subscribeOn(Schedulers.computation()) @@ -886,13 +882,6 @@ class HomeViewModel @Inject constructor( .filter { it.isPresent } .map { it.get()!! } .firstOrError() - .doOnSuccess { - if (!uiFlow.value.isCameraReady) { - uiFlow.update { - it.copy(isCameraReady = true) - } - } - } .flatMap { previewSize: CameraController.PreviewSize? -> view.getPreviewBuffer() .flatMap { imageData -> @@ -1045,9 +1034,18 @@ class HomeViewModel @Inject constructor( } fun openCashLink(deepLink: String?) { + Timber.d("openCashLink: deep link=$deepLink") val cashLink = deepLink?.trim()?.replace("\n", "") ?: return - if (cashLink.isEmpty()) return - if (openedLinks.contains(cashLink)) return + if (cashLink.isEmpty()) { + Timber.d("cash link empty") + return + } + if (openedLinks.contains(cashLink)) { + Timber.d("cash link already opened in session") + return + } + + analytics.cashLinkGrabStart() openedLinks.add(cashLink) @@ -1060,7 +1058,7 @@ class HomeViewModel @Inject constructor( viewModelScope.launch { withContext(Dispatchers.IO) { withTimeout(15000) { - balanceController.fetchBalance().blockingAwait() + balanceController.fetchBalanceSuspend() try { //Get the amount on the card val amount = client.receiveRemoteSuspend(giftCardAccount) @@ -1082,15 +1080,14 @@ class HomeViewModel @Inject constructor( vibrate = true ) removeLinkWithDelay(cashLink) - setDeepLinkHandled() } } catch (ex: Exception) { + ex.printStackTrace() Timber.e(ex) when (ex) { is RemoteSendException -> { onRemoteSendError(ex) removeLinkWithDelay(cashLink) - setDeepLinkHandled(withDelay = 0) } else -> { @@ -1133,13 +1130,6 @@ class HomeViewModel @Inject constructor( analytics.onAppStarted() } - private fun setDeepLinkHandled(withDelay: Long = 2000) { - CoroutineScope(Dispatchers.IO).launch { - delay(withDelay) - uiFlow.update { it.copy(isDeepLinkHandled = true) } - } - } - companion object { private val openedLinks = mutableListOf() private val scannedRendezvous = mutableListOf()