Skip to content

Commit

Permalink
Merge pull request #152 from muun/52-release-branch
Browse files Browse the repository at this point in the history
Apollo: Release source code for 52
  • Loading branch information
acrespo authored Jun 27, 2024
2 parents 5e66ac8 + 153fc63 commit 0c8a8b2
Show file tree
Hide file tree
Showing 24 changed files with 365 additions and 73 deletions.
8 changes: 8 additions & 0 deletions android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ follow [https://changelog.md/](https://changelog.md/) guidelines.

## [Unreleased]

## [52] - 2024-06-14

### ADDED

- Wallet delete
All users can now request wallet delete, to comply with Google Play Account Deletion Data Safety
policy.

## [51.11] - 2024-05-18

### FIXED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ abstract class Globals {
*/
abstract val rcLoginAuthorizePath: String

/**
* Get the path of this app's "Confirm Account Deletion" deeplink.
*/
abstract val confirmAccountDeletionPath: String

/**
* Get Lapp's URL.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import io.muun.apollo.data.os.GooglePlayServicesHelper;
import io.muun.apollo.data.os.HardwareCapabilitiesProvider;
import io.muun.apollo.data.os.OS_ExtensionsKt;
import io.muun.apollo.domain.errors.delete_wallet.NonEmptyWalletDeleteException;
import io.muun.apollo.domain.errors.delete_wallet.UnsettledOperationsWalletDeleteException;
import io.muun.apollo.domain.errors.newop.CyclicalSwapError;
import io.muun.apollo.domain.errors.newop.InvalidInvoiceException;
import io.muun.apollo.domain.errors.newop.InvoiceAlreadyUsedException;
Expand Down Expand Up @@ -42,6 +44,7 @@
import io.muun.apollo.domain.model.user.UserProfile;
import io.muun.common.Optional;
import io.muun.common.api.ChallengeSetupVerifyJson;
import io.muun.common.api.ChallengeSignatureJson;
import io.muun.common.api.CreateFirstSessionJson;
import io.muun.common.api.CreateLoginSessionJson;
import io.muun.common.api.CreateRcLoginSessionJson;
Expand Down Expand Up @@ -72,6 +75,7 @@
import io.muun.common.model.challenge.Challenge;
import io.muun.common.model.challenge.ChallengeSetup;
import io.muun.common.model.challenge.ChallengeSignature;
import io.muun.common.rx.CompletableFn;
import io.muun.common.rx.ObservableFn;
import io.muun.common.rx.RxHelper;
import io.muun.common.utils.Encodings;
Expand All @@ -80,6 +84,7 @@

import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import libwallet.MusigNonces;
import okhttp3.MediaType;
import rx.Completable;
Expand Down Expand Up @@ -346,7 +351,7 @@ public Observable<KeySet> loginCompatWithoutChallenge() {

/**
* Notify houston of a client logout. Not a critical request, in fact its just so Houston
* can now IN ADVANCE of a session expiration (otherwise will have to wait until a new create
* can know IN ADVANCE of a session expiration (otherwise will have to wait until a new create
* session to invalidate old ones). So, its a fire and forget call.
*/
public Observable<Void> notifyLogout(String authHeader) {
Expand Down Expand Up @@ -786,4 +791,29 @@ incomingSwap, new PreimageJson(Encodings.bytesToHex(preimage))
public Completable updateUserPreferences(final UserPreferences prefs) {
return getService().updateUserPreferences(prefs.toJson());
}

/**
* Irreversibly delete current user wallet.
*/
public Completable deleteWallet(@NonNull final ChallengeSignature challengeSignature) {
final ChallengeSignatureJson challengeSignatureJson =
apiMapper.mapChallengeSignature(challengeSignature);

return getService().deleteWallet(challengeSignatureJson)
.compose(CompletableFn.replaceHttpException(
ErrorCode.WALLET_NOT_EMPTY,
NonEmptyWalletDeleteException::new
))
.compose(CompletableFn.replaceHttpException(
ErrorCode.UNSETTLED_OPERATIONS,
UnsettledOperationsWalletDeleteException::new
));
}

/**
* Use an external confirmation link (received by email) to confirm account deletion.
*/
public Observable<Void> confirmAccountDeletion(String uuid) {
return getService().confirmAccountDeletion(new LinkActionJson(uuid));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.muun.apollo.domain.action.session.SyncApplicationDataAction;
import io.muun.apollo.domain.action.session.UseMuunLinkAction;
import io.muun.apollo.domain.action.session.rc_only.LogInWithRcAction;
import io.muun.apollo.domain.action.user.DeleteWalletAction;
import io.muun.apollo.domain.action.user.EmailLinkAction;
import io.muun.apollo.domain.action.user.UpdateProfilePictureAction;
import io.muun.apollo.domain.debug.DebugExecutable;
Expand Down Expand Up @@ -129,5 +130,7 @@ public interface ActionComponent {

UpdateContactsPermissionStateAction updateContactsPermissionStateAction();

DeleteWalletAction deleteWalletAction();

DebugExecutable debugExecutable();
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ open class UseMuunLinkAction @Inject constructor(
isEmailAuthorize(parser) -> houstonClient.useAuthorizeLink(uuid)
isEmailConfirm(parser) -> houstonClient.useConfirmLink(uuid)
isRcLoginAuthorize(parser) -> houstonClient.authorizeLoginWithRecoveryCode(uuid)
isAccountDeletionConfirm(parser) -> houstonClient.confirmAccountDeletion(uuid)

else ->
throw MissingCaseError(linkUri, "Muun links")
Expand All @@ -63,4 +64,7 @@ open class UseMuunLinkAction @Inject constructor(

private fun isRcLoginAuthorize(p: UriParser) =
p.pathWithSlash == Globals.INSTANCE.rcLoginAuthorizePath

private fun isAccountDeletionConfirm(p: UriParser) =
p.pathWithSlash == Globals.INSTANCE.confirmAccountDeletionPath
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.muun.apollo.domain.action.user

import io.muun.apollo.data.net.HoustonClient
import io.muun.apollo.domain.action.base.BaseAsyncAction0
import io.muun.apollo.domain.action.challenge_keys.SignChallengeAction
import io.muun.common.Optional
import io.muun.common.crypto.ChallengeType
import io.muun.common.model.challenge.Challenge
import rx.Observable
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class DeleteWalletAction @Inject constructor(
private val signChallenge: SignChallengeAction,
private val houstonClient: HoustonClient,
) : BaseAsyncAction0<Void>() {

override fun action(): Observable<Void> {
return Observable.defer {
houstonClient.requestChallenge(ChallengeType.USER_KEY)
.map { maybeChallenge: Optional<Challenge> ->
val challenge = maybeChallenge.orElseThrow() // empty only for legacy apps
signChallenge.signWithUserKey(challenge)
}
.flatMap { challengeSignature ->
houstonClient.deleteWallet(challengeSignature)
.andThen(Observable.just(null))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,16 @@ sealed class AnalyticsEvent(metadataKeyValues: List<Pair<String, Any>> = listOf(
class E_RECOVERY_CODE_SET_UP : AnalyticsEvent()
class E_LOG_OUT : AnalyticsEvent()
class E_WALLET_CREATED : AnalyticsEvent()
class E_WALLET_DELETED : AnalyticsEvent()
class E_WALLET_DELETE(val state: WalletDeleteState) : AnalyticsEvent(
listOf("type" to state.name.lowercase(Locale.getDefault()))
)

enum class WalletDeleteState {
STARTED,
ERROR,
SUCCESS,
}

class E_SIGN_IN_ABORTED : AnalyticsEvent()

class E_SIGN_IN_SUCCESSFUL(val type: LoginType) : AnalyticsEvent(
Expand Down Expand Up @@ -441,8 +450,7 @@ sealed class AnalyticsEvent(metadataKeyValues: List<Pair<String, Any>> = listOf(
RC_SETUP_START_CONNECTION_ERROR,
RC_SETUP_FINISH_CONNECTION_ERROR,
RC_STALE_ERROR,
RC_CREDENTIALS_DONT_MATCH_ERROR,
CRASHLYTICS
RC_CREDENTIALS_DONT_MATCH_ERROR
}

class E_ERROR(val type: ERROR_TYPE, vararg extras: Any) : AnalyticsEvent(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.muun.apollo.domain.errors.delete_wallet

import io.muun.apollo.domain.errors.MuunError

class NonEmptyWalletDeleteException(cause: Throwable) : MuunError(cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.muun.apollo.domain.errors.delete_wallet

import io.muun.apollo.domain.errors.MuunError

class UnsettledOperationsWalletDeleteException(cause: Throwable) : MuunError(cause)
7 changes: 5 additions & 2 deletions android/apolloui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,23 @@ static def configExternalLinks(productFlavor, String host) {
String authorizePath = "/link/authorize/index.html"
String changePasswdPath = "/link/confirm/index.html"
String rcLoginAuthPath = "/link/authorize-rc/index.html"
String confirmAccountDeletionPath = "/link/confirm-account-deletion/index.html"

// Required for AndroidManifest:
productFlavor.resValue "string", "muun_link_host", host
productFlavor.resValue "string", "verify_link_path", verifyPath
productFlavor.resValue "string", "authorize_link_path", authorizePath
productFlavor.resValue "string", "confirm_link_path", changePasswdPath
productFlavor.resValue "string", "rc_login_authorize_link_path", rcLoginAuthPath
productFlavor.resValue "string", "confirm_account_deletion_path", confirmAccountDeletionPath

// Required for code access in action layer:
productFlavor.buildConfigField "String", "MUUN_LINK_HOST", quote(host)
productFlavor.buildConfigField "String", "VERIFY_LINK_PATH", quote(verifyPath)
productFlavor.buildConfigField "String", "AUTHORIZE_LINK_PATH", quote(authorizePath)
productFlavor.buildConfigField "String", "CONFIRM_LINK_PATH", quote(changePasswdPath)
productFlavor.buildConfigField "String", "RC_LOGIN_AUTHORIZE_LINK_PATH", quote(rcLoginAuthPath)
productFlavor.buildConfigField "String", "CONFIRM_ACCOUNT_DELETION_PATH", quote(confirmAccountDeletionPath)
}


Expand All @@ -91,8 +94,8 @@ android {
applicationId "io.muun.apollo"
minSdkVersion 19
targetSdkVersion 33
versionCode 1111
versionName "51.11"
versionCode 1200
versionName "52"

// Needed to make sure these classes are available in the main DEX file for API 19
// See: https://spin.atomicobject.com/2018/07/16/support-kitkat-multidex/
Expand Down
10 changes: 10 additions & 0 deletions android/apolloui/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@
android:path="@string/rc_login_authorize_link_path" />
</intent-filter>

<intent-filter android:label="@string/confirm_link_handler_label">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:scheme="https"
android:host="@string/muun_link_host"
android:path="@string/confirm_account_deletion_path" />
</intent-filter>

</activity>

<!-- SignupActivity -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class GlobalsImpl : Globals() {
override val rcLoginAuthorizePath: String
get() = BuildConfig.RC_LOGIN_AUTHORIZE_LINK_PATH

override val confirmAccountDeletionPath: String
get() = BuildConfig.CONFIRM_ACCOUNT_DELETION_PATH

override val lappUrl: String
get() = BuildConfig.LAPP_URL
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ class AlertDialogExtension @Inject constructor() : ActivityExtension() {
dismissDialog()

// ON dialog dismiss, dispose android's dialog reference to avoid memory leaks
dialog.addOnDismissAction {
activeDialog = null
dialog.addOnDismissAction { dismissedDialog ->
// If activeDialog has changed, it means dismissedDialog is already dismissed and a new
// dialog is being shown. We don't want to lose that ref (to properly dismiss it later).
if (activeDialog == dismissedDialog) {
activeDialog = null
}
}

activeDialog = dialog.show(activity)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ class EmergencyKitSaveFragment : SingleFragment<EmergencyKitSavePresenter>(),
/** User's choice of application the last time the share dialog was displayed. */
private var chosenShareTarget: String? = null

/** Whether the loading dialog is currently on screen */
/** Whether the loading dialog is currently on screen. Required due to limitations of
* AlertDialogExtensions. It can't handle multiple dismissDialogs() calls prompted by
* handleStates(). */
private var showingLoadingDialog = false

override fun inject() =
Expand Down
Loading

0 comments on commit 0c8a8b2

Please sign in to comment.