Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PaymentSheet.resetCustomer #5340

Merged
merged 10 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,7 +2,12 @@

## X.X.X

### PaymentSheet

* [ADDED][5340](https://github.com/stripe/stripe-android/pull/5340) Add a `reset` method to `PaymentSheet`, that clears any persisted state.
brnunes-stripe marked this conversation as resolved.
Show resolved Hide resolved

## 20.8.0 - 2022-08-01

This release contains several bug fixes for Payments, PaymentSheet, deprecates `createForCompose`, and adds new `rememberLauncher` features for Payments

### PaymentSheet
Expand All @@ -26,7 +31,7 @@ This release contains several bug fixes for Payments, PaymentSheet, deprecates `

## 20.7.0 - 2022-07-06

* This release adds additional support for Afterpay/Clearpay in PaymentSheet.
This release adds additional support for Afterpay/Clearpay in PaymentSheet.

### PaymentSheet

Expand Down
@@ -1,5 +1,7 @@
package com.stripe.android.link.account

import android.content.Context
import androidx.annotation.RestrictTo
import java.security.MessageDigest
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -8,9 +10,20 @@ import javax.inject.Singleton
* Persistent cookies storage.
*/
@Singleton
internal class CookieStore @Inject constructor(
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class CookieStore @Inject internal constructor(
private val store: EncryptedStore
) {

constructor(context: Context) : this(EncryptedStore(context))

/**
* Clear all local data.
*/
fun clear() {
allCookies.forEach { store.delete(it) }
}

/**
* Update authentication session cookie according to the following rules:
*
Expand All @@ -24,7 +37,7 @@ internal class CookieStore @Inject constructor(
* | Any other value | Store |
* +-----------------------------+---------+
*/
fun updateAuthSessionCookie(cookie: String?) = cookie?.let {
internal fun updateAuthSessionCookie(cookie: String?) = cookie?.let {
if (it.isEmpty()) {
store.delete(AUTH_SESSION_COOKIE)
} else {
Expand All @@ -35,44 +48,50 @@ internal class CookieStore @Inject constructor(
/**
* Retrieve and return the current authentication session cookie.
*/
fun getAuthSessionCookie() = store.read(AUTH_SESSION_COOKIE)
internal fun getAuthSessionCookie() = store.read(AUTH_SESSION_COOKIE)

/**
* Delete the current authentication session cookie and store the hash of the email so that the
* user won't be automatically redirected to the verification screen next time.
*/
fun logout(email: String) {
internal fun logout(email: String) {
storeLoggedOutEmail(email)
store.delete(AUTH_SESSION_COOKIE)
}

/**
* Check whether this is the most recently logged out email.
*/
fun isEmailLoggedOut(email: String) =
internal fun isEmailLoggedOut(email: String) =
store.read(LOGGED_OUT_EMAIL_HASH) == email.sha256()

fun storeLoggedOutEmail(email: String) =
internal fun storeLoggedOutEmail(email: String) =
store.write(LOGGED_OUT_EMAIL_HASH, email.sha256())

/**
* Store the email that has recently signed up on this device so that the user is remembered.
*/
fun storeNewUserEmail(email: String) =
internal fun storeNewUserEmail(email: String) =
store.write(SIGNED_UP_EMAIL, email)

/**
* Retrieve the email that has recently signed up on this device.
*/
fun getNewUserEmail() =
internal fun getNewUserEmail() =
store.read(SIGNED_UP_EMAIL).also {
store.delete(SIGNED_UP_EMAIL)
}

companion object {
internal companion object {
const val AUTH_SESSION_COOKIE = "auth_session_cookie"
const val LOGGED_OUT_EMAIL_HASH = "logged_out_email_hash"
const val SIGNED_UP_EMAIL = "signed_up_email"

val allCookies = arrayOf(
AUTH_SESSION_COOKIE,
LOGGED_OUT_EMAIL_HASH,
SIGNED_UP_EMAIL
)
}

private fun String.sha256(): String =
Expand Down
Expand Up @@ -42,6 +42,15 @@ class CookieStoreTest {
verify(store).write(eq(AUTH_SESSION_COOKIE), eq(cookie))
}

@Test
fun `clear deletes all data`() {
createCookieStore().clear()

CookieStore.allCookies.forEach {
verify(store).delete(it)
}
}

@Test
fun `logout stores hashed email and clears cookie`() {
val cookieStore = createCookieStore()
Expand Down
Expand Up @@ -90,7 +90,7 @@ internal class AppearancePlaygroundActivity : BasePaymentSheetActivity() {
) {
CustomizationUi(appearance)
MainButton(
label = stringResource(R.string.reset_defaults),
label = stringResource(R.string.reset),
enabled = !inProgress
) {
viewModel.appearance.postValue(PaymentSheet.Appearance())
Expand Down
Expand Up @@ -146,7 +146,9 @@ class PaymentSheetPlaygroundActivity : AppCompatActivity() {
countryCurrencyPairs.map { it.first }
)

viewBinding.resetDefaultsButton.setOnClickListener {
viewBinding.resetButton.setOnClickListener {
PaymentSheet.reset(this)

setToggles(
customer = Toggle.Customer.default.toString(),
link = Toggle.Link.default as Boolean,
Expand Down Expand Up @@ -303,7 +305,7 @@ class PaymentSheetPlaygroundActivity : AppCompatActivity() {
stripeSupportedCurrencies.indexOf(currency)
)
viewBinding.merchantCountrySpinner.setSelection(
countryCurrencyPairs.map{it.first.code.value}.indexOf(merchantCountryCode)
countryCurrencyPairs.map { it.first.code.value }.indexOf(merchantCountryCode)
)

when (mode) {
Expand Down
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/playground"
android:layout_width="match_parent"
android:layout_height="match_parent">
Expand Down Expand Up @@ -343,24 +342,24 @@
android:checked="false" />
</RadioGroup>

<Button
android:id="@+id/reset_defaults_button"
android:text="@string/reset_defaults"
android:layout_width="wrap_content"
android:layout_height="48dp"
<Button
android:id="@+id/reset_button"
android:text="@string/reset"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/allowsDelayedPaymentMethods_radio_group" />

<Button
android:id="@+id/reload_button"
android:text="@string/reload_paymentsheet"
android:layout_width="wrap_content"
android:layout_height="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/reset_defaults_button" />
<Button
android:id="@+id/reload_button"
android:text="@string/reload_paymentsheet"
android:layout_width="wrap_content"
android:layout_height="48dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/reset_button" />

<View
android:id="@+id/divider"
Expand Down
2 changes: 1 addition & 1 deletion paymentsheet-example/src/main/res/values/strings.xml
Expand Up @@ -35,7 +35,7 @@
<string name="shipping_address">Shipping Address</string>
<string name="allowsDelayedPaymentMethods">Delayed PMs</string>
<string name="automatic_pm">Automatic PMs</string>
<string name="reset_defaults">Reset Defaults</string>
<string name="reset">Reset</string>
<string name="confirm">confirm</string>
<string name="currency_usd">USD</string>
<string name="currency_eur">EUR</string>
Expand Down
5 changes: 5 additions & 0 deletions paymentsheet/api/paymentsheet.api
Expand Up @@ -63,6 +63,7 @@ public final class com/stripe/android/paymentsheet/PaymentOptionsViewModel_Facto

public final class com/stripe/android/paymentsheet/PaymentSheet {
public static final field $stable I
public static final field Companion Lcom/stripe/android/paymentsheet/PaymentSheet$Companion;
public fun <init> (Landroidx/activity/ComponentActivity;Lcom/stripe/android/paymentsheet/PaymentSheetResultCallback;)V
public fun <init> (Landroidx/fragment/app/Fragment;Lcom/stripe/android/paymentsheet/PaymentSheetResultCallback;)V
public final fun presentWithPaymentIntent (Ljava/lang/String;)V
Expand Down Expand Up @@ -256,6 +257,10 @@ public final class com/stripe/android/paymentsheet/PaymentSheet$Colors$Creator :
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/stripe/android/paymentsheet/PaymentSheet$Companion {
public final fun reset (Landroid/content/Context;)V
}

public final class com/stripe/android/paymentsheet/PaymentSheet$Configuration : android/os/Parcelable {
public static final field $stable I
public static final field CREATOR Landroid/os/Parcelable$Creator;
Expand Down
Expand Up @@ -54,7 +54,13 @@ internal class DefaultPrefsRepository(
return "customer[$customerId]"
}

private companion object {
companion object {
private const val PREF_FILE = "DefaultPrefsRepository"

fun reset(context: Context) =
with(context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE).edit()) {
clear()
apply()
}
}
}
Expand Up @@ -9,6 +9,7 @@ import androidx.annotation.FontRes
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.fragment.app.Fragment
import com.stripe.android.link.account.CookieStore
import com.stripe.android.model.PaymentIntent
import com.stripe.android.model.SetupIntent
import com.stripe.android.paymentsheet.flowcontroller.FlowControllerFactory
Expand Down Expand Up @@ -781,4 +782,20 @@ class PaymentSheet internal constructor(
}
}
}

companion object {
/**
* Deletes all persisted state.
brnunes-stripe marked this conversation as resolved.
Show resolved Hide resolved
*
* You must call this method when the user logs out from your app.
* This will ensure that any persisted state in PaymentSheet, such as
* authentication cookies, is also cleared during logout.
*
* @param context the Application [Context].
*/
fun reset(context: Context) {
CookieStore(context).clear()
DefaultPrefsRepository.reset(context)
}
}
}
@@ -1,5 +1,6 @@
package com.stripe.android.paymentsheet

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import com.stripe.android.model.PaymentMethodFixtures
Expand All @@ -17,10 +18,11 @@ import kotlin.test.Test
internal class DefaultPrefsRepositoryTest {
private val testDispatcher = UnconfinedTestDispatcher()

private var context = ApplicationProvider.getApplicationContext<Context>()
private var isGooglePayReady = true
private var isLinkAvailable = true
private val prefsRepository = DefaultPrefsRepository(
ApplicationProvider.getApplicationContext(),
context,
"cus_123",
testDispatcher
)
Expand Down Expand Up @@ -82,4 +84,16 @@ internal class DefaultPrefsRepositoryTest {
)
)
}

@Test
fun `clear deletes all content`() = runTest {
prefsRepository.savePaymentSelection(PaymentSelection.Saved(PaymentMethodFixtures.CARD_PAYMENT_METHOD))
assertThat(prefsRepository.getSavedSelection(isGooglePayReady, isLinkAvailable))
.isEqualTo(SavedSelection.PaymentMethod("pm_123456789"))

DefaultPrefsRepository.reset(context)

assertThat(prefsRepository.getSavedSelection(isGooglePayReady, isLinkAvailable))
.isEqualTo(SavedSelection.None)
}
}