Add initial PayPal implementation behind a feature flag.

main
Alex Hart 2022-11-30 12:43:46 -04:00 zatwierdzone przez Cody Henthorne
rodzic b70b4fac91
commit 979f87db78
47 zmienionych plików z 1382 dodań i 144 usunięć

Wyświetl plik

@ -282,6 +282,10 @@ class GiftFlowConfirmationFragment :
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest)) 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) { override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(gatewayRequest)) findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(gatewayRequest))
} }

Wyświetl plik

@ -32,7 +32,7 @@ object InAppDonations {
* Whether the user is in a region that supports PayPal, based off local phone number. * Whether the user is in a region that supports PayPal, based off local phone number.
*/ */
fun isPayPalAvailable(): Boolean { fun isPayPalAvailable(): Boolean {
return false return FeatureFlags.paypalDonations() && !LocaleFeatureFlags.isPayPalDisabled()
} }
/** /**

Wyświetl plik

@ -5,6 +5,7 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney import org.signal.core.util.money.FiatMoney
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost 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.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.services.DonationsService import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.internal.ServiceResponse import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.DonationProcessor
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Currency import java.util.Currency
import java.util.Locale import java.util.Locale
@ -31,6 +33,16 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
companion object { companion object {
private val TAG = Log.tag(OneTimeDonationRepository::class.java) 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>>> { fun getBoosts(): Single<Map<Currency, List<Boost>>> {
@ -62,6 +74,7 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
badgeRecipient: RecipientId, badgeRecipient: RecipientId,
additionalMessage: String?, additionalMessage: String?,
badgeLevel: Long, badgeLevel: Long,
donationProcessor: DonationProcessor
): Completable { ): Completable {
val isBoost = badgeRecipient == Recipient.self().id val isBoost = badgeRecipient == Recipient.self().id
val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT
@ -81,9 +94,9 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
val countDownLatch = CountDownLatch(1) val countDownLatch = CountDownLatch(1)
var finalJobState: JobTracker.JobState? = null var finalJobState: JobTracker.JobState? = null
val chain = if (isBoost) { val chain = if (isBoost) {
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId) BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor)
} else { } else {
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel) BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor)
} }
chain.enqueue { _, jobState -> chain.enqueue { _, jobState ->

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -9,9 +9,9 @@ import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi import org.signal.donations.GooglePayApi
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeApi import org.signal.donations.StripeApi
import org.signal.donations.StripeIntentAccessor import org.signal.donations.StripeIntentAccessor
import org.signal.donations.StripePaymentSourceType
import org.signal.donations.json.StripeIntentStatus 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.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
@ -87,13 +87,13 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
price: FiatMoney, price: FiatMoney,
badgeRecipient: RecipientId, badgeRecipient: RecipientId,
badgeLevel: Long, badgeLevel: Long,
paymentSourceType: StripePaymentSourceType paymentSourceType: PaymentSourceType
): Single<StripeIntentAccessor> { ): Single<StripeIntentAccessor> {
Log.d(TAG, "Creating payment intent for $price...", true) Log.d(TAG, "Creating payment intent for $price...", true)
return stripeApi.createPaymentIntent(price, badgeLevel) return stripeApi.createPaymentIntent(price, badgeLevel)
.onErrorResumeNext { .onErrorResumeNext {
handleCreatePaymentIntentError(it, badgeRecipient, paymentSourceType) OneTimeDonationRepository.handleCreatePaymentIntentError(it, badgeRecipient, paymentSourceType)
} }
.flatMap { result -> .flatMap { result ->
val recipient = Recipient.resolved(badgeRecipient) val recipient = Recipient.resolved(badgeRecipient)
@ -200,7 +200,7 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
fun setDefaultPaymentMethod( fun setDefaultPaymentMethod(
paymentMethodId: String, paymentMethodId: String,
paymentSourceType: StripePaymentSourceType paymentSourceType: PaymentSourceType
): Completable { ): Completable {
return Single.fromCallable { return Single.fromCallable {
Log.d(TAG, "Getting the subscriber...") 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...") Log.d(TAG, "Creating credit card payment source via Stripe api...")
return stripeApi.createPaymentSourceFromCardData(cardData).map { return stripeApi.createPaymentSourceFromCardData(cardData).map {
when (it) { 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 is StripeApi.CreatePaymentSourceFromCardDataResult.Success -> it.paymentSource
} }
} }
@ -236,15 +236,5 @@ class StripeRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, Str
companion object { companion object {
private val TAG = Log.tag(StripeRepository::class.java) 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))
}
}
} }
} }

Wyświetl plik

@ -452,6 +452,15 @@ class DonateToSignalFragment :
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest)) 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) { override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayRequest)) findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayRequest))
} }

Wyświetl plik

@ -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.GatewayRequest
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse 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.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.StripePaymentInProgressFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
@ -77,12 +78,17 @@ class DonationCheckoutDelegate(
val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!! val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!!
handleCreditCardResult(result) handleCreditCardResult(result)
} }
fragment.setFragmentResultListener(PayPalPaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
val result: DonationProcessorActionResult = bundle.getParcelable(PayPalPaymentInProgressFragment.REQUEST_KEY)!!
handleDonationProcessorActionResult(result)
}
} }
private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) { private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) {
when (gatewayResponse.gateway) { when (gatewayResponse.gateway) {
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse) 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) 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) { private fun launchGooglePay(gatewayResponse: GatewayResponse) {
viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request) viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request)
donationPaymentComponent.stripeRepository.requestTokenFromGooglePay( donationPaymentComponent.stripeRepository.requestTokenFromGooglePay(
@ -186,6 +200,7 @@ class DonationCheckoutDelegate(
interface Callback { interface Callback {
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest) fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)
fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) fun navigateToCreditCardForm(gatewayRequest: GatewayRequest)
fun onPaymentComplete(gatewayRequest: GatewayRequest) fun onPaymentComplete(gatewayRequest: GatewayRequest)
fun onProcessorActionProcessed() fun onProcessorActionProcessed()

Wyświetl plik

@ -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.InAppDonations
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType 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.GooglePayButton
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.payments.FiatMoneyUtil import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
@ -40,6 +41,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: DSLSettingsAdapter) {
BadgeDisplay112.register(adapter) BadgeDisplay112.register(adapter)
GooglePayButton.register(adapter) GooglePayButton.register(adapter)
PayPalButton.register(adapter)
lifecycleDisposable.bindTo(viewLifecycleOwner) 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()) { if (InAppDonations.isCreditCardAvailable()) {
space(12.dp) space(8.dp)
primaryButton( primaryButton(
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__credit_or_debit_card), text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__credit_or_debit_card),

Wyświetl plik

@ -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()
}
}
}
}

Wyświetl plik

@ -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
)
}
}
}

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -15,19 +15,19 @@ import androidx.navigation.fragment.navArgs
import org.signal.donations.StripeIntentAccessor import org.signal.donations.StripeIntentAccessor
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.Stripe3dsDialogFragmentBinding import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
import org.thoughtcrime.securesms.util.visible import org.thoughtcrime.securesms.util.visible
/** /**
* Full-screen dialog for displaying Stripe 3DS confirmation. * 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 { companion object {
const val REQUEST_KEY = "stripe_3ds_dialog_fragment" 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.clearCache(true)
it.webView.clearHistory() it.webView.clearHistory()
} }

Wyświetl plik

@ -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.DonationProcessorAction
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage 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.LifecycleDisposable
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate 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 { companion object {
private val TAG = Log.tag(StripePaymentInProgressFragment::class.java) 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" 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 args: StripePaymentInProgressFragmentArgs by navArgs()
private val disposables = LifecycleDisposable() private val disposables = LifecycleDisposable()

Wyświetl plik

@ -12,9 +12,9 @@ import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.donations.GooglePayPaymentSource import org.signal.donations.GooglePayPaymentSource
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeApi import org.signal.donations.StripeApi
import org.signal.donations.StripeIntentAccessor 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.MonthlyDonationRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository 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.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.rx.RxStore import org.thoughtcrime.securesms.util.rx.RxStore
import org.whispersystems.signalservice.api.util.Preconditions import org.whispersystems.signalservice.api.util.Preconditions
import org.whispersystems.signalservice.internal.push.DonationProcessor
class StripePaymentInProgressViewModel( class StripePaymentInProgressViewModel(
private val stripeRepository: StripeRepository, private val stripeRepository: StripeRepository,
@ -93,11 +94,11 @@ class StripePaymentInProgressViewModel(
paymentData == null && cardData == null -> error("No payment provider available.") paymentData == null && cardData == null -> error("No payment provider available.")
paymentData != null && cardData != null -> error("Too many providers available") paymentData != null && cardData != null -> error("Too many providers available")
paymentData != null -> PaymentSourceProvider( paymentData != null -> PaymentSourceProvider(
StripePaymentSourceType.GOOGLE_PAY, PaymentSourceType.Stripe.GooglePay,
Single.just<StripeApi.PaymentSource>(GooglePayPaymentSource(paymentData)).doAfterTerminate { clearPaymentInformation() } Single.just<StripeApi.PaymentSource>(GooglePayPaymentSource(paymentData)).doAfterTerminate { clearPaymentInformation() }
) )
cardData != null -> PaymentSourceProvider( cardData != null -> PaymentSourceProvider(
StripePaymentSourceType.CREDIT_CARD, PaymentSourceType.Stripe.CreditCard,
stripeRepository.createCreditCardPaymentSource(errorSource, cardData).doAfterTerminate { clearPaymentInformation() } stripeRepository.createCreditCardPaymentSource(errorSource, cardData).doAfterTerminate { clearPaymentInformation() }
) )
else -> error("This should never happen.") else -> error("This should never happen.")
@ -187,11 +188,12 @@ class StripePaymentInProgressViewModel(
.flatMap { stripeRepository.getStatusAndPaymentMethodId(it) } .flatMap { stripeRepository.getStatusAndPaymentMethodId(it) }
.flatMapCompletable { .flatMapCompletable {
oneTimeDonationRepository.waitForOneTimeRedemption( oneTimeDonationRepository.waitForOneTimeRedemption(
amount, price = amount,
paymentIntent.intentId, paymentIntentId = paymentIntent.intentId,
request.recipientId, badgeRecipient = request.recipientId,
request.additionalMessage, additionalMessage = request.additionalMessage,
request.level badgeLevel = request.level,
donationProcessor = DonationProcessor.STRIPE
) )
} }
}.subscribeBy( }.subscribeBy(
@ -257,7 +259,7 @@ class StripePaymentInProgressViewModel(
} }
private data class PaymentSourceProvider( private data class PaymentSourceProvider(
val paymentSourceType: StripePaymentSourceType, val paymentSourceType: PaymentSourceType,
val paymentSource: Single<StripeApi.PaymentSource> val paymentSource: Single<StripeApi.PaymentSource>
) )

Wyświetl plik

@ -5,9 +5,9 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeDeclineCode import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeError import org.signal.donations.StripeError
import org.signal.donations.StripePaymentSourceType
sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) { 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. * 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. * 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 * 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. * charge has occurred.
*/ */
@JvmStatic @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) { return if (throwable is StripeError.PostError) {
val declineCode: StripeDeclineCode? = throwable.declineCode val declineCode: StripeDeclineCode? = throwable.declineCode
val errorCode: String? = throwable.errorCode val errorCode: String? = throwable.errorCode
when { when {
declineCode != null -> PaymentSetupError.DeclinedError(source, throwable, declineCode, method) declineCode != null && method is PaymentSourceType.Stripe -> PaymentSetupError.StripeDeclinedError(source, throwable, declineCode, method)
errorCode != null -> PaymentSetupError.CodedError(source, throwable, errorCode) errorCode != null && method is PaymentSourceType.Stripe -> PaymentSetupError.StripeCodedError(source, throwable, errorCode)
else -> PaymentSetupError.GenericError(source, throwable) else -> PaymentSetupError.GenericError(source, throwable)
} }
} else { } else {

Wyświetl plik

@ -2,8 +2,8 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.errors
import android.content.Context import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeDeclineCode import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripePaymentSourceType
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
class DonationErrorParams<V> private constructor( class DonationErrorParams<V> private constructor(
@ -25,7 +25,7 @@ class DonationErrorParams<V> private constructor(
): DonationErrorParams<V> { ): DonationErrorParams<V> {
return when (throwable) { return when (throwable) {
is DonationError.GiftRecipientVerificationError -> getVerificationErrorParams(context, throwable, callback) 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( is DonationError.PaymentSetupError -> DonationErrorParams(
title = R.string.DonationsErrors__error_processing_payment, title = R.string.DonationsErrors__error_processing_payment,
message = R.string.DonationsErrors__your_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) { val getStripeDeclineCodePositiveActionParams: (Context, Callback<V>, Int) -> DonationErrorParams<V> = when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> this::getTryCreditCardAgainParams PaymentSourceType.Stripe.GooglePay -> this::getTryCreditCardAgainParams
StripePaymentSourceType.GOOGLE_PAY -> this::getGoToGooglePayParams PaymentSourceType.Stripe.CreditCard -> this::getGoToGooglePayParams
} }
return when (declinedError.declineCode) { return when (declinedError.declineCode) {
@ -99,66 +99,66 @@ class DonationErrorParams<V> private constructor(
StripeDeclineCode.Code.APPROVE_WITH_ID -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.APPROVE_WITH_ID -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again PaymentSourceType.Stripe.CreditCard -> 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.GooglePay -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again
} }
) )
StripeDeclineCode.Code.CALL_ISSUER -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.CALL_ISSUER -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again_if_the_problem_continues PaymentSourceType.Stripe.CreditCard -> 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.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.CARD_NOT_SUPPORTED -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_support_this_type_of_purchase)
StripeDeclineCode.Code.EXPIRED_CARD -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.EXPIRED_CARD -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_has_expired_verify_your_card_details PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__your_card_has_expired_verify_your_card_details
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__your_card_has_expired PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__your_card_has_expired
} }
) )
StripeDeclineCode.Code.INCORRECT_NUMBER -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.INCORRECT_NUMBER -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details PaymentSourceType.Stripe.CreditCard -> 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.GooglePay -> R.string.DeclineCode__your_card_number_is_incorrect
} }
) )
StripeDeclineCode.Code.INCORRECT_CVC -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.INCORRECT_CVC -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details PaymentSourceType.Stripe.CreditCard -> 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.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.INSUFFICIENT_FUNDS -> getLearnMoreParams(context, callback, R.string.DeclineCode__your_card_does_not_have_sufficient_funds)
StripeDeclineCode.Code.INVALID_CVC -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.INVALID_CVC -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect_verify_your_card_details PaymentSourceType.Stripe.CreditCard -> 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.GooglePay -> R.string.DeclineCode__your_cards_cvc_number_is_incorrect
} }
) )
StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.INVALID_EXPIRY_MONTH -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__the_expiration_month_on_your_card_is_incorrect PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__the_expiration_month_on_your_card_is_incorrect
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__the_expiration_month PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__the_expiration_month
} }
) )
StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.INVALID_EXPIRY_YEAR -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__the_expiration_year_on_your_card_is_incorrect PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__the_expiration_year_on_your_card_is_incorrect
StripePaymentSourceType.GOOGLE_PAY -> R.string.DeclineCode__the_expiration_year PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__the_expiration_year
} }
) )
StripeDeclineCode.Code.INVALID_NUMBER -> getStripeDeclineCodePositiveActionParams( StripeDeclineCode.Code.INVALID_NUMBER -> getStripeDeclineCodePositiveActionParams(
context, callback, context, callback,
when (declinedError.method) { when (declinedError.method) {
StripePaymentSourceType.CREDIT_CARD -> R.string.DeclineCode__your_card_number_is_incorrect_verify_your_card_details PaymentSourceType.Stripe.CreditCard -> 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.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) StripeDeclineCode.Code.ISSUER_NOT_AVAILABLE -> getLearnMoreParams(context, callback, R.string.DeclineCode__try_completing_the_payment_again)

Wyświetl plik

@ -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() }
}
}
}

