Add initial PayPal implementation behind a feature flag.
This commit is contained in:
parent
b70b4fac91
commit
979f87db78
47 changed files with 1382 additions and 144 deletions
|
@ -282,6 +282,10 @@ class GiftFlowConfirmationFragment :
|
|||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
|
||||
}
|
||||
|
||||
override fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(gatewayRequest))
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ object InAppDonations {
|
|||
* Whether the user is in a region that supports PayPal, based off local phone number.
|
||||
*/
|
||||
fun isPayPalAvailable(): Boolean {
|
||||
return false
|
||||
return FeatureFlags.paypalDonations() && !LocaleFeatureFlags.isPayPalDisabled()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Single
|
|||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
|
@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
|||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
@ -31,6 +33,16 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
|||
|
||||
companion object {
|
||||
private val TAG = Log.tag(OneTimeDonationRepository::class.java)
|
||||
|
||||
fun <T> handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId, paymentSourceType: PaymentSourceType): Single<T> {
|
||||
return if (throwable is DonationError) {
|
||||
Single.error(throwable)
|
||||
} else {
|
||||
val recipient = Recipient.resolved(badgeRecipient)
|
||||
val errorSource = if (recipient.isSelf) DonationErrorSource.BOOST else DonationErrorSource.GIFT
|
||||
Single.error(DonationError.getPaymentSetupError(errorSource, throwable, paymentSourceType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBoosts(): Single<Map<Currency, List<Boost>>> {
|
||||
|
@ -62,6 +74,7 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
|||
badgeRecipient: RecipientId,
|
||||
additionalMessage: String?,
|
||||
badgeLevel: Long,
|
||||
donationProcessor: DonationProcessor
|
||||
): Completable {
|
||||
val isBoost = badgeRecipient == Recipient.self().id
|
||||
val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT
|
||||
|
@ -81,9 +94,9 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
|||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
val chain = if (isBoost) {
|
||||
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId)
|
||||
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor)
|
||||
} else {
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel)
|
||||
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor)
|
||||
}
|
||||
|
||||
chain.enqueue { _, jobState ->
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalConfirmationResult
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Repository that deals directly with PayPal API calls. Since we don't interact with the PayPal APIs directly (yet)
|
||||
* we can do everything here in one place.
|
||||
*/
|
||||
class PayPalRepository(private val donationsService: DonationsService) {
|
||||
|
||||
companion object {
|
||||
const val ONE_TIME_RETURN_URL = "https://signaldonations.org/return/onetime"
|
||||
const val MONTHLY_RETURN_URL = "https://signaldonations.org/return/monthly"
|
||||
const val CANCEL_URL = "https://signaldonations.org/cancel"
|
||||
}
|
||||
|
||||
fun createOneTimePaymentIntent(
|
||||
amount: FiatMoney,
|
||||
badgeRecipient: RecipientId,
|
||||
badgeLevel: Long
|
||||
): Single<PayPalCreatePaymentIntentResponse> {
|
||||
return Single.fromCallable {
|
||||
donationsService
|
||||
.createPayPalOneTimePaymentIntent(
|
||||
Locale.getDefault(),
|
||||
amount.currency.currencyCode,
|
||||
amount.minimumUnitPrecisionString,
|
||||
badgeLevel,
|
||||
ONE_TIME_RETURN_URL,
|
||||
CANCEL_URL
|
||||
)
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.onErrorResumeNext { OneTimeDonationRepository.handleCreatePaymentIntentError(it, badgeRecipient, PaymentSourceType.PayPal) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun confirmOneTimePaymentIntent(
|
||||
amount: FiatMoney,
|
||||
badgeLevel: Long,
|
||||
paypalConfirmationResult: PayPalConfirmationResult
|
||||
): Single<PayPalConfirmPaymentIntentResponse> {
|
||||
return Single.fromCallable {
|
||||
donationsService
|
||||
.confirmPayPalOneTimePaymentIntent(
|
||||
amount.currency.currencyCode,
|
||||
amount.minimumUnitPrecisionString,
|
||||
badgeLevel,
|
||||
paypalConfirmationResult.payerId,
|
||||
paypalConfirmationResult.paymentId,
|
||||
paypalConfirmationResult.paymentToken
|
||||
)
|
||||
}.flatMap { it.flattenResult() }.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun createPaymentMethod(): Single<PayPalCreatePaymentMethodResponse> {
|
||||
return Single.fromCallable {
|
||||
donationsService.createPayPalPaymentMethod(
|
||||
Locale.getDefault(),
|
||||
SignalStore.donationsValues().requireSubscriber().subscriberId,
|
||||
MONTHLY_RETURN_URL,
|
||||
CANCEL_URL
|
||||
)
|
||||
}.flatMap { it.flattenResult() }.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun setDefaultPaymentMethod(paymentMethodId: String): Completable {
|
||||
return Single.fromCallable {
|
||||
donationsService.setDefaultPayPalPaymentMethod(
|
||||
SignalStore.donationsValues().requireSubscriber().subscriberId,
|
||||
paymentMethodId
|
||||
)
|
||||
}.flatMap { it.flattenResult() }.ignoreElement().andThen {
|
||||
SignalStore.donationsValues().setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
|
@ -9,9 +9,9 @@ import org.signal.core.util.concurrent.SignalExecutors
|
|||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.signal.donations.StripePaymentSourceType
|
||||
import org.signal.donations.json.StripeIntentStatus
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
|
@ -87,13 +87,13 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
|||
price: FiatMoney,
|
||||
badgeRecipient: RecipientId,
|
||||
badgeLevel: Long,
|
||||
paymentSourceType: StripePaymentSourceType
|
||||
paymentSourceType: PaymentSourceType
|
||||
): Single<StripeIntentAccessor> {
|
||||
Log.d(TAG, "Creating payment intent for $price...", true)
|
||||
|
||||
return stripeApi.createPaymentIntent(price, badgeLevel)
|
||||
.onErrorResumeNext {
|
||||
handleCreatePaymentIntentError(it, badgeRecipient, paymentSourceType)
|
||||
OneTimeDonationRepository.handleCreatePaymentIntentError(it, badgeRecipient, paymentSourceType)
|
||||
}
|
||||
.flatMap { result ->
|
||||
val recipient = Recipient.resolved(badgeRecipient)
|
||||
|
@ -200,7 +200,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
|||
|
||||
fun setDefaultPaymentMethod(
|
||||
paymentMethodId: String,
|
||||
paymentSourceType: StripePaymentSourceType
|
||||
paymentSourceType: PaymentSourceType
|
||||
): Completable {
|
||||
return Single.fromCallable {
|
||||
Log.d(TAG, "Getting the subscriber...")
|
||||
|
@ -223,7 +223,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
|||
Log.d(TAG, "Creating credit card payment source via Stripe api...")
|
||||
return stripeApi.createPaymentSourceFromCardData(cardData).map {
|
||||
when (it) {
|
||||
is StripeApi.CreatePaymentSourceFromCardDataResult.Failure -> throw DonationError.getPaymentSetupError(donationErrorSource, it.reason, StripePaymentSourceType.CREDIT_CARD)
|
||||
is StripeApi.CreatePaymentSourceFromCardDataResult.Failure -> throw DonationError.getPaymentSetupError(donationErrorSource, it.reason, PaymentSourceType.Stripe.CreditCard)
|
||||
is StripeApi.CreatePaymentSourceFromCardDataResult.Success -> it.paymentSource
|
||||
}
|
||||
}
|
||||
|
@ -236,15 +236,5 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
|
|||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StripeRepository::class.java)
|
||||
|
||||
private fun <T> handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId, paymentSourceType: StripePaymentSourceType): Single<T> {
|
||||
return if (throwable is DonationError) {
|
||||
Single.error(throwable)
|
||||
} else {
|
||||
val recipient = Recipient.resolved(badgeRecipient)
|
||||
val errorSource = if (recipient.isSelf) DonationErrorSource.BOOST else DonationErrorSource.GIFT
|
||||
Single.error(DonationError.getPaymentSetupError(errorSource, throwable, paymentSourceType))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -452,6 +452,15 @@ class DonateToSignalFragment :
|
|||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
|
||||
}
|
||||
|
||||
override fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(
|
||||
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
gatewayRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
|
||||
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayRequest))
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.ca
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
|
@ -77,12 +78,17 @@ class DonationCheckoutDelegate(
|
|||
val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!!
|
||||
handleCreditCardResult(result)
|
||||
}
|
||||
|
||||
fragment.setFragmentResultListener(PayPalPaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: DonationProcessorActionResult = bundle.getParcelable(PayPalPaymentInProgressFragment.REQUEST_KEY)!!
|
||||
handleDonationProcessorActionResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) {
|
||||
when (gatewayResponse.gateway) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
||||
GatewayResponse.Gateway.PAYPAL -> error("PayPal is not currently supported.")
|
||||
GatewayResponse.Gateway.PAYPAL -> launchPayPal(gatewayResponse)
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +130,14 @@ class DonationCheckoutDelegate(
|
|||
}
|
||||
}
|
||||
|
||||
private fun launchPayPal(gatewayResponse: GatewayResponse) {
|
||||
if (InAppDonations.isPayPalAvailable()) {
|
||||
callback.navigateToPayPalPaymentInProgress(gatewayResponse.request)
|
||||
} else {
|
||||
error("PayPal is not currently enabled.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
|
||||
viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request)
|
||||
donationPaymentComponent.stripeRepository.requestTokenFromGooglePay(
|
||||
|
@ -186,6 +200,7 @@ class DonationCheckoutDelegate(
|
|||
|
||||
interface Callback {
|
||||
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToCreditCardForm(gatewayRequest: GatewayRequest)
|
||||
fun onPaymentComplete(gatewayRequest: GatewayRequest)
|
||||
fun onProcessorActionProcessed()
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
@ -40,6 +41,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
|||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgeDisplay112.register(adapter)
|
||||
GooglePayButton.register(adapter)
|
||||
PayPalButton.register(adapter)
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
|
||||
|
@ -80,11 +82,23 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
|||
)
|
||||
}
|
||||
|
||||
// PayPal
|
||||
if (InAppDonations.isPayPalAvailable()) {
|
||||
space(8.dp)
|
||||
|
||||
customPref(
|
||||
PayPalButton.Model(
|
||||
onClick = {
|
||||
findNavController().popBackStack()
|
||||
val response = GatewayResponse(GatewayResponse.Gateway.PAYPAL, args.request)
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
|
||||
},
|
||||
isEnabled = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Credit Card
|
||||
if (InAppDonations.isCreditCardAvailable()) {
|
||||
space(12.dp)
|
||||
space(8.dp)
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__credit_or_debit_card),
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Full-screen dialog for displaying PayPal confirmation.
|
||||
*/
|
||||
class PayPalConfirmationDialogFragment : DialogFragment(R.layout.donation_webview_fragment) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(PayPalConfirmationDialogFragment::class.java)
|
||||
|
||||
const val REQUEST_KEY = "paypal_confirmation_dialog_fragment"
|
||||
}
|
||||
|
||||
private val binding by ViewBinderDelegate(DonationWebviewFragmentBinding::bind) {
|
||||
it.webView.clearCache(true)
|
||||
it.webView.clearHistory()
|
||||
}
|
||||
|
||||
private val args: PayPalConfirmationDialogFragmentArgs by navArgs()
|
||||
|
||||
private var result: Bundle? = null
|
||||
private var isFinished = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen)
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.webView.webViewClient = PayPalWebClient()
|
||||
binding.webView.settings.javaScriptEnabled = true
|
||||
binding.webView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
binding.webView.loadUrl(args.uri.toString())
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
val result = this.result
|
||||
this.result = null
|
||||
setFragmentResult(REQUEST_KEY, result ?: Bundle())
|
||||
}
|
||||
|
||||
private inner class PayPalWebClient : WebViewClient() {
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
if (!isFinished) {
|
||||
binding.progress.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(view: WebView?, url: String?) {
|
||||
if (!isFinished) {
|
||||
binding.progress.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if (url?.startsWith(PayPalRepository.ONE_TIME_RETURN_URL) == true) {
|
||||
val confirmationResult = PayPalConfirmationResult.fromUrl(url)
|
||||
if (confirmationResult != null) {
|
||||
Log.d(TAG, "Setting confirmation result on request key...")
|
||||
result = bundleOf(REQUEST_KEY to confirmationResult)
|
||||
} else {
|
||||
Log.w(TAG, "One-Time return URL was missing a required parameter.", false)
|
||||
result = null
|
||||
}
|
||||
isFinished = true
|
||||
dismissAllowingStateLoss()
|
||||
} else if (url?.startsWith(PayPalRepository.CANCEL_URL) == true) {
|
||||
Log.d(TAG, "User cancelled.")
|
||||
result = null
|
||||
isFinished = true
|
||||
dismissAllowingStateLoss()
|
||||
} else if (url?.startsWith(PayPalRepository.MONTHLY_RETURN_URL) == true) {
|
||||
Log.d(TAG, "User confirmed monthly subscription.")
|
||||
result = bundleOf(REQUEST_KEY to true)
|
||||
isFinished = true
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class PayPalConfirmationResult(
|
||||
val payerId: String,
|
||||
val paymentId: String,
|
||||
val paymentToken: String
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
private const val KEY_PAYER_ID = "PayerID"
|
||||
private const val KEY_PAYMENT_ID = "paymentId"
|
||||
private const val KEY_PAYMENT_TOKEN = "token"
|
||||
|
||||
fun fromUrl(url: String): PayPalConfirmationResult? {
|
||||
val uri = Uri.parse(url)
|
||||
return PayPalConfirmationResult(
|
||||
payerId = uri.getQueryParameter(KEY_PAYER_ID) ?: return null,
|
||||
paymentId = uri.getQueryParameter(KEY_PAYMENT_ID) ?: return null,
|
||||
paymentToken = uri.getQueryParameter(KEY_PAYMENT_TOKEN) ?: return null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentResultListener
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse
|
||||
|
||||
class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_progress_fragment) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(PayPalPaymentInProgressFragment::class.java)
|
||||
|
||||
const val REQUEST_KEY = "REQUEST_KEY"
|
||||
}
|
||||
|
||||
private val disposables = LifecycleDisposable()
|
||||
private val binding by ViewBinderDelegate(DonationInProgressFragmentBinding::bind)
|
||||
private val args: PayPalPaymentInProgressFragmentArgs by navArgs()
|
||||
|
||||
private val viewModel: PayPalPaymentInProgressViewModel by navGraphViewModels(R.id.donate_to_signal, factoryProducer = {
|
||||
PayPalPaymentInProgressViewModel.Factory()
|
||||
})
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
isCancelable = false
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (savedInstanceState == null) {
|
||||
viewModel.onBeginNewAction()
|
||||
when (args.action) {
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION -> {
|
||||
viewModel.processNewDonation(args.request, this::routeToOneTimeConfirmation, this::routeToMonthlyConfirmation)
|
||||
}
|
||||
DonationProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.request)
|
||||
}
|
||||
DonationProcessorAction.CANCEL_SUBSCRIPTION -> {
|
||||
viewModel.cancelSubscription()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
disposables += viewModel.state.subscribeBy { stage ->
|
||||
presentUiState(stage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentUiState(stage: DonationProcessorStage) {
|
||||
when (stage) {
|
||||
DonationProcessorStage.INIT -> binding.progressCardStatus.setText(R.string.SubscribeFragment__processing_payment)
|
||||
DonationProcessorStage.PAYMENT_PIPELINE -> binding.progressCardStatus.setText(R.string.SubscribeFragment__processing_payment)
|
||||
DonationProcessorStage.FAILED -> {
|
||||
viewModel.onEndAction()
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
REQUEST_KEY to DonationProcessorActionResult(
|
||||
action = args.action,
|
||||
request = args.request,
|
||||
status = DonationProcessorActionResult.Status.FAILURE
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
DonationProcessorStage.COMPLETE -> {
|
||||
viewModel.onEndAction()
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(
|
||||
REQUEST_KEY,
|
||||
bundleOf(
|
||||
REQUEST_KEY to DonationProcessorActionResult(
|
||||
action = args.action,
|
||||
request = args.request,
|
||||
status = DonationProcessorActionResult.Status.SUCCESS
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
DonationProcessorStage.CANCELLING -> binding.progressCardStatus.setText(R.string.StripePaymentInProgressFragment__cancelling)
|
||||
}
|
||||
}
|
||||
|
||||
private fun routeToOneTimeConfirmation(createPaymentIntentResponse: PayPalCreatePaymentIntentResponse): Single<PayPalConfirmationResult> {
|
||||
return Single.create<PayPalConfirmationResult> { emitter ->
|
||||
val listener = FragmentResultListener { _, bundle ->
|
||||
val result: PayPalConfirmationResult? = bundle.getParcelable(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
if (result != null) {
|
||||
emitter.onSuccess(result)
|
||||
} else {
|
||||
emitter.onError(Exception("User did not complete paypal confirmation."))
|
||||
}
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(PayPalConfirmationDialogFragment.REQUEST_KEY, this, listener)
|
||||
|
||||
findNavController().safeNavigate(
|
||||
PayPalPaymentInProgressFragmentDirections.actionPaypalPaymentInProgressFragmentToPaypalConfirmationFragment(
|
||||
Uri.parse(createPaymentIntentResponse.approvalUrl)
|
||||
)
|
||||
)
|
||||
|
||||
emitter.setCancellable {
|
||||
parentFragmentManager.clearFragmentResultListener(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
}
|
||||
}.subscribeOn(AndroidSchedulers.mainThread()).observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun routeToMonthlyConfirmation(createPaymentIntentResponse: PayPalCreatePaymentMethodResponse): Single<PayPalPaymentMethodId> {
|
||||
return Single.create<PayPalPaymentMethodId> { emitter ->
|
||||
val listener = FragmentResultListener { _, bundle ->
|
||||
val result: Boolean = bundle.getBoolean(REQUEST_KEY)
|
||||
if (result) {
|
||||
emitter.onSuccess(PayPalPaymentMethodId(createPaymentIntentResponse.token))
|
||||
} else {
|
||||
emitter.onError(Exception("User did not confirm paypal setup."))
|
||||
}
|
||||
}
|
||||
|
||||
parentFragmentManager.setFragmentResultListener(PayPalConfirmationDialogFragment.REQUEST_KEY, this, listener)
|
||||
|
||||
findNavController().safeNavigate(
|
||||
PayPalPaymentInProgressFragmentDirections.actionPaypalPaymentInProgressFragmentToPaypalConfirmationFragment(
|
||||
Uri.parse(createPaymentIntentResponse.approvalUrl)
|
||||
)
|
||||
)
|
||||
|
||||
emitter.setCancellable {
|
||||
parentFragmentManager.clearFragmentResultListener(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
}
|
||||
}.subscribeOn(AndroidSchedulers.mainThread()).observeOn(Schedulers.io())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
|
||||
class PayPalPaymentInProgressViewModel(
|
||||
private val payPalRepository: PayPalRepository,
|
||||
private val monthlyDonationRepository: MonthlyDonationRepository,
|
||||
private val oneTimeDonationRepository: OneTimeDonationRepository
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(PayPalPaymentInProgressViewModel::class.java)
|
||||
}
|
||||
|
||||
private val store = RxStore(DonationProcessorStage.INIT)
|
||||
val state: Flowable<DonationProcessorStage> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
override fun onCleared() {
|
||||
store.dispose()
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun onBeginNewAction() {
|
||||
Preconditions.checkState(!store.state.isInProgress)
|
||||
|
||||
Log.d(TAG, "Beginning a new action. Ensuring cleared state.", true)
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun onEndAction() {
|
||||
Preconditions.checkState(store.state.isTerminal)
|
||||
|
||||
Log.d(TAG, "Ending current state. Clearing state and setting stage to INIT", true)
|
||||
store.update { DonationProcessorStage.INIT }
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun processNewDonation(
|
||||
request: GatewayRequest,
|
||||
routeToOneTimeConfirmation: (PayPalCreatePaymentIntentResponse) -> Single<PayPalConfirmationResult>,
|
||||
routeToMonthlyConfirmation: (PayPalCreatePaymentMethodResponse) -> Single<PayPalPaymentMethodId>
|
||||
) {
|
||||
Log.d(TAG, "Proceeding with donation...", true)
|
||||
|
||||
return when (request.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> proceedOneTime(request, routeToOneTimeConfirmation)
|
||||
DonateToSignalType.MONTHLY -> proceedMonthly(request, routeToMonthlyConfirmation)
|
||||
DonateToSignalType.GIFT -> proceedOneTime(request, routeToOneTimeConfirmation)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSubscription(request: GatewayRequest) {
|
||||
Log.d(TAG, "Beginning subscription update...", true)
|
||||
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscriptionIfNecessary().andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString()))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
Log.w(TAG, "Completed subscription update", true)
|
||||
store.update { DonationProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failed to update subscription", throwable, true)
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelSubscription() {
|
||||
Log.d(TAG, "Beginning cancellation...", true)
|
||||
|
||||
store.update { DonationProcessorStage.CANCELLING }
|
||||
disposables += monthlyDonationRepository.cancelActiveSubscription().subscribeBy(
|
||||
onComplete = {
|
||||
Log.d(TAG, "Cancellation succeeded", true)
|
||||
SignalStore.donationsValues().updateLocalStateForManualCancellation()
|
||||
MultiDeviceSubscriptionSyncRequestJob.enqueue()
|
||||
monthlyDonationRepository.syncAccountRecord().subscribe()
|
||||
store.update { DonationProcessorStage.COMPLETE }
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Cancellation failed", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun proceedOneTime(
|
||||
request: GatewayRequest,
|
||||
routeToPaypalConfirmation: (PayPalCreatePaymentIntentResponse) -> Single<PayPalConfirmationResult>
|
||||
) {
|
||||
Log.d(TAG, "Proceeding with one-time payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.PAYMENT_PIPELINE }
|
||||
|
||||
disposables += payPalRepository
|
||||
.createOneTimePaymentIntent(
|
||||
amount = request.fiat,
|
||||
badgeRecipient = request.recipientId,
|
||||
badgeLevel = request.level
|
||||
)
|
||||
.flatMap(routeToPaypalConfirmation)
|
||||
.flatMap { result ->
|
||||
payPalRepository.confirmOneTimePaymentIntent(
|
||||
amount = request.fiat,
|
||||
badgeLevel = request.level,
|
||||
paypalConfirmationResult = result
|
||||
)
|
||||
}
|
||||
.flatMapCompletable { response ->
|
||||
oneTimeDonationRepository.waitForOneTimeRedemption(
|
||||
price = request.fiat,
|
||||
paymentIntentId = response.paymentId,
|
||||
badgeRecipient = request.recipientId,
|
||||
additionalMessage = request.additionalMessage,
|
||||
badgeLevel = request.level,
|
||||
donationProcessor = DonationProcessor.PAYPAL
|
||||
)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribeBy(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in one-time payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.BOOST)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished one-time payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.COMPLETE }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun proceedMonthly(request: GatewayRequest, routeToPaypalConfirmation: (PayPalCreatePaymentMethodResponse) -> Single<PayPalPaymentMethodId>) {
|
||||
Log.d(TAG, "Proceeding with monthly payment pipeline...")
|
||||
|
||||
val setup = monthlyDonationRepository.ensureSubscriberId()
|
||||
.andThen(monthlyDonationRepository.cancelActiveSubscriptionIfNecessary())
|
||||
.andThen(payPalRepository.createPaymentMethod())
|
||||
.flatMap(routeToPaypalConfirmation)
|
||||
.flatMapCompletable { payPalRepository.setDefaultPaymentMethod(it.paymentId) }
|
||||
.onErrorResumeNext { Completable.error(DonationError.getPaymentSetupError(DonationErrorSource.SUBSCRIPTION, it, PaymentSourceType.PayPal)) }
|
||||
|
||||
disposables += setup.andThen(monthlyDonationRepository.setSubscriptionLevel(request.level.toString()))
|
||||
.subscribeBy(
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Failure in monthly payment pipeline...", throwable, true)
|
||||
store.update { DonationProcessorStage.FAILED }
|
||||
|
||||
val donationError: DonationError = if (throwable is DonationError) {
|
||||
throwable
|
||||
} else {
|
||||
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.SUBSCRIPTION)
|
||||
}
|
||||
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
|
||||
},
|
||||
onComplete = {
|
||||
Log.d(TAG, "Finished subscription payment pipeline...", true)
|
||||
store.update { DonationProcessorStage.COMPLETE }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val payPalRepository: PayPalRepository = PayPalRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val monthlyDonationRepository: MonthlyDonationRepository = MonthlyDonationRepository(ApplicationDependencies.getDonationsService()),
|
||||
private val oneTimeDonationRepository: OneTimeDonationRepository = OneTimeDonationRepository(ApplicationDependencies.getDonationsService())
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(PayPalPaymentInProgressViewModel(payPalRepository, monthlyDonationRepository, oneTimeDonationRepository)) as T
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@JvmInline
|
||||
value class PayPalPaymentMethodId(val paymentId: String) : Parcelable
|
|
@ -15,19 +15,19 @@ import androidx.navigation.fragment.navArgs
|
|||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.databinding.Stripe3dsDialogFragmentBinding
|
||||
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Full-screen dialog for displaying Stripe 3DS confirmation.
|
||||
*/
|
||||
class Stripe3DSDialogFragment : DialogFragment(R.layout.stripe_3ds_dialog_fragment) {
|
||||
class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragment) {
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "stripe_3ds_dialog_fragment"
|
||||
}
|
||||
|
||||
val binding by ViewBinderDelegate(Stripe3dsDialogFragmentBinding::bind) {
|
||||
val binding by ViewBinderDelegate(DonationWebviewFragmentBinding::bind) {
|
||||
it.webView.clearCache(true)
|
||||
it.webView.clearHistory()
|
||||
}
|
||||
|
|
|
@ -25,12 +25,12 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.databinding.StripePaymentInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class StripePaymentInProgressFragment : DialogFragment(R.layout.stripe_payment_in_progress_fragment) {
|
||||
class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_progress_fragment) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(StripePaymentInProgressFragment::class.java)
|
||||
|
@ -38,7 +38,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.stripe_payment_i
|
|||
const val REQUEST_KEY = "REQUEST_KEY"
|
||||
}
|
||||
|
||||
private val binding by ViewBinderDelegate(StripePaymentInProgressFragmentBinding::bind)
|
||||
private val binding by ViewBinderDelegate(DonationInProgressFragmentBinding::bind)
|
||||
private val args: StripePaymentInProgressFragmentArgs by navArgs()
|
||||
private val disposables = LifecycleDisposable()
|
||||
|
||||
|
|
|
@ -12,9 +12,9 @@ import io.reactivex.rxjava3.kotlin.plusAssign
|
|||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.signal.donations.StripePaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
|
@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceSubscriptionSyncRequestJob
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.util.Preconditions
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
|
||||
class StripePaymentInProgressViewModel(
|
||||
private val stripeRepository: StripeRepository,
|
||||
|
@ -93,11 +94,11 @@ class StripePaymentInProgressViewModel(
|
|||
paymentData == null && cardData == null -> error("No payment provider available.")
|
||||
paymentData != null && cardData != null -> error("Too many providers available")
|
||||
paymentData != null -> PaymentSourceProvider(
|
||||
StripePaymentSourceType.GOOGLE_PAY,
|
||||
PaymentSourceType.Stripe.GooglePay,
|
||||
Single.just<StripeApi.PaymentSource>(GooglePayPaymentSource(paymentData)).doAfterTerminate { clearPaymentInformation() }
|
||||
)
|
||||
cardData != null -> PaymentSourceProvider(
|
||||
StripePaymentSourceType.CREDIT_CARD,
|
||||
PaymentSourceType.Stripe.CreditCard,
|
||||
stripeRepository.createCreditCardPaymentSource(errorSource, cardData).doAfterTerminate { clearPaymentInformation() }
|
||||
)
|
||||
else -> error("This should never happen.")
|
||||
|
@ -187,11 +188,12 @@ class StripePaymentInProgressViewModel(
|
|||
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it) }
|
||||
.flatMapCompletable {
|
||||
oneTimeDonationRepository.waitForOneTimeRedemption(
|
||||
amount,
|
||||
paymentIntent.intentId,
|
||||
request.recipientId,
|
||||
request.additionalMessage,
|
||||
request.level
|
||||
price = amount,
|
||||
paymentIntentId = paymentIntent.intentId,
|
||||
badgeRecipient = request.recipientId,
|
||||
additionalMessage = request.additionalMessage,
|
||||
badgeLevel = request.level,
|
||||
donationProcessor = DonationProcessor.STRIPE
|
||||
)
|
||||
}
|
||||
}.subscribeBy(
|
||||
|
@ -257,7 +259,7 @@ class StripePaymentInProgressViewModel(
|
|||
}
|
||||
|
||||
private data class PaymentSourceProvider(
|
||||
val paymentSourceType: StripePaymentSourceType,
|
||||
val paymentSourceType: PaymentSourceType,
|
||||
val paymentSource: Single<StripeApi.PaymentSource>
|
||||
)
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ import io.reactivex.rxjava3.core.Observable
|
|||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripeError
|
||||
import org.signal.donations.StripePaymentSourceType
|
||||
|
||||
sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) {
|
||||
|
||||
|
@ -51,12 +51,12 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
|||
/**
|
||||
* Payment setup failed in some way, which we are told about by Stripe.
|
||||
*/
|
||||
class CodedError(source: DonationErrorSource, cause: Throwable, val errorCode: String) : PaymentSetupError(source, cause)
|
||||
class StripeCodedError(source: DonationErrorSource, cause: Throwable, val errorCode: String) : PaymentSetupError(source, cause)
|
||||
|
||||
/**
|
||||
* Payment failed by the credit card processor, with a specific reason told to us by Stripe.
|
||||
*/
|
||||
class DeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode, val method: StripePaymentSourceType) : PaymentSetupError(source, cause)
|
||||
class StripeDeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode, val method: PaymentSourceType.Stripe) : PaymentSetupError(source, cause)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -129,18 +129,18 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
|||
|
||||
/**
|
||||
* Converts a throwable into a payment setup error. This should only be used when
|
||||
* handling errors handed back via the Stripe API, when we know for sure that no
|
||||
* handling errors handed back via the Stripe API or via PayPal, when we know for sure that no
|
||||
* charge has occurred.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPaymentSetupError(source: DonationErrorSource, throwable: Throwable, method: StripePaymentSourceType): DonationError {
|
||||
fun getPaymentSetupError(source: DonationErrorSource, throwable: Throwable, method: PaymentSourceType): DonationError {
|
||||
return if (throwable is StripeError.PostError) {
|
||||
val declineCode: StripeDeclineCode? = throwable.declineCode
|
||||
val errorCode: String? = throwable.errorCode
|
||||
|
||||
when {
|
||||
declineCode != null -> PaymentSetupError.DeclinedError(source, throwable, declineCode, method)
|
||||
errorCode != null -> PaymentSetupError.CodedError(source, throwable, errorCode)
|
||||
declineCode != null && method is PaymentSourceType.Stripe -> PaymentSetupError.StripeDeclinedError(source, throwable, declineCode, method)
|
||||
errorCode != null && method is PaymentSourceType.Stripe -> PaymentSetupError.StripeCodedError(source, throwable, errorCode)
|
||||
else -> PaymentSetupError.GenericError(source, throwable)
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors
|
|||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeDeclineCode
|
||||
import org.signal.donations.StripePaymentSourceType
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class DonationErrorParams<V> private constructor(
|
||||
|
@ -25,7 +25,7 @@ class DonationErrorParams<V> private constructor(
|
|||
): DonationErrorParams<V> {
|
||||
return when (throwable) {
|
||||
is DonationError.GiftRecipientVerificationError -> getVerificationErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError.DeclinedError -> getDeclinedErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError.StripeDeclinedError -> getDeclinedErrorParams(context, throwable, callback)
|
||||
is DonationError.PaymentSetupError -> DonationErrorParams(
|
||||
title = R.string.DonationsErrors__error_processing_payment,
|
||||
message = R.string.DonationsErrors__your_payment,
|
||||
|
@ -88,10 +88,10 @@ class DonationErrorParams<V> private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun <V> getDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.DeclinedError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
private fun <V> getDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.StripeDeclinedError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
val getStripeDeclineCodePositiveActionParams: (Context, Callback<V>, Int) -> DonationErrorParams<V> = when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> this::getTryCreditCardAgainParams
|
||||
StripePaymentSourceType.GOOGLE_PAY -> this::getGoToGooglePayParams
|
||||
PaymentSourceType.Stripe.GooglePay -> this::getTryCreditCardAgainParams
|
||||
PaymentSourceType.Stripe.CreditCard -> this::getGoToGooglePayParams
|
||||
}
|
||||
|
||||
return when (declinedError.declineCode) {
|
||||
|
@ -99,66 +99,66 @@ class DonationErrorParams<V> private constructor(
|
|||
StripeDeclineCode.Code.APPROVE_WITH_ID -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.CALL_ISSUER -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again_if_the_problem_continues
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again_if_the_problem
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again_if_the_problem_continues
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again_if_the_problem
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.CARD_NOT_SUPPORTED -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_support_this_type_of_purchase)
|
||||
StripeDeclineCode.Code.EXPIRED_CARD -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_has_expired_verify_your_card_details
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_card_has_expired
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_has_expired_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_has_expired
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.INCORRECT_NUMBER -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_card_number_is_incorrect
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_number_is_incorrect
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.INCORRECT_CVC -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.INSUFFICIENT_FUNDS -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_have_sufficient_funds)
|
||||
StripeDeclineCode.Code.INVALID_CVC -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__the_expiration_month_on_your_card_is_incorrect
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__the_expiration_month
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__the_expiration_month_on_your_card_is_incorrect
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__the_expiration_month
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__the_expiration_year_on_your_card_is_incorrect
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__the_expiration_year
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__the_expiration_year_on_your_card_is_incorrect
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__the_expiration_year
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.INVALID_NUMBER -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details
|
||||
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_card_number_is_incorrect
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_number_is_incorrect
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.ISSUER_NOT_AVAILABLE -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_completing_the_payment_again)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.models
|
||||
|
||||
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.BindingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
object PayPalButton {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, BindingFactory(::ViewHolder, PaypalButtonBinding::inflate))
|
||||
}
|
||||
|
||||
class Model(val onClick: () -> Unit, val isEnabled: Boolean) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = isEnabled == newItem.isEnabled
|
||||
}
|
||||
|
||||
class ViewHolder(binding: PaypalButtonBinding) : BindingViewHolder<Model, PaypalButtonBinding>(binding) {
|
||||
override fun bind(model: Model) {
|
||||
binding.paypalButton.isEnabled = model.isEnabled
|
||||
binding.paypalButton.setOnClickListener { model.onClick() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -44,18 +45,20 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
private static final String BOOST_QUEUE = "BoostReceiptRedemption";
|
||||
private static final String GIFT_QUEUE = "GiftReceiptRedemption";
|
||||
|
||||
private static final String DATA_REQUEST_BYTES = "data.request.bytes";
|
||||
private static final String DATA_PAYMENT_INTENT_ID = "data.payment.intent.id";
|
||||
private static final String DATA_ERROR_SOURCE = "data.error.source";
|
||||
private static final String DATA_BADGE_LEVEL = "data.badge.level";
|
||||
private static final String DATA_REQUEST_BYTES = "data.request.bytes";
|
||||
private static final String DATA_PAYMENT_INTENT_ID = "data.payment.intent.id";
|
||||
private static final String DATA_ERROR_SOURCE = "data.error.source";
|
||||
private static final String DATA_BADGE_LEVEL = "data.badge.level";
|
||||
private static final String DATA_DONATION_PROCESSOR = "data.donation.processor";
|
||||
|
||||
private ReceiptCredentialRequestContext requestContext;
|
||||
|
||||
private final DonationErrorSource donationErrorSource;
|
||||
private final String paymentIntentId;
|
||||
private final long badgeLevel;
|
||||
private final DonationProcessor donationProcessor;
|
||||
|
||||
private static BoostReceiptRequestResponseJob createJob(String paymentIntentId, DonationErrorSource donationErrorSource, long badgeLevel) {
|
||||
private static BoostReceiptRequestResponseJob createJob(String paymentIntentId, DonationErrorSource donationErrorSource, long badgeLevel, DonationProcessor donationProcessor) {
|
||||
return new BoostReceiptRequestResponseJob(
|
||||
new Parameters
|
||||
.Builder()
|
||||
|
@ -67,12 +70,13 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
null,
|
||||
paymentIntentId,
|
||||
donationErrorSource,
|
||||
badgeLevel
|
||||
badgeLevel,
|
||||
donationProcessor
|
||||
);
|
||||
}
|
||||
|
||||
public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId) {
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL));
|
||||
public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId, @NonNull DonationProcessor donationProcessor) {
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor);
|
||||
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost();
|
||||
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost();
|
||||
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
|
||||
|
@ -87,9 +91,10 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
public static JobManager.Chain createJobChainForGift(@NonNull String paymentIntentId,
|
||||
@NonNull RecipientId recipientId,
|
||||
@Nullable String additionalMessage,
|
||||
long badgeLevel)
|
||||
long badgeLevel,
|
||||
@NonNull DonationProcessor donationProcessor)
|
||||
{
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel);
|
||||
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.GIFT, badgeLevel, donationProcessor);
|
||||
GiftSendJob giftSendJob = new GiftSendJob(recipientId, additionalMessage);
|
||||
|
||||
|
||||
|
@ -102,20 +107,23 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
@Nullable ReceiptCredentialRequestContext requestContext,
|
||||
@NonNull String paymentIntentId,
|
||||
@NonNull DonationErrorSource donationErrorSource,
|
||||
long badgeLevel)
|
||||
long badgeLevel,
|
||||
@NonNull DonationProcessor donationProcessor)
|
||||
{
|
||||
super(parameters);
|
||||
this.requestContext = requestContext;
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
this.donationErrorSource = donationErrorSource;
|
||||
this.badgeLevel = badgeLevel;
|
||||
this.donationProcessor = donationProcessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
Data.Builder builder = new Data.Builder().putString(DATA_PAYMENT_INTENT_ID, paymentIntentId)
|
||||
.putString(DATA_ERROR_SOURCE, donationErrorSource.serialize())
|
||||
.putLong(DATA_BADGE_LEVEL, badgeLevel);
|
||||
.putLong(DATA_BADGE_LEVEL, badgeLevel)
|
||||
.putString(DATA_DONATION_PROCESSOR, donationProcessor.getCode());
|
||||
|
||||
if (requestContext != null) {
|
||||
builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize());
|
||||
|
@ -153,7 +161,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
|
||||
Log.d(TAG, "Submitting credential to server", true);
|
||||
ServiceResponse<ReceiptCredentialResponse> response = ApplicationDependencies.getDonationsService()
|
||||
.submitBoostReceiptCredentialRequestSync(paymentIntentId, requestContext.getRequest());
|
||||
.submitBoostReceiptCredentialRequestSync(paymentIntentId, requestContext.getRequest(), donationProcessor);
|
||||
|
||||
if (response.getApplicationError().isPresent()) {
|
||||
handleApplicationError(context, response, donationErrorSource);
|
||||
|
@ -258,18 +266,20 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
|
|||
public static class Factory implements Job.Factory<BoostReceiptRequestResponseJob> {
|
||||
@Override
|
||||
public @NonNull BoostReceiptRequestResponseJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID);
|
||||
DonationErrorSource donationErrorSource = DonationErrorSource.deserialize(data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.BOOST.serialize()));
|
||||
long badgeLevel = data.getLongOrDefault(DATA_BADGE_LEVEL, Long.parseLong(SubscriptionLevels.BOOST_LEVEL));
|
||||
String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID);
|
||||
DonationErrorSource donationErrorSource = DonationErrorSource.deserialize(data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.BOOST.serialize()));
|
||||
long badgeLevel = data.getLongOrDefault(DATA_BADGE_LEVEL, Long.parseLong(SubscriptionLevels.BOOST_LEVEL));
|
||||
String rawDonationProcessor = data.getStringOrDefault(DATA_DONATION_PROCESSOR, DonationProcessor.STRIPE.getCode());
|
||||
DonationProcessor donationProcessor = DonationProcessor.fromCode(rawDonationProcessor);
|
||||
|
||||
try {
|
||||
if (data.hasString(DATA_REQUEST_BYTES)) {
|
||||
byte[] blob = data.getStringAsBlob(DATA_REQUEST_BYTES);
|
||||
ReceiptCredentialRequestContext requestContext = new ReceiptCredentialRequestContext(blob);
|
||||
|
||||
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel);
|
||||
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor);
|
||||
} else {
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel);
|
||||
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor);
|
||||
}
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IllegalStateException(e);
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.donations.PaymentSourceType;
|
||||
import org.signal.donations.StripeDeclineCode;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
|
@ -295,20 +296,27 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
|
|||
|
||||
StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason());
|
||||
DonationError.PaymentSetupError paymentSetupError;
|
||||
PaymentSourceType paymentSourceType = SignalStore.donationsValues().getSubscriptionPaymentSourceType();
|
||||
boolean isStripeSource = paymentSourceType instanceof PaymentSourceType.Stripe;
|
||||
|
||||
if (declineCode.isKnown()) {
|
||||
paymentSetupError = new DonationError.PaymentSetupError.DeclinedError(
|
||||
if (declineCode.isKnown() && isStripeSource) {
|
||||
paymentSetupError = new DonationError.PaymentSetupError.StripeDeclinedError(
|
||||
getErrorSource(),
|
||||
new Exception(chargeFailure.getMessage()),
|
||||
declineCode,
|
||||
SignalStore.donationsValues().getSubscriptionPaymentSourceType()
|
||||
(PaymentSourceType.Stripe) paymentSourceType
|
||||
);
|
||||
} else {
|
||||
paymentSetupError = new DonationError.PaymentSetupError.CodedError(
|
||||
} else if (isStripeSource) {
|
||||
paymentSetupError = new DonationError.PaymentSetupError.StripeCodedError(
|
||||
getErrorSource(),
|
||||
new Exception("Card was declined. " + chargeFailure.getCode()),
|
||||
chargeFailure.getCode()
|
||||
);
|
||||
} else {
|
||||
paymentSetupError = new DonationError.PaymentSetupError.GenericError(
|
||||
getErrorSource(),
|
||||
new Exception("Payment Failed for " + paymentSourceType.getCode())
|
||||
);
|
||||
}
|
||||
|
||||
Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true);
|
||||
|
|
|
@ -5,8 +5,8 @@ import io.reactivex.rxjava3.core.Observable
|
|||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.signal.donations.StripeApi
|
||||
import org.signal.donations.StripePaymentSourceType
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
||||
|
@ -450,12 +450,12 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
|
|||
remove(SUBSCRIPTION_CREDENTIAL_RECEIPT)
|
||||
}
|
||||
|
||||
fun setSubscriptionPaymentSourceType(stripePaymentSourceType: StripePaymentSourceType) {
|
||||
putString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, stripePaymentSourceType.code)
|
||||
fun setSubscriptionPaymentSourceType(paymentSourceType: PaymentSourceType) {
|
||||
putString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, paymentSourceType.code)
|
||||
}
|
||||
|
||||
fun getSubscriptionPaymentSourceType(): StripePaymentSourceType {
|
||||
return StripePaymentSourceType.fromCode(getString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, null))
|
||||
fun getSubscriptionPaymentSourceType(): PaymentSourceType {
|
||||
return PaymentSourceType.fromCode(getString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, null))
|
||||
}
|
||||
|
||||
var subscriptionEndOfPeriodConversionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_CONVERT, 0L)
|
||||
|
|
|
@ -107,6 +107,7 @@ public final class FeatureFlags {
|
|||
private static final String CDS_HARD_LIMIT = "android.cds.hardLimit";
|
||||
private static final String PAYMENTS_IN_CHAT_MESSAGES = "android.payments.inChatMessages";
|
||||
private static final String CHAT_FILTERS = "android.chat.filters";
|
||||
private static final String PAYPAL_DONATIONS = "android.donations.paypal";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -166,7 +167,8 @@ public final class FeatureFlags {
|
|||
KEEP_MUTED_CHATS_ARCHIVED,
|
||||
CDS_HARD_LIMIT,
|
||||
PAYMENTS_IN_CHAT_MESSAGES,
|
||||
CHAT_FILTERS
|
||||
CHAT_FILTERS,
|
||||
PAYPAL_DONATIONS
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -538,8 +540,6 @@ public final class FeatureFlags {
|
|||
|
||||
/**
|
||||
* Whether or not we should allow credit card payments for donations
|
||||
*
|
||||
* WARNING: This feature is not done, and this should not be enabled.
|
||||
*/
|
||||
public static boolean creditCardPayments() {
|
||||
return getBoolean(CREDIT_CARD_PAYMENTS, Environment.IS_STAGING);
|
||||
|
@ -597,6 +597,13 @@ public final class FeatureFlags {
|
|||
return getBoolean(CHAT_FILTERS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should allow PayPal payments for donations
|
||||
*/
|
||||
public static boolean paypalDonations() {
|
||||
return getBoolean(PAYPAL_DONATIONS, Environment.IS_STAGING);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
43
app/src/main/res/drawable/paypal.xml
Normal file
43
app/src/main/res/drawable/paypal.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="92dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="92"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M0,0h92v24h-92z"
|
||||
android:fillColor="#00000000"/>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M1.4,0h89.28v24h-89.28z"/>
|
||||
<path
|
||||
android:pathData="M34.672,4.908H29.748C29.411,4.908 29.124,5.156 29.072,5.492L27.08,18.246C27.041,18.497 27.234,18.724 27.486,18.724H29.837C30.174,18.724 30.461,18.477 30.513,18.14L31.05,14.7C31.102,14.364 31.389,14.116 31.726,14.116H33.285C36.528,14.116 38.4,12.531 38.889,9.389C39.109,8.015 38.898,6.935 38.261,6.178C37.561,5.348 36.32,4.908 34.672,4.908ZM35.24,9.567C34.971,11.351 33.621,11.351 32.315,11.351H31.572L32.094,8.018C32.125,7.817 32.297,7.668 32.499,7.668H32.84C33.729,7.668 34.568,7.668 35.001,8.18C35.259,8.486 35.339,8.94 35.24,9.567Z"
|
||||
android:fillColor="#253B80"/>
|
||||
<path
|
||||
android:pathData="M49.391,9.509H47.033C46.832,9.509 46.659,9.657 46.627,9.859L46.523,10.525L46.358,10.284C45.848,9.535 44.709,9.285 43.573,9.285C40.968,9.285 38.742,11.278 38.309,14.075C38.083,15.47 38.404,16.804 39.187,17.734C39.906,18.589 40.934,18.945 42.157,18.945C44.257,18.945 45.421,17.582 45.421,17.582L45.316,18.244C45.276,18.497 45.469,18.724 45.72,18.724H47.844C48.182,18.724 48.467,18.476 48.52,18.14L49.795,9.988C49.835,9.737 49.643,9.509 49.391,9.509ZM46.104,14.145C45.877,15.505 44.807,16.419 43.444,16.419C42.759,16.419 42.212,16.197 41.86,15.777C41.512,15.359 41.379,14.765 41.49,14.103C41.703,12.754 42.79,11.811 44.133,11.811C44.802,11.811 45.347,12.036 45.705,12.46C46.064,12.888 46.207,13.486 46.104,14.145Z"
|
||||
android:fillColor="#253B80"/>
|
||||
<path
|
||||
android:pathData="M61.949,9.509H59.58C59.354,9.509 59.141,9.622 59.013,9.812L55.745,14.675L54.36,10.002C54.272,9.71 54.005,9.509 53.703,9.509H51.375C51.091,9.509 50.895,9.788 50.985,10.057L53.595,17.794L51.141,21.293C50.948,21.569 51.143,21.948 51.476,21.948H53.843C54.067,21.948 54.278,21.837 54.405,21.651L62.286,10.16C62.475,9.885 62.281,9.509 61.949,9.509Z"
|
||||
android:fillColor="#253B80"/>
|
||||
<path
|
||||
android:pathData="M69.794,4.908H64.869C64.533,4.908 64.247,5.156 64.194,5.492L62.203,18.246C62.163,18.497 62.356,18.724 62.607,18.724H65.134C65.369,18.724 65.57,18.551 65.607,18.316L66.172,14.7C66.224,14.364 66.511,14.116 66.847,14.116H68.405C71.65,14.116 73.521,12.531 74.011,9.389C74.232,8.015 74.019,6.935 73.382,6.178C72.683,5.348 71.442,4.908 69.794,4.908ZM70.362,9.567C70.094,11.351 68.744,11.351 67.438,11.351H66.695L67.217,8.018C67.248,7.817 67.42,7.668 67.622,7.668H67.963C68.851,7.668 69.691,7.668 70.124,8.18C70.382,8.486 70.461,8.94 70.362,9.567Z"
|
||||
android:fillColor="#179BD7"/>
|
||||
<path
|
||||
android:pathData="M84.512,9.509H82.156C81.954,9.509 81.782,9.657 81.751,9.859L81.647,10.525L81.481,10.284C80.971,9.535 79.833,9.285 78.697,9.285C76.091,9.285 73.867,11.278 73.433,14.075C73.209,15.47 73.527,16.804 74.311,17.734C75.031,18.589 76.058,18.945 77.281,18.945C79.38,18.945 80.545,17.582 80.545,17.582L80.439,18.244C80.4,18.497 80.593,18.724 80.845,18.724H82.969C83.305,18.724 83.592,18.476 83.644,18.14L84.919,9.988C84.958,9.737 84.765,9.509 84.512,9.509ZM81.226,14.145C81,15.505 79.929,16.419 78.565,16.419C77.882,16.419 77.333,16.197 76.982,15.777C76.633,15.359 76.503,14.765 76.612,14.103C76.826,12.754 77.911,11.811 79.254,11.811C79.924,11.811 80.468,12.036 80.827,12.46C81.188,12.888 81.33,13.486 81.226,14.145Z"
|
||||
android:fillColor="#179BD7"/>
|
||||
<path
|
||||
android:pathData="M87.292,5.258L85.271,18.246C85.232,18.497 85.425,18.724 85.676,18.724H87.708C88.046,18.724 88.332,18.477 88.384,18.14L90.377,5.387C90.416,5.135 90.224,4.908 89.972,4.908H87.697C87.496,4.908 87.323,5.057 87.292,5.258Z"
|
||||
android:fillColor="#179BD7"/>
|
||||
<path
|
||||
android:pathData="M6.632,21.203L7.008,18.787L6.169,18.767H2.164L4.947,0.94C4.956,0.886 4.984,0.836 5.025,0.8C5.066,0.764 5.119,0.745 5.174,0.745H11.927C14.169,0.745 15.717,1.216 16.524,2.146C16.903,2.583 17.144,3.039 17.261,3.54C17.383,4.067 17.385,4.696 17.266,5.463L17.257,5.519V6.011L17.636,6.228C17.955,6.399 18.208,6.594 18.403,6.818C18.727,7.191 18.936,7.665 19.025,8.228C19.116,8.806 19.086,9.494 18.936,10.273C18.764,11.169 18.484,11.949 18.107,12.588C17.76,13.176 17.318,13.664 16.793,14.042C16.292,14.401 15.696,14.674 15.023,14.849C14.371,15.02 13.627,15.107 12.811,15.107H12.286C11.91,15.107 11.545,15.244 11.258,15.489C10.971,15.739 10.781,16.081 10.723,16.455L10.683,16.672L10.018,20.93L9.987,21.087C9.98,21.136 9.966,21.161 9.946,21.177C9.928,21.193 9.902,21.203 9.877,21.203H6.632Z"
|
||||
android:fillColor="#253B80"/>
|
||||
<path
|
||||
android:pathData="M17.995,5.576C17.974,5.706 17.951,5.839 17.925,5.976C17.035,10.595 13.988,12.191 10.096,12.191H8.115C7.639,12.191 7.238,12.54 7.164,13.014L6.149,19.513L5.862,21.355C5.814,21.666 6.051,21.947 6.362,21.947H9.877C10.293,21.947 10.646,21.642 10.712,21.227L10.746,21.047L11.408,16.805L11.45,16.572C11.515,16.156 11.87,15.851 12.286,15.851H12.811C16.216,15.851 18.882,14.455 19.661,10.414C19.986,8.726 19.818,7.316 18.956,6.325C18.696,6.026 18.373,5.778 17.995,5.576Z"
|
||||
android:fillColor="#179BD7"/>
|
||||
<path
|
||||
android:pathData="M17.063,5.201C16.927,5.161 16.786,5.124 16.642,5.092C16.498,5.06 16.349,5.031 16.197,5.007C15.663,4.919 15.077,4.878 14.45,4.878H9.157C9.026,4.878 8.902,4.908 8.792,4.961C8.547,5.08 8.366,5.313 8.322,5.599L7.196,12.804L7.164,13.014C7.238,12.54 7.639,12.191 8.115,12.191H10.096C13.988,12.191 17.035,10.594 17.925,5.976C17.952,5.839 17.974,5.706 17.995,5.576C17.769,5.455 17.525,5.352 17.262,5.264C17.198,5.242 17.131,5.221 17.063,5.201Z"
|
||||
android:fillColor="#222D65"/>
|
||||
<path
|
||||
android:pathData="M8.322,5.599C8.366,5.313 8.547,5.08 8.792,4.962C8.903,4.908 9.026,4.879 9.157,4.879H14.45C15.077,4.879 15.663,4.92 16.197,5.007C16.349,5.032 16.498,5.06 16.642,5.092C16.786,5.125 16.927,5.161 17.063,5.201C17.131,5.222 17.198,5.243 17.263,5.264C17.526,5.352 17.77,5.456 17.995,5.576C18.26,3.869 17.993,2.707 17.079,1.655C16.072,0.496 14.254,0 11.928,0H5.174C4.699,0 4.294,0.349 4.22,0.824L1.407,18.835C1.352,19.191 1.624,19.513 1.98,19.513H6.149L7.196,12.804L8.322,5.599Z"
|
||||
android:fillColor="#253B80"/>
|
||||
</group>
|
||||
</vector>
|
24
app/src/main/res/layout/paypal_button.xml
Normal file
24
app/src/main/res/layout/paypal_button.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/paypal_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="@dimen/dsl_settings_gutter"
|
||||
android:layout_marginEnd="@dimen/dsl_settings_gutter"
|
||||
android:insetTop="2dp"
|
||||
android:insetBottom="2dp"
|
||||
app:cornerRadius="59dp"
|
||||
app:icon="@drawable/paypal"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconTint="@null"
|
||||
app:backgroundTint="#EEEEEE"
|
||||
app:strokeColor="@color/paypal_outline"
|
||||
app:strokeWidth="1.5dp" />
|
||||
</FrameLayout>
|
|
@ -39,6 +39,9 @@
|
|||
<action
|
||||
android:id="@+id/action_donateToSignalFragment_to_creditCardFragment"
|
||||
app:destination="@id/creditCardFragment" />
|
||||
<action
|
||||
android:id="@+id/action_donateToSignalFragment_to_paypalPaymentInProgressFragment"
|
||||
app:destination="@id/paypalPaymentInProgressFragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
|
@ -74,7 +77,7 @@
|
|||
android:id="@+id/stripePaymentInProgressFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment"
|
||||
android:label="stripe_payment_in_progress_fragment"
|
||||
tools:layout="@layout/stripe_payment_in_progress_fragment">
|
||||
tools:layout="@layout/donation_in_progress_fragment">
|
||||
|
||||
<argument
|
||||
android:name="action"
|
||||
|
@ -133,7 +136,7 @@
|
|||
android:id="@+id/stripe3dsDialogFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment"
|
||||
android:label="stripe_3ds_dialog_fragment"
|
||||
tools:layout="@layout/stripe_3ds_dialog_fragment">
|
||||
tools:layout="@layout/donation_webview_fragment">
|
||||
|
||||
<argument
|
||||
android:name="uri"
|
||||
|
@ -152,4 +155,37 @@
|
|||
android:label="your_information_is_private_bottom_sheet"
|
||||
tools:layout="@layout/dsl_settings_bottom_sheet" />
|
||||
|
||||
<dialog
|
||||
android:id="@+id/paypalPaymentInProgressFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment"
|
||||
android:label="paypal_payment_in_progress"
|
||||
tools:layout="@layout/donation_in_progress_fragment">
|
||||
|
||||
<argument
|
||||
android:name="action"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="request"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
|
||||
app:nullable="false" />
|
||||
<action
|
||||
android:id="@+id/action_paypalPaymentInProgressFragment_to_paypalConfirmationFragment"
|
||||
app:destination="@id/paypalConfirmationFragment" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/paypalConfirmationFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalConfirmationDialogFragment"
|
||||
android:label="paypal_confirmation_dialog_fragment"
|
||||
tools:layout="@layout/donation_webview_fragment">
|
||||
|
||||
<argument
|
||||
android:name="uri"
|
||||
app:argType="android.net.Uri"
|
||||
app:nullable="false" />
|
||||
|
||||
</dialog>
|
||||
|
||||
</navigation>
|
|
@ -59,6 +59,9 @@
|
|||
<action
|
||||
android:id="@+id/action_giftFlowConfirmationFragment_to_gatewaySelectorBottomSheet"
|
||||
app:destination="@id/gatewaySelectorBottomSheet" />
|
||||
<action
|
||||
android:id="@+id/action_giftFlowConfirmationFragment_to_paypalPaymentInProgressFragment"
|
||||
app:destination="@id/paypalPaymentInProgressFragment" />
|
||||
</fragment>
|
||||
|
||||
<dialog
|
||||
|
@ -78,7 +81,7 @@
|
|||
android:id="@+id/stripePaymentInProgressFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment"
|
||||
android:label="stripe_payment_in_progress_fragment"
|
||||
tools:layout="@layout/stripe_payment_in_progress_fragment">
|
||||
tools:layout="@layout/donation_in_progress_fragment">
|
||||
|
||||
<argument
|
||||
android:name="action"
|
||||
|
@ -114,7 +117,7 @@
|
|||
android:id="@+id/stripe3dsDialogFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment"
|
||||
android:label="stripe_3ds_dialog_fragment"
|
||||
tools:layout="@layout/stripe_3ds_dialog_fragment">
|
||||
tools:layout="@layout/donation_webview_fragment">
|
||||
|
||||
<argument
|
||||
android:name="uri"
|
||||
|
@ -132,4 +135,37 @@
|
|||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.YourInformationIsPrivateBottomSheet"
|
||||
android:label="your_information_is_private_bottom_sheet"
|
||||
tools:layout="@layout/dsl_settings_bottom_sheet" />
|
||||
|
||||
<dialog
|
||||
android:id="@+id/paypalPaymentInProgressFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment"
|
||||
android:label="paypal_payment_in_progress"
|
||||
tools:layout="@layout/donation_in_progress_fragment">
|
||||
|
||||
<argument
|
||||
android:name="action"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction"
|
||||
app:nullable="false" />
|
||||
|
||||
<argument
|
||||
android:name="request"
|
||||
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
|
||||
app:nullable="false" />
|
||||
<action
|
||||
android:id="@+id/action_paypalPaymentInProgressFragment_to_paypalConfirmationFragment"
|
||||
app:destination="@id/paypalConfirmationFragment" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
android:id="@+id/paypalConfirmationFragment"
|
||||
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalConfirmationDialogFragment"
|
||||
android:label="paypal_confirmation_dialog_fragment"
|
||||
tools:layout="@layout/donation_webview_fragment">
|
||||
|
||||
<argument
|
||||
android:name="uri"
|
||||
app:argType="android.net.Uri"
|
||||
app:nullable="false" />
|
||||
|
||||
</dialog>
|
||||
</navigation>
|
|
@ -1,5 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="paypal_outline">#00000000</color>
|
||||
|
||||
<color name="conversation_toolbar_color">@color/signal_colorSurface</color>
|
||||
<color name="conversation_toolbar_color_wallpaper">@color/signal_colorTransparentInverse5</color>
|
||||
<color name="conversation_toolbar_color_wallpaper_scrolled">@color/signal_colorTransparentInverse5</color>
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
<color name="transparent_white_90">#e6ffffff</color>
|
||||
<color name="transparent_white_95">#f3ffffff</color>
|
||||
|
||||
<color name="paypal_outline">#80838089</color>
|
||||
|
||||
<color name="conversation_compose_divider">#32000000</color>
|
||||
|
||||
<color name="conversation_item_selected_system_ui">#4d4d4d</color>
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import android.app.Application
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = Application::class)
|
||||
class PayPalConfirmationResultTest {
|
||||
|
||||
companion object {
|
||||
private val PAYER_ID = "asdf"
|
||||
private val PAYMENT_ID = "sdfg"
|
||||
private val PAYMENT_TOKEN = "dfgh"
|
||||
|
||||
private val TEST_URL = "${PayPalRepository.ONE_TIME_RETURN_URL}?PayerID=$PAYER_ID&paymentId=$PAYMENT_ID&token=$PAYMENT_TOKEN"
|
||||
private val TEST_MISSING_PARAM_URL = "${PayPalRepository.ONE_TIME_RETURN_URL}?paymentId=$PAYMENT_ID&token=$PAYMENT_TOKEN"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenATestUrl_whenIFromUri_thenIExpectCorrectResult() {
|
||||
val result = PayPalConfirmationResult.fromUrl(TEST_URL)
|
||||
|
||||
assertEquals(
|
||||
PayPalConfirmationResult(PAYER_ID, PAYMENT_ID, PAYMENT_TOKEN),
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenATestUrlWithMissingField_whenIFromUri_thenIExpectNull() {
|
||||
val result = PayPalConfirmationResult.fromUrl(TEST_MISSING_PARAM_URL)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import org.json.JSONObject
|
|||
class CreditCardPaymentSource(
|
||||
private val payload: JSONObject
|
||||
) : StripeApi.PaymentSource {
|
||||
override val type = StripePaymentSourceType.CREDIT_CARD
|
||||
override val type = PaymentSourceType.Stripe.CreditCard
|
||||
override fun parameterize(): JSONObject = payload
|
||||
override fun getTokenId(): String = parameterize().getString("id")
|
||||
override fun email(): String? = null
|
||||
|
|
|
@ -4,7 +4,7 @@ import com.google.android.gms.wallet.PaymentData
|
|||
import org.json.JSONObject
|
||||
|
||||
class GooglePayPaymentSource(private val paymentData: PaymentData) : StripeApi.PaymentSource {
|
||||
override val type = StripePaymentSourceType.GOOGLE_PAY
|
||||
override val type = PaymentSourceType.Stripe.GooglePay
|
||||
|
||||
override fun parameterize(): JSONObject {
|
||||
val jsonData = JSONObject(paymentData.toJson())
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package org.signal.donations
|
||||
|
||||
sealed class PaymentSourceType {
|
||||
abstract val code: String
|
||||
|
||||
object Unknown : PaymentSourceType() {
|
||||
override val code: String = Codes.UNKNOWN.code
|
||||
}
|
||||
|
||||
object PayPal : PaymentSourceType() {
|
||||
override val code: String = Codes.PAY_PAL.code
|
||||
}
|
||||
|
||||
sealed class Stripe(override val code: String) : PaymentSourceType() {
|
||||
object CreditCard : Stripe(Codes.CREDIT_CARD.code)
|
||||
object GooglePay : Stripe(Codes.GOOGLE_PAY.code)
|
||||
}
|
||||
|
||||
private enum class Codes(val code: String) {
|
||||
UNKNOWN("unknown"),
|
||||
PAY_PAL("paypal"),
|
||||
CREDIT_CARD("credit_card"),
|
||||
GOOGLE_PAY("google_pay")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String?): PaymentSourceType {
|
||||
return when (Codes.values().firstOrNull { it.code == code } ?: Codes.UNKNOWN) {
|
||||
Codes.UNKNOWN -> Unknown
|
||||
Codes.PAY_PAL -> PayPal
|
||||
Codes.CREDIT_CARD -> Stripe.CreditCard
|
||||
Codes.GOOGLE_PAY -> Stripe.GooglePay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -520,7 +520,7 @@ class StripeApi(
|
|||
) : Parcelable
|
||||
|
||||
interface PaymentSource {
|
||||
val type: StripePaymentSourceType
|
||||
val type: PaymentSourceType
|
||||
fun parameterize(): JSONObject
|
||||
fun getTokenId(): String
|
||||
fun email(): String?
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package org.signal.donations
|
||||
|
||||
enum class StripePaymentSourceType(val code: String) {
|
||||
CREDIT_CARD("credit_card"),
|
||||
GOOGLE_PAY("google_pay");
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String?): StripePaymentSourceType {
|
||||
return values().firstOrNull { it.code == code } ?: GOOGLE_PAY
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,9 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
|||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId;
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
|
||||
|
@ -16,6 +19,7 @@ import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
|||
import org.whispersystems.signalservice.internal.EmptyResponse;
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -87,8 +91,8 @@ public class DonationsService {
|
|||
* @param paymentIntentId PaymentIntent ID from a boost donation intent response.
|
||||
* @param receiptCredentialRequest Client-generated request token
|
||||
*/
|
||||
public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
|
||||
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest), 200));
|
||||
public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) {
|
||||
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest, processor), 200));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -217,24 +221,129 @@ public class DonationsService {
|
|||
|
||||
public ServiceResponse<EmptyResponse> setDefaultStripePaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
|
||||
return wrapInServiceResponse(() -> {
|
||||
pushServiceSocket.setDefaultSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
|
||||
pushServiceSocket.setDefaultStripeSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
|
||||
return new Pair<>(EmptyResponse.INSTANCE, 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subscriberId The subscriber ID to create a payment method for.
|
||||
* @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs
|
||||
* but instead with the SetupIntent stripe APIs.
|
||||
* @param subscriberId The subscriber ID to create a payment method for.
|
||||
* @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs
|
||||
* but instead with the SetupIntent stripe APIs.
|
||||
*/
|
||||
public ServiceResponse<StripeClientSecret> createStripeSubscriptionPaymentMethod(SubscriberId subscriberId) {
|
||||
return wrapInServiceResponse(() -> {
|
||||
StripeClientSecret clientSecret = pushServiceSocket.createSubscriptionPaymentMethod(subscriberId.serialize());
|
||||
StripeClientSecret clientSecret = pushServiceSocket.createStripeSubscriptionPaymentMethod(subscriberId.serialize());
|
||||
return new Pair<>(clientSecret, 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PayPal one-time payment and returns the approval URL
|
||||
* Response Codes
|
||||
* 200 — success
|
||||
* 400 — request error
|
||||
* 409 — level requires a valid currency/amount combination that does not match
|
||||
*
|
||||
* @param locale User locale for proper language presentation
|
||||
* @param currencyCode 3 letter currency code of the desired currency
|
||||
* @param amount Stringified minimum precision amount
|
||||
* @param level The badge level to purchase
|
||||
* @param returnUrl The 'return' url after a successful login and confirmation
|
||||
* @param cancelUrl The 'cancel' url for a cancelled confirmation
|
||||
* @return Wrapped response with either an error code or a payment id and approval URL
|
||||
*/
|
||||
public ServiceResponse<PayPalCreatePaymentIntentResponse> createPayPalOneTimePaymentIntent(Locale locale,
|
||||
String currencyCode,
|
||||
String amount,
|
||||
long level,
|
||||
String returnUrl,
|
||||
String cancelUrl)
|
||||
{
|
||||
return wrapInServiceResponse(() -> {
|
||||
PayPalCreatePaymentIntentResponse response = pushServiceSocket.createPayPalOneTimePaymentIntent(
|
||||
locale,
|
||||
currencyCode.toUpperCase(Locale.US), // Chris Eager to make this case insensitive in the next build
|
||||
Long.parseLong(amount),
|
||||
level,
|
||||
returnUrl,
|
||||
cancelUrl
|
||||
);
|
||||
return new Pair<>(response, 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms a PayPal one-time payment and returns the paymentId for receipt credentials
|
||||
* Response Codes
|
||||
* 200 — success
|
||||
* 400 — request error
|
||||
* 409 — level requires a valid currency/amount combination that does not match
|
||||
*
|
||||
* @param currency 3 letter currency code of the desired currency
|
||||
* @param amount Stringified minimum precision amount
|
||||
* @param level The badge level to purchase
|
||||
* @param payerId Passed as a URL parameter back to returnUrl
|
||||
* @param paymentId Passed as a URL parameter back to returnUrl
|
||||
* @param paymentToken Passed as a URL parameter back to returnUrl
|
||||
* @return Wrapped response with either an error code or a payment id
|
||||
*/
|
||||
public ServiceResponse<PayPalConfirmPaymentIntentResponse> confirmPayPalOneTimePaymentIntent(String currency,
|
||||
String amount,
|
||||
long level,
|
||||
String payerId,
|
||||
String paymentId,
|
||||
String paymentToken)
|
||||
{
|
||||
return wrapInServiceResponse(() -> {
|
||||
PayPalConfirmPaymentIntentResponse response = pushServiceSocket.confirmPayPalOneTimePaymentIntent(currency, amount, level, payerId, paymentId, paymentToken);
|
||||
return new Pair<>(response, 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a payment method via PayPal for recurring charges.
|
||||
*
|
||||
* Response Codes
|
||||
* 200 — success
|
||||
* 403 — subscriberId password mismatches OR account authentication is present
|
||||
* 404 — subscriberId is not found or malformed
|
||||
*
|
||||
* @param locale User locale
|
||||
* @param subscriberId User subscriber id
|
||||
* @param returnUrl A success URL
|
||||
* @param cancelUrl A cancel URL
|
||||
* @return A response with an approval url and token
|
||||
*/
|
||||
public ServiceResponse<PayPalCreatePaymentMethodResponse> createPayPalPaymentMethod(Locale locale,
|
||||
SubscriberId subscriberId,
|
||||
String returnUrl,
|
||||
String cancelUrl) {
|
||||
return wrapInServiceResponse(() -> {
|
||||
PayPalCreatePaymentMethodResponse response = pushServiceSocket.createPayPalPaymentMethod(locale, subscriberId.serialize(), returnUrl, cancelUrl);
|
||||
return new Pair<>(response, 200);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given payment method as the default in PayPal
|
||||
*
|
||||
* Response Codes
|
||||
* 200 — success
|
||||
* 403 — subscriberId password mismatches OR account authentication is present
|
||||
* 404 — subscriberId is not found or malformed
|
||||
* 409 — subscriber record is missing customer ID - must call POST /v1/subscription/{subscriberId}/create_payment_method first
|
||||
*
|
||||
* @param subscriberId User subscriber id
|
||||
* @param paymentMethodId Payment method id to make default
|
||||
*/
|
||||
public ServiceResponse<EmptyResponse> setDefaultPayPalPaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
|
||||
return wrapInServiceResponse(() -> {
|
||||
pushServiceSocket.setDefaultPaypalSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
|
||||
return new Pair<>(EmptyResponse.INSTANCE, 200);
|
||||
});
|
||||
}
|
||||
|
||||
public ServiceResponse<ReceiptCredentialResponse> submitReceiptCredentialRequestSync(SubscriberId subscriberId, ReceiptCredentialRequest receiptCredentialRequest) {
|
||||
return wrapInServiceResponse(() -> {
|
||||
ReceiptCredentialResponse response = pushServiceSocket.submitReceiptCredentials(subscriberId.serialize(), receiptCredentialRequest);
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package org.whispersystems.signalservice.api.subscriptions;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Response object from creating a payment intent via PayPal
|
||||
*/
|
||||
public class PayPalConfirmPaymentIntentResponse {
|
||||
|
||||
private final String paymentId;
|
||||
|
||||
@JsonCreator
|
||||
public PayPalConfirmPaymentIntentResponse(@JsonProperty("paymentId") String paymentId) {
|
||||
this.paymentId = paymentId;
|
||||
}
|
||||
|
||||
public String getPaymentId() {
|
||||
return paymentId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.whispersystems.signalservice.api.subscriptions;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Response object from creating a payment intent via PayPal
|
||||
*/
|
||||
public class PayPalCreatePaymentIntentResponse {
|
||||
|
||||
private final String approvalUrl;
|
||||
private final String paymentId;
|
||||
|
||||
@JsonCreator
|
||||
public PayPalCreatePaymentIntentResponse(@JsonProperty("approvalUrl") String approvalUrl, @JsonProperty("paymentId") String paymentId) {
|
||||
this.approvalUrl = approvalUrl;
|
||||
this.paymentId = paymentId;
|
||||
}
|
||||
|
||||
public String getApprovalUrl() {
|
||||
return approvalUrl;
|
||||
}
|
||||
|
||||
public String getPaymentId() {
|
||||
return paymentId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package org.whispersystems.signalservice.api.subscriptions;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PayPalCreatePaymentMethodResponse {
|
||||
private final String approvalUrl;
|
||||
private final String token;
|
||||
|
||||
@JsonCreator
|
||||
public PayPalCreatePaymentMethodResponse(@JsonProperty("approvalUrl") String approvalUrl, @JsonProperty("token") String token) {
|
||||
this.approvalUrl = approvalUrl;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getApprovalUrl() {
|
||||
return approvalUrl;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
}
|
|
@ -12,8 +12,12 @@ class BoostReceiptCredentialRequestJson {
|
|||
@JsonProperty("receiptCredentialRequest")
|
||||
private final String receiptCredentialRequest;
|
||||
|
||||
BoostReceiptCredentialRequestJson(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
|
||||
@JsonProperty("processor")
|
||||
private final String processor;
|
||||
|
||||
BoostReceiptCredentialRequestJson(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) {
|
||||
this.paymentIntentId = paymentIntentId;
|
||||
this.receiptCredentialRequest = Base64.encodeBytes(receiptCredentialRequest.serialize());
|
||||
this.processor = processor.getCode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Represents the processor being used for a given payment, required when accessing
|
||||
* receipt credentials.
|
||||
*/
|
||||
public enum DonationProcessor {
|
||||
STRIPE("STRIPE"),
|
||||
PAYPAL("BRAINTREE");
|
||||
|
||||
private final String code;
|
||||
|
||||
DonationProcessor(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public static DonationProcessor fromCode(String code) {
|
||||
for (final DonationProcessor value : values()) {
|
||||
if (Objects.equals(code, value.code)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(code);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Request JSON for confirming a PayPal one-time payment intent
|
||||
*/
|
||||
class PayPalConfirmOneTimePaymentIntentPayload {
|
||||
@JsonProperty
|
||||
private String amount;
|
||||
|
||||
@JsonProperty
|
||||
private String currency;
|
||||
|
||||
@JsonProperty
|
||||
private long level;
|
||||
|
||||
@JsonProperty
|
||||
private String payerId;
|
||||
|
||||
@JsonProperty
|
||||
private String paymentId;
|
||||
|
||||
@JsonProperty
|
||||
private String paymentToken;
|
||||
|
||||
public PayPalConfirmOneTimePaymentIntentPayload(String amount, String currency, long level, String payerId, String paymentId, String paymentToken) {
|
||||
this.amount = amount;
|
||||
this.currency = currency;
|
||||
this.level = level;
|
||||
this.payerId = payerId;
|
||||
this.paymentId = paymentId;
|
||||
this.paymentToken = paymentToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Request JSON for creating a PayPal one-time payment intent
|
||||
*/
|
||||
class PayPalCreateOneTimePaymentIntentPayload {
|
||||
@JsonProperty
|
||||
private long amount;
|
||||
|
||||
@JsonProperty
|
||||
private String currency;
|
||||
|
||||
@JsonProperty
|
||||
private long level;
|
||||
|
||||
@JsonProperty
|
||||
private String returnUrl;
|
||||
|
||||
@JsonProperty
|
||||
private String cancelUrl;
|
||||
|
||||
public PayPalCreateOneTimePaymentIntentPayload(long amount, String currency, long level, String returnUrl, String cancelUrl) {
|
||||
this.amount = amount;
|
||||
this.currency = currency;
|
||||
this.level = level;
|
||||
this.returnUrl = returnUrl;
|
||||
this.cancelUrl = cancelUrl;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package org.whispersystems.signalservice.internal.push;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
class PayPalCreatePaymentMethodPayload {
|
||||
@JsonProperty
|
||||
private String returnUrl;
|
||||
|
||||
@JsonProperty
|
||||
private String cancelUrl;
|
||||
|
||||
PayPalCreatePaymentMethodPayload(String returnUrl, String cancelUrl) {
|
||||
this.returnUrl = returnUrl;
|
||||
this.cancelUrl = cancelUrl;
|
||||
}
|
||||
}
|
|
@ -86,6 +86,9 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedExc
|
|||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
||||
import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
|
||||
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
|
@ -261,17 +264,21 @@ public class PushServiceSocket {
|
|||
|
||||
private static final String DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt";
|
||||
|
||||
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels";
|
||||
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
|
||||
private static final String SUBSCRIPTION = "/v1/subscription/%s";
|
||||
private static final String CREATE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
|
||||
private static final String DEFAULT_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
|
||||
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
|
||||
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
|
||||
private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift";
|
||||
private static final String CREATE_BOOST_PAYMENT_INTENT = "/v1/subscription/boost/create";
|
||||
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
|
||||
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
|
||||
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels";
|
||||
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
|
||||
private static final String SUBSCRIPTION = "/v1/subscription/%s";
|
||||
private static final String CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
|
||||
private static final String CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method/paypal";
|
||||
private static final String DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
|
||||
private static final String DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/paypal/%s";
|
||||
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
|
||||
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
|
||||
private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift";
|
||||
private static final String CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/create";
|
||||
private static final String CREATE_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/create";
|
||||
private static final String CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/confirm";
|
||||
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
|
||||
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
|
||||
|
||||
private static final String CDSI_AUTH = "/v2/directory/auth";
|
||||
|
||||
|
@ -1019,10 +1026,33 @@ public class PushServiceSocket {
|
|||
|
||||
public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, long amount, long level) throws IOException {
|
||||
String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level));
|
||||
String result = makeServiceRequestWithoutAuthentication(CREATE_BOOST_PAYMENT_INTENT, "POST", payload);
|
||||
String result = makeServiceRequestWithoutAuthentication(CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT, "POST", payload);
|
||||
return JsonUtil.fromJsonResponse(result, StripeClientSecret.class);
|
||||
}
|
||||
|
||||
public PayPalCreatePaymentIntentResponse createPayPalOneTimePaymentIntent(Locale locale, String currencyCode, long amount, long level, String returnUrl, String cancelUrl) throws IOException {
|
||||
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
|
||||
String payload = JsonUtil.toJson(new PayPalCreateOneTimePaymentIntentPayload(amount, currencyCode, level, returnUrl, cancelUrl));
|
||||
String result = makeServiceRequestWithoutAuthentication(CREATE_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload, headers, NO_HANDLER);
|
||||
|
||||
return JsonUtil.fromJsonResponse(result, PayPalCreatePaymentIntentResponse.class);
|
||||
}
|
||||
|
||||
public PayPalConfirmPaymentIntentResponse confirmPayPalOneTimePaymentIntent(String currency, String amount, long level, String payerId, String paymentId, String paymentToken) throws IOException {
|
||||
String payload = JsonUtil.toJson(new PayPalConfirmOneTimePaymentIntentPayload(amount, currency, level, payerId, paymentId, paymentToken));
|
||||
Log.d(TAG, payload);
|
||||
String result = makeServiceRequestWithoutAuthentication(CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload);
|
||||
return JsonUtil.fromJsonResponse(result, PayPalConfirmPaymentIntentResponse.class);
|
||||
}
|
||||
|
||||
public PayPalCreatePaymentMethodResponse createPayPalPaymentMethod(Locale locale, String subscriberId, String returnUrl, String cancelUrl) throws IOException {
|
||||
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
|
||||
String payload = JsonUtil.toJson(new PayPalCreatePaymentMethodPayload(returnUrl, cancelUrl));
|
||||
String result = makeServiceRequestWithoutAuthentication(String.format(CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", payload);
|
||||
return JsonUtil.fromJsonResponse(result, PayPalCreatePaymentMethodResponse.class);
|
||||
}
|
||||
|
||||
|
||||
public Map<String, List<BigDecimal>> getBoostAmounts() throws IOException {
|
||||
String result = makeServiceRequestWithoutAuthentication(BOOST_AMOUNTS, "GET", null);
|
||||
TypeReference<HashMap<String, List<BigDecimal>>> typeRef = new TypeReference<HashMap<String, List<BigDecimal>>>() {};
|
||||
|
@ -1040,8 +1070,8 @@ public class PushServiceSocket {
|
|||
return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);
|
||||
}
|
||||
|
||||
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {
|
||||
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest));
|
||||
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) throws IOException {
|
||||
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest, processor));
|
||||
String response = makeServiceRequestWithoutAuthentication(
|
||||
BOOST_RECEIPT_CREDENTIALS,
|
||||
"POST",
|
||||
|
@ -1082,13 +1112,17 @@ public class PushServiceSocket {
|
|||
makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "DELETE", null);
|
||||
}
|
||||
|
||||
public StripeClientSecret createSubscriptionPaymentMethod(String subscriberId) throws IOException {
|
||||
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", "");
|
||||
public StripeClientSecret createStripeSubscriptionPaymentMethod(String subscriberId) throws IOException {
|
||||
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", "");
|
||||
return JsonUtil.fromJson(response, StripeClientSecret.class);
|
||||
}
|
||||
|
||||
public void setDefaultSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
|
||||
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
|
||||
public void setDefaultStripeSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
|
||||
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
|
||||
}
|
||||
|
||||
public void setDefaultPaypalSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
|
||||
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
|
||||
}
|
||||
|
||||
public ReceiptCredentialResponse submitReceiptCredentials(String subscriptionId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {
|
||||
|
|
Loading…
Add table
Reference in a new issue