Skip to content

Comments

feat: support mainnet invites#1462

Open
HashEngineering wants to merge 23 commits intomasterfrom
feat/mainnet-invites-v2
Open

feat: support mainnet invites#1462
HashEngineering wants to merge 23 commits intomasterfrom
feat/mainnet-invites-v2

Conversation

@HashEngineering
Copy link
Collaborator

@HashEngineering HashEngineering commented Feb 11, 2026

Issue being fixed or feature implemented

Related PR's and Dependencies

Screenshots / Videos

How Has This Been Tested?

  • QA (Mobile Team)

Checklist:

  • I have performed a self-review of my own code and added comments where necessary
  • I have added or updated relevant unit/integration/functional/e2e tests

Summary by CodeRabbit

  • New Features

    • Persistent unique device ID and deep-link handling enabled for production and test variants (verified app links)
  • Bug Fixes

    • Graceful handling when InstantSendLock is absent
    • Invite affordability check to prevent sending without funds
    • Skip processing invitations with existing validation state
    • Safer username/voting field updates
  • Refactor

    • Centralized activity lifecycle tracking; AppsFlyer/deep-link init reworked
  • Chores

    • Updated ProGuard rules, source sets, build config and ignore paths; enabled invites and tx metadata in more flavors
  • Style

    • Dialog dismiss button changed to blue

@HashEngineering HashEngineering self-assigned this Feb 11, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Moves AppsFlyer initialization into WalletApplication with Play Services checks and deep-link extraction; adds UUID persistence; introduces WalletActivityTracker; adjusts manifests, BuildConfig and ProGuard rules; hardens invite/top-up validation; adds lint suppressions and assorted UI, API, and minor refactor changes.

Changes