Wyświetl plik

@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels; import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.push.DonationProcessor;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;
@ -44,18 +45,20 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
private static final String BOOST_QUEUE = "BoostReceiptRedemption"; private static final String BOOST_QUEUE = "BoostReceiptRedemption";
private static final String GIFT_QUEUE = "GiftReceiptRedemption"; private static final String GIFT_QUEUE = "GiftReceiptRedemption";
private static final String DATA_REQUEST_BYTES = "data.request.bytes"; 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_PAYMENT_INTENT_ID = "data.payment.intent.id";
private static final String DATA_ERROR_SOURCE = "data.error.source"; 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_BADGE_LEVEL = "data.badge.level";
private static final String DATA_DONATION_PROCESSOR = "data.donation.processor";
private ReceiptCredentialRequestContext requestContext; private ReceiptCredentialRequestContext requestContext;
private final DonationErrorSource donationErrorSource; private final DonationErrorSource donationErrorSource;
private final String paymentIntentId; private final String paymentIntentId;
private final long badgeLevel; 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( return new BoostReceiptRequestResponseJob(
new Parameters new Parameters
.Builder() .Builder()
@ -67,12 +70,13 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
null, null,
paymentIntentId, paymentIntentId,
donationErrorSource, donationErrorSource,
badgeLevel badgeLevel,
donationProcessor
); );
} }
public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId) { public static JobManager.Chain createJobChainForBoost(@NonNull String paymentIntentId, @NonNull DonationProcessor donationProcessor) {
BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL)); BoostReceiptRequestResponseJob requestReceiptJob = createJob(paymentIntentId, DonationErrorSource.BOOST, Long.parseLong(SubscriptionLevels.BOOST_LEVEL), donationProcessor);
DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost(); DonationReceiptRedemptionJob redeemReceiptJob = DonationReceiptRedemptionJob.createJobForBoost();
RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost(); RefreshOwnProfileJob refreshOwnProfileJob = RefreshOwnProfileJob.forBoost();
MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob(); MultiDeviceProfileContentUpdateJob multiDeviceProfileContentUpdateJob = new MultiDeviceProfileContentUpdateJob();
@ -87,9 +91,10 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
public static JobManager.Chain createJobChainForGift(@NonNull String paymentIntentId, public static JobManager.Chain createJobChainForGift(@NonNull String paymentIntentId,
@NonNull RecipientId recipientId, @NonNull RecipientId recipientId,
@Nullable String additionalMessage, @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); GiftSendJob giftSendJob = new GiftSendJob(recipientId, additionalMessage);
@ -102,20 +107,23 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
@Nullable ReceiptCredentialRequestContext requestContext, @Nullable ReceiptCredentialRequestContext requestContext,
@NonNull String paymentIntentId, @NonNull String paymentIntentId,
@NonNull DonationErrorSource donationErrorSource, @NonNull DonationErrorSource donationErrorSource,
long badgeLevel) long badgeLevel,
@NonNull DonationProcessor donationProcessor)
{ {
super(parameters); super(parameters);
this.requestContext = requestContext; this.requestContext = requestContext;
this.paymentIntentId = paymentIntentId; this.paymentIntentId = paymentIntentId;
this.donationErrorSource = donationErrorSource; this.donationErrorSource = donationErrorSource;
this.badgeLevel = badgeLevel; this.badgeLevel = badgeLevel;
this.donationProcessor = donationProcessor;
} }
@Override @Override
public @NonNull Data serialize() { public @NonNull Data serialize() {
Data.Builder builder = new Data.Builder().putString(DATA_PAYMENT_INTENT_ID, paymentIntentId) Data.Builder builder = new Data.Builder().putString(DATA_PAYMENT_INTENT_ID, paymentIntentId)
.putString(DATA_ERROR_SOURCE, donationErrorSource.serialize()) .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) { if (requestContext != null) {
builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize()); builder.putBlobAsString(DATA_REQUEST_BYTES, requestContext.serialize());
@ -153,7 +161,7 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
Log.d(TAG, "Submitting credential to server", true); Log.d(TAG, "Submitting credential to server", true);
ServiceResponse<ReceiptCredentialResponse> response = ApplicationDependencies.getDonationsService() ServiceResponse<ReceiptCredentialResponse> response = ApplicationDependencies.getDonationsService()
.submitBoostReceiptCredentialRequestSync(paymentIntentId, requestContext.getRequest()); .submitBoostReceiptCredentialRequestSync(paymentIntentId, requestContext.getRequest(), donationProcessor);
if (response.getApplicationError().isPresent()) { if (response.getApplicationError().isPresent()) {
handleApplicationError(context, response, donationErrorSource); handleApplicationError(context, response, donationErrorSource);
@ -258,18 +266,20 @@ public class BoostReceiptRequestResponseJob extends BaseJob {
public static class Factory implements Job.Factory<BoostReceiptRequestResponseJob> { public static class Factory implements Job.Factory<BoostReceiptRequestResponseJob> {
@Override @Override
public @NonNull BoostReceiptRequestResponseJob create(@NonNull Parameters parameters, @NonNull Data data) { public @NonNull BoostReceiptRequestResponseJob create(@NonNull Parameters parameters, @NonNull Data data) {
String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID); String paymentIntentId = data.getString(DATA_PAYMENT_INTENT_ID);
DonationErrorSource donationErrorSource = DonationErrorSource.deserialize(data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.BOOST.serialize())); DonationErrorSource donationErrorSource = DonationErrorSource.deserialize(data.getStringOrDefault(DATA_ERROR_SOURCE, DonationErrorSource.BOOST.serialize()));
long badgeLevel = data.getLongOrDefault(DATA_BADGE_LEVEL, Long.parseLong(SubscriptionLevels.BOOST_LEVEL)); 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 { try {
if (data.hasString(DATA_REQUEST_BYTES)) { if (data.hasString(DATA_REQUEST_BYTES)) {
byte[] blob = data.getStringAsBlob(DATA_REQUEST_BYTES); byte[] blob = data.getStringAsBlob(DATA_REQUEST_BYTES);
ReceiptCredentialRequestContext requestContext = new ReceiptCredentialRequestContext(blob); ReceiptCredentialRequestContext requestContext = new ReceiptCredentialRequestContext(blob);
return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel); return new BoostReceiptRequestResponseJob(parameters, requestContext, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor);
} else { } else {
return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel); return new BoostReceiptRequestResponseJob(parameters, null, paymentIntentId, donationErrorSource, badgeLevel, donationProcessor);
} }
} catch (InvalidInputException e) { } catch (InvalidInputException e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);

Wyświetl plik

@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.donations.PaymentSourceType;
import org.signal.donations.StripeDeclineCode; import org.signal.donations.StripeDeclineCode;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.VerificationFailedException;
@ -295,20 +296,27 @@ public class SubscriptionReceiptRequestResponseJob extends BaseJob {
StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason()); StripeDeclineCode declineCode = StripeDeclineCode.Companion.getFromCode(chargeFailure.getOutcomeNetworkReason());
DonationError.PaymentSetupError paymentSetupError; DonationError.PaymentSetupError paymentSetupError;
PaymentSourceType paymentSourceType = SignalStore.donationsValues().getSubscriptionPaymentSourceType();
boolean isStripeSource = paymentSourceType instanceof PaymentSourceType.Stripe;
if (declineCode.isKnown()) { if (declineCode.isKnown() && isStripeSource) {
paymentSetupError = new DonationError.PaymentSetupError.DeclinedError( paymentSetupError = new DonationError.PaymentSetupError.StripeDeclinedError(
getErrorSource(), getErrorSource(),
new Exception(chargeFailure.getMessage()), new Exception(chargeFailure.getMessage()),
declineCode, declineCode,
SignalStore.donationsValues().getSubscriptionPaymentSourceType() (PaymentSourceType.Stripe) paymentSourceType
); );
} else { } else if (isStripeSource) {
paymentSetupError = new DonationError.PaymentSetupError.CodedError( paymentSetupError = new DonationError.PaymentSetupError.StripeCodedError(
getErrorSource(), getErrorSource(),
new Exception("Card was declined. " + chargeFailure.getCode()), new Exception("Card was declined. " + chargeFailure.getCode()),
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); Log.w(TAG, "Not for a keep-alive and we have a charge failure. Routing a payment setup error...", true);

Wyświetl plik

@ -5,8 +5,8 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.BehaviorSubject import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.Subject import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeApi import org.signal.donations.StripeApi
import org.signal.donations.StripePaymentSourceType
import org.signal.libsignal.zkgroup.InvalidInputException import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.VerificationFailedException import org.signal.libsignal.zkgroup.VerificationFailedException
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
@ -450,12 +450,12 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
remove(SUBSCRIPTION_CREDENTIAL_RECEIPT) remove(SUBSCRIPTION_CREDENTIAL_RECEIPT)
} }
fun setSubscriptionPaymentSourceType(stripePaymentSourceType: StripePaymentSourceType) { fun setSubscriptionPaymentSourceType(paymentSourceType: PaymentSourceType) {
putString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, stripePaymentSourceType.code) putString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, paymentSourceType.code)
} }
fun getSubscriptionPaymentSourceType(): StripePaymentSourceType { fun getSubscriptionPaymentSourceType(): PaymentSourceType {
return StripePaymentSourceType.fromCode(getString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, null)) return PaymentSourceType.fromCode(getString(SUBSCRIPTION_PAYMENT_SOURCE_TYPE, null))
} }
var subscriptionEndOfPeriodConversionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_CONVERT, 0L) var subscriptionEndOfPeriodConversionStarted by longValue(SUBSCRIPTION_EOP_STARTED_TO_CONVERT, 0L)

Wyświetl plik

@ -107,6 +107,7 @@ public final class FeatureFlags {
private static final String CDS_HARD_LIMIT = "android.cds.hardLimit"; 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 PAYMENTS_IN_CHAT_MESSAGES = "android.payments.inChatMessages";
private static final String CHAT_FILTERS = "android.chat.filters"; 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 * 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, KEEP_MUTED_CHATS_ARCHIVED,
CDS_HARD_LIMIT, CDS_HARD_LIMIT,
PAYMENTS_IN_CHAT_MESSAGES, PAYMENTS_IN_CHAT_MESSAGES,
CHAT_FILTERS CHAT_FILTERS,
PAYPAL_DONATIONS
); );
@VisibleForTesting @VisibleForTesting
@ -538,8 +540,6 @@ public final class FeatureFlags {
/** /**
* Whether or not we should allow credit card payments for donations * 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() { public static boolean creditCardPayments() {
return getBoolean(CREDIT_CARD_PAYMENTS, Environment.IS_STAGING); return getBoolean(CREDIT_CARD_PAYMENTS, Environment.IS_STAGING);
@ -597,6 +597,13 @@ public final class FeatureFlags {
return getBoolean(CHAT_FILTERS, false); 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. */ /** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() { public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES); return new TreeMap<>(REMOTE_VALUES);

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -39,6 +39,9 @@
<action <action
android:id="@+id/action_donateToSignalFragment_to_creditCardFragment" android:id="@+id/action_donateToSignalFragment_to_creditCardFragment"
app:destination="@id/creditCardFragment" /> app:destination="@id/creditCardFragment" />
<action
android:id="@+id/action_donateToSignalFragment_to_paypalPaymentInProgressFragment"
app:destination="@id/paypalPaymentInProgressFragment" />
</fragment> </fragment>
@ -74,7 +77,7 @@
android:id="@+id/stripePaymentInProgressFragment" android:id="@+id/stripePaymentInProgressFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment" android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment"
android:label="stripe_payment_in_progress_fragment" android:label="stripe_payment_in_progress_fragment"
tools:layout="@layout/stripe_payment_in_progress_fragment"> tools:layout="@layout/donation_in_progress_fragment">
<argument <argument
android:name="action" android:name="action"
@ -133,7 +136,7 @@
android:id="@+id/stripe3dsDialogFragment" android:id="@+id/stripe3dsDialogFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment" android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment"
android:label="stripe_3ds_dialog_fragment" android:label="stripe_3ds_dialog_fragment"
tools:layout="@layout/stripe_3ds_dialog_fragment"> tools:layout="@layout/donation_webview_fragment">
<argument <argument
android:name="uri" android:name="uri"
@ -152,4 +155,37 @@
android:label="your_information_is_private_bottom_sheet" android:label="your_information_is_private_bottom_sheet"
tools:layout="@layout/dsl_settings_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> </navigation>

Wyświetl plik

@ -59,6 +59,9 @@
<action <action
android:id="@+id/action_giftFlowConfirmationFragment_to_gatewaySelectorBottomSheet" android:id="@+id/action_giftFlowConfirmationFragment_to_gatewaySelectorBottomSheet"
app:destination="@id/gatewaySelectorBottomSheet" /> app:destination="@id/gatewaySelectorBottomSheet" />
<action
android:id="@+id/action_giftFlowConfirmationFragment_to_paypalPaymentInProgressFragment"
app:destination="@id/paypalPaymentInProgressFragment" />
</fragment> </fragment>
<dialog <dialog
@ -78,7 +81,7 @@
android:id="@+id/stripePaymentInProgressFragment" android:id="@+id/stripePaymentInProgressFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment" android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment"
android:label="stripe_payment_in_progress_fragment" android:label="stripe_payment_in_progress_fragment"
tools:layout="@layout/stripe_payment_in_progress_fragment"> tools:layout="@layout/donation_in_progress_fragment">
<argument <argument
android:name="action" android:name="action"
@ -114,7 +117,7 @@
android:id="@+id/stripe3dsDialogFragment" android:id="@+id/stripe3dsDialogFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment" android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment"
android:label="stripe_3ds_dialog_fragment" android:label="stripe_3ds_dialog_fragment"
tools:layout="@layout/stripe_3ds_dialog_fragment"> tools:layout="@layout/donation_webview_fragment">
<argument <argument
android:name="uri" android:name="uri"
@ -132,4 +135,37 @@
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.YourInformationIsPrivateBottomSheet" android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.YourInformationIsPrivateBottomSheet"
android:label="your_information_is_private_bottom_sheet" android:label="your_information_is_private_bottom_sheet"
tools:layout="@layout/dsl_settings_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> </navigation>

Wyświetl plik

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="paypal_outline">#00000000</color>
<color name="conversation_toolbar_color">@color/signal_colorSurface</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">@color/signal_colorTransparentInverse5</color>
<color name="conversation_toolbar_color_wallpaper_scrolled">@color/signal_colorTransparentInverse5</color> <color name="conversation_toolbar_color_wallpaper_scrolled">@color/signal_colorTransparentInverse5</color>

Wyświetl plik

@ -38,6 +38,8 @@
<color name="transparent_white_90">#e6ffffff</color> <color name="transparent_white_90">#e6ffffff</color>
<color name="transparent_white_95">#f3ffffff</color> <color name="transparent_white_95">#f3ffffff</color>
<color name="paypal_outline">#80838089</color>
<color name="conversation_compose_divider">#32000000</color> <color name="conversation_compose_divider">#32000000</color>
<color name="conversation_item_selected_system_ui">#4d4d4d</color> <color name="conversation_item_selected_system_ui">#4d4d4d</color>

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -8,7 +8,7 @@ import org.json.JSONObject
class CreditCardPaymentSource( class CreditCardPaymentSource(
private val payload: JSONObject private val payload: JSONObject
) : StripeApi.PaymentSource { ) : StripeApi.PaymentSource {
override val type = StripePaymentSourceType.CREDIT_CARD override val type = PaymentSourceType.Stripe.CreditCard
override fun parameterize(): JSONObject = payload override fun parameterize(): JSONObject = payload
override fun getTokenId(): String = parameterize().getString("id") override fun getTokenId(): String = parameterize().getString("id")
override fun email(): String? = null override fun email(): String? = null

Wyświetl plik

@ -4,7 +4,7 @@ import com.google.android.gms.wallet.PaymentData
import org.json.JSONObject import org.json.JSONObject
class GooglePayPaymentSource(private val paymentData: PaymentData) : StripeApi.PaymentSource { class GooglePayPaymentSource(private val paymentData: PaymentData) : StripeApi.PaymentSource {
override val type = StripePaymentSourceType.GOOGLE_PAY override val type = PaymentSourceType.Stripe.GooglePay
override fun parameterize(): JSONObject { override fun parameterize(): JSONObject {
val jsonData = JSONObject(paymentData.toJson()) val jsonData = JSONObject(paymentData.toJson())

Wyświetl plik

@ -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
}
}
}
}

Wyświetl plik

@ -520,7 +520,7 @@ class StripeApi(
) : Parcelable ) : Parcelable
interface PaymentSource { interface PaymentSource {
val type: StripePaymentSourceType val type: PaymentSourceType
fun parameterize(): JSONObject fun parameterize(): JSONObject
fun getTokenId(): String fun getTokenId(): String
fun email(): String? fun email(): String?

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -9,6 +9,9 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; 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.StripeClientSecret;
import org.whispersystems.signalservice.api.subscriptions.SubscriberId; import org.whispersystems.signalservice.api.subscriptions.SubscriberId;
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels; 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.EmptyResponse;
import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.DonationProcessor;
import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import java.io.IOException; import java.io.IOException;
@ -87,8 +91,8 @@ public class DonationsService {
* @param paymentIntentId PaymentIntent ID from a boost donation intent response. * @param paymentIntentId PaymentIntent ID from a boost donation intent response.
* @param receiptCredentialRequest Client-generated request token * @param receiptCredentialRequest Client-generated request token
*/ */
public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) { public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest), 200)); 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) { public ServiceResponse<EmptyResponse> setDefaultStripePaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
return wrapInServiceResponse(() -> { return wrapInServiceResponse(() -> {
pushServiceSocket.setDefaultSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId); pushServiceSocket.setDefaultStripeSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
return new Pair<>(EmptyResponse.INSTANCE, 200); return new Pair<>(EmptyResponse.INSTANCE, 200);
}); });
} }
/** /**
* * @param subscriberId The subscriber ID to create a payment method for.
* @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
* @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs * but instead with the SetupIntent stripe APIs.
* but instead with the SetupIntent stripe APIs.
*/ */
public ServiceResponse<StripeClientSecret> createStripeSubscriptionPaymentMethod(SubscriberId subscriberId) { public ServiceResponse<StripeClientSecret> createStripeSubscriptionPaymentMethod(SubscriberId subscriberId) {
return wrapInServiceResponse(() -> { return wrapInServiceResponse(() -> {
StripeClientSecret clientSecret = pushServiceSocket.createSubscriptionPaymentMethod(subscriberId.serialize()); StripeClientSecret clientSecret = pushServiceSocket.createStripeSubscriptionPaymentMethod(subscriberId.serialize());
return new Pair<>(clientSecret, 200); 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) { public ServiceResponse<ReceiptCredentialResponse> submitReceiptCredentialRequestSync(SubscriberId subscriberId, ReceiptCredentialRequest receiptCredentialRequest) {
return wrapInServiceResponse(() -> { return wrapInServiceResponse(() -> {
ReceiptCredentialResponse response = pushServiceSocket.submitReceiptCredentials(subscriberId.serialize(), receiptCredentialRequest); ReceiptCredentialResponse response = pushServiceSocket.submitReceiptCredentials(subscriberId.serialize(), receiptCredentialRequest);

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -12,8 +12,12 @@ class BoostReceiptCredentialRequestJson {
@JsonProperty("receiptCredentialRequest") @JsonProperty("receiptCredentialRequest")
private final String 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.paymentIntentId = paymentIntentId;
this.receiptCredentialRequest = Base64.encodeBytes(receiptCredentialRequest.serialize()); this.receiptCredentialRequest = Base64.encodeBytes(receiptCredentialRequest.serialize());
this.processor = processor.getCode();
} }
} }

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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.push.exceptions.UsernameTakenException;
import org.whispersystems.signalservice.api.storage.StorageAuthResponse; import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription; 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.StripeClientSecret;
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels; import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
import org.whispersystems.signalservice.api.util.CredentialsProvider; 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 DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt";
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels"; 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 UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
private static final String SUBSCRIPTION = "/v1/subscription/%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 CREATE_STRIPE_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 CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method/paypal";
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials"; private static final String DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts"; private static final String DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/paypal/%s";
private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift"; private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
private static final String CREATE_BOOST_PAYMENT_INTENT = "/v1/subscription/boost/create"; private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials"; private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift";
private static final String BOOST_BADGES = "/v1/subscription/boost/badges"; 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"; 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 { public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, long amount, long level) throws IOException {
String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level)); 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); 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 { public Map<String, List<BigDecimal>> getBoostAmounts() throws IOException {
String result = makeServiceRequestWithoutAuthentication(BOOST_AMOUNTS, "GET", null); String result = makeServiceRequestWithoutAuthentication(BOOST_AMOUNTS, "GET", null);
TypeReference<HashMap<String, List<BigDecimal>>> typeRef = new TypeReference<HashMap<String, List<BigDecimal>>>() {}; 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); return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);
} }
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException { public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) throws IOException {
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest)); String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest, processor));
String response = makeServiceRequestWithoutAuthentication( String response = makeServiceRequestWithoutAuthentication(
BOOST_RECEIPT_CREDENTIALS, BOOST_RECEIPT_CREDENTIALS,
"POST", "POST",
@ -1082,13 +1112,17 @@ public class PushServiceSocket {
makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "DELETE", null); makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "DELETE", null);
} }
public StripeClientSecret createSubscriptionPaymentMethod(String subscriberId) throws IOException { public StripeClientSecret createStripeSubscriptionPaymentMethod(String subscriberId) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", ""); String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", "");
return JsonUtil.fromJson(response, StripeClientSecret.class); return JsonUtil.fromJson(response, StripeClientSecret.class);
} }
public void setDefaultSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException { public void setDefaultStripeSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", ""); 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 { public ReceiptCredentialResponse submitReceiptCredentials(String subscriptionId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {