将Stripe支付API集成到Jetpack Compose中

3

我无法弄清楚如何将Stripe API集成到Compose应用程序中。

这是Stripe提供的代码片段:

    class CheckoutActivity : AppCompatActivity() {
  lateinit var paymentSheet: PaymentSheet

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    paymentSheet = PaymentSheet(this, ::onPaymentSheetResult)
  }

  fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) {
    // implemented in the next steps
  }
}

在我的情况下,我不知道应该在compose代码中放置 paymentSheet = PaymentSheet(this, ::onPaymentSheetResult),因为它显示出以下信息:无法使用提供的参数调用以下任何函数

(ComponentActivity, PaymentSheetResultCallback) defined in com.stripe.android.paymentsheet.PaymentSheet

(Fragment, PaymentSheetResultCallback) defined in com.stripe.android.paymentsheet.PaymentSheet

请注意保留HTML标签,并使内容更加通俗易懂。
class MainActivity : ComponentActivity() {
    lateinit var paymentSheet: PaymentSheet
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PayTheme {
                LoginUi()
            }
        }

        fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) {
            // implemented in the next steps
        }
    }
}
3个回答

2
首先,您可以在 Github 上查看 Stripe 组合示例: stripe-android(ComposeExampleActivity.kt)

添加 stripe 依赖项

implementation "com.stripe:stripe-android:20.17.0"

在 Application 类中初始化 stripe PaymentConfiguration

@HiltAndroidApp
class BookstoreApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        PaymentConfiguration.init(applicationContext, BuildConfig.STRIPE_PUBLISHABLE_KEY)
    }
}

Stripe提供了许多实现应用程序中支付的方法。让我们考虑使用PaymentSheetContractPaymentLauncher进行付款确认。
示例#1:使用PaymentSheetContract确认付款 rememberLauncherForActivityResult() PaymentSheetContract()

PaymentScreen.kt(Compose)

@Composable
fun PaymentScreen(
    viewModel: PaymentViewModel = hiltViewModel()
) {
    val stripeLauncher = rememberLauncherForActivityResult(
        contract = PaymentSheetContract(),
        onResult = {
            viewModel.handlePaymentResult(it)
        }
    )
    val clientSecret by viewModel.clientSecret.collectAsStateWithLifecycle()
    clientSecret?.let {
        val args = PaymentSheetContract.Args.createPaymentIntentArgs(it)
        stripeLauncher.launch(args)
        viewModel.onPaymentLaunched()
    }
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Button(
            onClick = {
                viewModel.makePayment()
            }
        ) {
            Text(text = "Confirm payment")
        }
    }
}

PaymentViewModel.kt

@HiltViewModel
class PaymentViewModel @Inject constructor(
    private val repository: PaymentRepository
) : ViewModel() {
    private val _clientSecret = MutableStateFlow<String?>(null)
    val clientSecret = _clientSecret.asStateFlow()

    fun makePayment() {
        val paymentIntent = repository.createPaymentIntent()
        _clientSecret.update { paymentIntent.clientSecret }
    }

    fun onPaymentLaunched() {
        _clientSecret.update { null }
    }

    fun handlePaymentResult(result: PaymentSheetResult) {
        when(result) {
            PaymentSheetResult.Canceled -> TODO()
            PaymentSheetResult.Completed -> TODO()
            is PaymentSheetResult.Failed -> TODO()
        }
    }
}

示例2:使用PaymentLauncher确认付款

rememberLauncherForActivityResult() PaymentSheetContract()

PaymentScreen.kt(Compose)

@Composable
fun PaymentScreen(
    viewModel: PaymentViewModel = hiltViewModel()
) {
    val paymentLauncher = rememberPaymentLauncher(viewModel::handlePaymentResult)
    val confirmPaymentParams by viewModel.confirmPaymentParams.collectAsStateWithLifecycle()
    confirmPaymentParams?.let { payment ->
        paymentLauncher.confirm(payment)
        viewModel.onPaymentLaunched()
    }
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Button(
            onClick = {
                viewModel.makePayment()
            }
        ) {
            Text(text = "Confirm payment")
        }
    }
}

@Composable
fun rememberPaymentLauncher(
    callback: PaymentLauncher.PaymentResultCallback
): PaymentLauncher {
    val config = PaymentConfiguration.getInstance(LocalContext.current)
    return PaymentLauncher.rememberLauncher(
        publishableKey = config.publishableKey,
        stripeAccountId = config.stripeAccountId,
        callback = callback
    )
}

PaymentViewModel.kt

@HiltViewModel
class PaymentViewModel @Inject constructor(
    private val repository: PaymentRepository
) : ViewModel() {
    private val _confirmPaymentParams = MutableStateFlow<ConfirmPaymentIntentParams?>(null)
    val confirmPaymentParams = _confirmPaymentParams.asStateFlow()

    fun makePayment() {
        val paymentIntent = repository.createPaymentIntent()
        // For example, pay with hardcoded test card
        val configuration = ConfirmPaymentIntentParams.createWithPaymentMethodCreateParams(
            paymentMethodCreateParams = PaymentMethodCreateParams.create(
                card = PaymentMethodCreateParams.Card(
                    number = "4242424242424242",
                    expiryMonth = 1,
                    expiryYear = 24,
                    cvc = "111"
                )
            ),
            clientSecret = paymentIntent.clientSecret
        )
        _confirmPaymentParams.update { configuration }
    }

    fun onPaymentLaunched() {
        _confirmPaymentParams.update { null }
    }

    fun handlePaymentResult(result: PaymentResult) {
        when(result) {
            PaymentResult.Canceled -> TODO()
            PaymentResult.Completed -> TODO()
            is PaymentResult.Failed -> TODO()
        }
    }
}

数据层

支付意图 client_secret

请阅读Stripe的接受付款文档以更好地理解。 您也可以观看YouTube视频:如何在Android Studio 2022中集成Stripe

PaymentRepository.kt

class PaymentRepository @Inject constructor(
    private val stripeApiService: StripeApiService,
    private val paymentDao: PaymentDao
) {
    /*
        Create customer before payment (attach to app user)
     */
    suspend fun createCustomer() = withContext(Dispatchers.IO) {
        val customer = stripeApiService.createCustomer()
        // save customer in the database or preferences
        // customerId required to confirm payment
        paymentDao.insertCustomer(customer)
    }

    suspend fun refreshCustomerEphemeralKey() = withContext(Dispatchers.IO) {
        val customer = paymentDao.getCustomer()
        val key = stripeApiService.createEphemeralKey(customer.customerId)
        paymentDao.insertEphemeralKey(key)
    }

    suspend fun createPaymentIntent() = withContext(Dispatchers.IO) {
        val customer = paymentDao.getCustomer()
        refreshCustomerEphemeralKey()
        val paymentIntent = stripeApiService.createPaymentIntent(
            customerId = customer.customerId,
            amount = 1000,
            currency = "usd", // or your currency
            autoPaymentMethodsEnable = true
        )
        return@withContext paymentIntent
    }
}

StripeApiService.kt

private const val SECRET = BuildConfig.STRIPE_SECRET_KEY

interface StripeApiService {

    @Headers(
        "Authorization: Bearer $SECRET",
        "Stripe-Version: 2022-08-01"
    )
    @POST("v1/customers")
    suspend fun createCustomer() : CustomerApiModel

    @Headers(
        "Authorization: Bearer $SECRET",
        "Stripe-Version: 2022-08-01"
    )
    @POST("v1/ephemeral_keys")
    suspend fun createEphemeralKey(
        @Query("customer") customerId: String
    ): EphemeralKeyApiModel

    @Headers(
        "Authorization: Bearer $SECRET"
    )
    @POST("v1/payment_intents")
    suspend fun createPaymentIntent(
        @Query("customer") customerId: String,
        @Query("amount") amount: Int,
        @Query("currency") currency: String,
        @Query("automatic_payment_methods[enabled]") autoPaymentMethodsEnable: Boolean,
    ): PaymentIntentApiModel

}

1
抱歉回复晚了。我有一个关于“PaymentRepository”的问题,它是通过添加Stripe库来声明的,还是我应该自己声明,因为它目前未解决? - Tomee
1
@Tomee 当然,你应该创建自己的存储库。我已经添加了一个“PaymentRepository”的“示例代码”。 正如我在答案中所描述的那样,你应该只从后端请求付款意图。如果你开发一个示例项目,可以在Android应用程序中实现这个Stripe逻辑(例如在存储库中)。 - lincollincol

0

我发现设置 Android Activity 并从可组合函数启动是一种易于实现且有效的解决方案,适用于许多类型的第三方库,可以启动表或菜单。我所做的步骤是创建一个名为 StripePaymentSheetActivity.kt 的文件。

class StripePaymentSheetActivity(): ComponentActivity() {
    lateinit var paymentSheet: PaymentSheet
    lateinit var customerConfig: PaymentSheet.CustomerConfiguration
    lateinit var paymentSetupIntentClientSecret: String
    lateinit var paymentIntentClientSecret: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        paymentSheet = PaymentSheet(this, ::onPaymentSheetResult)
        // you can retrieve strings from jetpack compose with putExtra
        val customerId = getIntent().getStringExtra("customerId")
        // you need to make a server call here to get your clientSecret
        // and ephemeralKey here
        resp = YourServerRequestToGetClientSecretAndEpheralKeyHere()

        // secret will be in the form seti_*****
        paymentSetupIntentClientSecret = resp.clientSecret
        // Switch for making a payment and will be in the form pi_****
        // paymentIntentClientSecret = resp.clientSecret
        customerConfig = PaymentSheet.CustomerConfiguration(
            id = customerId,
            ephemeralKeySecret = resp.ephemeralKey,
        )
        // publishableKey is your pk_test or pk_live key you find on stripe
        PaymentConfiguration.init(this, publishableKey)
        // this where it is a little different from the stipe documentation.
        // This method onCreate will be hit once we launch the activity but 
        // there is no way to call a function after launching an activity
        // in compose so we will just do it when this is activity is created.
        presentPaymentSheet()
    }

    fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) {
        Log.d("PaymentSheet", "PaymentSheet result: $paymentSheetResult")
        when(paymentSheetResult) {
            is PaymentSheetResult.Canceled -> {
                // this registers the callback into the compose function
                setResult(RESULT_CANCELED)
                finish()
            }
            is PaymentSheetResult.Failed -> {
                // this registers the callback into the compose function
                setResult(RESULT_ERROR)
                finish()
            }
            is PaymentSheetResult.Completed -> {
                // this registers the callback into the compose function
                setResult(RESULT_OK)
                finish()
            }
        }
    }

    fun presentPaymentSheet() {
        // this is for storing payment methods like when you
        // have an app that has continuous payments like Uber. 
        paymentSheet.presentWithSetupIntent(
            paymentSetupIntentClientSecret,
            PaymentSheet.Configuration(
                merchantDisplayName = "Your Merchant Name",
                customer = customerConfig,
                // Set `allowsDelayedPaymentMethods` to true if your business
                // can handle payment methods that complete payment after a delay, like SEPA Debit and Sofort.
                allowsDelayedPaymentMethods = true
            )
        )
        // or you can use this with a payment intent
//        paymentSheet.presentWithPaymentIntent(
//            paymentIntentClientSecret,
//            PaymentSheet.Configuration(
//                merchantDisplayName = "Your Company Name",
//                customer = customerConfig,
//                allowsDelayedPaymentMethods = true
//            )
//        )
    }

    override public fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
    }
}

然后在您的compose函数中添加

@Composable
fun YourComposeFunctionHere(){
    var intent = Intent(user.context, StripePaymentSheetActivity::class.java)
    // you can send strings into your activity with something like this
    // customer id should be in the form of cus_*****
    intent.putExtra("customerId", customerId)

    // make an activity launcher that has a callback for when the activity
    // has a result ready
    val stripeLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult()
    ){  result: ActivityResult  ->
        if(result.resultCode == RESULT_OK) {
             // do what you need to do if its ok
        }else if(result.resultCode == RESULT_CANCELED){
             // do what you need to do if its canceled 
        }else if(result.resultCode == RESULT_ERROR){
             // do what you need to do if its canceled 
        }else{
             // you shouldn't get here
        }
    }
    // GUI Code here
        TextButton(
            // this will launch StripePaymentSheetActivity
            // and then the method onCreate will run presentPaymentSheet()
            onClick = { stripeLauncher.launch(intent) },
            ),
        ) {
            Text(text = "Pay")
        }

}

最后将您的活动添加到AndroidManifest.xml中

        <activity android:name="com.example.myapplication.StripePaymentSheetActivity"
            android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation" />

请注意,您需要将特定路径添加到StripePaymentSheetActivity.kt中。
此方法使您能够与推荐的Stripe文档保持极其接近,并且还是启动其他活动(如Facebook登录视图或Braintree支付表)的良好结构。

0
首先要赞扬上面@lincollincol提供的答案,非常详细和高效。
但是,从Stripe SDK版本"20.26.0"开始,通过接受的答案中的两种方法不建议使用,也不需要compose中这样做。
因为PaymentSheetContractPaymentLauncher已经被弃用,并将在将来从SDK中删除。
相反,现在我们可以直接在Compose方法中使用rememberPaymentSheet()。 以下是具体步骤: 步骤1:在我的Compose屏幕中初始化"PaymentSheet"并设置Checkout ViewModel
@Composable
fun CheckoutScreen(
    checkoutVM: CheckoutVM = hiltViewModel(),
) {
val context = LocalContext.current
val checkoutState = checkoutVM.checkoutState.collectAsStateWithLifecycle()

val paymentSheet =
    rememberPaymentSheet(paymentResultCallback = checkoutVM::onPaymentSheetResult)

LaunchedEffect(key1 = checkoutState.value.paymentSheetResult, block = {

    when (checkoutState.value.paymentSheetResult) {
        is PaymentSheetResult.Canceled -> {}
        is PaymentSheetResult.Completed -> {}
        is PaymentSheetResult.Failed -> {}
        null -> {}
    }

})

LaunchedEffect(key1 = checkoutState.value.checkoutSessionDM, block = {
//        If Checkout DM is not null hence the Payment Details are successfully fetched,
//        then show the Stripe Payment Sheet

    checkoutState.value.checkoutSessionDM?.run {
//            Present the Stripe payment sheet
        checkoutVM.onEvent(CheckoutEvent.OnCheckOut(paymentSheet))
    }
})

Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
) {
    Text(modifier = Modifier.clickable {
        //Performs a Server Side request to get the Stripe PaymentSheet required parameters
        checkoutVM.onEvent(CheckoutEvent.OnInitPayment(paymentSheet))

    }, text = "Checkout Screen")
}

}

在上面的代码中,我通过获取支付表格。
    val paymentSheet =
    rememberPaymentSheet(paymentResultCallback = checkoutVM::onPaymentSheetResult)

回调方法: 对于回调,我传递了在我的Checkout ViewModel中定义的一个方法,名为onPaymentSheetResult(paymentSheetResult: PaymentSheetResult)
引用: 回调也可以在可组合中处理,使用LaunchedEffect并观察状态变化,如上所示。 您可以选择适合您的代码架构的方法
所有事件都在我的Hilt ViewModel中处理,名为"CheckoutVM"。 在点击TextComp时,我调用我的服务器端API来获取付款详情或所需的付款表参数值 例如clientSecret或publishable key等。
我使用onEvent()方法将paymentSheet实例传递给checkoutVM viewModel。
这是我的CheckoutVM的代码。
 @HiltViewModel
