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))
}
override fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
}
override fun navigateToCreditCardForm(gatewayRequest: 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.
*/
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 org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.DonationProcessor
import java.math.BigDecimal
import java.util.Currency
import java.util.Locale
@ -31,6 +33,16 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
companion object {
private val TAG = Log.tag(OneTimeDonationRepository::class.java)
fun <T> handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId, paymentSourceType: PaymentSourceType): Single<T> {
return if (throwable is DonationError) {
Single.error(throwable)
} else {
val recipient = Recipient.resolved(badgeRecipient)
val errorSource = if (recipient.isSelf) DonationErrorSource.BOOST else DonationErrorSource.GIFT
Single.error(DonationError.getPaymentSetupError(errorSource, throwable, paymentSourceType))
}
}
}
fun getBoosts(): Single<Map<Currency, List<Boost>>> {
@ -62,6 +74,7 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
badgeRecipient: RecipientId,
additionalMessage: String?,
badgeLevel: Long,
donationProcessor: DonationProcessor
): Completable {
val isBoost = badgeRecipient == Recipient.self().id
val donationErrorSource: DonationErrorSource = if (isBoost) DonationErrorSource.BOOST else DonationErrorSource.GIFT
@ -81,9 +94,9 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
val countDownLatch = CountDownLatch(1)
var finalJobState: JobTracker.JobState? = null
val chain = if (isBoost) {
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId)
BoostReceiptRequestResponseJob.createJobChainForBoost(paymentIntentId, donationProcessor)
} else {
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel)
BoostReceiptRequestResponseJob.createJobChainForGift(paymentIntentId, badgeRecipient, additionalMessage, badgeLevel, donationProcessor)
}
chain.enqueue { _, jobState ->

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

Wyświetl plik

@ -452,6 +452,15 @@ class DonateToSignalFragment :
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToStripePaymentInProgressFragment(DonationProcessorAction.PROCESS_NEW_DONATION, gatewayRequest))
}
override fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(
DonateToSignalFragmentDirections.actionDonateToSignalFragmentToPaypalPaymentInProgressFragment(
DonationProcessorAction.PROCESS_NEW_DONATION,
gatewayRequest
)
)
}
override fun navigateToCreditCardForm(gatewayRequest: GatewayRequest) {
findNavController().safeNavigate(DonateToSignalFragmentDirections.actionDonateToSignalFragmentToCreditCardFragment(gatewayRequest))
}

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.GatewayResponse
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
@ -77,12 +78,17 @@ class DonationCheckoutDelegate(
val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!!
handleCreditCardResult(result)
}
fragment.setFragmentResultListener(PayPalPaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
val result: DonationProcessorActionResult = bundle.getParcelable(PayPalPaymentInProgressFragment.REQUEST_KEY)!!
handleDonationProcessorActionResult(result)
}
}
private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) {
when (gatewayResponse.gateway) {
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
GatewayResponse.Gateway.PAYPAL -> error("PayPal is not currently supported.")
GatewayResponse.Gateway.PAYPAL -> launchPayPal(gatewayResponse)
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
}
}
@ -124,6 +130,14 @@ class DonationCheckoutDelegate(
}
}
private fun launchPayPal(gatewayResponse: GatewayResponse) {
if (InAppDonations.isPayPalAvailable()) {
callback.navigateToPayPalPaymentInProgress(gatewayResponse.request)
} else {
error("PayPal is not currently enabled.")
}
}
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
viewModel.provideGatewayRequestForGooglePay(gatewayResponse.request)
donationPaymentComponent.stripeRepository.requestTokenFromGooglePay(
@ -186,6 +200,7 @@ class DonationCheckoutDelegate(
interface Callback {
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)
fun navigateToCreditCardForm(gatewayRequest: GatewayRequest)
fun onPaymentComplete(gatewayRequest: GatewayRequest)
fun onProcessorActionProcessed()

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.donate.DonateToSignalType
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable
@ -40,6 +41,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
override fun bindAdapter(adapter: DSLSettingsAdapter) {
BadgeDisplay112.register(adapter)
GooglePayButton.register(adapter)
PayPalButton.register(adapter)
lifecycleDisposable.bindTo(viewLifecycleOwner)
@ -80,11 +82,23 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
)
}
// PayPal
if (InAppDonations.isPayPalAvailable()) {
space(8.dp)
customPref(
PayPalButton.Model(
onClick = {
findNavController().popBackStack()
val response = GatewayResponse(GatewayResponse.Gateway.PAYPAL, args.request)
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to response))
},
isEnabled = true
)
)
}
// Credit Card
if (InAppDonations.isCreditCardAvailable()) {
space(12.dp)
space(8.dp)
primaryButton(
text = DSLSettingsText.from(R.string.GatewaySelectorBottomSheet__credit_or_debit_card),

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.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.Stripe3dsDialogFragmentBinding
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
import org.thoughtcrime.securesms.util.visible
/**
* Full-screen dialog for displaying Stripe 3DS confirmation.
*/
class Stripe3DSDialogFragment : DialogFragment(R.layout.stripe_3ds_dialog_fragment) {
class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragment) {
companion object {
const val REQUEST_KEY = "stripe_3ds_dialog_fragment"
}
val binding by ViewBinderDelegate(Stripe3dsDialogFragmentBinding::bind) {
val binding by ViewBinderDelegate(DonationWebviewFragmentBinding::bind) {
it.webView.clearCache(true)
it.webView.clearHistory()
}

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.DonationProcessorActionResult
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
import org.thoughtcrime.securesms.databinding.StripePaymentInProgressFragmentBinding
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class StripePaymentInProgressFragment : DialogFragment(R.layout.stripe_payment_in_progress_fragment) {
class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_progress_fragment) {
companion object {
private val TAG = Log.tag(StripePaymentInProgressFragment::class.java)
@ -38,7 +38,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.stripe_payment_i
const val REQUEST_KEY = "REQUEST_KEY"
}
private val binding by ViewBinderDelegate(StripePaymentInProgressFragmentBinding::bind)
private val binding by ViewBinderDelegate(DonationInProgressFragmentBinding::bind)
private val args: StripePaymentInProgressFragmentArgs by navArgs()
private val disposables = LifecycleDisposable()

Wyświetl plik

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

Wyświetl plik

@ -5,9 +5,9 @@ import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.util.logging.Log
import org.signal.donations.PaymentSourceType
import org.signal.donations.StripeDeclineCode
import org.signal.donations.StripeError
import org.signal.donations.StripePaymentSourceType
sealed class DonationError(val source: DonationErrorSource, cause: Throwable) : Exception(cause) {
@ -51,12 +51,12 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
/**
* Payment setup failed in some way, which we are told about by Stripe.
*/
class CodedError(source: DonationErrorSource, cause: Throwable, val errorCode: String) : PaymentSetupError(source, cause)
class StripeCodedError(source: DonationErrorSource, cause: Throwable, val errorCode: String) : PaymentSetupError(source, cause)
/**
* Payment failed by the credit card processor, with a specific reason told to us by Stripe.
*/
class DeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode, val method: StripePaymentSourceType) : PaymentSetupError(source, cause)
class StripeDeclinedError(source: DonationErrorSource, cause: Throwable, val declineCode: StripeDeclineCode, val method: PaymentSourceType.Stripe) : PaymentSetupError(source, cause)
}
/**
@ -129,18 +129,18 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
/**
* Converts a throwable into a payment setup error. This should only be used when
* handling errors handed back via the Stripe API, when we know for sure that no
* handling errors handed back via the Stripe API or via PayPal, when we know for sure that no
* charge has occurred.
*/
@JvmStatic
fun getPaymentSetupError(source: DonationErrorSource, throwable: Throwable, method: StripePaymentSourceType): DonationError {
fun getPaymentSetupError(source: DonationErrorSource, throwable: Throwable, method: PaymentSourceType): DonationError {
return if (throwable is StripeError.PostError) {
val declineCode: StripeDeclineCode? = throwable.declineCode
val errorCode: String? = throwable.errorCode
when {
declineCode != null -> PaymentSetupError.DeclinedError(source, throwable, declineCode, method)
errorCode != null -> PaymentSetupError.CodedError(source, throwable, errorCode)
declineCode != null && method is PaymentSourceType.Stripe -> PaymentSetupError.StripeDeclinedError(source, throwable, declineCode, method)
errorCode != null && method is PaymentSourceType.Stripe -> PaymentSetupError.StripeCodedError(source, throwable, errorCode)
else -> PaymentSetupError.GenericError(source, throwable)
}
} else {

Wyświetl plik

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

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

Wyświetl plik

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

Wyświetl plik

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

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 PAYMENTS_IN_CHAT_MESSAGES = "android.payments.inChatMessages";
private static final String CHAT_FILTERS = "android.chat.filters";
private static final String PAYPAL_DONATIONS = "android.donations.paypal";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -166,7 +167,8 @@ public final class FeatureFlags {
KEEP_MUTED_CHATS_ARCHIVED,
CDS_HARD_LIMIT,
PAYMENTS_IN_CHAT_MESSAGES,
CHAT_FILTERS
CHAT_FILTERS,
PAYPAL_DONATIONS
);
@VisibleForTesting
@ -538,8 +540,6 @@ public final class FeatureFlags {
/**
* Whether or not we should allow credit card payments for donations
*
* WARNING: This feature is not done, and this should not be enabled.
*/
public static boolean creditCardPayments() {
return getBoolean(CREDIT_CARD_PAYMENTS, Environment.IS_STAGING);
@ -597,6 +597,13 @@ public final class FeatureFlags {
return getBoolean(CHAT_FILTERS, false);
}
/**
* Whether or not we should allow PayPal payments for donations
*/
public static boolean paypalDonations() {
return getBoolean(PAYPAL_DONATIONS, Environment.IS_STAGING);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

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
android:id="@+id/action_donateToSignalFragment_to_creditCardFragment"
app:destination="@id/creditCardFragment" />
<action
android:id="@+id/action_donateToSignalFragment_to_paypalPaymentInProgressFragment"
app:destination="@id/paypalPaymentInProgressFragment" />
</fragment>
@ -74,7 +77,7 @@
android:id="@+id/stripePaymentInProgressFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment"
android:label="stripe_payment_in_progress_fragment"
tools:layout="@layout/stripe_payment_in_progress_fragment">
tools:layout="@layout/donation_in_progress_fragment">
<argument
android:name="action"
@ -133,7 +136,7 @@
android:id="@+id/stripe3dsDialogFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment"
android:label="stripe_3ds_dialog_fragment"
tools:layout="@layout/stripe_3ds_dialog_fragment">
tools:layout="@layout/donation_webview_fragment">
<argument
android:name="uri"
@ -152,4 +155,37 @@
android:label="your_information_is_private_bottom_sheet"
tools:layout="@layout/dsl_settings_bottom_sheet" />
<dialog
android:id="@+id/paypalPaymentInProgressFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment"
android:label="paypal_payment_in_progress"
tools:layout="@layout/donation_in_progress_fragment">
<argument
android:name="action"
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction"
app:nullable="false" />
<argument
android:name="request"
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
app:nullable="false" />
<action
android:id="@+id/action_paypalPaymentInProgressFragment_to_paypalConfirmationFragment"
app:destination="@id/paypalConfirmationFragment" />
</dialog>
<dialog
android:id="@+id/paypalConfirmationFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalConfirmationDialogFragment"
android:label="paypal_confirmation_dialog_fragment"
tools:layout="@layout/donation_webview_fragment">
<argument
android:name="uri"
app:argType="android.net.Uri"
app:nullable="false" />
</dialog>
</navigation>

Wyświetl plik

@ -59,6 +59,9 @@
<action
android:id="@+id/action_giftFlowConfirmationFragment_to_gatewaySelectorBottomSheet"
app:destination="@id/gatewaySelectorBottomSheet" />
<action
android:id="@+id/action_giftFlowConfirmationFragment_to_paypalPaymentInProgressFragment"
app:destination="@id/paypalPaymentInProgressFragment" />
</fragment>
<dialog
@ -78,7 +81,7 @@
android:id="@+id/stripePaymentInProgressFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment"
android:label="stripe_payment_in_progress_fragment"
tools:layout="@layout/stripe_payment_in_progress_fragment">
tools:layout="@layout/donation_in_progress_fragment">
<argument
android:name="action"
@ -114,7 +117,7 @@
android:id="@+id/stripe3dsDialogFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.Stripe3DSDialogFragment"
android:label="stripe_3ds_dialog_fragment"
tools:layout="@layout/stripe_3ds_dialog_fragment">
tools:layout="@layout/donation_webview_fragment">
<argument
android:name="uri"
@ -132,4 +135,37 @@
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.YourInformationIsPrivateBottomSheet"
android:label="your_information_is_private_bottom_sheet"
tools:layout="@layout/dsl_settings_bottom_sheet" />
<dialog
android:id="@+id/paypalPaymentInProgressFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalPaymentInProgressFragment"
android:label="paypal_payment_in_progress"
tools:layout="@layout/donation_in_progress_fragment">
<argument
android:name="action"
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction"
app:nullable="false" />
<argument
android:name="request"
app:argType="org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest"
app:nullable="false" />
<action
android:id="@+id/action_paypalPaymentInProgressFragment_to_paypalConfirmationFragment"
app:destination="@id/paypalConfirmationFragment" />
</dialog>
<dialog
android:id="@+id/paypalConfirmationFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalConfirmationDialogFragment"
android:label="paypal_confirmation_dialog_fragment"
tools:layout="@layout/donation_webview_fragment">
<argument
android:name="uri"
app:argType="android.net.Uri"
app:nullable="false" />
</dialog>
</navigation>

Wyświetl plik

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

Wyświetl plik

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

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(
private val payload: JSONObject
) : StripeApi.PaymentSource {
override val type = StripePaymentSourceType.CREDIT_CARD
override val type = PaymentSourceType.Stripe.CreditCard
override fun parameterize(): JSONObject = payload
override fun getTokenId(): String = parameterize().getString("id")
override fun email(): String? = null

Wyświetl plik

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

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
interface PaymentSource {
val type: StripePaymentSourceType
val type: PaymentSourceType
fun parameterize(): JSONObject
fun getTokenId(): 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.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
import org.whispersystems.signalservice.api.subscriptions.SubscriberId;
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
@ -16,6 +19,7 @@ import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.EmptyResponse;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.DonationProcessor;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import java.io.IOException;
@ -87,8 +91,8 @@ public class DonationsService {
* @param paymentIntentId PaymentIntent ID from a boost donation intent response.
* @param receiptCredentialRequest Client-generated request token
*/
public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest), 200));
public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest, processor), 200));
}
/**
@ -217,24 +221,129 @@ public class DonationsService {
public ServiceResponse<EmptyResponse> setDefaultStripePaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.setDefaultSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
pushServiceSocket.setDefaultStripeSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
/**
*
* @param subscriberId The subscriber ID to create a payment method for.
* @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs
* but instead with the SetupIntent stripe APIs.
* @param subscriberId The subscriber ID to create a payment method for.
* @return Client secret for a SetupIntent. It should not be used with the PaymentIntent stripe APIs
* but instead with the SetupIntent stripe APIs.
*/
public ServiceResponse<StripeClientSecret> createStripeSubscriptionPaymentMethod(SubscriberId subscriberId) {
return wrapInServiceResponse(() -> {
StripeClientSecret clientSecret = pushServiceSocket.createSubscriptionPaymentMethod(subscriberId.serialize());
StripeClientSecret clientSecret = pushServiceSocket.createStripeSubscriptionPaymentMethod(subscriberId.serialize());
return new Pair<>(clientSecret, 200);
});
}
/**
* Creates a PayPal one-time payment and returns the approval URL
* Response Codes
* 200 success
* 400 request error
* 409 level requires a valid currency/amount combination that does not match
*
* @param locale User locale for proper language presentation
* @param currencyCode 3 letter currency code of the desired currency
* @param amount Stringified minimum precision amount
* @param level The badge level to purchase
* @param returnUrl The 'return' url after a successful login and confirmation
* @param cancelUrl The 'cancel' url for a cancelled confirmation
* @return Wrapped response with either an error code or a payment id and approval URL
*/
public ServiceResponse<PayPalCreatePaymentIntentResponse> createPayPalOneTimePaymentIntent(Locale locale,
String currencyCode,
String amount,
long level,
String returnUrl,
String cancelUrl)
{
return wrapInServiceResponse(() -> {
PayPalCreatePaymentIntentResponse response = pushServiceSocket.createPayPalOneTimePaymentIntent(
locale,
currencyCode.toUpperCase(Locale.US), // Chris Eager to make this case insensitive in the next build
Long.parseLong(amount),
level,
returnUrl,
cancelUrl
);
return new Pair<>(response, 200);
});
}
/**
* Confirms a PayPal one-time payment and returns the paymentId for receipt credentials
* Response Codes
* 200 success
* 400 request error
* 409 level requires a valid currency/amount combination that does not match
*
* @param currency 3 letter currency code of the desired currency
* @param amount Stringified minimum precision amount
* @param level The badge level to purchase
* @param payerId Passed as a URL parameter back to returnUrl
* @param paymentId Passed as a URL parameter back to returnUrl
* @param paymentToken Passed as a URL parameter back to returnUrl
* @return Wrapped response with either an error code or a payment id
*/
public ServiceResponse<PayPalConfirmPaymentIntentResponse> confirmPayPalOneTimePaymentIntent(String currency,
String amount,
long level,
String payerId,
String paymentId,
String paymentToken)
{
return wrapInServiceResponse(() -> {
PayPalConfirmPaymentIntentResponse response = pushServiceSocket.confirmPayPalOneTimePaymentIntent(currency, amount, level, payerId, paymentId, paymentToken);
return new Pair<>(response, 200);
});
}
/**
* Sets up a payment method via PayPal for recurring charges.
*
* Response Codes
* 200 success
* 403 subscriberId password mismatches OR account authentication is present
* 404 subscriberId is not found or malformed
*
* @param locale User locale
* @param subscriberId User subscriber id
* @param returnUrl A success URL
* @param cancelUrl A cancel URL
* @return A response with an approval url and token
*/
public ServiceResponse<PayPalCreatePaymentMethodResponse> createPayPalPaymentMethod(Locale locale,
SubscriberId subscriberId,
String returnUrl,
String cancelUrl) {
return wrapInServiceResponse(() -> {
PayPalCreatePaymentMethodResponse response = pushServiceSocket.createPayPalPaymentMethod(locale, subscriberId.serialize(), returnUrl, cancelUrl);
return new Pair<>(response, 200);
});
}
/**
* Sets the given payment method as the default in PayPal
*
* Response Codes
* 200 success
* 403 subscriberId password mismatches OR account authentication is present
* 404 subscriberId is not found or malformed
* 409 subscriber record is missing customer ID - must call POST /v1/subscription/{subscriberId}/create_payment_method first
*
* @param subscriberId User subscriber id
* @param paymentMethodId Payment method id to make default
*/
public ServiceResponse<EmptyResponse> setDefaultPayPalPaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.setDefaultPaypalSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
public ServiceResponse<ReceiptCredentialResponse> submitReceiptCredentialRequestSync(SubscriberId subscriberId, ReceiptCredentialRequest receiptCredentialRequest) {
return wrapInServiceResponse(() -> {
ReceiptCredentialResponse response = pushServiceSocket.submitReceiptCredentials(subscriberId.serialize(), receiptCredentialRequest);

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

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.storage.StorageAuthResponse;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
@ -261,17 +264,21 @@ public class PushServiceSocket {
private static final String DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt";
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels";
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
private static final String SUBSCRIPTION = "/v1/subscription/%s";
private static final String CREATE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
private static final String DEFAULT_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift";
private static final String CREATE_BOOST_PAYMENT_INTENT = "/v1/subscription/boost/create";
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
private static final String SUBSCRIPTION_LEVELS = "/v1/subscription/levels";
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
private static final String SUBSCRIPTION = "/v1/subscription/%s";
private static final String CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method";
private static final String CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method/paypal";
private static final String DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/%s";
private static final String DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/paypal/%s";
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
private static final String BOOST_AMOUNTS = "/v1/subscription/boost/amounts";
private static final String GIFT_AMOUNT = "/v1/subscription/boost/amounts/gift";
private static final String CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/create";
private static final String CREATE_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/create";
private static final String CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/confirm";
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
private static final String BOOST_BADGES = "/v1/subscription/boost/badges";
private static final String CDSI_AUTH = "/v2/directory/auth";
@ -1019,10 +1026,33 @@ public class PushServiceSocket {
public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, long amount, long level) throws IOException {
String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level));
String result = makeServiceRequestWithoutAuthentication(CREATE_BOOST_PAYMENT_INTENT, "POST", payload);
String result = makeServiceRequestWithoutAuthentication(CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT, "POST", payload);
return JsonUtil.fromJsonResponse(result, StripeClientSecret.class);
}
public PayPalCreatePaymentIntentResponse createPayPalOneTimePaymentIntent(Locale locale, String currencyCode, long amount, long level, String returnUrl, String cancelUrl) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String payload = JsonUtil.toJson(new PayPalCreateOneTimePaymentIntentPayload(amount, currencyCode, level, returnUrl, cancelUrl));
String result = makeServiceRequestWithoutAuthentication(CREATE_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload, headers, NO_HANDLER);
return JsonUtil.fromJsonResponse(result, PayPalCreatePaymentIntentResponse.class);
}
public PayPalConfirmPaymentIntentResponse confirmPayPalOneTimePaymentIntent(String currency, String amount, long level, String payerId, String paymentId, String paymentToken) throws IOException {
String payload = JsonUtil.toJson(new PayPalConfirmOneTimePaymentIntentPayload(amount, currency, level, payerId, paymentId, paymentToken));
Log.d(TAG, payload);
String result = makeServiceRequestWithoutAuthentication(CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload);
return JsonUtil.fromJsonResponse(result, PayPalConfirmPaymentIntentResponse.class);
}
public PayPalCreatePaymentMethodResponse createPayPalPaymentMethod(Locale locale, String subscriberId, String returnUrl, String cancelUrl) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String payload = JsonUtil.toJson(new PayPalCreatePaymentMethodPayload(returnUrl, cancelUrl));
String result = makeServiceRequestWithoutAuthentication(String.format(CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", payload);
return JsonUtil.fromJsonResponse(result, PayPalCreatePaymentMethodResponse.class);
}
public Map<String, List<BigDecimal>> getBoostAmounts() throws IOException {
String result = makeServiceRequestWithoutAuthentication(BOOST_AMOUNTS, "GET", null);
TypeReference<HashMap<String, List<BigDecimal>>> typeRef = new TypeReference<HashMap<String, List<BigDecimal>>>() {};
@ -1040,8 +1070,8 @@ public class PushServiceSocket {
return JsonUtil.fromJsonResponse(result, SubscriptionLevels.class);
}
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest));
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) throws IOException {
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest, processor));
String response = makeServiceRequestWithoutAuthentication(
BOOST_RECEIPT_CREDENTIALS,
"POST",
@ -1082,13 +1112,17 @@ public class PushServiceSocket {
makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "DELETE", null);
}
public StripeClientSecret createSubscriptionPaymentMethod(String subscriberId) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", "");
public StripeClientSecret createStripeSubscriptionPaymentMethod(String subscriberId) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", "");
return JsonUtil.fromJson(response, StripeClientSecret.class);
}
public void setDefaultSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
public void setDefaultStripeSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
}
public void setDefaultPaypalSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, paymentMethodId), "POST", "");
}
public ReceiptCredentialResponse submitReceiptCredentials(String subscriptionId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {