Skip to content

fix(sync): create audio_files for offline-synced recordings + feat: unsubscribe reason#5947

Open
sungdark wants to merge 2 commits intoBasedHardware:mainfrom
sungdark:feat/unsubscribe-reason-dialog
Open

fix(sync): create audio_files for offline-synced recordings + feat: unsubscribe reason#5947
sungdark wants to merge 2 commits intoBasedHardware:mainfrom
sungdark:feat/unsubscribe-reason-dialog

Conversation

@sungdark
Copy link
Copy Markdown

@sungdark sungdark commented Mar 23, 2026

Summary

This PR includes two fixes:

Fix 1: Unsubscribe reason selection (#5904)

Before confirming subscription cancellation, users are now asked to select a reason from predefined options, with an optional details field.

Fix 2: Offline sync no audio player (#5906)

When offline recordings are synced via sync_local_files, the process_segment function creates conversations with transcript segments but never creates audio_files entries. This causes the frontend to not show an audio player for offline-synced recordings.

Solution:

  1. Added _create_audio_files_for_synced_segment helper that:

    • Uploads the audio segment from the syncing/ bucket to the chunks/ bucket in GCS
    • Creates audio_files entries via create_audio_files_from_chunks
    • Updates the conversation with the audio_files
    • Triggers precaching of the merged audio
  2. Calls this helper in process_segment after:

    • Creating a new conversation (for brand new offline recordings)
    • Updating an existing conversation (for recordings merged with existing conversations)

This enables the audio player to show for offline-synced recordings, matching the behavior of live recordings.

Closes #5906


Changes

For Fix 1 (#5904):

Frontend:

  • UnsubscribeReasonDialog.tsx (new): A dialog component with 6 reason options
  • PlansSheet.tsx: Replaced the simple ConfirmDialog cancel flow with the new UnsubscribeReasonDialog
  • api.ts: Updated cancelSubscription() to accept optional reason and details parameters

Backend:

  • payment.py: Updated cancel_subscription_endpoint to accept reason and details query parameters and log them

For Fix 2 (#5906):

Backend:

  • backend/routers/sync.py: Added _create_audio_files_for_synced_segment helper and calls to it in process_segment

Before confirming cancellation, users are now asked to select a reason
from predefined options (too expensive, not using enough, missing features,
found alternative, temporary break, other). Optional details can also be
provided. The reason and details are passed to the backend and logged.

- Add UnsubscribeReasonDialog component with 6 reason options and
  optional details textarea
- Update PlansSheet to show reason dialog before cancel confirmation
- Update cancelSubscription API to accept reason and details parameters
- Update backend endpoint to accept and log cancellation reason
Fixes BasedHardware#5904
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR adds an unsubscribe reason collection step before subscription cancellation, replacing the simple ConfirmDialog with a new UnsubscribeReasonDialog that presents six predefined reason options and an optional free-text field. The reason and details are forwarded as query parameters to the backend, which logs them for churn analysis. The overall approach is sound and the UI is well-built, but there is one important behavioural issue:

  • "Skip" button does not cancel (UnsubscribeReasonDialog.tsx line 139): The "Skip" button is a Dialog.Close that dismisses the dialog without invoking onSubmit, so the subscription remains active. Users who want to cancel without sharing a reason will click "Skip" expecting cancellation to proceed, but nothing happens. The button should either be renamed to "Keep Subscription" to clarify its intent, or it should trigger cancellation with an empty/skipped reason.
  • Unused import (PlansSheet.tsx line 21): ConfirmDialog is still imported after being replaced.
  • Free-text details logged verbatim (payment.py line 372): User-supplied details content is written to INFO logs, which could capture PII; consider truncating or sanitising before logging.
  • reason not validated server-side (payment.py line 364): The backend accepts any string for reason rather than restricting it to the frontend's known enum values; adding a regex or Enum constraint would keep collected data clean.

Confidence Score: 3/5

  • Not safe to merge until the "Skip" button behaviour is clarified — in its current form it silently abandons the cancellation flow, which is a confusing regression for users who want to cancel without providing a reason.
  • The backend and API layer changes are straightforward and correct. The UI component is well-structured, but the "Skip" button is functionally broken as a cancellation pathway — it closes the dialog without canceling the subscription, directly contradicting user expectations. This affects the primary user action the PR is designed to support. Once that label/behaviour is corrected the PR is very close to mergeable.
  • web/app/src/components/ui/UnsubscribeReasonDialog.tsx — the "Skip" button behaviour needs to be resolved before merge.

Important Files Changed

Filename Overview
web/app/src/components/ui/UnsubscribeReasonDialog.tsx New dialog component for collecting cancellation reasons — well-structured UI, but the "Skip" button closes the dialog without canceling the subscription, which will confuse users who want to cancel without providing a reason.
web/app/src/components/settings/PlansSheet.tsx Cleanly swaps ConfirmDialog for UnsubscribeReasonDialog; leaves an unused ConfirmDialog import that should be removed.
web/app/src/lib/api.ts Correctly adds optional reason/details query params to the DELETE request; no issues.
backend/routers/payment.py Accepts and logs reason/details query params; reason is not validated against the allowed enum, and free-text details are logged verbatim which could capture PII.

Sequence Diagram

sequenceDiagram
    actor User
    participant PlansSheet
    participant UnsubscribeReasonDialog
    participant API (api.ts)
    participant Backend (payment.py)

    User->>PlansSheet: Click "Cancel Subscription"
    PlansSheet->>UnsubscribeReasonDialog: open=true
    UnsubscribeReasonDialog-->>User: Show reason options + details textarea

    alt User selects a reason & clicks "Cancel Subscription"
        User->>UnsubscribeReasonDialog: Select reason, optionally fill details
        UnsubscribeReasonDialog->>PlansSheet: onSubmit(reason, details)
        PlansSheet->>API (api.ts): cancelSubscription(reason, details)
        API (api.ts)->>Backend (payment.py): DELETE /v1/payments/subscription?reason=X&details=Y
        Backend (payment.py)-->>API (api.ts): {status: "ok"}
        API (api.ts)-->>PlansSheet: result
        PlansSheet->>PlansSheet: onSubscriptionUpdate(), close dialogs
    else User clicks "Skip" ⚠️
        User->>UnsubscribeReasonDialog: Click Skip (Dialog.Close)
        UnsubscribeReasonDialog->>PlansSheet: onOpenChange(false)
        Note over PlansSheet: Subscription is NOT canceled
    end
Loading

Reviews (1): Last reviewed commit: "feat: Add unsubscribe reason selection d..." | Re-trigger Greptile

Comment on lines +139 to +150
<Dialog.Close asChild>
<button
className={cn(
'flex-1 px-4 py-2.5 rounded-xl',
'bg-bg-tertiary hover:bg-bg-quaternary',
'text-text-secondary text-sm font-medium',
'transition-colors'
)}
>
Skip
</button>
</Dialog.Close>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 "Skip" button does not cancel subscription

The "Skip" button is a Dialog.Close that simply closes the dialog — it does not invoke onSubmit, so the subscription is never canceled when a user clicks it.

Users who want to cancel without providing a reason will naturally click "Skip" (interpreting it as "skip the reason collection but proceed with cancellation"), but they'll be silently returned to the plans sheet with their subscription still active. This is a meaningful UX regression from the old ConfirmDialog flow, where a user could always confirm cancellation without extra steps.

Either:

  1. Rename the button to something unambiguous like "Keep Subscription" to clarify that clicking it aborts the cancellation flow, or
  2. Have "Skip" actually cancel without a reason (call onSubmit with a fallback/empty reason).
Suggested change
<Dialog.Close asChild>
<button
className={cn(
'flex-1 px-4 py-2.5 rounded-xl',
'bg-bg-tertiary hover:bg-bg-quaternary',
'text-text-secondary text-sm font-medium',
'transition-colors'
)}
>
Skip
</button>
</Dialog.Close>
<button
onClick={() => {
onOpenChange(false);
}}
className={cn(
'flex-1 px-4 py-2.5 rounded-xl',
'bg-bg-tertiary hover:bg-bg-quaternary',
'text-text-secondary text-sm font-medium',
'transition-colors'
)}
>
Keep Subscription
</button>

@@ -19,6 +19,7 @@ import type {
AvailablePlansResponse,
} from '@/types/user';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unused import

ConfirmDialog is imported but no longer used in this file after being replaced by UnsubscribeReasonDialog. This will cause a lint warning and should be removed.

Suggested change
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';

Comment on lines +371 to +372
if reason:
logger.info(f"User {uid} canceling subscription. Reason: {reason}. Details: {details}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 User-provided free text logged at INFO level

details is a free-form textarea field filled in by the user. Logging it verbatim could capture PII or sensitive content (e.g. a user writing their email address, account number, or personal frustration). Consider sanitising or truncating the field before logging, or dropping it from the log entirely and storing it separately in a structured datastore if you need to analyse it later.

Suggested change
if reason:
logger.info(f"User {uid} canceling subscription. Reason: {reason}. Details: {details}")
if reason:
safe_details = (details or '')[:200] # truncate to limit log size
logger.info(f"User {uid} canceling subscription. Reason: {reason}. Details: {safe_details!r}")

Comment on lines +364 to +365
reason: str | None = Query(default=None, description="Cancellation reason"),
details: str | None = Query(default=None, description="Cancellation details"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 reason is not validated against the allowed enum values

The frontend restricts choices to UNSUBSCRIBE_REASONS, but the backend accepts any arbitrary string for reason. A caller could send reason=anything_they_want. Consider using a Literal type or a str | None with an explicit allowed-values check so that the data you log and eventually analyse is clean.

Suggested change
reason: str | None = Query(default=None, description="Cancellation reason"),
details: str | None = Query(default=None, description="Cancellation details"),
reason: str | None = Query(
default=None,
description="Cancellation reason",
pattern=r"^(too_expensive|not_using|missing_features|found_alternative|temporary|other)$",
),

When offline recordings are synced via `sync_local_files`, the
`process_segment` function creates conversations with transcript
segments but never creates `audio_files` entries. This causes
the frontend to not show an audio player for offline-synced
recordings.

This fix:
1. Adds `_create_audio_files_for_synced_segment` helper that:
   - Uploads the audio segment from syncing/ to chunks/ in GCS
   - Creates `audio_files` entries via `create_audio_files_from_chunks`
   - Updates the conversation with the `audio_files`
   - Triggers precaching of the merged audio

2. Calls this helper in `process_segment` after:
   - Creating a new conversation (for brand new offline recordings)
   - Updating an existing conversation (for recordings merged with existing)

This enables the audio player to show for offline-synced recordings,
matching the behavior of live recordings.

Fixes BasedHardware#5906
@sungdark sungdark changed the title feat: Add unsubscribe reason selection before cancel (#5904) fix(sync): create audio_files for offline-synced recordings + feat: unsubscribe reason Mar 23, 2026
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.

Offline sync: no audio player for synced recordings

1 participant