Skip to content

[feature] android: in-app account deletion, and a web deletion route for both platforms - #235

Merged
YJack0000 merged 1 commit into
mainfrom
feat/android-account-deletion
Aug 14, 2026
Merged

YJack0000 merged 1 commit into
mainfrom
feat/android-account-deletion

Conversation

@YJack0000

Copy link
Copy Markdown
Contributor

Why

This unblocks Google Play submission. Play requires that any app offering account creation provide both:

  1. an in-app way to delete the account, and
  2. a publicly reachable web URL where a user can request deletion without installing the app.

Parley Android had neither. DELETE /me has existed on the backend and shipped on iOS since launch, but the Android CloudClient never implemented it (android/docs/api-cloud.md listed it under "not implemented"), and website/privacy/index.html told users to delete "from Settings in the iOS app" — a dead end for an Android-only user. Either gap alone gets the listing rejected.

What changed

CloudClient.deleteAccount()

DELETE /me with the bearer token, same contract as iOS ParleyKit/CloudClient.swift.

The backend refuses (409 owned_organizations) while the account still owns a shared workspace — deleting it silently would take its other members' recordings with it. That refusal gets its own typed flag, CloudException.ownsOrganizations, alongside the file's existing isAuthExpired / isForbidden / isQuotaExhausted conventions, rather than collapsing into a generic failure. It is the one deletion error the user can actually act on, and it is deliberately not isRetryable — telling them to "try again" would be a lie.

In-app deletion UX

Mirrors iOS Settings → Account exactly, in Material3:

  • A destructive "Delete account" TextButton in the account sheet, shown once me() has confirmed who is signed in (same condition as iOS — no point offering a call that cannot reach the server either).
  • It only arms an AlertDialog. The dialog body names what is destroyed — the account, every synced recording and its audio, transcripts, summaries, folders, usage history, permanently and irreversibly, plus the not-yet-uploaded queue on the device — instead of asking "are you sure?".
  • The confirm button repeats the action ("Delete my account and data", error-coloured) rather than saying OK, so the destructive tap is never ambiguous. Two deliberate taps on two separately-labelled destructive controls; one stray tap can never delete an account. iOS uses the same two-step (a confirmationDialog, not a typed confirmation), so this matches rather than inventing a stricter gesture.
  • On success: the pending-upload queue is wiped (those recordings have no account to land in, and the dialog promised the audio would be gone) and the token is cleared, not signed out — the server already destroyed the session, so there is nothing to revoke. Clearing the token is what returns the app to the sign-in wall; ParleyRoot already watches isSignedIn.
  • On the org-owner refusal: a specific, actionable line — "You still own at least one organization. Transfer or delete it in the desktop app first, then delete your account." — rather than a retry prompt. Every other failure gets the generic connection message.

Queue-then-token ordering is deliberate: the sign-in screen must never appear over a still-populated queue that a re-sign-in would upload into a brand-new account.

Strings

All eight new strings in both values/strings.xml and values-zh-rTW/strings.xml. Verified programmatically: 96 names, zero asymmetry.

Website

New page at /account-deletion/ (website/account-deletion/index.html) — zero-build static like the rest of the site, legal.css, zh-Hant with an English section, same structure as privacy/ and support/. It states what deletion removes, the in-app route for each platform (iOS Settings → Account; Android account sheet), the shared-organization exception, and the contact route at contact@pathors.com for someone who cannot open the app. Added to sitemap.xml and linked from the landing footer, privacy and support.

website/privacy/index.html corrected: the deletion paragraph, the summary callout and the English summary all claimed iOS-only deletion. website/support/index.html had the same claim in its contact list.

No invented policy. No retention window, no response-time SLA, no "within N days" — nothing that is not already stated somewhere in the repo. The identity check for emailed requests ("email from the address the account is registered to") is procedure, not a commitment.

Docs

android/docs/api-cloud.md: DELETE /me moved out of "not implemented" into the method table, with a section covering the clear-don't-sign-out ordering and the ownsOrganizations refusal, plus a row in the error table.

Verification

JAVA_HOME=$(/usr/libexec/java_home -v 24) ./gradlew assembleDebug :parleykit:test :app:testDebugUnitTestBUILD SUCCESSFUL, 45 tasks.

New CloudClientDeleteAccountTest (5 tests, MockWebServer — added testImplementation(libs.okhttp.mockwebserver) to :app, which :parleykit already used) covers the error mapping: the authenticated DELETE /me on the wire, 409 owned_organizationsownsOrganizations + not retryable, an unrelated 409 → not the org refusal, 401 → isAuthExpired + onUnauthorized fired once, 503 → still retryable.

The website is zero-build; HTML well-formedness and every relative link were checked instead.

Not in this PR

android/AppStore/ is untouched — #232 (docs/android-store-listing) owns that, and documents this same gap in data-safety.md. That PR's data-safety declaration and this PR's implementation should land together for the submission.

…for both platforms

Google Play requires any app offering account creation to provide both an
in-app way to delete the account and a publicly reachable web URL where
deletion can be requested without installing the app. Parley Android had
neither: `DELETE /me` has existed on the backend and shipped on iOS since
launch, but the Android CloudClient never implemented it, and the privacy
page told everyone to delete "from Settings in the iOS app" — a dead end
for an Android-only user. This is a listing blocker.

- CloudClient gains `deleteAccount()`. The backend's 409 refusal for an
  account that still owns a shared organization gets its own typed flag,
  `CloudException.ownsOrganizations`, rather than collapsing into a generic
  failure: it is the one deletion error the user can act on, and telling
  them to "try again" would be a lie.