class CheckoutVM @Inject constructor(
    private val checkoutRepoDec: CheckoutRepoDec,
) : ViewModel() {

private val _checkoutState = MutableStateFlow(CheckoutState())
val checkoutState: StateFlow<CheckoutState> = _checkoutState


fun onEvent(event: CheckoutEvent) {
    when (event) {
        is CheckoutEvent.OnInitPayment -> {
            getPaymentSessionDetails(event)
        }

        is CheckoutEvent.OnCheckOut -> {
            viewModelScope.launch {
                checkoutRepoDec.presentPaymentSheet(event.paymentSheet)
            }
        }

    }

}

private fun getPaymentSessionDetails(event: CheckoutEvent.OnInitPayment) {
    viewModelScope.launch {
        checkoutRepoDec.initPaymentSheet(event.paymentSheet)?.collect { result ->

            when (result) {
                is Resource.Error -> {
                    _checkoutState.update {
                        it.copy(
                            checkoutSessionDM = null,
                            checkoutSessionError = result.error
                        )
                    }
                }

                is Resource.Loading -> {
                   _checkoutState.update {
                       it.copy(isLoading = result.isLoading)
                   }
                }

                is Resource.Success -> {
                    _checkoutState.update {
                        it.copy(
                            checkoutSessionDM = result.data,
                            checkoutSessionError = null
                        )
                    }
                }
            }

        }
    }
}

fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) {

//        To Handle the response using "LaunchedEffect" in my Checkout Screen Composable
    _checkoutState.update {
        it.copy(
            paymentSheetResult = paymentSheetResult
        )
    }

//        To handle it here in Checkout ViewModel
    when (paymentSheetResult) {
        is PaymentSheetResult.Canceled -> {
            showLog(TAG, "paymentSheetResult - Canceled")

        }

        is PaymentSheetResult.Failed -> {
            showLog(TAG, "paymentSheetResult - Error: ${paymentSheetResult.error}")
        }

        is PaymentSheetResult.Completed -> {
            // Display for example, an order confirmation screen
            showLog(TAG, "paymentSheetResult - Completed")
        }
    }
}


}

第二步:根据Stripe文档,我将通过执行服务器端请求获取付款详情。
我在我的CheckoutVM视图模型中使用Retrofit执行服务器端请求,在我的CheckoutRepo中返回Checkout Result,该结果以数据类CheckoutSessionDM的形式返回。
data class CheckoutSessionDM(
val customer: String,
val ephemeralKey: String,
val paymentIntent: String,
val paymentIntentId: String,
val publishableKey: String
)

使用Retrofit执行网络请求,并在我的ViewModel中更新我的CheckoutState数据变量。
data class CheckoutState(
val isLoading: Boolean? = null,
val isCompleted: Boolean? = null,
val isCanceled: Boolean? = null,
val isFailed: Boolean? = null,
val paymentSheetResult: PaymentSheetResult? = null,

//Updated after Payment Details are fetched from app API Server
val checkoutSessionDM: CheckoutSessionDM? = null,
val checkoutSessionError: String? = null,
)

注意:要获取CheckoutSessionDM值,您需要在API服务器上设置一个Stripe的端点,如Stripe文档中所述Add-Server-Endpoint
通过您自己的方法执行API请求,使用"App API Server"请求来获取CheckoutSessionDM数据。我从CheckoutRepo返回一个流到我的CheckoutVM viewModel,如下所示。
我使用一个资源类作为我的结果包装器,您可以自由地使用自己的方法将响应返回给您的视图模型并跳过此包装器类。
以下是我的包装器类的代码,如果有人想使用它,请将其放在一个名为Resource.kt的文件中。
sealed class Resource<T>(
    val data: T? = null,
    val error: String? = "Unable to display error!"
) {
class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)
class Loading<T>(val isLoading: Boolean = true) : Resource<T>()
}  

