Skip to content

Commit fc309b3

Browse files
committed
Shop Pay Confirmation
1 parent b4ceed8 commit fc309b3

File tree

9 files changed

+578
-0
lines changed

9 files changed

+578
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.stripe.android.paymentelement.confirmation.shoppay
2+
3+
import androidx.activity.result.ActivityResultCaller
4+
import com.stripe.android.core.strings.resolvableString
5+
import com.stripe.android.paymentelement.confirmation.ConfirmationDefinition
6+
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
7+
import com.stripe.android.paymentelement.confirmation.intent.DeferredIntentConfirmationType
8+
import com.stripe.android.shoppay.ShopPayActivityResult
9+
import com.stripe.android.shoppay.ShopPayLauncher
10+
import javax.inject.Inject
11+
12+
internal class ShopPayConfirmationDefinition @Inject constructor(
13+
private val shopPayLauncher: ShopPayLauncher
14+
) : ConfirmationDefinition<ShopPayConfirmationOption, ShopPayLauncher, Unit, ShopPayActivityResult> {
15+
override val key = "ShopPay"
16+
17+
override fun option(confirmationOption: ConfirmationHandler.Option): ShopPayConfirmationOption? {
18+
return confirmationOption as? ShopPayConfirmationOption
19+
}
20+
21+
override fun toResult(
22+
confirmationOption: ShopPayConfirmationOption,
23+
confirmationParameters: ConfirmationDefinition.Parameters,
24+
deferredIntentConfirmationType: DeferredIntentConfirmationType?,
25+
result: ShopPayActivityResult
26+
): ConfirmationDefinition.Result {
27+
val error = Throwable("ShopPay is not supported yet")
28+
return ConfirmationDefinition.Result.Failed(
29+
cause = error,
30+
message = error.message.orEmpty().resolvableString,
31+
type = ConfirmationHandler.Result.Failed.ErrorType.Payment,
32+
)
33+
}
34+
35+
override fun createLauncher(
36+
activityResultCaller: ActivityResultCaller,
37+
onResult: (ShopPayActivityResult) -> Unit
38+
): ShopPayLauncher {
39+
return shopPayLauncher.apply {
40+
register(activityResultCaller, onResult)
41+
}
42+
}
43+
44+
override fun launch(
45+
launcher: ShopPayLauncher,
46+
arguments: Unit,
47+
confirmationOption: ShopPayConfirmationOption,
48+
confirmationParameters: ConfirmationDefinition.Parameters
49+
) {
50+
launcher.present(confirmationOption.checkoutUrl)
51+
}
52+
53+
override suspend fun action(
54+
confirmationOption: ShopPayConfirmationOption,
55+
confirmationParameters: ConfirmationDefinition.Parameters
56+
): ConfirmationDefinition.Action<Unit> {
57+
return ConfirmationDefinition.Action.Launch(
58+
launcherArguments = Unit,
59+
receivesResultInProcess = false,
60+
deferredIntentConfirmationType = null,
61+
)
62+
}
63+
64+
override fun unregister(launcher: ShopPayLauncher) {
65+
launcher.unregister()
66+
}
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.stripe.android.paymentelement.confirmation.shoppay
2+
3+
import com.stripe.android.paymentelement.confirmation.ConfirmationHandler
4+
import kotlinx.parcelize.Parcelize
5+
6+
@Parcelize
7+
data class ShopPayConfirmationOption(
8+
val checkoutUrl: String,
9+
) : ConfirmationHandler.Option
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.stripe.android.shoppay
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import androidx.activity.ComponentActivity
7+
import androidx.activity.compose.setContent
8+
import androidx.compose.foundation.layout.Box
9+
import androidx.compose.foundation.layout.Column
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.material.Button
14+
import androidx.compose.material.Icon
15+
import androidx.compose.material.IconButton
16+
import androidx.compose.material.Text
17+
import androidx.compose.runtime.Composable
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.res.painterResource
21+
import androidx.core.os.BundleCompat
22+
import androidx.core.os.bundleOf
23+
import com.stripe.android.common.ui.BottomSheetScaffold
24+
import com.stripe.android.common.ui.ElementsBottomSheetLayout
25+
import com.stripe.android.link.theme.AppBarHeight
26+
import com.stripe.android.paymentsheet.R
27+
import com.stripe.android.uicore.elements.bottomsheet.rememberStripeBottomSheetState
28+
29+
/**
30+
* Activity that handles Shop Pay authentication via WebView.
31+
*/
32+
internal class ShopPayActivity : ComponentActivity() {
33+
private var args: ShopPayArgs? = null
34+
35+
override fun onCreate(savedInstanceState: Bundle?) {
36+
super.onCreate(savedInstanceState)
37+
38+
args = intent.extras?.let { args ->
39+
BundleCompat.getParcelable(args, EXTRA_ARGS, ShopPayArgs::class.java)
40+
}
41+
42+
if (args == null) {
43+
dismissWithResult(ShopPayActivityResult.Failed(Throwable("No args")))
44+
return
45+
}
46+
47+
setContent {
48+
Content()
49+
}
50+
}
51+
52+
@Composable
53+
private fun Content() {
54+
val bottomSheetState = rememberStripeBottomSheetState()
55+
56+
ElementsBottomSheetLayout(
57+
state = bottomSheetState,
58+
onDismissed = {
59+
dismissWithResult(ShopPayActivityResult.Canceled)
60+
}
61+
) {
62+
BottomSheetScaffold(
63+
topBar = {
64+
Box(
65+
modifier = Modifier
66+
.fillMaxWidth()
67+
.height(AppBarHeight)
68+
) {
69+
IconButton(
70+
modifier = Modifier.align(Alignment.CenterStart),
71+
onClick = {
72+
dismissWithResult(ShopPayActivityResult.Canceled)
73+
}
74+
) {
75+
Icon(
76+
painter = painterResource(R.drawable.stripe_ic_paymentsheet_close),
77+
contentDescription = "Close"
78+
)
79+
}
80+
}
81+
},
82+
content = {
83+
Column(
84+
modifier = Modifier
85+
.fillMaxSize()
86+
) {
87+
Button(
88+
onClick = {
89+
dismissWithResult(ShopPayActivityResult.Completed("pm_1234"))
90+
}
91+
) {
92+
Text("Complete")
93+
}
94+
95+
Button(
96+
onClick = {
97+
dismissWithResult(ShopPayActivityResult.Canceled)
98+
}
99+
) {
100+
Text("Cancel")
101+
}
102+
}
103+
}
104+
)
105+
}
106+
}
107+
108+
private fun dismissWithResult(result: ShopPayActivityResult) {
109+
val bundle = bundleOf(
110+
ShopPayActivityContract.EXTRA_RESULT to result
111+
)
112+
setResult(RESULT_COMPLETE, Intent().putExtras(bundle))
113+
finish()
114+
}
115+
116+
companion object {
117+
internal const val EXTRA_ARGS = "shop_pay_args"
118+
internal const val RESULT_COMPLETE = 63636
119+
120+
internal fun createIntent(
121+
context: Context,
122+
args: ShopPayArgs
123+
): Intent {
124+
return Intent(context, ShopPayActivity::class.java)
125+
.putExtra(EXTRA_ARGS, args)
126+
}
127+
}
128+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.stripe.android.shoppay
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import androidx.activity.result.contract.ActivityResultContract
6+
import androidx.core.os.BundleCompat
7+
import javax.inject.Inject
8+
9+
internal class ShopPayActivityContract @Inject constructor() :
10+
ActivityResultContract<ShopPayActivityContract.Args, ShopPayActivityResult>() {
11+
12+
override fun createIntent(context: Context, input: Args): Intent {
13+
return ShopPayActivity.createIntent(context, ShopPayArgs(input.checkoutUrl))
14+
}
15+
16+
override fun parseResult(resultCode: Int, intent: Intent?): ShopPayActivityResult {
17+
val result = intent?.extras?.let {
18+
BundleCompat.getParcelable(it, EXTRA_RESULT, ShopPayActivityResult::class.java)
19+
}
20+
return result ?: ShopPayActivityResult.Failed(Throwable("No result"))
21+
}
22+
23+
data class Args(val checkoutUrl: String)
24+
25+
companion object {
26+
internal const val EXTRA_RESULT = "com.stripe.android.shoppay.ShopPayActivityContract.extra_result"
27+
}
28+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.stripe.android.shoppay
2+
3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
5+
6+
internal sealed interface ShopPayActivityResult : Parcelable {
7+
@Parcelize
8+
data class Completed(
9+
val shopPaymentMethodId: String,
10+
) : ShopPayActivityResult
11+
12+
@Parcelize
13+
data object Canceled : ShopPayActivityResult
14+
15+
@Parcelize
16+
data class Failed(val error: Throwable) : ShopPayActivityResult
17+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.stripe.android.shoppay
2+
3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
5+
6+
@Parcelize
7+
internal data class ShopPayArgs(
8+
val email: String,
9+
) : Parcelable
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.stripe.android.shoppay
2+
3+
import androidx.activity.result.ActivityResultCaller
4+
import androidx.activity.result.ActivityResultLauncher
5+
import androidx.activity.result.ActivityResultRegistry
6+
import javax.inject.Inject
7+
import javax.inject.Singleton
8+
9+
@Singleton
10+
internal class ShopPayLauncher @Inject internal constructor(
11+
private val shopPayActivityContract: ShopPayActivityContract
12+
) {
13+
14+
private var shopPayActivityResultLauncher:
15+
ActivityResultLauncher<ShopPayActivityContract.Args>? = null
16+
17+
fun register(
18+
activityResultRegistry: ActivityResultRegistry,
19+
callback: (ShopPayActivityResult) -> Unit,
20+
) {
21+
shopPayActivityResultLauncher = activityResultRegistry.register(
22+
"ShopPayLauncher",
23+
shopPayActivityContract,
24+
) { shopPayActivityResult ->
25+
handleActivityResult(shopPayActivityResult, callback)
26+
}
27+
}
28+
29+
fun register(
30+
activityResultCaller: ActivityResultCaller,
31+
callback: (ShopPayActivityResult) -> Unit,
32+
) {
33+
shopPayActivityResultLauncher = activityResultCaller.registerForActivityResult(
34+
shopPayActivityContract
35+
) { shopPayActivityResult ->
36+
handleActivityResult(shopPayActivityResult, callback)
37+
}
38+
}
39+
40+
private fun handleActivityResult(
41+
shopPayActivityResult: ShopPayActivityResult,
42+
nextStep: (ShopPayActivityResult) -> Unit
43+
) {
44+
when (shopPayActivityResult) {
45+
ShopPayActivityResult.Canceled -> Unit
46+
is ShopPayActivityResult.Completed -> Unit
47+
is ShopPayActivityResult.Failed -> Unit
48+
}
49+
nextStep(shopPayActivityResult)
50+
}
51+
52+
fun unregister() {
53+
shopPayActivityResultLauncher?.unregister()
54+
shopPayActivityResultLauncher = null
55+
}
56+
57+
/**
58+
* Launch the Link UI to process a payment.
59+
*
60+
* @param configuration The payment and customer settings
61+
*/
62+
fun present(
63+
checkoutUrl: String,
64+
) {
65+
val args = ShopPayActivityContract.Args(checkoutUrl)
66+
shopPayActivityResultLauncher?.launch(args)
67+
}
68+
}

0 commit comments

Comments
 (0)