fix(sync): create audio_files for offline-synced recordings + feat: unsubscribe reason#5947
fix(sync): create audio_files for offline-synced recordings + feat: unsubscribe reason#5947sungdark wants to merge 2 commits intoBasedHardware:mainfrom
Conversation
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 SummaryThis PR adds an unsubscribe reason collection step before subscription cancellation, replacing the simple
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
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
Reviews (1): Last reviewed commit: "feat: Add unsubscribe reason selection d..." | Re-trigger Greptile |
| <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> |
There was a problem hiding this comment.
"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:
- Rename the button to something unambiguous like "Keep Subscription" to clarify that clicking it aborts the cancellation flow, or
- Have "Skip" actually cancel without a reason (call
onSubmitwith a fallback/empty reason).
| <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'; | |||
| if reason: | ||
| logger.info(f"User {uid} canceling subscription. Reason: {reason}. Details: {details}") |
There was a problem hiding this comment.
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.
| 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}") |
| reason: str | None = Query(default=None, description="Cancellation reason"), | ||
| details: str | None = Query(default=None, description="Cancellation details"), |
There was a problem hiding this comment.
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.
| 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
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, theprocess_segmentfunction creates conversations with transcript segments but never createsaudio_filesentries. This causes the frontend to not show an audio player for offline-synced recordings.Solution:
Added
_create_audio_files_for_synced_segmenthelper that:audio_filesentries viacreate_audio_files_from_chunksaudio_filesCalls this helper in
process_segmentafter: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 optionsPlansSheet.tsx: Replaced the simple ConfirmDialog cancel flow with the new UnsubscribeReasonDialogapi.ts: UpdatedcancelSubscription()to accept optionalreasonanddetailsparametersBackend:
payment.py: Updatedcancel_subscription_endpointto acceptreasonanddetailsquery parameters and log themFor Fix 2 (#5906):
Backend:
backend/routers/sync.py: Added_create_audio_files_for_synced_segmenthelper and calls to it inprocess_segment