调用API -
 suspend fun initPaymentSheet(paymentSheet: PaymentSheet): Flow<Resource<CheckoutSessionDM>>? {

    return flow {

        try {
            val result = api.createSession("100", "INR")

            result?.body()?.run {
                showLog(TAG, "result - $this")

                val checkoutSessionDto = this
                paymentIntentClientSecret = checkoutSessionDto.paymentIntent
                customerConfig = PaymentSheet.CustomerConfiguration(
                    checkoutSessionDto.customer,
                    checkoutSessionDto.ephemeralKey
                )
                val publishableKey = checkoutSessionDto.publishableKey
                PaymentConfiguration.init(context, publishableKey)

                emit(
                    Resource.Success(
//                    checkoutSessionDto.toCheckoutSessionDM()  // Use a mapper function like this 
//                       or
                        
                        CheckoutSessionDM(
                            customer = checkoutSessionDto.customer,
                            ephemeralKey = checkoutSessionDto.ephemeralKey,
                            paymentIntent = checkoutSessionDto.paymentIntent,
                            paymentIntentId = checkoutSessionDto.paymentIntentId,
                            publishableKey = checkoutSessionDto.publishableKey
                        )
                    )
                )

            } ?: run {
                emit(
                    Resource.Error(
                        data = null,
                        message = "Server Error"
                    )
                )
            }

        } catch (e: Exception) {
            if (e is CancellationException) {
                showLog(TAG, "initPaymentSheet() CancellationException $e")
                throw e
            } else if (e is HttpException) {
                showLog(TAG, "initPaymentSheet() HttpException $e")
            }
            emit(
                Resource.Error(
                    data = null,
                    message = "App Error : $e"
                )
            )
            return@flow
        }
    }

    }

API的响应保存在`CheckoutSessionDto`数据类中,该数据类与`CheckoutSessionDM`相同。使用两个相同的类只是为了保持干净的架构。 步骤3:展示Stripe支付表单 一旦我们在`CheckoutVM`的`getPaymentSessionDetails()`方法中收到`Resource.Success`,我们就可以调用我们的`presentPaymentSheet()`方法。
它可以从两个地方调用:
1. 在我们的`getPaymentSessionDetails()`方法中收到`Resource.Success`时,可以在`CheckoutVM`自身调用。 2. 通过观察`CheckoutState`数据类中的`CheckoutSessionDM`的值,可以在`CheckoutScreen`组合中调用。
在我的示例中,我使用了第二种方法,如步骤1所示。
在我的CheckoutScreen组合中,我观察到CheckoutState中的CheckoutSessionDM值的变化。 当CheckoutSessionDM变为非空时,我在我的CheckoutVM viewModel中调用OnCheckOut事件。
LaunchedEffect(key1 = checkoutState.value.checkoutSessionDM, block = {
//        If Checkout DM is not null hence the Payment Details are successfully fetched,
//        then show the Stripe Payment Sheet

    checkoutState.value.checkoutSessionDM?.run {
//            Present the Stripe payment sheet
        checkoutVM.onEvent(CheckoutEvent.OnCheckOut(paymentSheet))
    }
})
CheckoutVM 进一步调用了我存储库类中的 presentPaymentSheet() 方法。
override suspend fun presentPaymentSheet(paymentSheet: PaymentSheet?) {
    paymentSheet?.presentWithPaymentIntent(
        paymentIntentClientSecret,
        PaymentSheet.Configuration(
            merchantDisplayName = "Display Name",
            customer = customerConfig,
            // Set `allowsDelayedPaymentMethods` to true if your business handles
            // delayed notification payment methods like US bank accounts.
            allowsDelayedPaymentMethods = true
        )
    )
}

最后,Payment Sheet的响应可以在我们的回调方法onPaymentSheetResult()中处理,该方法位于CheckoutVM的viewmodel中。

或者

CheckoutScreen组合中使用LaunchedEffect块来处理checkoutState.value.paymentSheetResult

希望对某人有所帮助 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接