- The account sheet gets a destructive "Delete account" action, mirroring
  iOS Settings → Account: the button only arms a confirmation dialog that
  spells out what is destroyed (account, every synced recording, its audio,
  permanently), and a second separately-labelled destructive tap commits.
  On success the pending-upload queue is wiped and the token cleared — the
  server already destroyed the session, so there is nothing to revoke — and
  ParleyRoot returns the app to the sign-in wall on its own.
- New strings in both values/ and values-zh-rTW/.
- New page at /account-deletion/, zh-Hant with an English section like the
  rest of the site: what deletion removes, the in-app route for iOS and
  Android, the shared-organization exception, and the contact route for
  someone who cannot open the app. Linked from the landing footer, privacy
  and support; privacy's iOS-only deletion claim is corrected.
@sonarqubecloud

Copy link
Copy Markdown

@YJack0000
YJack0000 merged commit 26790b0 into main Aug 14, 2026
1 check passed
YJack0000 added a commit that referenced this pull request Aug 18, 2026
…eature claims (#256)

Five of the six desktop screenshots predated the v0.22.14–v0.22.16 removals.
The worst was the hero, which is both the largest image on parley.tw and the
first image in the README: its right-hand column was the intel board, removed
in #247. The page around it had already been rewritten in #252 to stop
promising that feature; the picture kept promising it anyway.

Reshot against current main, seeded through the store with a fictional renewal
negotiation (Northwind Logistics / Dana Whitfield — no real customer data):

- showcase-hero — the live cockpit as it actually is now: transcript, coach
  feed, agenda checklist. No intel board, because there is no intel board.
- showcase-transcript — the transcript posture with the findings column.
- showcase-study — the report: brief with clickable timestamp pills, action
  items with severity, filed into a customer folder.
- showcase-qa — Ask, in the drawer it actually lives in now. The old shot
  still had the v0.11 "Ask / TODO" tab pair and the old delivery panel.
- showcase-eval — Settings → Evaluations, showing the built-in playbooks and
  an editable prompt, which is what "your checks" means concretely.

showcase-voicetyping is kept: the overlay has only had behaviour fixes since it
was shot (screen placement, tap-to-toggle, preview cap), no visual redesign.
The two iPhone shots are also kept — they predate the #248 restyle, so they are
next in line, but they show nothing that no longer exists.

The capture shims (a store handle in main.tsx, a thread seeder in AskPanel) are
reverted, not committed, per the standing recipe.

Alongside that, the store listings still sold "deal intelligence" / 成交情報 —
the mini-CRM removed in #245 — in all four locale files, in the paragraph that
tells a phone user what they get on the desktop. Replaced with cross-meeting
search, which is what actually replaced it.

Also settled two stale open items that #235 had already resolved without the
docs catching up: Android does ship in-app account deletion, and
parley.tw/account-deletion/ is the Play-required web route, so
`android/AppStore/data-safety.md` no longer opens with a TODO gate and the
listings point at the real URL rather than the privacy policy. The remaining
`[TODO: confirm with Jack]` markers there are genuine questions for a human
(whether to over-declare usage data, and whether api.parley.tw retains
account-linked request logs) and are left alone.

Smaller corrections: iOS is 1.2 on the App Store, not 1.1; the iOS listing's
rationale notes described a name and a subtitle it does not ship; CLAUDE.md
listed "interpreter" among the secondary windows, which went with #242; and
docs/design/mini-crm.md now says up front that it documents a subsystem that
was built and then removed, rather than reading as current design.

Verified: bunx tsc --noEmit and bunx vitest run (221 tests) pass, and all eight
media slots load at their intended dimensions on a local serve of website/.
Lanznx added a commit that referenced this pull request Sep 16, 2026
… comes from the right source (#386)

* [docs] Android: the store packet stops contradicting itself

Three claims outlived the thing they described.

`data-safety.md` still answered Play's deletion question with "the honest answer
today is request by other means", and its cross-check called deletion "the one
real contradiction between the two stores". In-app account deletion has been
there since #235, and per-recording deletion landed with 1.13 — which also
clears what the phone kept locally: the audio saved for playback, a queued
re-transcription, and the retry ledger entry. Android now goes further than iOS
here, which is the opposite of what the file said.

`review-notes.md` said the foreground-service demo video "still has to be
hosted". `fgs-declaration.md` has recorded it as uploaded since 2026-08-19, with
the URL. Two files in the same directory disagreeing about whether a submission
blocker exists is worse than either answer.

The warning that replaces it is the one that will actually be needed: that video
shows meeting recording only, and Play reviews the declaration against the
permission, so the release carrying voice typing needs a re-shoot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* [fix] the Play badge comes from the source Google's guidelines name

The badge shipped as a PNG from `play.google.com/intl/en_us/badges/static/...`.
That URL still serves, but the documented badges page now redirects to the
Partner Marketing Hub, and the guidelines say to use the current assets from
there. The lockup is visually identical, so nothing looked wrong — but the
provenance was a stale copy rather than the source the rules point at, and a
small multi-colour mark on a dark background at HiDPI is exactly where a
downscaled bitmap shows its seams.

The swap is not just a file change, and this is the part worth reading: the
official SVG is edge-to-edge, while the legacy PNG carried 41px of transparent
padding that was silently supplying 11.8px of the 12.1px of required clear
space. Dropping the new file in without re-deriving the sizing would have
shipped a badge with essentially no clear space — out of compliance, on a public
page, looking fine. Height is now 48px with a 12px margin carrying the full
quarter-height itself. Rendered size is unchanged at 162×48.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: YJack0000 <jack@pathors.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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