Payments.

Co-authored-by: Alan Evans <alan@signal.org>
Co-authored-by: Alex Hart <alex@signal.org>
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
Android Team
2021-04-06 13:03:33 -03:00
committed by Alan Evans
parent c42023855b
commit fddba2906a
311 changed files with 18956 additions and 235 deletions

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.keyvalue;
public enum PaymentsAvailability {
NOT_IN_REGION(false, false),
DISABLED_REMOTELY(false, false),
REGISTRATION_AVAILABLE(false, true),
WITHDRAW_ONLY(true, true),
WITHDRAW_AND_SEND(true, true);
private final boolean showPaymentsMenu;
private final boolean isEnabled;
PaymentsAvailability(boolean isEnabled, boolean showPaymentsMenu) {
this.showPaymentsMenu = showPaymentsMenu;
this.isEnabled = isEnabled;
}
public boolean isEnabled() {
return isEnabled;
}
public boolean showPaymentsMenu() {
return showPaymentsMenu;
}
public boolean isSendAllowed() {
return this == WITHDRAW_AND_SEND;
}
public boolean canRegister() {
return this == REGISTRATION_AVAILABLE;
}
}

View File

@@ -0,0 +1,365 @@
package org.thoughtcrime.securesms.keyvalue;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import com.google.protobuf.InvalidProtocolBufferException;
import com.mobilecoin.lib.Mnemonics;
import com.mobilecoin.lib.exceptions.BadMnemonicException;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.payments.Balance;
import org.thoughtcrime.securesms.payments.Entropy;
import org.thoughtcrime.securesms.payments.GeographicalRestrictions;
import org.thoughtcrime.securesms.payments.Mnemonic;
import org.thoughtcrime.securesms.payments.MobileCoinLedgerWrapper;
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil;
import org.thoughtcrime.securesms.payments.proto.MobileCoinLedger;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.api.payments.Money;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Locale;
public final class PaymentsValues extends SignalStoreValues {
private static final String TAG = Log.tag(PaymentsValues.class);
private static final String PAYMENTS_ENTROPY = "payments_entropy";
private static final String MOB_PAYMENTS_ENABLED = "mob_payments_enabled";
private static final String MOB_LEDGER = "mob_ledger";
private static final String PAYMENTS_CURRENT_CURRENCY = "payments_current_currency";
private static final String DEFAULT_CURRENCY_CODE = "GBP";
private static final String USER_CONFIRMED_MNEMONIC = "mob_payments_user_confirmed_mnemonic";
private static final String SHOW_ABOUT_MOBILE_COIN_INFO_CARD = "mob_payments_show_about_mobile_coin_info_card";
private static final String SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD = "mob_payments_show_adding_to_your_wallet_info_card";
private static final String SHOW_CASHING_OUT_INFO_CARD = "mob_payments_show_cashing_out_info_card";
private static final String SHOW_RECOVERY_PHRASE_INFO_CARD = "mob_payments_show_recovery_phrase_info_card";
private static final String SHOW_UPDATE_PIN_INFO_CARD = "mob_payments_show_update_pin_info_card";
private static final Money.MobileCoin LARGE_BALANCE_THRESHOLD = Money.mobileCoin(BigDecimal.valueOf(500));
private final MutableLiveData<Currency> liveCurrentCurrency;
private final MutableLiveData<MobileCoinLedgerWrapper> liveMobileCoinLedger;
private final LiveData<Balance> liveMobileCoinBalance;
PaymentsValues(@NonNull KeyValueStore store) {
super(store);
this.liveCurrentCurrency = new MutableLiveData<>(currentCurrency());
this.liveMobileCoinLedger = new MutableLiveData<>(mobileCoinLatestFullLedger());
this.liveMobileCoinBalance = Transformations.map(liveMobileCoinLedger, MobileCoinLedgerWrapper::getBalance);
}
@Override void onFirstEverAppLaunch() {
}
@Override
@NonNull List<String> getKeysToIncludeInBackup() {
return Arrays.asList(PAYMENTS_ENTROPY,
MOB_PAYMENTS_ENABLED,
MOB_LEDGER,
PAYMENTS_CURRENT_CURRENCY,
DEFAULT_CURRENCY_CODE,
USER_CONFIRMED_MNEMONIC,
SHOW_ABOUT_MOBILE_COIN_INFO_CARD,
SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD,
SHOW_CASHING_OUT_INFO_CARD,
SHOW_RECOVERY_PHRASE_INFO_CARD,
SHOW_UPDATE_PIN_INFO_CARD);
}
public boolean userConfirmedMnemonic() {
return getStore().getBoolean(USER_CONFIRMED_MNEMONIC, false);
}
public void setUserConfirmedMnemonic(boolean userConfirmedMnemonic) {
getStore().beginWrite().putBoolean(USER_CONFIRMED_MNEMONIC, userConfirmedMnemonic).commit();
}
/**
* Consider using {@link #getPaymentsAvailability} which includes feature flag and region status.
*/
public boolean mobileCoinPaymentsEnabled() {
KeyValueReader reader = getStore().beginRead();
return reader.getBoolean(MOB_PAYMENTS_ENABLED, false);
}
/**
* Applies feature flags and region restrictions to return an enum which describes the available feature set for the user.
*/
public PaymentsAvailability getPaymentsAvailability() {
Context context = ApplicationDependencies.getApplication();
if (!TextSecurePreferences.isPushRegistered(context) ||
!GeographicalRestrictions.e164Allowed(TextSecurePreferences.getLocalNumber(context)))
{
return PaymentsAvailability.NOT_IN_REGION;
}
if (FeatureFlags.payments()) {
if (mobileCoinPaymentsEnabled()) {
return PaymentsAvailability.WITHDRAW_AND_SEND;
} else {
return PaymentsAvailability.REGISTRATION_AVAILABLE;
}
} else {
if (mobileCoinPaymentsEnabled()) {
return PaymentsAvailability.WITHDRAW_ONLY;
} else {
return PaymentsAvailability.DISABLED_REMOTELY;
}
}
}
@WorkerThread
public void setMobileCoinPaymentsEnabled(boolean isMobileCoinPaymentsEnabled) {
if (mobileCoinPaymentsEnabled() == isMobileCoinPaymentsEnabled) {
return;
}
if (isMobileCoinPaymentsEnabled) {
Entropy entropy = getPaymentsEntropy();
if (entropy == null) {
entropy = Entropy.generateNew();
Log.i(TAG, "Generated new payments entropy");
}
getStore().beginWrite()
.putBlob(PAYMENTS_ENTROPY, entropy.getBytes())
.putBoolean(MOB_PAYMENTS_ENABLED, true)
.putString(PAYMENTS_CURRENT_CURRENCY, currentCurrency().getCurrencyCode())
.commit();
} else {
getStore().beginWrite()
.putBoolean(MOB_PAYMENTS_ENABLED, false)
.putBoolean(USER_CONFIRMED_MNEMONIC, false)
.commit();
}
DatabaseFactory.getRecipientDatabase(ApplicationDependencies.getApplication()).markNeedsSync(Recipient.self().getId());
StorageSyncHelper.scheduleSyncForDataChange();
}
public @NonNull Mnemonic getPaymentsMnemonic() {
Entropy paymentsEntropy = getPaymentsEntropy();
if (paymentsEntropy == null) {
throw new IllegalStateException("Entropy has not been set");
}
return paymentsEntropy.asMnemonic();
}
/**
* True if a local entropy is set, regardless of whether payments is currently enabled.
*/
public boolean hasPaymentsEntropy() {
return getPaymentsEntropy() != null;
}
/**
* Returns the local payments entropy, regardless of whether payments is currently enabled.
* <p>
* And null if has never been set.
*/
public @Nullable Entropy getPaymentsEntropy() {
return Entropy.fromBytes(getStore().getBlob(PAYMENTS_ENTROPY, null));
}
public @NonNull Balance mobileCoinLatestBalance() {
return mobileCoinLatestFullLedger().getBalance();
}
public @NonNull LiveData<MobileCoinLedgerWrapper> liveMobileCoinLedger() {
return liveMobileCoinLedger;
}
public @NonNull LiveData<Balance> liveMobileCoinBalance() {
return liveMobileCoinBalance;
}
public void setCurrentCurrency(@NonNull Currency currentCurrency) {
getStore().beginWrite()
.putString(PAYMENTS_CURRENT_CURRENCY, currentCurrency.getCurrencyCode())
.commit();
liveCurrentCurrency.postValue(currentCurrency);
}
public @NonNull Currency currentCurrency() {
String currencyCode = getStore().getString(PAYMENTS_CURRENT_CURRENCY, null);
return currencyCode == null ? determineCurrency()
: Currency.getInstance(currencyCode);
}
public @NonNull MutableLiveData<Currency> liveCurrentCurrency() {
return liveCurrentCurrency;
}
public boolean showAboutMobileCoinInfoCard() {
return getStore().getBoolean(SHOW_ABOUT_MOBILE_COIN_INFO_CARD, true);
}
public boolean showAddingToYourWalletInfoCard() {
return getStore().getBoolean(SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD, true);
}
public boolean showCashingOutInfoCard() {
return getStore().getBoolean(SHOW_CASHING_OUT_INFO_CARD, true);
}
public boolean showRecoveryPhraseInfoCard() {
if (userHasLargeBalance()) {
return getStore().getBoolean(SHOW_CASHING_OUT_INFO_CARD, true);
} else {
return false;
}
}
public boolean showUpdatePinInfoCard() {
if (userHasLargeBalance() &&
SignalStore.kbsValues().hasPin() &&
!SignalStore.kbsValues().hasOptedOut() &&
SignalStore.pinValues().getKeyboardType().equals(PinKeyboardType.NUMERIC)) {
return getStore().getBoolean(SHOW_CASHING_OUT_INFO_CARD, true);
} else {
return false;
}
}
public void dismissAboutMobileCoinInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_ABOUT_MOBILE_COIN_INFO_CARD, false)
.apply();
}
public void dismissAddingToYourWalletInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_ADDING_TO_YOUR_WALLET_INFO_CARD, false)
.apply();
}
public void dismissCashingOutInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_CASHING_OUT_INFO_CARD, false)
.apply();
}
public void dismissRecoveryPhraseInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_RECOVERY_PHRASE_INFO_CARD, false)
.apply();
}
public void dismissUpdatePinInfoCard() {
getStore().beginWrite()
.putBoolean(SHOW_UPDATE_PIN_INFO_CARD, false)
.apply();
}
public void setMobileCoinFullLedger(@NonNull MobileCoinLedgerWrapper ledger) {
getStore().beginWrite()
.putBlob(MOB_LEDGER, ledger.serialize())
.commit();
liveMobileCoinLedger.postValue(ledger);
}
public @NonNull MobileCoinLedgerWrapper mobileCoinLatestFullLedger() {
byte[] blob = getStore().getBlob(MOB_LEDGER, null);
if (blob == null) {
return new MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance());
}
try {
return new MobileCoinLedgerWrapper(MobileCoinLedger.parseFrom(blob));
} catch (InvalidProtocolBufferException e) {
Log.w(TAG, "Bad cached ledger, clearing", e);
setMobileCoinFullLedger(new MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance()));
throw new AssertionError(e);
}
}
private @NonNull Currency determineCurrency() {
String localE164 = TextSecurePreferences.getLocalNumber(ApplicationDependencies.getApplication());
if (localE164 == null) {
localE164 = "";
}
return Util.firstNonNull(CurrencyUtil.getCurrencyByE164(localE164),
CurrencyUtil.getCurrencyByLocale(Locale.getDefault()),
Currency.getInstance(DEFAULT_CURRENCY_CODE));
}
public void setEnabledAndEntropy(boolean enabled, @Nullable Entropy entropy) {
KeyValueStore.Writer writer = getStore().beginWrite();
if (entropy != null) {
writer.putBlob(PAYMENTS_ENTROPY, entropy.getBytes());
}
writer.putBoolean(MOB_PAYMENTS_ENABLED, enabled)
.commit();
StorageSyncHelper.scheduleSyncForDataChange();
}
@WorkerThread
public WalletRestoreResult restoreWallet(@NonNull String mnemonic) {
byte[] entropyFromMnemonic;
try {
entropyFromMnemonic = Mnemonics.bip39EntropyFromMnemonic(mnemonic);
} catch (BadMnemonicException e) {
return WalletRestoreResult.MNEMONIC_ERROR;
}
Entropy paymentsEntropy = getPaymentsEntropy();
if (paymentsEntropy != null) {
byte[] existingEntropy = paymentsEntropy.getBytes();
if (Arrays.equals(existingEntropy, entropyFromMnemonic)) {
setMobileCoinPaymentsEnabled(true);
setUserConfirmedMnemonic(true);
return WalletRestoreResult.ENTROPY_UNCHANGED;
}
}
getStore().beginWrite()
.putBlob(PAYMENTS_ENTROPY, entropyFromMnemonic)
.putBoolean(MOB_PAYMENTS_ENABLED, true)
.remove(MOB_LEDGER)
.putBoolean(USER_CONFIRMED_MNEMONIC, true)
.commit();
liveMobileCoinLedger.postValue(new MobileCoinLedgerWrapper(MobileCoinLedger.getDefaultInstance()));
StorageSyncHelper.scheduleSyncForDataChange();
return WalletRestoreResult.ENTROPY_CHANGED;
}
public enum WalletRestoreResult {
ENTROPY_CHANGED,
ENTROPY_UNCHANGED,
MNEMONIC_ERROR
}
private boolean userHasLargeBalance() {
return mobileCoinLatestBalance().getFullAmount().requireMobileCoin().greaterThan(LARGE_BALANCE_THRESHOLD);
}
}

View File

@@ -33,6 +33,7 @@ public final class SignalStore {
private final PhoneNumberPrivacyValues phoneNumberPrivacyValues;
private final OnboardingValues onboardingValues;
private final WallpaperValues wallpaperValues;
private final PaymentsValues paymentsValues;
private final ProxyValues proxyValues;
private SignalStore() {
@@ -52,6 +53,7 @@ public final class SignalStore {
this.phoneNumberPrivacyValues = new PhoneNumberPrivacyValues(store);
this.onboardingValues = new OnboardingValues(store);
this.wallpaperValues = new WallpaperValues(store);
this.paymentsValues = new PaymentsValues(store);
this.proxyValues = new ProxyValues(store);
}
@@ -71,6 +73,7 @@ public final class SignalStore {
phoneNumberPrivacy().onFirstEverAppLaunch();
onboarding().onFirstEverAppLaunch();
wallpaper().onFirstEverAppLaunch();
paymentsValues().onFirstEverAppLaunch();
proxy().onFirstEverAppLaunch();
}
@@ -91,6 +94,7 @@ public final class SignalStore {
keys.addAll(phoneNumberPrivacy().getKeysToIncludeInBackup());
keys.addAll(onboarding().getKeysToIncludeInBackup());
keys.addAll(wallpaper().getKeysToIncludeInBackup());
keys.addAll(paymentsValues().getKeysToIncludeInBackup());
keys.addAll(proxy().getKeysToIncludeInBackup());
return keys;
}
@@ -164,6 +168,10 @@ public final class SignalStore {
return INSTANCE.wallpaperValues;
}
public static @NonNull PaymentsValues paymentsValues() {
return INSTANCE.paymentsValues;
}
public static @NonNull ProxyValues proxy() {
return INSTANCE.proxyValues;
}