Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="80dp"
android:viewportWidth="80"
android:viewportHeight="80">
<path
android:pathData="M26.8975 47.0833 C27.3464 45.955 27.7139 44.7969 28 43.6092 C28.2861 42.4208 28.4292 41.2178 28.4292 40 C28.4292 38.7822 28.2947 37.5708 28.0258 36.3658 C27.7564 35.1603 27.3631 34.0106 26.8458 32.9167 L23.3017 34.2308 C23.7844 35.0897 24.1294 36.015 24.3367 37.0067 C24.5439 37.9978 24.6475 38.9956 24.6475 40 C24.6475 41.0044 24.5214 41.9969 24.2692 42.9775 C24.0169 43.9581 23.6944 44.9058 23.3017 45.8208 Z"
android:fillColor="#FFFFFF" />
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="80dp"
android:viewportWidth="80"
android:viewportHeight="80">
<path
android:pathData="M38.1025 51.7692 C38.8764 49.8992 39.4331 47.975 39.7725 45.9967 C40.1119 44.0183 40.2817 42.02 40.2817 40.0017 C40.2817 37.9833 40.1119 35.9933 39.7725 34.0317 C39.4331 32.0694 38.8764 30.1636 38.1025 28.3142 L34.5067 29.5642 C35.2328 31.2008 35.7467 32.8983 36.0483 34.6567 C36.3494 36.4156 36.5 38.1967 36.5 40 C36.5 41.8033 36.3408 43.5928 36.0225 45.3683 C35.7042 47.1439 35.1989 48.8608 34.5067 50.5192 Z"
android:fillColor="#FFFFFF" />
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="80dp"
android:viewportWidth="80"
android:viewportHeight="80">
<path
android:pathData="M49.0192 56.3717 C50.0919 53.7572 50.8794 51.0822 51.3817 48.3467 C51.8833 45.6111 52.1342 42.8289 52.1342 40 C52.1342 37.1711 51.8833 34.3889 51.3817 31.6533 C50.8794 28.9178 50.0919 26.2597 49.0192 23.6792 L45.4742 25.0958 C46.4575 27.4492 47.1831 29.8736 47.6508 32.3692 C48.1186 34.8647 48.3525 37.4058 48.3525 39.9925 C48.3525 42.5786 48.1186 45.1353 47.6508 47.6625 C47.1831 50.1897 46.4575 52.6205 45.4742 54.955 Z"
android:fillColor="#FFFFFF" />
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="80dp"
android:viewportWidth="80"
android:viewportHeight="80">
<path
android:pathData="M40.0058 71.6667 C35.6603 71.6667 31.5658 70.8356 27.7225 69.1733 C23.8792 67.5111 20.5183 65.2414 17.64 62.3642 C14.7617 59.4869 12.4908 56.1275 10.8275 52.2858 C9.16472 48.4447 8.33333 44.3514 8.33333 40.0058 C8.33333 35.6258 9.16444 31.5089 10.8267 27.655 C12.4889 23.8011 14.7586 20.4489 17.6358 17.5983 C20.5131 14.7478 23.8725 12.4908 27.7142 10.8275 C31.5553 9.16472 35.6486 8.33333 39.9942 8.33333 C44.3742 8.33333 48.4911 9.16444 52.345 10.8267 C56.1989 12.4889 59.5511 14.7447 62.4017 17.5942 C65.2522 20.4436 67.5092 23.7944 69.1725 27.6467 C70.8353 31.4989 71.6667 35.6147 71.6667 39.9942 C71.6667 44.3397 70.8356 48.4342 69.1733 52.2775 C67.5111 56.1208 65.2553 59.4817 62.4058 62.36 C59.5564 65.2383 56.2056 67.5092 52.3533 69.1725 C48.5011 70.8353 44.3853 71.6667 40.0058 71.6667Z M40 67.8842 C47.7606 67.8842 54.3481 65.1686 59.7625 59.7375 C65.1769 54.3058 67.8842 47.7267 67.8842 40 C67.8842 32.2394 65.1769 25.6519 59.7625 20.2375 C54.3481 14.8231 47.7606 12.1158 40 12.1158 C32.2733 12.1158 25.6942 14.8231 20.2625 20.2375 C14.8314 25.6519 12.1158 32.2394 12.1158 40 C12.1158 47.7267 14.8314 54.3058 20.2625 59.7375 C25.6942 65.1686 32.2733 67.8842 40 67.8842Z"
android:fillColor="#FFFFFF"
android:fillType="evenOdd" />
</vector>
2 changes: 2 additions & 0 deletions paymentsheet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ to be saved and used in future checkout sessions. -->
<string name="stripe_tap_to_add_card_added_title">Card added</string>
<!-- Button label for a button that launches a flow for tapping a card on the device -->
<string name="stripe_tap_to_add_card_button_label">Tap to add</string>
<!-- Instruction shown below the NFC coil animation while waiting for a card tap -->
<string name="stripe_nfc_scan_hold_card_behind_phone">Hold card behind phone</string>
<!-- Error message shown in Tap to Add when the card the user tapped was declined. -->
<string name="stripe_tap_to_add_card_declined_error_title">Card declined</string>
<!-- Error message shown to the user to try tapping their card again as their next action. -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
package com.stripe.android.common.nfcscan

import android.content.Intent
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.lifecycle.lifecycleScope
import com.stripe.android.common.nfcscan.ui.NfcScanningScreen
import com.stripe.android.common.nfcscan.ui.NfcScanningTheme
import com.stripe.android.uicore.utils.collectAsState
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import android.graphics.Color as AndroidColor

internal class NfcScanningActivity : AppCompatActivity() {
private val viewModel by viewModels<NfcScanningViewModel> {
NfcScanningViewModel.factory()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

lifecycleScope.launch {
viewModel.result.collectLatest { result ->
finishWithResult(result)
}
}

enableEdgeToEdge(
statusBarStyle = SystemBarStyle.light(
scrim = AndroidColor.TRANSPARENT,
darkScrim = AndroidColor.TRANSPARENT,
),
navigationBarStyle = SystemBarStyle.light(
scrim = AndroidColor.TRANSPARENT,
darkScrim = AndroidColor.TRANSPARENT,
),
)

setContent {
NfcScanningTheme {
val viewState by viewModel.viewState.collectAsState()

NfcScanningScreen(
state = viewState,
viewActionHandler = viewModel::handleViewAction,
)
}
}
}

override fun onStop() {
super.onStop()
if (!isFinishing && !isChangingConfigurations) {
setResult(NfcScanningContract.Result.Canceled)
finish()
finishWithResult(NfcScanningContract.Result.Canceled)
}
}

private fun setResult(result: NfcScanningContract.Result) {
private fun finishWithResult(result: NfcScanningContract.Result) {
setResult(
RESULT_OK,
Intent().putExtras(result.toBundle()),
)
finish()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.stripe.android.common.nfcscan

internal sealed interface NfcScanningViewAction {
data object Close : NfcScanningViewAction
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.stripe.android.common.nfcscan

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.stripe.android.common.nfcscan.tapzone.TapZoneResolver
import com.stripe.android.core.utils.requireApplication
import com.stripe.android.uicore.utils.stateFlowOf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

internal class NfcScanningViewModel @Inject constructor(
tapZoneResolver: TapZoneResolver,
) : ViewModel() {
private val tapZone = tapZoneResolver.get()

private val _result = MutableSharedFlow<NfcScanningContract.Result>()
val result = _result.asSharedFlow()

val viewState: StateFlow<NfcScanningViewState> = stateFlowOf(NfcScanningViewState(tapZone))

fun handleViewAction(viewAction: NfcScanningViewAction) {
when (viewAction) {
is NfcScanningViewAction.Close -> {
viewModelScope.launch {
_result.emit(NfcScanningContract.Result.Canceled)
}
}
}
}

companion object {
fun factory(): ViewModelProvider.Factory {
return viewModelFactory {
initializer<NfcScanningViewModel> {
DaggerNfcScanningViewModelComponent.factory()
.create(requireApplication())
.viewModel
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.stripe.android.common.nfcscan

import android.app.Application
import com.stripe.android.common.nfcscan.hardware.NfcHardwareDelegateModule
import com.stripe.android.common.nfcscan.tapzone.TapZoneModule
import dagger.BindsInstance
import dagger.Component
import dagger.Module

@Component(
modules = [NfcScanningViewModelModule::class]
)
internal interface NfcScanningViewModelComponent {
val viewModel: NfcScanningViewModel

@Component.Factory
interface Factory {
fun create(
@BindsInstance application: Application,
): NfcScanningViewModelComponent
}
}

@Module(
includes = [
NfcHardwareDelegateModule::class,
TapZoneModule::class,
]
)
internal interface NfcScanningViewModelModule
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.stripe.android.common.nfcscan

import com.stripe.android.common.nfcscan.tapzone.TapZone

internal data class NfcScanningViewState(
val tapZone: TapZone,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.stripe.android.common.nfcscan.tapzone

internal data class TapZone(
val xBias: Float,
val yBias: Float,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.stripe.android.common.nfcscan.tapzone

import dagger.Binds
import dagger.Module

@Module
internal interface TapZoneModule {
@Binds
fun bindsTapZoneResolver(resolver: DefaultTapZoneResolver): TapZoneResolver
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.stripe.android.common.nfcscan.tapzone

import javax.inject.Inject

internal interface TapZoneResolver {
fun get(): TapZone
}

internal class DefaultTapZoneResolver @Inject constructor() : TapZoneResolver {
override fun get(): TapZone {
return DEFAULT
}

private companion object {
val DEFAULT = TapZone(xBias = 0.5f, yBias = 0.5f)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.stripe.android.common.nfcscan.ui

import android.content.Context
import android.os.Build
import android.view.Surface
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext

internal enum class DeviceRotation {
Portrait,
LandscapeLeft,
LandscapeRight,
UpsideDown
}

@Composable
internal fun rememberDeviceRotation(): DeviceRotation {
val context = LocalContext.current

return remember(context) {
context.getDeviceRotation()
}
}

internal fun Context.getDeviceRotation(): DeviceRotation {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we annotate this as @VisibleForTesting?

@Suppress("DEPRECATION")
val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display
} else {
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.defaultDisplay
}

return when (display?.rotation) {
Surface.ROTATION_0 -> DeviceRotation.Portrait
Surface.ROTATION_90 -> DeviceRotation.LandscapeLeft
Surface.ROTATION_180 -> DeviceRotation.UpsideDown
Surface.ROTATION_270 -> DeviceRotation.LandscapeRight
else -> DeviceRotation.Portrait
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.stripe.android.common.nfcscan.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.stripe.android.uicore.stripeColors
import com.stripe.android.ui.core.R as PaymentsUiCoreR

@Composable
internal fun NfcCloseButton(
onPress: () -> Unit,
) {
val isDarkTheme = isSystemInDarkTheme()
val outline = if (isDarkTheme) {
Modifier.border(width = Dp.Hairline, color = Color.White.copy(alpha = 0.2f), shape = CircleShape)
} else {
Modifier.shadow(elevation = 3.dp, shape = CircleShape)
}

Box(
modifier = Modifier
.size(48.dp)
.then(outline)
.background(MaterialTheme.colors.surface, CircleShape)
.clip(CircleShape)
.clickable { onPress() },
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(PaymentsUiCoreR.drawable.stripe_ic_rounded_close),
contentDescription = "Cancel",
tint = MaterialTheme.stripeColors.appBarIcon,
modifier = Modifier.size(16.dp)
)
}
}
Loading
Loading