Skip to content

feat(apps): disable Update App button when there are no changes (#3559)#8026

Merged
mdmohsin7 merged 11 commits into
mainfrom
caleb/disable-update-app-no-changes
Jun 20, 2026
Merged

feat(apps): disable Update App button when there are no changes (#3559)#8026
mdmohsin7 merged 11 commits into
mainfrom
caleb/disable-update-app-no-changes

Conversation

@mdmohsin7

@mdmohsin7 mdmohsin7 commented Jun 19, 2026

Copy link
Copy Markdown
Member

Closes #3559.

What

Disable the Update App button on the Omi App Store "Update App" page until the user actually makes a change, and re-disable it if they revert everything.

How

AddAppProvider now tracks dirty state:

  • prepareUpdate snapshots the loaded app into _originalApp and attaches change listeners to every text controller (so the button reacts as you type, including fields outside the metadata Form).
  • checkValidity() recomputes hasChanges = hasDataChanged(_originalApp) on every field change.
  • hasDataChanged was rewritten to compare all editable fields, order-insensitively: name, description, visibility, category, capabilities (by id), pricing (plan + price, only when paid), conversation/chat prompts, source-code URL, external-integration fields, proactive-notification scopes, thumbnails, and a newly-picked logo.

update_app.dart gates the button on isValid && hasChangesonTap is null and the fill turns grey when there are no changes.

Acceptance criteria

  • Disabled on initial load (no changes).
  • Any edit to a field enables it.
  • Reverting all fields back to original disables it again.
  • Disabled state is visually clear (grey fill) and non-clickable.

Notes — why this isn't the earlier reverted attempt (#7975)

That PR was reverted because it broke the build: app.capabilities.map((c) => c.id)app.capabilities is Set<String>, so String has no .id. Here capabilities are compared with setEquals(selectedCapabilities.map((c) => c.id).toSet(), app.capabilities.toSet()). It also would have falsely marked free apps dirty (priceController.text != "0.0" when the price field is empty) — fixed by only comparing price/plan when the app is paid.

Tests

test/providers/add_app_provider_test.dart — 10 cases covering the no-change baseline, each field's edit→dirty, revert→clean, the free-app empty-price regression, capability-by-id, and logo pick. All green via flutter test. Verified flutter pub get + dart analyze clean on the changed files (no new issues).

Review in cubic

Add hasChanges + _originalApp to AddAppProvider. prepareUpdate snapshots the app
and attaches change listeners to all text controllers; checkValidity recomputes
hasChanges via a corrected hasDataChanged that compares all editable fields
(metadata, capabilities by id, pricing only when paid, prompts, source url,
external integration, proactive scopes, thumbnails) order-insensitively.
Gate onTap and the enabled color on isValid && hasChanges so a no-op state is
non-clickable and visibly greyed; reverting all edits disables it again.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 3 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread app/lib/pages/apps/providers/add_app_provider.dart
Comment thread app/lib/pages/apps/providers/add_app_provider.dart Outdated
… url & actions

Per cubic review:
- setIsPaid / setPaymentPlan / setIsPrivate only called notifyListeners, so
  toggling visibility/paid/plan left hasChanges stale and the button disabled.
  Add checkValidity() to each.
- hasDataChanged omitted the external-integration auth URL and actions, so editing
  only those kept hasChanges false. Compare both (actions order-insensitive).
@mdmohsin7

Copy link
Copy Markdown
Member Author

Both valid — fixed in 126cbcf (identified by cubic):

  1. P1 (dirty state wiring): setIsPaid, setPaymentPlan, and setIsPrivate only called notifyListeners(), so toggling visibility/paid/plan left hasChanges stale. Added checkValidity() to all three (category, capability, and scope setters already had it; text fields are covered by controller listeners).
  2. P2 (external-integration coverage): hasDataChanged now also compares the auth-step URL and the actions set (order-insensitive), so editing only those enables Update.

Added test coverage for the auth-URL and actions cases; full provider suite is green (13 tests).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 3 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread app/lib/pages/apps/providers/add_app_provider.dart
Per cubic review: prepareUpdate assigned app's thumbnail lists by reference, so
in-place add/remove also mutated the _originalApp snapshot and setEquals always
matched -> Update stayed disabled for thumbnail-only changes. Copy the lists, and
call checkValidity() after a thumbnail upload (removeThumbnail already did).
@mdmohsin7

Copy link
Copy Markdown
Member Author

Valid — fixed in the latest commit (identified by cubic). prepareUpdate assigned app.thumbnailUrls/thumbnailIds by reference, so the in-place add/removeAt in pickThumbnail/removeThumbnail mutated the _originalApp snapshot too, making setEquals always match. Now copying both lists with List.of(...), plus added checkValidity() after a thumbnail upload (removeThumbnail already had it). Added a thumbnail dirty-state test; suite green (14).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 3 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread app/lib/pages/apps/providers/add_app_provider.dart Outdated
Per cubic: the external-integration comparison was gated on ext != null, so
adding those fields to an app that had no integration wasn't detected. Compare
null-safe instead. Applied the same null-safe treatment to proactive-notification
scopes to avoid the identical gap.
@mdmohsin7

Copy link
Copy Markdown
Member Author

Fixed in the latest commit (identified by cubic). The external-integration block was gated on ext != null, so adding those fields to an app that had no integration wasn't detected — now compared null-safe. I also applied the same null-safe treatment to the proactive-notification scopes check, which had the identical gap, to head it off. Added a regression test (external field on a non-integration app). Suite green (15).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 3 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread app/lib/pages/apps/providers/add_app_provider.dart
…dirty

prepareUpdate only seeds optional fields (integration/scopes/prompts) when present
on the app, and the provider is reused across add/update flows. With the dirty
check now comparing those fields unconditionally, leftover values from a prior
session could mark an unchanged app as dirty. Call clear() first so the baseline
is always clean.
@mdmohsin7

Copy link
Copy Markdown
Member Author

Fixed in the latest commit (identified by cubic). Root cause: the provider is reused across the add/update flows and prepareUpdate only seeds optional fields (integration/scopes/prompts) when the app has them; once the dirty check compares those unconditionally, stale leftover values read as a false change. Fix is systemic — prepareUpdate now calls clear() first so the baseline is always clean for every field (not just the ones flagged). Added a clear() regression test. Suite green (16).

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 3 files

Re-trigger cubic

@mdmohsin7 mdmohsin7 merged commit 95d9149 into main Jun 20, 2026
3 checks passed
@mdmohsin7 mdmohsin7 deleted the caleb/disable-update-app-no-changes branch June 20, 2026 08:46
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.

Disable "Update App" button in Omi app store when no changes are made

1 participant