* feat(rate-dialog): native store review + prompt after a productive win
Baseline of PR #8680 (squashed) so review improvements land as a separate,
cherry-pickable commit on top.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(rate-dialog): calm banner, feedback cooldown, delayed win prompt
Review + UX follow-ups on top of PR #8680:
- Fix (correctness): re-check full eligibility at prompt fire-time, not just
opt-out, so a crash/data-damage recorded after arming still suppresses it.
- Fix: iOS advances the rating cadence only once the native request resolves;
a reject leaves eligibility intact instead of burning a lifetime prompt.
- Fix: Android review-flow failure now logs and abandons instead of opening the
Play Store unprompted (the trigger is an automatic win, not a user tap).
- UX: web/electron/F-Droid now show a calm, non-modal banner that opens the full
rate/feedback dialog on request, rather than a modal shoved in mid-flow.
- UX: 'give feedback' no longer permanently opts out — it starts a long cooldown
(~90 app-start days) so an engaged user can still be asked once more later.
- UX: the win prompt fires a few seconds after the completion, not on the tap.
- UX: dedicated 'Send feedback' entry in the Help menu (GitHub Discussions).
- UX: show the maintainer email as selectable text (mailto: dead-end fallback).
- Refactor: move selectTodayProgress into work-context.selectors (colocation);
idempotency guard on the win subscription.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(rate-dialog): import WIN_PROMPT_DELAY_MS in spec instead of mirroring it
Export the delay constant from the service and reference it in the spec so the
test can't silently drift from the real value. Found by a review pass.
Co-Authored-By: Claude <noreply@anthropic.com>
* feat(rate-dialog): recurring cadence, version-age gate, GitHub star CTA
Growth-focused follow-ups (review found the lifetime cap starves review
recency/velocity, which is what stores rank on):
- Recurring cadence: after the fixed onboarding tiers (32/96 app-start days) the
prompt no longer stops forever — it recurs every ~180 app-start days (~6+
months of real use), well inside Apple's ~3/365 allowance and Play's own
quota. Still calm: win-timed, opt-out/crash/feedback-gated, OS-throttled.
- Version-age gate: hold the prompt for 7 days after the app version changes so a
fresh (possibly regressed) release isn't asked to be rated immediately. Tracked
via two device-local LS keys, checked at arm time and fire time.
- Play tier-burn is now only a deferral, not a lifetime loss (recurring cadence).
- Web/Electron CTA: 'Star us on GitHub' (the desktop-distribution equivalent of
store ranking) instead of the near-zero-conversion how-to-rate doc.
Tests cover recurrence at + beyond the last tier, the version-age gate, and the
updated cadence expectations. 60/60 green; tsc clean.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(rate-dialog): register iOS plugin, drop version-age gate, cut per-tick churn
Second multi-review pass (7 reviewers):
- CRITICAL (Codex, verified): the iOS StoreReview Capacitor plugin was never
registered — CustomViewController registers WebDavHttpPlugin but not
StoreReviewPlugin, so requestReview() rejected and the native App Store review
card never showed. Register it via registerPluginInstance.
- Remove the version-age gate (4 reviewers): getAppVersionStr() changes every
release and the app ships ~weekly, so the 7-day window kept re-arming on every
update (web SW reload / Electron auto-update) and near-permanently suppressed
the prompt on desktop — the platform where the GitHub-star intent matters most.
It also duplicated the 30-day crash gate, which already covers crashing
regressions. Drops 2 LS keys, a constant, and ~40 LOC.
- Perf: add distinctUntilChanged on the armed win stream so the 1s time-tracking
tick (new {done,total} with identical numbers) no longer re-runs scan/filter
every second for the whole session.
59/59 specs green; tsc clean. Kotlin/Swift can't be compiled here — the iOS
registration needs an on-device/simulator smoke test.
Co-Authored-By: Claude <noreply@anthropic.com>
* chore(i18n): translate new rate-prompt strings into all locales
Add BANNER_ACTION, BTN_STAR_GITHUB (F.D_RATE) and SEND_FEEDBACK (MH.HM) to all
27 non-en locale files — previously they existed only in en.json and fell back
to English for everyone else. SEND_FEEDBACK reuses each locale's existing
feedback wording for consistency; GitHub is kept untranslated.
Deliberately edits locale files beyond the usual en-only workflow, per request.
Best-effort translations — a native check on the less-common languages is
welcome.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(build): keep @electron/asar on minimatch v3 for MAS universal build
The `overrides.app-builder-lib.minimatch` pin to v10 cascaded into
@electron/asar (a child of both app-builder-lib and @electron/universal),
which only works with minimatch v3. minimatch v9/v10 ship as ESM with
`__esModule: true` and no `default` export, so @electron/asar's compiled
default import (`minimatch_1.default(...)`) resolved to undefined and threw
`TypeError: (0 , minimatch_1.default) is not a function` during the
universal-app asar merge (makeUniversalApp -> mergeASARs -> shouldUnpackPath),
failing `dist:mac:mas:buildOnly` in the Mac Store release workflow.
Carve @electron/asar back to minimatch 3.1.2 via nested overrides while
app-builder-lib (and @electron/universal) keep minimatch v10 as intended.
* fix(ci): declare export compliance for iOS App Store submission
The iOS release build, upload and processing all succeed, but
`upload_to_app_store` fails at submit-for-review with:
[!] Export compliance is required to submit
Example: submission_information: { export_compliance_uses_encryption: false }
Super Productivity only relies on exempt encryption (HTTPS / standard OS
crypto), so declare that the app does not use non-exempt encryption:
- ios/App/App/Info.plist: ITSAppUsesNonExemptEncryption=false (canonical,
build-time declaration; also covers TestFlight, auto-resolved by ASC).
- fastlane/Fastfile: export_compliance_uses_encryption: false in
submission_information, so the submit-for-review API call carries the
declaration regardless of the binary.
* refactor(build): simplify @electron/asar minimatch override to top-level
Follow-up to the minimatch carve-out. Hoisting a top-level
`overrides["@electron/asar"].minimatch` pin produces a byte-identical
package-lock.json to the previous nested form, but is simpler and covers
every @electron/asar consumer (app-builder-lib, @electron/universal,
electron-winstaller) regardless of which parent wins hoisting — rather than
relying on a nested branch under app-builder-lib (whose @electron/universal
sub-branch was dead config given asar dedupes to a single instance).
* test(op-log): de-flake LockService mutex-invariant timeout test
"should preserve mutex invariant after timeout" flaked on the macOS CI
runner (TZ=America/Los_Angeles leg): it asserted that C always times out
waiting for the lock, but on a slow/loaded runner A can release the lock
before C's 50ms timeout fires, so C legitimately acquires it and runs —
after A has finished. The observed `['a-start','a-end','c-start']` actually
satisfies the invariant (C ran after A, never concurrently); only the
brittle "C must time out" assertion failed.
Assert the real no-concurrent-execution invariant instead: C must never
start before A ends. This still catches the original regression (concurrent
C would push 'c-start' before 'a-end') but is independent of runner timing.
---------
Co-authored-by: Claude <noreply@anthropic.com>
iOS reads a stale sync-data.json on every WebDAV sync: URLSession's
default configuration caches GET responses, so the client never sees
remote changes and — because the pre-upload conflict check re-reads the
same cached body — blindly overwrites newer remote data. Android (OkHttp,
no cache) and web (fetch `cache: 'no-store'`) are unaffected, which is why
this is iOS-only.
- iOS WebDavHttpPlugin: disable the HTTP cache (urlCache=nil +
reloadIgnoringLocalCacheData) so sync reads always hit the network.
- WebDavHttpAdapter: send `Cache-Control: no-cache, no-store` + `Pragma`
on the native request path, covering an upstream proxy/CDN cache too.
- Log allowlisted, non-sensitive cache-relevant response headers so a
future log capture localises a stale read (client cache vs proxy).
Fixes#7144
The app uses only local notifications — no @capacitor/push-notifications,
no FCM, no registerForRemoteNotifications anywhere. Declaring
remote-notification in UIBackgroundModes claims a push capability the
app never uses, which Apple may flag at App Store review.
The app targets ES2022+/Safari 18+ but the iOS deployment target was 14.0.
On older iOS versions (e.g., 15), the JavaScript fails to parse, leaving
users stuck on a white splash screen with no feedback.
- Add an inline ES5-compatible browser check in index.html that detects
unsupported browsers and shows a helpful update message
- Raise iOS deployment target from 14.0 to 16.0 in both project.pbxproj
and Podfile
- Guard productivity tips script against null elements when compat check
replaces splash screen content
Install capacitor-plugin-safe-area to read native safe area insets and
inject them as CSS variables, working around Capacitor 7's broken
adjustMarginsForEdgeToEdge and Android WebView's unreliable
env(safe-area-inset-*) values.
- Add _initSafeAreaInsets() to GlobalThemeService
- Remove broken adjustMarginsForEdgeToEdge: 'auto' from capacitor config
- Clamp context menu trigger Y to safe area top
- Fix light mode status bar color to match theme (#f8f8f7)
The plugin was compiled but never registered on iOS, causing
"WebDavHttp plugin not implemented" errors. Add a custom
CAPBridgeViewController subclass that registers the plugin,
mirroring the Android registration in CapacitorMainActivity.
- fix(tasks): preserve selectAllTasks memoization by only filtering when
undefined entities exist, avoiding new array allocation on every call
- fix(sync): replace mutable ARGON2_PARAMS export with getter/setter to
prevent test pollution across spec files
- fix(sync): convert tag-task-page from async pipe to toSignal pattern
to fix OnPush change detection after bulk sync state updates
- fix(sync): remove E2E navigation workarounds that masked the rendering
bug now fixed by the toSignal conversion
- fix(sync): add clarifying comments for archive-wins sibling conflict
resolution and waitForSyncWindow switchMap concurrency semantics
- fix(ios): add hex preview of first 16 bytes to WebDAV UTF-8 decode
error for easier debugging
- docs: add JSDoc to getDiffInWeeks noting negative diff behavior
https://claude.ai/code/session_01Y51QDFEdvrJ9VVgp9XfWLJ
Co-authored-by: Claude <noreply@anthropic.com>
Reject with DECODE_ERROR instead of silently returning empty string
when UTF-8 decoding fails on non-empty response data, preventing
the sync layer from misinterpreting decode failures as empty files.
iOS WebDAV sync could write but not read data due to Capacitor's native
bridge auto-parsing responses as JSON and replacing data with error strings.
Add a native URLSession-based plugin (matching the existing Android one)
that bypasses CapacitorHttp entirely.
Fixes#6317
- Add redirect_uri parameter to OAuth flow for mobile platforms
- Create OAuthCallbackHandlerService to handle deep link callbacks
- Register custom URI scheme (com.super-productivity.app://) in Android/iOS
- Add platform-specific UI for OAuth flow (automatic vs manual)
- Implement proper error handling for OAuth callback errors
- Add comprehensive unit tests for callback handler
- Fix memory leak by properly cleaning up event listeners
- Use IS_NATIVE_PLATFORM constant for consistent platform detection
Web/Electron continue using manual code entry (no regression).
Mobile (iOS/Android) now use automatic redirect with deep linking.
Fixes Dropbox OAuth authentication on iOS and Android platforms.
iOS renders transparent pixels as white, causing a white frame around the
app icon. This fix generates a 1024x1024 RGB PNG (no alpha channel) from
the existing build/icons/sq2160x2160.png source.
Changes:
- Add tools/generate-ios-icon.js script using Sharp to resize and remove alpha
- Add npm script 'generate:ios-icon' for reproducible icon generation
- Update AppIcon-512@2x.png to RGB format (was RGBA)
- Install Sharp as dev dependency for image processing
Icon is now fully opaque with correct brand blue color and white checkmark.