Cohort / File(s) Summary
Ignore & ProGuard
/.gitignore, features/exploredash/proguard-rules.pro, integrations/uphold/proguard-rules.pro, wallet/proguard.cfg
Narrowed ignored path to wallet/prod/release; added/adjusted ProGuard rules (androidx.paging, Google Maps/Location, com.madgag.spongycastle); removed legacy android.support rules; updated Retrofit and Credentials Manager rules.
Build / Manifests
wallet/build.gradle, wallet/prod/AndroidManifest.xml, wallet/testNet3/AndroidManifest.xml, wallet/AndroidManifest.xml
Added prod and _testNet3 source sets and APPSFLYER_TEMPLATE_ID BuildConfig fields; added deep-link activity entries for prod/testNet3; removed a OneLink https intent-filter from main manifest.
Configuration / UUID
common/src/main/java/org/dash/wallet/common/Configuration.java
Added PREFS_UUID constant and getUniqueId() to generate, persist, and return a UUID in SharedPreferences.
AppsFlyer & Deep-link flow
wallet/src/de/schildbach/wallet/WalletApplication.java, wallet/src/de/schildbach/wallet/WalletApplicationExt.kt, wallet/androidTest/.../AppsFlyerLinkIntegrationTest.kt
AppsFlyer init moved into WalletApplication with Google Play Services availability check and consolidated deep-link extraction (af_dp, deep_link_value, link, af_sub1); removed previous extension-based init; test updated to set brand domain.
Activity lifecycle tracking
wallet/src/de/schildbach/wallet/WalletActivityTracker.java, wallet/src/de/schildbach/wallet/WalletApplication.java
Added WalletActivityTracker and integrated it into WalletApplication; replaced prior lifecycle approach; removed public currentActivity and applied related API/annotation adjustments and deprecations.
Invite / TopUp validation
wallet/src/de/schildbach/wallet/data/InvitationLinkData.kt, wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt
InvitationLinkData now uses toUri() and stronger validation (blank checks + try/catch); TopUpRepository guards InstantSendLock creation when absent and skips lock-related assignments.
Lint & API modernizations
wallet/src/.../BlockchainServiceImpl.kt, .../BootstrapReceiver.java, .../WalletBalanceWidgetProvider.java, .../CreateIdentityNotification.kt, .../MainActivity.kt
Added @SuppressLint("WrongConstant") annotations, migrated preference imports to androidx, replaced @TargetApi with @RequiresApi, and removed unused imports.
UI, Notifications & UX logic
wallet/res/layout/dialog_username_types.xml, wallet/src/.../NotificationItemUserAlert.kt, wallet/src/.../UserAlert.kt, wallet/src/.../notification/UserAlertViewHolder.kt, wallet/src/.../more/MoreFragment.kt, wallet/res/layout/fragment_request_username.xml
Style change for dismiss button; added getUserAlertId() and getIdFromStringRes() helpers; dismissal callback now null-safe and uses mapped alert ID; explicit enum case for NONE; made username requirements area scrollable.
Invite UI / Dialogs / ViewModels
wallet/src/de/schildbach/wallet/ui/invite/*, wallet/src/de/schildbach/wallet/ui/dashpay/*
Made isAbleToCreateInvite private; switched to spendable balance checks in invite dialogs; InviteSendContactRequestDialog now stores profile in fragment arguments; affordability checks added to ConfirmInviteDialogFragment; minor logger visibility and state clears.
Other minor changes
wallet/src/de/schildbach/wallet/Constants.java, wallet/src/de/schildbach/wallet/data/NotificationItemUserAlert.kt, wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt
Toggled feature flags (invites/tx metadata) for flavors; added getUserAlertId(); refined invitation processing condition to respect validationState and use the latest invitation value.

Sequence Diagram(s)

sequenceDiagram
    participant WalletApp as WalletApplication
    participant GPS as GooglePlayServices
    participant AppsFlyer as AppsFlyerLib
    participant MainThread as MainLooper
    participant Invite as InviteHandlerActivity

    WalletApp->>GPS: checkAvailability()
    alt Play Services available
        WalletApp->>AppsFlyer: initialize(with template id, setCustomerId)
        AppsFlyer-->>WalletApp: registerConversionListener()
        AppsFlyer->>WalletApp: onConversionData(data)
        WalletApp->>MainThread: post { extract deep-link (af_dp, deep_link_value, link, af_sub1) }
        MainThread->>Invite: startActivity(with extracted deep-link)
    else Play Services unavailable
        WalletApp-->>AppsFlyer: skip initialization
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A tiny UUID hops into place,
AppsFlyer whispers links through space,
Activities counted, lifecycle neat,
Deep-links routed, validations sweet,
ProGuard trimmed and manifests aligned — this rabbit hops upbeat!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: support mainnet invites' accurately summarizes the main objective of the pull request, which is to enable invite functionality on the mainnet.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/mainnet-invites-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wallet/build.gradle (1)

389-409: ⚠️ Potential issue | 🟡 Minor

devnet flavor inherits mainnet APPSFLYER_TEMPLATE_ID — likely should use testnet variant.

The all block sets APPSFLYER_TEMPLATE_ID from the mainnet property. Both _testNet3 and staging override it with APPSFLYER_TEMPLATE_ID_TESTNET, but devnet does not. Since devnet is a development/test network, it should probably also use the testnet template ID.

Suggested fix
         devnet {
             applicationId = "org.dash.dashpay.devnet"
             ...
             buildConfigField("String", "ZENLEDGER_CLIENT_ID", "\"\"")
             buildConfigField("String", "ZENLEDGER_CLIENT_SECRET", "\"\"")
+
+            def appsFlyerTemplateId = props.getProperty("APPSFLYER_TEMPLATE_ID_TESTNET", "\"\"")
+            buildConfigField("String", "APPSFLYER_TEMPLATE_ID", appsFlyerTemplateId)
         }
🤖 Fix all issues with AI agents
In `@wallet/src/de/schildbach/wallet/WalletActivityTracker.java`:
- Around line 119-123: logState() can NPE when currentActivity is null; add a
null guard before accessing currentActivity.getClass().getSimpleName() (e.g.,
compute a safe activityName variable or conditional expression) so the log call
always receives a non-null string; update the logState() method to use that safe
name and ensure callers like onActivityDestroyed that clear currentActivity
still log safely (refer to logState(), currentActivity, onActivityDestroyed, and
logName).
- Line 39: currentActivity is a strong Application-scoped reference causing a
leak; update WalletActivityTracker so onActivityDestroyed clears that reference.
Specifically, in the WalletActivityTracker class implement or modify
onActivityDestroyed(Activity activity) to check if activity == currentActivity
and if so set currentActivity to null (and ensure any related listeners or
callbacks tied to currentActivity are also removed/cleared there).

In `@wallet/src/de/schildbach/wallet/WalletApplication.java`:
- Around line 337-366: Remove production logging of full deep-link URLs (which
may contain PARAM_PRIVATE_KEY/WIF) in handleDeepLinkData and extractDeepLink:
stop calling log.info with the raw deepLinkValue/stringValue; instead either (a)
only log that a deep link was found and its source key (e.g., in extractDeepLink
log the key name but not the URL), or (b) conditionally log a redacted value
only when BuildConfig.DEBUG is true. Update handleDeepLinkData to avoid logging
deepLinkValue (use a presence/intent-launched message or debug-only masked
logging) and update extractDeepLink to log only the key name or a masked
representation (no full URL or query params).
- Around line 297-303: Remove the redundant call that sets a random UUID before
the real persistent ID: in the AppsFlyer initialization block (AppsFlyerLib
appsFlyerLib = AppsFlyerLib.getInstance(); appsFlyerLib.init(...); ...),
eliminate the call appsFlyerLib.setCustomerUserId(UUID.randomUUID().toString())
so only appsFlyerLib.setCustomerUserId(customerId) (where customerId =
config.getUniqueId()) is used; if a fallback is desired instead, wrap the
UUID.randomUUID() assignment to be used only when config.getUniqueId() is
null/empty and then call setCustomerUserId once with the chosen value.
- Line 300: The AppsFlyer debug logging is unconditionally enabled via
appsFlyerLib.setDebugLog(true); change it to enable debug logging only for
non-production builds by gating the call with BuildConfig.DEBUG (or equivalent
debug flag) so setDebugLog(true) runs only when BuildConfig.DEBUG is true and
setDebugLog(false) or omit the call in release; update the code near the
appsFlyerLib.setDebugLog invocation in WalletApplication.java to check
BuildConfig.DEBUG before enabling debug logging.
- Around line 247-253: The background thread running initializeAppsFlyer can
access config before it's assigned, causing an NPE; move the Configuration
initialization (config = new Configuration(...)) and any dependent setup
(autoLogout = new AutoLogout(config);
autoLogout.registerDeviceInteractiveReceiver(this);
registerActivityLifecycleCallbacks(...)) so that config is fully initialized on
the main thread before starting new Thread(this::initializeAppsFlyer).
Alternatively, capture a final local reference to the newly created
Configuration and pass it into the thread/initializeAppsFlyer call (or otherwise
synchronize access) so initializeAppsFlyer and its use of config.getUniqueId()
cannot run before config is set.
🧹 Nitpick comments (9)
wallet/src/de/schildbach/wallet/Constants.java (1)

99-99: SUPPORTS_INVITES is now true in every flavor — consider removing the flag.

With this PR, all three flavor branches (prod, staging/_testNet3, devnet) set SUPPORTS_INVITES = true. The flag is effectively always-on dead code. The existing TODO on line 65 already calls this out. Since the flag is non-final, it can't be eliminated by the compiler, so every call-site still evaluates the branch at runtime.

Not blocking, but worth tracking cleanup as a fast-follow.

wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt (1)

828-834: Swallowed exception loses context for debugging.

When the ISDLOCK_VERSION parse fails, the exception is silently discarded before retrying with ISLOCK_VERSION. If both fail, only the second exception propagates, making it harder to diagnose the root cause. Consider logging the first failure at debug level.

Proposed fix
                         } catch (e: Exception) {
+                            log.debug("ISDLOCK_VERSION parse failed, trying ISLOCK_VERSION", e)
                             InstantSendLock(
wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityNotification.kt (1)

58-86: Consider using the int-based stopForeground API instead of suppressing the lint warning.

The @SuppressLint("WrongConstant") is masking the use of the deprecated stopForeground(boolean) overload on line 85. Since the project's min SDK is 24, you can use the non-deprecated int-based API directly, which also eliminates the need for the suppression:

♻️ Suggested fix
-    `@SuppressLint`("WrongConstant")
     fun displayErrorAndStopService(isInvite: Boolean) {
         ...
-        service.stopForeground(true)
+        service.stopForeground(Service.STOP_FOREGROUND_REMOVE)
wallet/src/de/schildbach/wallet/WalletBalanceWidgetProvider.java (1)

114-114: Consider identifying and fixing the root cause of the "WrongConstant" lint warning instead of suppressing it.

A blanket @SuppressLint("WrongConstant") on this entire method hides the specific issue. If possible, identify which constant is flagged (likely getResources().getColor() without a theme on line 160, or a similar API misuse) and fix it directly. If the suppression is intentional, adding a brief comment explaining why would help future maintainers.

wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt (1)

458-458: Six identical @SuppressLint("WrongConstant") annotations scattered across this file — consider a targeted class-level or file-level suppression with a comment explaining the root cause.

If these are all triggered by the same underlying lint issue (e.g., getSystemService() string constants or stopForeground(true)), a single @SuppressLint at the class level with an explanatory comment would be cleaner than decorating individual methods. Alternatively, fixing the root causes (e.g., using ContextCompat.getSystemService() or stopForeground(STOP_FOREGROUND_REMOVE)) would eliminate the need for suppressions entirely.

Also applies to: 623-623, 694-694, 1345-1345, 1792-1792, 2050-2050

features/exploredash/proguard-rules.pro (1)

23-25: The androidx.paging.** keep rule is quite broad.

-keep class androidx.paging.** { *; } prevents all shrinking/obfuscation of the entire paging library. Typically only the DataSource subclass rule (line 25) is needed. The broad keep on line 24 may unnecessarily increase APK size. Consider whether it's actually needed or was added preemptively.

wallet/src/de/schildbach/wallet/service/BootstrapReceiver.java (1)

61-63: Class-level @SuppressLint("WrongConstant") is broad — consider scoping to the specific method.

Applying the suppression at the class level masks all WrongConstant warnings in the entire class. If only one method triggers the lint warning, applying it at that method level preserves lint coverage for the rest of the class.

common/src/main/java/org/dash/wallet/common/Configuration.java (1)

507-515: Unnecessary UUID.randomUUID() as getString default — use a simple sentinel instead.

On line 509, since prefs.contains(PREFS_UUID) is already checked, the default value of getString should never be reached. However, using UUID.randomUUID().toString() as a default is misleading and wasteful — a new UUID object is allocated on every call even when the key exists. Worse, if an edge case causes the default to be returned (e.g., corrupted prefs returning null for a present key), the UUID won't be persisted, leading to a different ID on each call.

Simpler and safer approach
     public String getUniqueId() {
-        if (prefs.contains(PREFS_UUID)) {
-            return prefs.getString(PREFS_UUID, UUID.randomUUID().toString());
-        } else {
+        String uuid = prefs.getString(PREFS_UUID, null);
+        if (uuid != null) {
+            return uuid;
+        } else {
-            String uuid = UUID.randomUUID().toString();
+            uuid = UUID.randomUUID().toString();
             prefs.edit().putString(PREFS_UUID, uuid).apply();
             return uuid;
         }
     }
wallet/build.gradle (1)

341-343: prod flavor APPSFLYER_TEMPLATE_ID override is redundant.

The all block on line 318-320 already reads from the same APPSFLYER_TEMPLATE_ID property. This override reads the identical property and sets the identical BuildConfig field. It can be removed unless you plan to change the property key in the future.

private static final Logger log = LoggerFactory.getLogger(WalletActivityTracker.class);

private final WalletApplication app;
private Activity currentActivity;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

currentActivity holds a strong reference — memory leak risk.

currentActivity is an Application-scoped field that is never cleared. After the last Activity is destroyed (onActivityDestroyed), it still holds a reference to that Activity, preventing it from being garbage collected for the lifetime of the process. This can retain the entire view hierarchy and associated resources.

Proposed fix: clear the reference in onActivityDestroyed
     public void onActivityDestroyed(`@NonNull` Activity activity) {
         log.info("{}: activity {} destroyed", logName, activity.getClass().getSimpleName());
         activityCount--;
-        logState();
+        if (currentActivity == activity) {
+            currentActivity = null;
+        }
+        if (currentActivity != null) {
+            logState();
+        }
         if (activityCount == 0)
             log.info("{}: app closed", logName);
     }
🤖 Prompt for AI Agents
In `@wallet/src/de/schildbach/wallet/WalletActivityTracker.java` at line 39,
currentActivity is a strong Application-scoped reference causing a leak; update
WalletActivityTracker so onActivityDestroyed clears that reference.
Specifically, in the WalletActivityTracker class implement or modify
onActivityDestroyed(Activity activity) to check if activity == currentActivity
and if so set currentActivity to null (and ensure any related listeners or
callbacks tied to currentActivity are also removed/cleared there).

Comment on lines +119 to +123
private void logState() {
log.info("{}: current: {}, activities: {} visible: {} foreground: {}", logName,
currentActivity.getClass().getSimpleName(), activityCount,
visibleActivityCount, foregroundActivityCount);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

logState() will NPE if currentActivity is null.

currentActivity is initialized to null and is never null-checked in logState(). While the normal Activity lifecycle should prevent this, clearing the reference in onActivityDestroyed (recommended above) would trigger this. Add a null guard.

Proposed fix
     private void logState() {
+        String activityName = currentActivity != null
+                ? currentActivity.getClass().getSimpleName()
+                : "none";
-        log.info("{}: current: {}, activities: {} visible: {} foreground: {}", logName,
-                currentActivity.getClass().getSimpleName(), activityCount,
+        log.info("{}: current: {}, activities: {} visible: {} foreground: {}", logName,
+                activityName, activityCount,
                 visibleActivityCount, foregroundActivityCount);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void logState() {
log.info("{}: current: {}, activities: {} visible: {} foreground: {}", logName,
currentActivity.getClass().getSimpleName(), activityCount,
visibleActivityCount, foregroundActivityCount);
}
private void logState() {
String activityName = currentActivity != null
? currentActivity.getClass().getSimpleName()
: "none";
log.info("{}: current: {}, activities: {} visible: {} foreground: {}", logName,
activityName, activityCount,
visibleActivityCount, foregroundActivityCount);
}
🤖 Prompt for AI Agents
In `@wallet/src/de/schildbach/wallet/WalletActivityTracker.java` around lines 119
- 123, logState() can NPE when currentActivity is null; add a null guard before
accessing currentActivity.getClass().getSimpleName() (e.g., compute a safe
activityName variable or conditional expression) so the log call always receives
a non-null string; update the logState() method to use that safe name and ensure
callers like onActivityDestroyed that clear currentActivity still log safely
(refer to logState(), currentActivity, onActivityDestroyed, and logName).

Comment on lines +247 to +253
new Thread(this::initializeAppsFlyer).start();
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
log.info("WalletApplication.onCreate()");
config = new Configuration(PreferenceManager.getDefaultSharedPreferences(this));
autoLogout = new AutoLogout(config);
autoLogout.registerDeviceInteractiveReceiver(this);
registerActivityLifecycleCallbacks(new ActivitiesTracker() {
int activityCount = 0;
int foregroundActivityCount = 0;
int visibleActivityCount = 0;
final String logName = "activity lifecycle";
@Override
protected void onStartedFirst(Activity activity) {

}

@Override
protected void onStartedAny(boolean isTheFirstOne, Activity activity) {
super.onStartedAny(isTheFirstOne, activity);
// force restart if the app was updated
// this ensures that v6.x or previous will go through the PIN upgrade process
if (!BuildConfig.DEBUG && myPackageReplaced) {
log.info("restarting app due to upgrade");
myPackageReplaced = false;
restartService.performRestart(activity, true, true);
}
}

@Override
protected void onStoppedLast() {
autoLogout.setAppWentBackground(true);
if (config.getAutoLogoutEnabled() && config.getAutoLogoutMinutes() == 0) {
sendBroadcast(new Intent(InteractionAwareActivity.FORCE_FINISH_ACTION));
}
}

public void onActivityCreated(@NonNull Activity activity, Bundle bundle) {
if (activityCount == 0)
log.info("{}: app started", logName);
activityCount++;
log.info("{}: activity {} created", logName, activity.getClass().getSimpleName());
logState();
currentActivity = activity;
}

public void onActivityDestroyed(@NonNull Activity activity) {
log.info("{}: activity {} destroyed", logName, activity.getClass().getSimpleName());
activityCount--;
logState();
if (activityCount == 0)
log.info("{}: app closed", logName);
}

public void onActivityResumed(@NonNull Activity activity) {
foregroundActivityCount++;
logState();
currentActivity = activity;
}

public void onActivityPaused(@NonNull Activity activity) {
foregroundActivityCount--;
logState();
}

public void onActivityStarted(@NonNull Activity activity) {
visibleActivityCount++;
logState();
currentActivity = activity;
}

public void onActivityStopped(Activity activity) {
visibleActivityCount--;
logState();
}

private void logState() {
log.info("{}: activities: {} visible: {} foreground: {}", logName,
activityCount, visibleActivityCount, foregroundActivityCount);
}
});
registerActivityLifecycleCallbacks(new WalletActivityTracker(this, config, autoLogout, restartService));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Race condition: config may be null when initializeAppsFlyer runs.

initializeAppsFlyer is launched on a background thread at line 247, but config is initialized at line 250 on the main thread. The background thread can reach config.getUniqueId() (line 302) before config is assigned, causing a NullPointerException.

Proposed fix: initialize config before starting the thread
         initLogging();
         FirebaseApp.initializeApp(this);
-        new Thread(this::initializeAppsFlyer).start();
         AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
         log.info("WalletApplication.onCreate()");
         config = new Configuration(PreferenceManager.getDefaultSharedPreferences(this));
+        new Thread(this::initializeAppsFlyer).start();
         autoLogout = new AutoLogout(config);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
new Thread(this::initializeAppsFlyer).start();
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
log.info("WalletApplication.onCreate()");
config = new Configuration(PreferenceManager.getDefaultSharedPreferences(this));
autoLogout = new AutoLogout(config);
autoLogout.registerDeviceInteractiveReceiver(this);
registerActivityLifecycleCallbacks(new ActivitiesTracker() {
int activityCount = 0;
int foregroundActivityCount = 0;
int visibleActivityCount = 0;
final String logName = "activity lifecycle";
@Override
protected void onStartedFirst(Activity activity) {
}
@Override
protected void onStartedAny(boolean isTheFirstOne, Activity activity) {
super.onStartedAny(isTheFirstOne, activity);
// force restart if the app was updated
// this ensures that v6.x or previous will go through the PIN upgrade process
if (!BuildConfig.DEBUG && myPackageReplaced) {
log.info("restarting app due to upgrade");
myPackageReplaced = false;
restartService.performRestart(activity, true, true);
}
}
@Override
protected void onStoppedLast() {
autoLogout.setAppWentBackground(true);
if (config.getAutoLogoutEnabled() && config.getAutoLogoutMinutes() == 0) {
sendBroadcast(new Intent(InteractionAwareActivity.FORCE_FINISH_ACTION));
}
}
public void onActivityCreated(@NonNull Activity activity, Bundle bundle) {
if (activityCount == 0)
log.info("{}: app started", logName);
activityCount++;
log.info("{}: activity {} created", logName, activity.getClass().getSimpleName());
logState();
currentActivity = activity;
}
public void onActivityDestroyed(@NonNull Activity activity) {
log.info("{}: activity {} destroyed", logName, activity.getClass().getSimpleName());
activityCount--;
logState();
if (activityCount == 0)
log.info("{}: app closed", logName);
}
public void onActivityResumed(@NonNull Activity activity) {
foregroundActivityCount++;
logState();
currentActivity = activity;
}
public void onActivityPaused(@NonNull Activity activity) {
foregroundActivityCount--;
logState();
}
public void onActivityStarted(@NonNull Activity activity) {
visibleActivityCount++;
logState();
currentActivity = activity;
}
public void onActivityStopped(Activity activity) {
visibleActivityCount--;
logState();
}
private void logState() {
log.info("{}: activities: {} visible: {} foreground: {}", logName,
activityCount, visibleActivityCount, foregroundActivityCount);
}
});
registerActivityLifecycleCallbacks(new WalletActivityTracker(this, config, autoLogout, restartService));
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
log.info("WalletApplication.onCreate()");
config = new Configuration(PreferenceManager.getDefaultSharedPreferences(this));
new Thread(this::initializeAppsFlyer).start();
autoLogout = new AutoLogout(config);
autoLogout.registerDeviceInteractiveReceiver(this);
registerActivityLifecycleCallbacks(new WalletActivityTracker(this, config, autoLogout, restartService));
🤖 Prompt for AI Agents
In `@wallet/src/de/schildbach/wallet/WalletApplication.java` around lines 247 -
253, The background thread running initializeAppsFlyer can access config before
it's assigned, causing an NPE; move the Configuration initialization (config =
new Configuration(...)) and any dependent setup (autoLogout = new
AutoLogout(config); autoLogout.registerDeviceInteractiveReceiver(this);
registerActivityLifecycleCallbacks(...)) so that config is fully initialized on
the main thread before starting new Thread(this::initializeAppsFlyer).
Alternatively, capture a final local reference to the newly created
Configuration and pass it into the thread/initializeAppsFlyer call (or otherwise
synchronize access) so initializeAppsFlyer and its use of config.getUniqueId()
cannot run before config is set.

Comment on lines +297 to +303
AppsFlyerLib appsFlyerLib = AppsFlyerLib.getInstance();
appsFlyerLib.init(BuildConfig.APPSFLYER_ID, null, this);
appsFlyerLib.setAppInviteOneLink(BuildConfig.APPSFLYER_TEMPLATE_ID);
appsFlyerLib.setDebugLog(true);
appsFlyerLib.setCustomerUserId(UUID.randomUUID().toString());
String customerId = config.getUniqueId();
appsFlyerLib.setCustomerUserId(customerId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant setCustomerUserId call — first value is immediately overwritten.

Line 301 sets a random UUID as customer ID, then lines 302-303 immediately overwrite it with the persistent UUID from config. The first call is dead code.

Proposed fix
                 appsFlyerLib.setDebugLog(true);
-                appsFlyerLib.setCustomerUserId(UUID.randomUUID().toString());
                 String customerId = config.getUniqueId();
                 appsFlyerLib.setCustomerUserId(customerId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AppsFlyerLib appsFlyerLib = AppsFlyerLib.getInstance();
appsFlyerLib.init(BuildConfig.APPSFLYER_ID, null, this);
appsFlyerLib.setAppInviteOneLink(BuildConfig.APPSFLYER_TEMPLATE_ID);
appsFlyerLib.setDebugLog(true);
appsFlyerLib.setCustomerUserId(UUID.randomUUID().toString());
String customerId = config.getUniqueId();
appsFlyerLib.setCustomerUserId(customerId);
AppsFlyerLib appsFlyerLib = AppsFlyerLib.getInstance();
appsFlyerLib.init(BuildConfig.APPSFLYER_ID, null, this);
appsFlyerLib.setAppInviteOneLink(BuildConfig.APPSFLYER_TEMPLATE_ID);
appsFlyerLib.setDebugLog(true);
String customerId = config.getUniqueId();
appsFlyerLib.setCustomerUserId(customerId);
🤖 Prompt for AI Agents
In `@wallet/src/de/schildbach/wallet/WalletApplication.java` around lines 297 -
303, Remove the redundant call that sets a random UUID before the real
persistent ID: in the AppsFlyer initialization block (AppsFlyerLib appsFlyerLib
= AppsFlyerLib.getInstance(); appsFlyerLib.init(...); ...), eliminate the call
appsFlyerLib.setCustomerUserId(UUID.randomUUID().toString()) so only
appsFlyerLib.setCustomerUserId(customerId) (where customerId =
config.getUniqueId()) is used; if a fallback is desired instead, wrap the
UUID.randomUUID() assignment to be used only when config.getUniqueId() is
null/empty and then call setCustomerUserId once with the chosen value.

AppsFlyerLib appsFlyerLib = AppsFlyerLib.getInstance();
appsFlyerLib.init(BuildConfig.APPSFLYER_ID, null, this);
appsFlyerLib.setAppInviteOneLink(BuildConfig.APPSFLYER_TEMPLATE_ID);
appsFlyerLib.setDebugLog(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

setDebugLog(true) is enabled for all builds including production.

This enables verbose AppsFlyer debug logging in release builds, which can expose SDK internals, API keys, and configuration details in device logs. Consider gating on BuildConfig.DEBUG.

Proposed fix
-                appsFlyerLib.setDebugLog(true);
+                appsFlyerLib.setDebugLog(BuildConfig.DEBUG);
🤖 Prompt for AI Agents
In `@wallet/src/de/schildbach/wallet/WalletApplication.java` at line 300, The
AppsFlyer debug logging is unconditionally enabled via
appsFlyerLib.setDebugLog(true); change it to enable debug logging only for
non-production builds by gating the call with BuildConfig.DEBUG (or equivalent
debug flag) so setDebugLog(true) runs only when BuildConfig.DEBUG is true and
setDebugLog(false) or omit the call in release; update the code near the
appsFlyerLib.setDebugLog invocation in WalletApplication.java to check
BuildConfig.DEBUG before enabling debug logging.

Comment on lines +337 to +366
private void handleDeepLinkData(Map<String, Object> data) {
String deepLinkValue = extractDeepLink(data);
if (deepLinkValue != null) {
log.info("Processing deep link: {}", deepLinkValue);
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), de.schildbach.wallet.ui.InviteHandlerActivity.class);
intent.setData(Uri.parse(deepLinkValue));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
});
} else {
log.warn("No deep link found in AppsFlyer data");
}
}

private String extractDeepLink(Map<String, Object> data) {
// Define keys in order of preference
String[] possibleKeys = {"af_dp", "deep_link_value", "link", "af_sub1"};
for (String key : possibleKeys) {
Object value = data.get(key);
if (value instanceof String) {
String stringValue = (String) value;
if (!stringValue.trim().isEmpty()) {
log.info("Found deep link in key '{}': {}", key, stringValue);
return stringValue;
}
}
}
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Deep link URLs containing private keys are logged in production.

handleDeepLinkData and extractDeepLink log the full deep link value (lines 340, 360). Invitation links include PARAM_PRIVATE_KEY (a WIF-encoded private key) in the URL query string. Logging this to device logs and logcat in production is a security risk — any app with READ_LOGS permission or adb access could capture it.

Consider redacting or omitting the URL value in logs, or only logging in debug builds.

🤖 Prompt for AI Agents
In `@wallet/src/de/schildbach/wallet/WalletApplication.java` around lines 337 -
366, Remove production logging of full deep-link URLs (which may contain
PARAM_PRIVATE_KEY/WIF) in handleDeepLinkData and extractDeepLink: stop calling
log.info with the raw deepLinkValue/stringValue; instead either (a) only log
that a deep link was found and its source key (e.g., in extractDeepLink log the
key name but not the URL), or (b) conditionally log a redacted value only when
BuildConfig.DEBUG is true. Update handleDeepLinkData to avoid logging
deepLinkValue (use a presence/intent-launched message or debug-only masked
logging) and update extractDeepLink to log only the key name or a masked
representation (no full URL or query params).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@wallet/src/de/schildbach/wallet/ui/dashpay/notification/UserAlertViewHolder.kt`:
- Around line 35-37: UserAlertViewHolder currently no-ops when
notificationItem.getUserAlertId() returns null (e.g., getIdFromStringRes/textMap
misses an entry); change the close button handler to detect a null id, log a
warning (mentioning getIdFromStringRes/textMap and the notificationItem) and
still perform a fallback dismiss so the button always works — for example invoke
onUserAlertDismiss with a safe sentinel id (e.g., "unknown_alert") or a
generated fallback from the notificationItem, and ensure the log uses your
existing logger so maintainers can trace missing textMap entries.
🧹 Nitpick comments (1)
wallet/src/de/schildbach/wallet/ui/dashpay/UserAlert.kt (1)

40-41: Consider a more idiomatic reverse lookup.

filter + map + firstOrNull creates intermediate collections. A simpler idiomatic form:

♻️ Suggested simplification
         fun getIdFromStringRes(`@StringRes` stringResId: Int) =
-            textMap.filter { it.value == stringResId }.map { it.key }.firstOrNull()
+            textMap.entries.firstOrNull { it.value == stringResId }?.key

Comment on lines +35 to +37
notificationItem.getUserAlertId()?.let { id ->
onUserAlertDismiss.invoke(id)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent no-op when getUserAlertId() returns null.

If getIdFromStringRes doesn't find a match (e.g., a new alert type is added without updating textMap), the close button silently does nothing — the user taps dismiss and the alert stays. Consider logging a warning or falling back so the button remains functional:

Suggested defensive handling
             closeBtn.setOnClickListener {
-                notificationItem.getUserAlertId()?.let { id ->
-                    onUserAlertDismiss.invoke(id)
-                }
+                val alertId = notificationItem.getUserAlertId()
+                if (alertId != null) {
+                    onUserAlertDismiss.invoke(alertId)
+                } else {
+                    log.warn("No alert ID found for stringResId={}", notificationItem.stringResId)
+                }
             }
🤖 Prompt for AI Agents
In
`@wallet/src/de/schildbach/wallet/ui/dashpay/notification/UserAlertViewHolder.kt`
around lines 35 - 37, UserAlertViewHolder currently no-ops when
notificationItem.getUserAlertId() returns null (e.g., getIdFromStringRes/textMap
misses an entry); change the close button handler to detect a null id, log a
warning (mentioning getIdFromStringRes/textMap and the notificationItem) and
still perform a fallback dismiss so the button always works — for example invoke
onUserAlertDismiss with a safe sentinel id (e.g., "unknown_alert") or a
generated fallback from the notificationItem, and ensure the log uses your
existing logger so maintainers can trace missing textMap entries.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Fix all issues with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@wallet/src/de/schildbach/wallet/ui/invite/CreateInviteViewModel.kt`:
- Line 54: The public property invitations currently exposes a mutable
MutableStateFlow allowing external mutation; change it to a private mutable
backing property named _invitations:
MutableStateFlow<List<Invitation>>(emptyList()) and expose a public read-only
invitations = _invitations.asStateFlow(); then update the collector that
currently assigns to invitations (the write on line referenced) to write to
_invitations instead; ensure imports for asStateFlow() are present and adjust
any call sites to the now-immutable invitations.

In `@wallet/src/de/schildbach/wallet/ui/more/ConfirmInviteDialogFragment.kt`:
- Around line 74-78: The catch block in ConfirmInviteDialogFragment that
currently calls log.info("error sending transaction:", e) should use log.error
(or log.warn) to reflect the severity; update the exception logging in the catch
within the method that sends the invite transaction (the catch that updates
binding.confirmMessage.text and binding.confirmMessage.isVisible) to call
log.error and include the exception object so the stacktrace is recorded.
- Line 55: The code computes Coin.valueOf(args.amount) twice; instead of
recomputing it for inviteAmount, reuse the previously assigned viewModel.amount:
remove the second Coin.valueOf(args.amount) and set inviteAmount (or use
inviteAmount variable) from viewModel.amount (or eliminate inviteAmount and
reference viewModel.amount directly) so you don't duplicate the conversion
(referencing viewModel.amount, args.amount, and inviteAmount to locate the
spots).
🧹 Nitpick comments (3)
🤖 Fix all nitpicks with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@wallet/src/de/schildbach/wallet/ui/invite/CreateInviteViewModel.kt`:
- Line 54: The public property invitations currently exposes a mutable
MutableStateFlow allowing external mutation; change it to a private mutable
backing property named _invitations:
MutableStateFlow<List<Invitation>>(emptyList()) and expose a public read-only
invitations = _invitations.asStateFlow(); then update the collector that
currently assigns to invitations (the write on line referenced) to write to
_invitations instead; ensure imports for asStateFlow() are present and adjust
any call sites to the now-immutable invitations.

In `@wallet/src/de/schildbach/wallet/ui/more/ConfirmInviteDialogFragment.kt`:
- Around line 74-78: The catch block in ConfirmInviteDialogFragment that
currently calls log.info("error sending transaction:", e) should use log.error
(or log.warn) to reflect the severity; update the exception logging in the catch
within the method that sends the invite transaction (the catch that updates
binding.confirmMessage.text and binding.confirmMessage.isVisible) to call
log.error and include the exception object so the stacktrace is recorded.
- Line 55: The code computes Coin.valueOf(args.amount) twice; instead of
recomputing it for inviteAmount, reuse the previously assigned viewModel.amount:
remove the second Coin.valueOf(args.amount) and set inviteAmount (or use
inviteAmount variable) from viewModel.amount (or eliminate inviteAmount and
reference viewModel.amount directly) so you don't duplicate the conversion
(referencing viewModel.amount, args.amount, and inviteAmount to locate the
spots).
wallet/src/de/schildbach/wallet/ui/invite/CreateInviteViewModel.kt (1)

54-54: invitations exposes a mutable MutableStateFlow publicly.

External consumers can mutate the state directly. Consider the private-mutable / public-immutable pattern:

-    val invitations = MutableStateFlow<List<Invitation>>(listOf())
+    private val _invitations = MutableStateFlow<List<Invitation>>(listOf())
+    val invitations = _invitations.asStateFlow()

And update the collector on line 58 to write to _invitations.

As per coding guidelines: "Use private mutable _uiState with public immutable uiState via asStateFlow() in ViewModels."

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@wallet/src/de/schildbach/wallet/ui/invite/CreateInviteViewModel.kt` at line
54, The public property invitations currently exposes a mutable MutableStateFlow
allowing external mutation; change it to a private mutable backing property
named _invitations: MutableStateFlow<List<Invitation>>(emptyList()) and expose a
public read-only invitations = _invitations.asStateFlow(); then update the
collector that currently assigns to invitations (the write on line referenced)
to write to _invitations instead; ensure imports for asStateFlow() are present
and adjust any call sites to the now-immutable invitations.
wallet/src/de/schildbach/wallet/ui/more/ConfirmInviteDialogFragment.kt (2)

74-78: Consider using log.error for the exception path.

log.info on an exception during invite transaction send understates the severity. log.error or log.warn would be more appropriate for error triage in production logs.

-                    log.info("error sending transaction:", e)
+                    log.error("error sending transaction:", e)
🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@wallet/src/de/schildbach/wallet/ui/more/ConfirmInviteDialogFragment.kt`
around lines 74 - 78, The catch block in ConfirmInviteDialogFragment that
currently calls log.info("error sending transaction:", e) should use log.error
(or log.warn) to reflect the severity; update the exception logging in the catch
within the method that sends the invite transaction (the catch that updates
binding.confirmMessage.text and binding.confirmMessage.isVisible) to call
log.error and include the exception object so the stacktrace is recorded.

55-55: Minor: inviteAmount duplicates viewModel.amount.

Coin.valueOf(args.amount) is computed on line 55 and assigned to viewModel.amount, then recomputed on line 59 as inviteAmount. You could reuse viewModel.amount directly to avoid the duplication.

-                    val inviteAmount = Coin.valueOf(args.amount)
+                    val inviteAmount = viewModel.amount

Also applies to: 59-59

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@wallet/src/de/schildbach/wallet/ui/more/ConfirmInviteDialogFragment.kt` at
line 55, The code computes Coin.valueOf(args.amount) twice; instead of
recomputing it for inviteAmount, reuse the previously assigned viewModel.amount:
remove the second Coin.valueOf(args.amount) and set inviteAmount (or use
inviteAmount variable) from viewModel.amount (or eliminate inviteAmount and
reference viewModel.amount directly) so you don't duplicate the conversion
(referencing viewModel.amount, args.amount, and inviteAmount to locate the
spots).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
wallet/src/de/schildbach/wallet/ui/InviteHandlerViewModel.kt (1)

58-59: Remove commented-out dead code.

_validationState / validationState have been fully commented out. They should either be deleted or tracked in an issue if the feature is deferred.

♻️ Proposed cleanup
-//    private val _validationState = MutableStateFlow(InvitationValidationState.NONE)
-//    val validationState: StateFlow<InvitationValidationState?> = _validationState.asStateFlow()
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/InviteHandlerViewModel.kt` around lines 58
- 59, Remove the dead commented-out state flow declarations in
InviteHandlerViewModel: delete the two commented lines containing
"_validationState" and "validationState" (and any leftover reference to
InvitationValidationState if unused), or if this feature is intentionally
deferred, create a tracking issue and replace the commented code with a single
TODO comment that references that issue and InvitationValidationState; ensure no
unused imports or references remain after removal.
wallet/res/layout/fragment_request_username.xml (1)

103-116: LGTM — ScrollView integration is correct.

The height="0dp" + constraintTop_toBottomOf/constraintBottom_toTopOf pair correctly fills the space between top_stack and the button. fillViewport="true" is appropriate to prevent collapsed layout on short content, and requirements_stack as the sole wrap_content child follows the expected ScrollView pattern.

One optional tidy-up: the Android ConstraintLayout docs recommend android:layout_width="0dp" with explicit app:layout_constraintStart_toStartOf="parent" and app:layout_constraintEnd_toEndOf="parent" over match_parent for direct children of ConstraintLayout, as match_parent can silently conflict with padding applied to the root. All existing children in this file use match_parent, so this is a pre-existing pattern and not a blocker.

♻️ Align with ConstraintLayout recommended width idiom (optional)
 <ScrollView
     android:id="@+id/scroll_view"
-    android:layout_width="match_parent"
+    android:layout_width="0dp"
     android:layout_height="0dp"
     android:fillViewport="true"
     android:layout_marginHorizontal="15dp"
     app:layout_constraintTop_toBottomOf="@+id/top_stack"
-    app:layout_constraintBottom_toTopOf="@+id/request_username_button">
+    app:layout_constraintBottom_toTopOf="@+id/request_username_button"
+    app:layout_constraintStart_toStartOf="parent"
+    app:layout_constraintEnd_toEndOf="parent">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/res/layout/fragment_request_username.xml` around lines 103 - 116, The
ScrollView currently uses android:layout_width="match_parent" while placed in a
ConstraintLayout; change its width to "0dp" and add
app:layout_constraintStart_toStartOf="parent" and
app:layout_constraintEnd_toEndOf="parent" so it follows the ConstraintLayout
recommended idiom (reference ids: ScrollView @+id/scroll_view and its child
LinearLayout @+id/requirements_stack); keep android:layout_height="0dp" and the
existing top/bottom constraints unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt`:
- Line 221: The observers and onLockScreenDeactivated call sites call
processInvitation without checking invitation.validationState, allowing re-entry
and re-showing dialogs; to fix, add a unified guard inside processInvitation
that returns early if invitation.validationState != null (and preserve existing
null/isSynced checks there), then revert the change at the LiveData observer
(the line that added validationState == null) back to only the null + isSynced
checks; update callers in the isBlockchainSynced observer and
onLockScreenDeactivated to call processInvitation as-is (no extra guards) so the
validationState check is enforced centrally within processInvitation.

---

Nitpick comments:
In `@wallet/res/layout/fragment_request_username.xml`:
- Around line 103-116: The ScrollView currently uses
android:layout_width="match_parent" while placed in a ConstraintLayout; change
its width to "0dp" and add app:layout_constraintStart_toStartOf="parent" and
app:layout_constraintEnd_toEndOf="parent" so it follows the ConstraintLayout
recommended idiom (reference ids: ScrollView @+id/scroll_view and its child
LinearLayout @+id/requirements_stack); keep android:layout_height="0dp" and the
existing top/bottom constraints unchanged.

In `@wallet/src/de/schildbach/wallet/ui/InviteHandlerViewModel.kt`:
- Around line 58-59: Remove the dead commented-out state flow declarations in
InviteHandlerViewModel: delete the two commented lines containing
"_validationState" and "validationState" (and any leftover reference to
InvitationValidationState if unused), or if this feature is intentionally
deferred, create a tracking issue and replace the commented code with a single
TODO comment that references that issue and InvitationValidationState; ensure no
unused imports or references remain after removal.

inviteHandlerViewModel.invitation.observe(viewLifecycleOwner) { invitation ->
val isSynced = viewModel.isBlockchainSynced.value == true
if (invitation != null && isSynced) {
if (invitation != null && isSynced && invitation.validationState == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard inconsistency: isBlockchainSynced and onLockScreenDeactivated paths bypass the validationState == null check

The new guard prevents re-entry from the invitation LiveData observer after validation. However, two other call sites are unguarded:

  • Line 184 (isBlockchainSynced observer): calls processInvitation(inviteHandlerViewModel.invitation.value!!, ...) unconditionally whenever sync state changes. If the invitation has already been validated (validationState != null) and a sync event fires afterwards, processInvitation will call showInviteValidationDialog with the validated state — potentially re-showing a dismissal dialog.
  • Lines 247–255 (onLockScreenDeactivated): same issue; no validationState == null guard before calling processInvitation.

Consider adding the same guard at those call sites, or moving the guard into processInvitation itself to enforce it uniformly:

♻️ Proposed fix: hoist the guard into processInvitation
 private suspend fun processInvitation(invitation: InvitationLinkData, isSynced: Boolean, isLockScreenActive: Boolean) {
+    if (invitation.validationState != null) return
     if (isSynced) {

Then revert line 221 back to only the null + isSynced checks:

-            if (invitation != null && isSynced && invitation.validationState == null) {
+            if (invitation != null && isSynced) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt` at
line 221, The observers and onLockScreenDeactivated call sites call
processInvitation without checking invitation.validationState, allowing re-entry
and re-showing dialogs; to fix, add a unified guard inside processInvitation
that returns early if invitation.validationState != null (and preserve existing
null/isSynced checks there), then revert the change at the LiveData observer
(the line that added validationState == null) back to only the null + isSynced
checks; update callers in the isBlockchainSynced observer and
onLockScreenDeactivated to call processInvitation as-is (no extra guards) so the
validationState check is enforced centrally within processInvitation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant