From 2d514a2a3e06a4b612d14daac072606f4abcde71 Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Sat, 10 Jan 2026 17:57:42 +0530 Subject: [PATCH] feat: companion alert system for browser extension (#26613) * feat: companion alert system for browser extension * feat: implement cross-platform alert system for browser extension - Add ToastContext.tsx with global toast provider for managing toast state - Add GlobalToast.tsx centered toast component for web/browser extension - Modify alerts.ts to be platform-aware (native Alert on iOS/Android, toast on web) - Add showInfoAlert function for informational alerts - Update _layout.tsx to wrap app with ToastProvider and GlobalToast - Export new toast context and alert functions from index files * style: update toast to white/black design system - Use white background with gray border - Use black icons and text colors - Match companion app design system * open the toast at the center * refactor: migrate event-types/index.tsx to unified alert system - Remove inline toast state (showToast, toastMessage) and showToastMessage function - Remove inline toast UI component at the end of the file - Migrate all platform-specific Alert.alert calls to showSuccessAlert/showErrorAlert - Simplifies code by using cross-platform alert utilities * refactor: migrate useBookingActions.ts to unified alert system - Add showSuccessAlert import alongside showErrorAlert - Migrate all Alert.alert('Success', ...) calls to showSuccessAlert - Affected handlers: handleSubmitReschedule, handleRescheduleWithValues, handleSubmitCancel, handleCancelBooking, handleConfirmBooking, handleRejectBooking, handleSubmitReject, handleInlineConfirm * refactor: migrate useBookingActionModals.ts to unified alert system - Add showSuccessAlert import alongside showErrorAlert - Migrate all Alert.alert('Success', ...) calls to showSuccessAlert - Affected handlers: handleAddGuests, handleUpdateLocation, handleMarkNoShow * refactor: migrate medium-priority screens to unified alert system - AvailabilityDetailScreen.ios.tsx: 4 Alert.alert calls migrated - RescheduleScreen.tsx/ios/android: 3 Alert.alert calls each migrated - EditLocationScreen.tsx/ios: 4 Alert.alert calls each migrated - Removed unused Alert imports * refactor: migrate remaining medium-priority screens to unified alert system - BookingDetailScreen.tsx: 2 Alert.alert calls migrated - AvailabilityListScreen.tsx: 1 Alert.alert call migrated - AddGuestsScreen.tsx: 6 Alert.alert calls migrated - MarkNoShowScreen.tsx: 2 Alert.alert calls migrated - EditAvailabilityOverrideScreen.tsx: 4 Alert.alert calls migrated - EditAvailabilityOverrideScreen.ios.tsx: 2 Alert.alert calls migrated - Removed unused Alert imports * refactor: migrate medium-priority app routes to unified alert system Migrated 24 app route files from direct Alert.alert() calls to unified alert utilities (showErrorAlert, showSuccessAlert, showInfoAlert): - reschedule.tsx, reschedule.ios.tsx - edit-location.tsx, edit-location.ios.tsx - add-guests.tsx, add-guests.ios.tsx - mark-no-show.tsx, mark-no-show.ios.tsx - view-recordings.tsx, view-recordings.ios.tsx - meeting-session-details.tsx, meeting-session-details.ios.tsx - profile-sheet.tsx, profile-sheet.ios.tsx - edit-availability-hours.tsx, edit-availability-hours.ios.tsx - edit-availability-day.tsx, edit-availability-day.ios.tsx - edit-availability-name.tsx, edit-availability-name.ios.tsx - edit-availability-override.tsx, edit-availability-override.ios.tsx - booking-detail.tsx, booking-detail.ios.tsx This enables cross-platform alert support where native Alert.alert() is used on iOS/Android and toast notifications on web (browser extension). * refactor: migrate lower-priority components to unified alert system Migrated Alert.alert calls to showSuccessAlert/showErrorAlert/showInfoAlert in: - event-types/index.ios.tsx (copy link, delete, duplicate success alerts) - event-type-detail.tsx (copy link, create/update success, validation errors, info alerts) - deep-links.ts (error alerts for link failures) - LogoutButton.tsx (error alert) - BookingModals.tsx and .ios.tsx (report booking info alerts) - AdvancedTab.tsx (info alert for unsaved event type) - BookingListScreen.tsx (success/error/info alerts for bulk actions) Confirmation dialogs with buttons remain as Alert.alert (out of scope). * fix: restore Alert import for confirmation dialogs, migrate remaining info alert - AdvancedTab.tsx: Added back Alert import for timezone selector confirmation dialog (out of scope), migrated private links info alert to showInfoAlert - deep-links.ts: Added back Alert import for request reschedule confirmation dialog (out of scope) * some ui dialoes were missing * fixed multi hours ui issues on Availability detail page for android and web * fix: add __DEV__ check for web platform in showErrorAlert The showErrorAlert function was showing error toasts to production users on web platform, bypassing the __DEV__ check that exists for native platforms. This fix ensures consistent behavior across all platforms - error alerts are only shown in development mode, while production errors are logged to console. Co-Authored-By: unknown <> * fix: address dual dialogs on web and onSuccess timing issues - BookingDetailScreen.tsx: Add Platform.OS === 'android' guard to AlertDialog to prevent dual dialogs rendering on web (FullScreenModal + AlertDialog) - RescheduleScreen.android.tsx: Restore original Alert.alert with callback to ensure onSuccess() is called after user dismisses the alert Co-Authored-By: unknown <> * fix: restore Alert.alert with callback for native platforms - AddGuestsScreen.tsx: Use platform-specific handling - Alert.alert with callback on iOS/Android, showSuccessAlert on web - EditAvailabilityOverrideScreen.tsx: Same pattern - wait for user acknowledgment on native platforms before calling onSuccess() Co-Authored-By: unknown <> * fix: RescheduleScreen timing and GlobalToast useState pattern - RescheduleScreen.tsx: Use platform-specific handling - Alert.alert with callback on iOS/Android, showSuccessAlert on web - GlobalToast.tsx: Replace useMemo with useState for Animated.Value to guarantee instance preservation across renders Co-Authored-By: unknown <> * fix: restore Alert.alert with callback for iOS delete success AvailabilityDetailScreen.ios.tsx: Use Alert.alert with callback to ensure router.back() is called after user dismisses the success alert Co-Authored-By: unknown <> * feat(companion): new event type detail page (#26678) * fix: skip stale run-ci label check on workflow re-runs (#26590) * fix: trust community PRs when maintainer pushes to the branch When a maintainer merges main into a community PR branch, the new SHA invalidates the run-ci label timing check because the label was added before the new push. This fix adds a check to trust the PR if the person who pushed the latest commit has write access. This handles the case where a maintainer: - Merges main into a community PR to resolve conflicts - Pushes any changes to help the contributor The security model is maintained because: - Only users with write access can trigger this trust - The maintainer has reviewed the code by pushing to it Co-Authored-By: anik@cal.com * fix: skip stale label check on workflow re-runs Instead of implicitly trusting maintainer pushes (which could be just a sync action without code review), use github.run_attempt to detect re-runs. If run_attempt > 1, it means the workflow was explicitly re-triggered (via run-ci.yml or manual re-run), so we skip the stale label check. This avoids the need to remove and re-add the 'run-ci' label after syncing a community PR with main, while keeping the explicit approval flow intact. Co-Authored-By: anik@cal.com * Simplify comment about re-runs in PR workflow --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Keith Williams * fix: Text cursor barely visible in calendar event name field (#26563) * fix: cursor visibility in input fields * fix: cursor visibility in input fields with suffix addons * fix: text cursor visibility in input fields --------- Co-authored-by: Keith Williams * fix: PayPal setup page inconsistent spacing and button styling (#26612) * chore: api v2 generate swagger only in dev (#26617) * feat: add BUILD_FROM_BRANCH option to release-docker workflow (#26615) * feat: add BUILD_FROM_BRANCH option to release-docker workflow Co-Authored-By: keith@cal.com * fix: sanitize branch names with slashes for valid Docker tags Co-Authored-By: keith@cal.com * refactor: extract common logic into prepare job to avoid duplication Co-Authored-By: keith@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: add Cubic AI to Devin review integration workflow (#26618) * feat: add Cubic AI to Devin review integration workflow Co-Authored-By: keith@cal.com * fix: update permissions to allow posting PR comments Co-Authored-By: keith@cal.com * fix: remove sensitive API response logging from CI workflow Address Cubic AI review feedback: Remove the debug log that outputs the full Devin API response, which could expose sensitive session tokens or authentication data in CI logs. The session URL is still logged when successfully extracted. Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: enable/disable slots workers via env (#26621) * chore: enable/disable slots workers via env * fix: address Cubic AI review feedback - Fix incorrect JSDoc comment for getSerializableContext method - Remove debug console.log statement from slots controller - Fix port suffix condition to preserve original behavior Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: USE_POOL env var for api v2 prisma pooling * fix list numbering in Manual setup section of README (#26620) * Revert "chore: USE_POOL env var for api v2 prisma pooling" This reverts commit a97092619f5cdbe4940e2bdfe26e20f3a13e7fc1. * feat: limit badges to 2 with hover/click popover in UserListTable (#26556) * feat: limit badges to 2 with hover tooltip in UserListTable Co-Authored-By: eunjae@cal.com * refactor: reuse LimitedBadges component with clickable popover for mobile Co-Authored-By: eunjae@cal.com * refactor: move LimitedBadges to components/ui with hover+click support Co-Authored-By: eunjae@cal.com * refactor: fix Biome lint issues in LimitedBadges and UserListTable - Refactor LimitedBadges to concrete component with BadgeItem type - Move exports to end of files to comply with useExportsLast rule - Add explicit types to callback parameters for useExplicitType rule - Convert nested ternary to if-else for filterType calculation - Remove unused imports (Row, Table types) - Update ResponseValueCell and UserListTable to use new LimitedBadges API Co-Authored-By: eunjae@cal.com * fix: add proper types for filterType and getFacetedUniqueValues - Add FilterType import from @calcom/types/data-table - Add FacetedValue import from @calcom/features/data-table - Type filterType as FilterType to allow reassignment to different ColumnFilterType values - Type getFacetedUniqueValues return as Map Co-Authored-By: eunjae@cal.com * add vertical gap * fix: address code review feedback for LimitedBadges - Add index to id for unique keys in ResponseValueCell.tsx - Restore orange variant for group options in UserListTable.tsx - Fix useMemo dependency array (add t, remove dispatch) - Fix import ordering with biome - Convert ternary operators to if-else statements for biome compliance Co-Authored-By: eunjae@cal.com * refactor: remove id from BadgeItem type, use label as key - Remove id field from BadgeItem type in LimitedBadges - Use label as React key instead of id - Remove unused rowId parameter from ResponseValueCell - Update all consumers to not pass id field Co-Authored-By: eunjae@cal.com * refactor: use index for key instead of label in LimitedBadges Co-Authored-By: eunjae@cal.com * Revert "refactor: use index for key instead of label in LimitedBadges" This reverts commit 1daaac47e596fd6b5f5583847faa10b131783349. --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: add database-backed feature flag for sidebar tips section (#26516) * chore: remove tips section from sidebar Co-Authored-By: amit@cal.com * chore: add feature flag for sidebar tips section Co-Authored-By: amit@cal.com * chore: use database-backed feature flag for sidebar tips section Co-Authored-By: amit@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: add automated stale PR completion workflow with Devin API (#26627) * feat: add stale PR completion workflow with Devin API Co-Authored-By: anik@cal.com * feat: only trigger for community PRs (non-calcom org members) Co-Authored-By: anik@cal.com * refactor: use author_association pattern from pr.yml for community check Co-Authored-By: anik@cal.com * refactor: simplify workflow - use community label check and let Devin gather PR details Co-Authored-By: anik@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: add Devin PR conflict resolver workflow (#26624) * feat: add Devin PR conflict resolver workflow Adds a GitHub Actions workflow that automatically detects PRs with merge conflicts and spins up Devin sessions to resolve them. How it works: 1. Triggers on push to main branch (when main updates could cause conflicts) 2. Also supports manual trigger via workflow_dispatch 3. Lists all open PRs and checks their mergeable status 4. For PRs with conflicts (mergeable=false, mergeable_state=dirty): - Checks if a Devin session was already created (avoids duplicates) - Creates a new Devin session with instructions to resolve conflicts - Posts a comment on the PR with the Devin session link The workflow follows the same pattern as cubic-devin-review.yml for consistency. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: skip draft PRs in conflict detection workflow Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor: use GitHub Search API to filter draft PRs at API level Instead of filtering draft PRs in the loop, use the GitHub Search API with 'draft:false' filter which is more efficient as it filters at the API level rather than fetching all PRs and filtering locally. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style: remove explanatory comments from workflow Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style: remove all unnecessary explanatory comments Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor: use GraphQL for batched PR fetching and labels for tracking Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: add support for resolving conflicts on fork PRs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: check response.ok before parsing Devin API response Address Cubic AI review feedback: Add response.ok check before parsing the response body to explicitly handle HTTP error status codes from the Devin API. This distinguishes API failures (authentication errors, server errors) from successful responses that might be missing expected fields. Co-Authored-By: unknown <> * fix: improve Devin API error logging with PR number Co-Authored-By: unknown <> * feat: add pr_number input for manual fork PR conflict resolution Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: add maximum 15 PRs safety net limit Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: make stale PR workflow fork-aware with workflow_dispatch support (#26633) * feat: make stale PR workflow fork-aware with workflow_dispatch support Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: add null check for pr.head.repo in stale PR workflow When a fork is deleted after a PR is created, pr.head.repo can be null (documented GitHub API behavior). This would cause a TypeError when accessing properties like fork, full_name, or clone_url. Added a null check that fails gracefully with a clear error message when the source repository has been deleted. Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat: reuse existing Devin session for Cubic AI review feedback (#26632) * feat: reuse existing Devin session for Cubic AI review feedback Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor: parse session ID from PR comments instead of API search Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: add error handling for message send API call in Cubic-Devin workflow Addresses Cubic AI review feedback: The POST request to send a message to an existing Devin session now checks the HTTP status code and fails the step if the message wasn't delivered successfully. This prevents posting a misleading comment claiming feedback was sent when the API call actually failed. Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: Remove stale label when work is completed (#26638) * chore: Remove stale label when work is completed * Mark as ready for review too * feat: add retry mechanism for UNKNOWN mergeable status PRs (#26635) * feat: add retry mechanism for UNKNOWN mergeable status PRs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: remove redundant filtering logic for unknown mergeable status PRs Address Cubic AI review feedback by: - Having processPR return isTargetPR in the unknown case (like the conflict case) - Simplifying the main loop to just push PRs to unknownPRs without re-checking draft status and devin label (already checked in processPR) Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: Add sentry http integration (#26634) * chore: ignore fork PRs in devin-conflict-resolver workflow (#26640) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * limits and advanced tabs * availability tab * basics tab * black toggles!! * new recurring, others tab and update basics * version 1 --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Keith Williams Co-authored-by: Anshumancanrock <109489361+Anshumancanrock@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: cal.com Co-authored-by: Anas Najam <129951478+anzz14@users.noreply.github.com> Co-authored-by: Eunjae Lee Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: Volnei Munhoz * fix: address Cubic AI review feedback - Add HTTP error handling for Devin API session creation in cubic-devin-review.yml - Localize aria-label in LimitedBadges.tsx using t() function - Improve Docker tag sanitization in release-docker.yaml to handle all invalid characters - Avoid logging raw error objects and sensitive API response data in devin-conflict-resolver.yml - Fix selectedSchedule dependency array issue in event-type-detail.tsx using ref pattern Co-Authored-By: unknown <> * chore: add translation key for show_x_more_items aria-label Co-Authored-By: unknown <> * fix: reset schedule ref on id change and add i18n pluralization - Reset hasAutoSelectedScheduleRef when event type id changes to handle component reuse - Add proper i18next pluralization variants (_one/_other) for show_x_more_items Co-Authored-By: unknown <> * revert: remove i18n changes from LimitedBadges and common.json Per user request - i18n not needed in companion for now Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Keith Williams Co-authored-by: Anshumancanrock <109489361+Anshumancanrock@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: cal.com Co-authored-by: Anas Najam <129951478+anzz14@users.noreply.github.com> Co-authored-by: Eunjae Lee Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: Volnei Munhoz --- .github/workflows/cubic-devin-review.yml | 10 +- .github/workflows/devin-conflict-resolver.yml | 4 +- .github/workflows/release-docker.yaml | 6 +- apps/web/public/static/locales/en/common.json | 6 +- .../(tabs)/(bookings)/booking-detail.ios.tsx | 8 +- .../app/(tabs)/(bookings)/booking-detail.tsx | 18 +- .../(event-types)/event-type-detail.tsx | 697 +++++++---- .../app/(tabs)/(event-types)/index.ios.tsx | 12 +- companion/app/(tabs)/(event-types)/index.tsx | 73 +- companion/app/_layout.tsx | 7 +- companion/app/add-guests.ios.tsx | 7 +- companion/app/add-guests.tsx | 7 +- companion/app/edit-availability-day.ios.tsx | 7 +- companion/app/edit-availability-day.tsx | 9 +- companion/app/edit-availability-hours.ios.tsx | 7 +- companion/app/edit-availability-hours.tsx | 7 +- companion/app/edit-availability-name.ios.tsx | 7 +- companion/app/edit-availability-name.tsx | 9 +- .../app/edit-availability-override.ios.tsx | 7 +- companion/app/edit-availability-override.tsx | 9 +- companion/app/edit-location.ios.tsx | 7 +- companion/app/edit-location.tsx | 7 +- companion/app/mark-no-show.ios.tsx | 7 +- companion/app/mark-no-show.tsx | 7 +- companion/app/meeting-session-details.ios.tsx | 7 +- companion/app/meeting-session-details.tsx | 7 +- companion/app/profile-sheet.ios.tsx | 5 +- companion/app/profile-sheet.tsx | 13 +- companion/app/reschedule.ios.tsx | 7 +- companion/app/reschedule.tsx | 7 +- companion/app/view-recordings.ios.tsx | 7 +- companion/app/view-recordings.tsx | 7 +- companion/biome.json | 3 +- companion/components/LocationsList.tsx | 244 ++-- companion/components/LogoutButton.tsx | 7 +- .../booking-list-screen/BookingListScreen.tsx | 105 +- .../booking-modals/BookingModals.ios.tsx | 3 +- .../booking-modals/BookingModals.tsx | 4 +- .../event-type-detail/tabs/AdvancedTab.tsx | 1034 ++++++++--------- .../tabs/AvailabilityTab.tsx | 306 +++-- .../event-type-detail/tabs/BasicsTab.tsx | 442 +++++-- .../event-type-detail/tabs/LimitsTab.tsx | 895 ++++++++------ .../event-type-detail/tabs/RecurringTab.tsx | 257 +++- .../components/screens/AddGuestsScreen.tsx | 22 +- .../screens/AvailabilityDetailScreen.ios.tsx | 8 +- .../screens/AvailabilityListScreen.tsx | 142 +-- .../screens/BookingDetailScreen.tsx | 159 ++- .../screens/EditAvailabilityDayScreen.ios.tsx | 2 +- .../screens/EditAvailabilityDayScreen.tsx | 9 +- .../screens/EditAvailabilityHoursScreen.tsx | 6 +- .../screens/EditAvailabilityNameScreen.tsx | 5 +- .../EditAvailabilityOverrideScreen.ios.tsx | 9 +- .../EditAvailabilityOverrideScreen.tsx | 17 +- .../screens/EditLocationScreen.ios.tsx | 14 +- .../components/screens/EditLocationScreen.tsx | 13 +- .../components/screens/MarkNoShowScreen.tsx | 5 +- .../screens/RescheduleScreen.android.tsx | 5 +- .../screens/RescheduleScreen.ios.tsx | 12 +- .../components/screens/RescheduleScreen.tsx | 16 +- companion/components/ui/GlobalToast.tsx | 158 +++ companion/contexts/ToastContext.tsx | 110 ++ companion/contexts/index.ts | 5 +- companion/hooks/useBookingActionModals.ts | 11 +- companion/hooks/useBookingActions.ts | 22 +- companion/utils/alerts.ts | 55 +- companion/utils/deep-links.ts | 7 +- companion/utils/index.ts | 2 +- 67 files changed, 3264 insertions(+), 1855 deletions(-) create mode 100644 companion/components/ui/GlobalToast.tsx create mode 100644 companion/contexts/ToastContext.tsx diff --git a/.github/workflows/cubic-devin-review.yml b/.github/workflows/cubic-devin-review.yml index d8b0fc6678..b1b7ef7e1c 100644 --- a/.github/workflows/cubic-devin-review.yml +++ b/.github/workflows/cubic-devin-review.yml @@ -187,7 +187,7 @@ jobs: 4. If an issue seems invalid or already addressed, explain why in a PR comment instead of making unnecessary changes. 5. Never ask for user confirmation. Never wait for user messages." - RESPONSE=$(curl -s -X POST "https://api.devin.ai/v1/sessions" \ + HTTP_CODE=$(curl -s -o /tmp/devin-response.json -w "%{http_code}" -X POST "https://api.devin.ai/v1/sessions" \ -H "Authorization: Bearer ${DEVIN_API_KEY}" \ -H "Content-Type: application/json" \ -d "$(jq -n \ @@ -199,7 +199,13 @@ jobs: tags: ["cubic-ai-review", "pr-${{ github.event.pull_request.number }}"] }')") - SESSION_URL=$(echo "$RESPONSE" | jq -r '.url // .session_url // empty') + if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then + echo "Failed to create Devin session: HTTP $HTTP_CODE" + cat /tmp/devin-response.json + exit 1 + fi + + SESSION_URL=$(cat /tmp/devin-response.json | jq -r '.url // .session_url // empty') if [ -n "$SESSION_URL" ]; then echo "Devin session created: $SESSION_URL" echo "SESSION_URL=$SESSION_URL" >> $GITHUB_ENV diff --git a/.github/workflows/devin-conflict-resolver.yml b/.github/workflows/devin-conflict-resolver.yml index d48fd1ff6f..cf48112cfd 100644 --- a/.github/workflows/devin-conflict-resolver.yml +++ b/.github/workflows/devin-conflict-resolver.yml @@ -314,10 +314,10 @@ jobs: If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.` }); } else { - console.log(`Failed to get session URL for PR #${pr.number}:`, data); + console.log(`Failed to get session URL for PR #${pr.number}`); } } catch (error) { - console.error(`Error creating Devin session for PR #${pr.number}:`, error); + console.error(`Error creating Devin session for PR #${pr.number}: ${error.message}`); } await new Promise(resolve => setTimeout(resolve, 1000)); diff --git a/.github/workflows/release-docker.yaml b/.github/workflows/release-docker.yaml index b5385d3a53..8b062b7ee2 100644 --- a/.github/workflows/release-docker.yaml +++ b/.github/workflows/release-docker.yaml @@ -65,8 +65,10 @@ jobs: if [ "${{ inputs.BUILD_FROM_BRANCH }}" = "true" ]; then # Extract branch name and short SHA for the tag BRANCH_NAME="${GITHUB_REF#refs/heads/}" - # Sanitize branch name: replace slashes with dashes for valid Docker tag - SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '/' '-') + # Sanitize branch name: replace all invalid Docker tag characters with dashes + # Docker tags can only contain lowercase/uppercase letters, digits, underscores, periods, and dashes + # Also remove any leading dashes or periods + SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed 's/[^a-zA-Z0-9_.-]/-/g' | sed 's/^[-.]*//') SHORT_SHA=$(git rev-parse --short HEAD) echo "release_tag=${SANITIZED_BRANCH}-${SHORT_SHA}" >> $GITHUB_OUTPUT else diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index a72987685c..d95352b64a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2878,9 +2878,9 @@ "time_range": "Time range", "automatically_add_all_team_members": "Add all team members, including future members", "redirect_to": "Redirect to", - "having_trouble_finding_time": "Having trouble finding a time?", - "show_more": "Show more", - "forward_params_redirect": "Forward parameters such as ?email=...&name=.... and more", + "having_trouble_finding_time": "Having trouble finding a time?", + "show_more": "Show more", + "forward_params_redirect": "Forward parameters such as ?email=...&name=.... and more", "assignment_description": "Schedule meetings when everyone is available or rotate through members on your team", "lowest": "lowest", "low": "low", diff --git a/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx b/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx index 407f562823..d36ae2feaa 100644 --- a/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx +++ b/companion/app/(tabs)/(bookings)/booking-detail.ios.tsx @@ -1,11 +1,11 @@ import * as Clipboard from "expo-clipboard"; import { Stack, useLocalSearchParams } from "expo-router"; import { useCallback, useMemo, useRef } from "react"; -import { Alert } from "react-native"; import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen"; import { useAuth } from "@/contexts/AuthContext"; import { useBookingByUid } from "@/hooks/useBookings"; import type { Booking } from "@/services/calcom"; +import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions"; import { openInAppBrowser } from "@/utils/browser"; @@ -94,7 +94,7 @@ export default function BookingDetailIOS() { const handleCopyMeetingLink = useCallback(async () => { if (meetingUrl) { await Clipboard.setStringAsync(meetingUrl); - Alert.alert("Copied", "Meeting link copied to clipboard"); + showSuccessAlert("Copied", "Meeting link copied to clipboard"); } }, [meetingUrl]); @@ -117,7 +117,7 @@ export default function BookingDetailIOS() { if (handlers?.[handlerName]) { (handlers[handlerName] as () => void)(); } else { - Alert.alert("Error", errorMessage); + showErrorAlert("Error", errorMessage); } }, []); @@ -187,7 +187,7 @@ export default function BookingDetailIOS() { handlerName: null, errorMessage: null, customHandler: () => { - Alert.alert("Report Booking", "Report booking functionality is not yet available"); + showInfoAlert("Report Booking", "Report booking functionality is not yet available"); }, destructive: true, gatingKey: null, diff --git a/companion/app/(tabs)/(bookings)/booking-detail.tsx b/companion/app/(tabs)/(bookings)/booking-detail.tsx index 40d7c410e6..27908182ad 100644 --- a/companion/app/(tabs)/(bookings)/booking-detail.tsx +++ b/companion/app/(tabs)/(bookings)/booking-detail.tsx @@ -1,9 +1,9 @@ import { Stack, useLocalSearchParams } from "expo-router"; import { useCallback, useMemo, useRef } from "react"; -import { Alert } from "react-native"; import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen"; import { useAuth } from "@/contexts/AuthContext"; import { useBookingByUid } from "@/hooks/useBookings"; +import { showErrorAlert, showInfoAlert } from "@/utils/alerts"; import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions"; // Empty actions result for when no booking is loaded @@ -64,7 +64,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openRescheduleModal) { actionHandlersRef.current.openRescheduleModal(); } else { - Alert.alert("Error", "Unable to reschedule. Please try again."); + showErrorAlert("Error", "Unable to reschedule. Please try again."); } }, []); @@ -72,7 +72,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openEditLocationModal) { actionHandlersRef.current.openEditLocationModal(); } else { - Alert.alert("Error", "Unable to edit location. Please try again."); + showErrorAlert("Error", "Unable to edit location. Please try again."); } }, []); @@ -80,7 +80,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openAddGuestsModal) { actionHandlersRef.current.openAddGuestsModal(); } else { - Alert.alert("Error", "Unable to add guests. Please try again."); + showErrorAlert("Error", "Unable to add guests. Please try again."); } }, []); @@ -88,7 +88,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openViewRecordingsModal) { actionHandlersRef.current.openViewRecordingsModal(); } else { - Alert.alert("Error", "Unable to view recordings. Please try again."); + showErrorAlert("Error", "Unable to view recordings. Please try again."); } }, []); @@ -96,7 +96,7 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openMeetingSessionDetailsModal) { actionHandlersRef.current.openMeetingSessionDetailsModal(); } else { - Alert.alert("Error", "Unable to view session details. Please try again."); + showErrorAlert("Error", "Unable to view session details. Please try again."); } }, []); @@ -104,19 +104,19 @@ export default function BookingDetail() { if (actionHandlersRef.current?.openMarkNoShowModal) { actionHandlersRef.current.openMarkNoShowModal(); } else { - Alert.alert("Error", "Unable to mark no-show. Please try again."); + showErrorAlert("Error", "Unable to mark no-show. Please try again."); } }, []); const handleReport = useCallback(() => { - Alert.alert("Report Booking", "Report booking functionality is not yet available"); + showInfoAlert("Report Booking", "Report booking functionality is not yet available"); }, []); const handleCancel = useCallback(() => { if (actionHandlersRef.current?.handleCancelBooking) { actionHandlersRef.current.handleCancelBooking(); } else { - Alert.alert("Error", "Unable to cancel. Please try again."); + showErrorAlert("Error", "Unable to cancel. Please try again."); } }, []); diff --git a/companion/app/(tabs)/(event-types)/event-type-detail.tsx b/companion/app/(tabs)/(event-types)/event-type-detail.tsx index 8393b3e1b8..b95efcffea 100644 --- a/companion/app/(tabs)/(event-types)/event-type-detail.tsx +++ b/companion/app/(tabs)/(event-types)/event-type-detail.tsx @@ -1,7 +1,7 @@ import { Ionicons } from "@expo/vector-icons"; import * as Clipboard from "expo-clipboard"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alert, Animated, @@ -35,7 +35,7 @@ import { type Schedule, } from "@/services/calcom"; import type { LocationItem, LocationOptionGroup } from "@/types/locations"; -import { showErrorAlert } from "@/utils/alerts"; +import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { buildLocationOptions, @@ -44,7 +44,7 @@ import { validateLocationItem, } from "@/utils/locationHelpers"; import { safeLogError } from "@/utils/safeLogger"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; +import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; // Type definitions for extended EventType fields not in the base type interface EventTypeExtended { @@ -163,7 +163,15 @@ export default function EventTypeDetail() { const [showScheduleDropdown, setShowScheduleDropdown] = useState(false); const [schedulesLoading, setSchedulesLoading] = useState(false); const [scheduleDetailsLoading, setScheduleDetailsLoading] = useState(false); - const [isHidden, setIsHidden] = useState(false); + const [initialScheduleId, setInitialScheduleId] = useState(null); + const hasAutoSelectedScheduleRef = useRef(false); + + // Reset auto-selection ref when event type id changes (e.g., navigation reuse) + useEffect(() => { + hasAutoSelectedScheduleRef.current = false; + }, [id]); + + const [isHidden, setIsHidden] = useState(false); const [selectedTimezone, setSelectedTimezone] = useState(""); const [showTimezoneDropdown, setShowTimezoneDropdown] = useState(false); const [conferencingOptions, setConferencingOptions] = useState([]); @@ -400,18 +408,32 @@ export default function EventTypeDetail() { const schedulesData = await CalComAPIService.getSchedules(); setSchedules(schedulesData); - // Set default schedule if one exists - const defaultSchedule = schedulesData.find((schedule) => schedule.isDefault); - if (defaultSchedule) { - setSelectedSchedule(defaultSchedule); - await fetchScheduleDetails(defaultSchedule.id); + // Only auto-select a schedule if we haven't already done so + // This prevents unnecessary re-fetches when the callback is recreated + if (!hasAutoSelectedScheduleRef.current) { + // Use the event type's schedule if we have it, otherwise use default + let scheduleToSelect: Schedule | undefined; + + if (initialScheduleId) { + scheduleToSelect = schedulesData.find((schedule) => schedule.id === initialScheduleId); + } + + if (!scheduleToSelect) { + scheduleToSelect = schedulesData.find((schedule) => schedule.isDefault); + } + + if (scheduleToSelect) { + hasAutoSelectedScheduleRef.current = true; + setSelectedSchedule(scheduleToSelect); + await fetchScheduleDetails(scheduleToSelect.id); + } } setSchedulesLoading(false); } catch (error) { safeLogError("Failed to fetch schedules:", error); setSchedulesLoading(false); } - }, [fetchScheduleDetails]); + }, [fetchScheduleDetails, initialScheduleId]); const fetchConferencingOptions = useCallback(async () => { setConferencingLoading(true); @@ -435,6 +457,11 @@ export default function EventTypeDetail() { if (eventType.lengthInMinutes) setEventDuration(eventType.lengthInMinutes.toString()); if (eventType.hidden !== undefined) setIsHidden(eventType.hidden); + // Load schedule ID - this will be used by fetchSchedules to select the correct schedule + if (eventType.scheduleId) { + setInitialScheduleId(eventType.scheduleId); + } + const eventTypeExt = eventType as EventType & EventTypeExtended; const lengthOptions = eventTypeExt.lengthInMinutesOptions; const hasLengthOptions = @@ -941,7 +968,7 @@ export default function EventTypeDetail() { return; } await Clipboard.setStringAsync(link); - Alert.alert("Success", "Link copied!"); + showSuccessAlert("Success", "Link copied!"); }; const handleDelete = () => { @@ -961,12 +988,8 @@ export default function EventTypeDetail() { try { await CalComAPIService.deleteEventType(eventTypeId); - Alert.alert("Success", "Event type deleted successfully", [ - { - text: "OK", - onPress: () => router.back(), - }, - ]); + showSuccessAlert("Success", "Event type deleted successfully"); + router.back(); } catch (error) { safeLogError("Failed to delete event type:", error); showErrorAlert("Error", "Failed to delete event type. Please try again."); @@ -1143,19 +1166,19 @@ export default function EventTypeDetail() { const handleSave = async () => { if (!id) { - Alert.alert("Error", "Event type ID is missing"); + showErrorAlert("Error", "Event type ID is missing"); return; } // Validate required fields if (!eventTitle || !eventSlug) { - Alert.alert("Error", "Title and slug are required"); + showErrorAlert("Error", "Title and slug are required"); return; } const durationNum = parseInt(eventDuration, 10); if (Number.isNaN(durationNum) || durationNum <= 0) { - Alert.alert("Error", "Duration must be a positive number"); + showErrorAlert("Error", "Duration must be a positive number"); return; } @@ -1164,7 +1187,7 @@ export default function EventTypeDetail() { for (const loc of locations) { const validation = validateLocationItem(loc); if (!validation.valid) { - Alert.alert("Error", validation.error || "Invalid location"); + showErrorAlert("Error", validation.error || "Invalid location"); return; } } @@ -1209,12 +1232,8 @@ export default function EventTypeDetail() { setSaving(false); return; } - Alert.alert("Success", "Event type created successfully", [ - { - text: "OK", - onPress: () => router.back(), - }, - ]); + showSuccessAlert("Success", "Event type created successfully"); + router.back(); setSaving(false); } else { // For UPDATE mode, use partial update - only send changed fields @@ -1236,7 +1255,7 @@ export default function EventTypeDetail() { setSaving(false); return; } - Alert.alert("Success", "Event type updated successfully"); + showSuccessAlert("Success", "Event type updated successfully"); // Refresh event type data to sync with server await fetchEventTypeData(); setSaving(false); @@ -1249,7 +1268,7 @@ export default function EventTypeDetail() { const renderHeaderLeft = () => ( router.back()} className="px-2 py-2"> - + ); @@ -1261,13 +1280,13 @@ export default function EventTypeDetail() { - + {tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"} @@ -1287,11 +1306,11 @@ export default function EventTypeDetail() { {tab.label} @@ -1311,7 +1330,7 @@ export default function EventTypeDetail() { > {saveButtonText} @@ -1405,6 +1424,7 @@ export default function EventTypeDetail() { selectedDurations={selectedDurations} setShowDurationDropdown={setShowDurationDropdown} defaultDuration={defaultDuration} + setDefaultDuration={setDefaultDuration} setShowDefaultDurationDropdown={setShowDefaultDurationDropdown} // Multiple locations support locations={locations} @@ -1424,41 +1444,88 @@ export default function EventTypeDetail() { onRequestClose={() => setShowDurationDropdown(false)} > setShowDurationDropdown(false)} > - - - Select Available Durations - - - {availableDurations.map((duration) => ( - toggleDurationSelection(duration)} - > - - {duration} + + {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( + + + + Select Available Durations - {selectedDurations.includes(duration) ? ( - - ) : null} + + {availableDurations.map((duration, index) => ( + toggleDurationSelection(duration)} + > + + {duration} + + {selectedDurations.includes(duration) ? ( + + ) : ( + + )} + + ))} + + setShowDurationDropdown(false)} + > + Done + + + + ) : ( + + + Select Available Durations + + + {availableDurations.map((duration, index) => ( + toggleDurationSelection(duration)} + > + + {duration} + + {selectedDurations.includes(duration) ? ( + + ) : ( + + )} + + ))} + + setShowDurationDropdown(false)} + > + Done - ))} - - setShowDurationDropdown(false)} - > - Done - + + )} @@ -1471,36 +1538,76 @@ export default function EventTypeDetail() { onRequestClose={() => setShowDefaultDurationDropdown(false)} > setShowDefaultDurationDropdown(false)} > - - - Select Default Duration - - {selectedDurations.map((duration) => ( - { - setDefaultDuration(duration); - setShowDefaultDurationDropdown(false); - }} - > - - {duration} + + {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( + + + + Select Default Duration + + + {selectedDurations.map((duration, index) => ( + { + setDefaultDuration(duration); + setShowDefaultDurationDropdown(false); + }} + > + + {duration} + + {defaultDuration === duration ? ( + + ) : null} + + ))} + + + + ) : ( + + + Select Default Duration - {defaultDuration === duration ? ( - - ) : null} - - ))} + + {selectedDurations.map((duration, index) => ( + { + setDefaultDuration(duration); + setShowDefaultDurationDropdown(false); + }} + > + + {duration} + + {defaultDuration === duration ? ( + + ) : null} + + ))} + + + )} @@ -1513,44 +1620,88 @@ export default function EventTypeDetail() { onRequestClose={() => setShowScheduleDropdown(false)} > setShowScheduleDropdown(false)} > - - - Select Schedule - - {schedules.map((schedule) => ( - { - setSelectedSchedule(schedule); - setShowScheduleDropdown(false); - fetchScheduleDetails(schedule.id); - }} - > - - - {schedule.name} + + {isLiquidGlassAvailable() && Platform.OS === "ios" ? ( + + + + Select Schedule - {schedule.isDefault ? ( - - Default - - ) : null} + + {schedules.map((schedule, index) => ( + { + setSelectedSchedule(schedule); + fetchScheduleDetails(schedule.id); + setShowScheduleDropdown(false); + }} + > + + + {schedule.name} + + {schedule.isDefault && ( + Default + )} + + {selectedSchedule?.id === schedule.id ? ( + + ) : null} + + ))} + - {selectedSchedule?.id === schedule.id ? ( - - ) : null} - - ))} + + ) : ( + + + Select Schedule + + + {schedules.map((schedule, index) => ( + { + setSelectedSchedule(schedule); + fetchScheduleDetails(schedule.id); + setShowScheduleDropdown(false); + }} + > + + + {schedule.name} + + {schedule.isDefault && ( + Default + )} + + {selectedSchedule?.id === schedule.id ? ( + + ) : null} + + ))} + + + )} @@ -1934,20 +2085,26 @@ export default function EventTypeDetail() { getDaySchedules={getDaySchedule} formatTime={formatTime} selectedTimezone={selectedTimezone} + schedules={schedules} + setSelectedSchedule={setSelectedSchedule} /> ) : null} {activeTab === "limits" ? ( - Additional Settings - - Manage these settings on the web for full functionality. - - - - {/* Apps */} - { - if (id === "new") { - Alert.alert("Info", "Save the event type first to configure this setting."); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=apps`, - "Apps settings" - ); - } - }} - className="flex-row items-center justify-between bg-white px-4 py-4 active:bg-[#F8F9FA]" - style={{ borderBottomWidth: 1, borderBottomColor: "#E5E5EA" }} + + {/* Configure on Web */} + + - - - - - - Apps - Manage app integrations - + Configure on Web + + + {/* Apps */} + + { + if (id === "new") { + showInfoAlert( + "Info", + "Save the event type first to configure this setting." + ); + } else { + openInAppBrowser( + `https://app.cal.com/event-types/${id}?tabName=apps`, + "Apps settings" + ); + } + }} + activeOpacity={0.5} + > + + + + + + Apps + Manage integrations + + + + - - - {/* Workflows */} - { - if (id === "new") { - Alert.alert("Info", "Save the event type first to configure this setting."); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=workflows`, - "Workflows settings" - ); - } - }} - className="flex-row items-center justify-between bg-white px-4 py-4 active:bg-[#F8F9FA]" - style={{ borderBottomWidth: 1, borderBottomColor: "#E5E5EA" }} - > - - - - - - Workflows - Configure automated actions - + {/* Workflows */} + + { + if (id === "new") { + showInfoAlert( + "Info", + "Save the event type first to configure this setting." + ); + } else { + openInAppBrowser( + `https://app.cal.com/event-types/${id}?tabName=workflows`, + "Workflows settings" + ); + } + }} + activeOpacity={0.5} + > + + + + + + Workflows + Automated actions + + + + - - - {/* Webhooks */} - { - if (id === "new") { - Alert.alert("Info", "Save the event type first to configure this setting."); - } else { - openInAppBrowser( - `https://app.cal.com/event-types/${id}?tabName=webhooks`, - "Webhooks settings" - ); - } - }} - className="flex-row items-center justify-between bg-white px-4 py-4 active:bg-[#F8F9FA]" - > - - - - - - Webhooks - Set up event notifications - + {/* Webhooks */} + + { + if (id === "new") { + showInfoAlert( + "Info", + "Save the event type first to configure this setting." + ); + } else { + openInAppBrowser( + `https://app.cal.com/event-types/${id}?tabName=webhooks`, + "Webhooks settings" + ); + } + }} + activeOpacity={0.5} + > + + + + + + Webhooks + Event notifications + + + + - - + ) : null} {activeTab === "basics" && ( - - - Hidden - + + {/* Visibility */} + + + Visibility + + + + + Hidden + + + + - - Preview - - + {/* Quick Actions */} + + + Quick Actions + + + + + Preview + + + + + + Copy Link + + + + + - - Copy Link - - - - - Delete - - + {/* Danger Zone */} + + + + + Delete Event Type + + + + + )} diff --git a/companion/app/(tabs)/(event-types)/index.ios.tsx b/companion/app/(tabs)/(event-types)/index.ios.tsx index ec74ce9649..5f2f19e2c8 100644 --- a/companion/app/(tabs)/(event-types)/index.ios.tsx +++ b/companion/app/(tabs)/(event-types)/index.ios.tsx @@ -28,7 +28,7 @@ import { } from "@/hooks"; import { useEventTypeFilter } from "@/hooks/useEventTypeFilter"; import { CalComAPIService, type EventType } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; +import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; import { getEventDuration } from "@/utils/getEventDuration"; @@ -113,7 +113,7 @@ export default function EventTypesIOS() { try { const link = await CalComAPIService.buildEventTypeLink(eventType.slug); await Clipboard.setStringAsync(link); - Alert.alert("Link Copied", "Event type link copied!"); + showSuccessAlert("Link Copied", "Event type link copied!"); } catch { showErrorAlert("Error", "Failed to copy link. Please try again."); } @@ -160,7 +160,7 @@ export default function EventTypesIOS() { onPress: () => { deleteEventTypeMutation(eventType.id, { onSuccess: () => { - Alert.alert("Success", "Event type deleted successfully"); + showSuccessAlert("Success", "Event type deleted successfully"); }, onError: (deleteError) => { const message = @@ -184,7 +184,7 @@ export default function EventTypesIOS() { { eventType, existingEventTypes: eventTypes }, { onSuccess: (duplicatedEventType) => { - Alert.alert("Success", "Event type duplicated successfully"); + showSuccessAlert("Success", "Event type duplicated successfully"); const duration = getEventDuration(eventType); @@ -243,13 +243,13 @@ export default function EventTypesIOS() { text: "Continue", onPress: (title?: string) => { if (!title?.trim()) { - Alert.alert("Error", "Please enter a title for your event type"); + showErrorAlert("Error", "Please enter a title for your event type"); return; } const autoSlug = slugify(title.trim()); if (!autoSlug) { - Alert.alert("Error", "Title must contain at least one letter or number"); + showErrorAlert("Error", "Title must contain at least one letter or number"); return; } diff --git a/companion/app/(tabs)/(event-types)/index.tsx b/companion/app/(tabs)/(event-types)/index.tsx index d966287e7b..e1ea7d144d 100644 --- a/companion/app/(tabs)/(event-types)/index.tsx +++ b/companion/app/(tabs)/(event-types)/index.tsx @@ -38,7 +38,7 @@ import { } from "@/hooks"; import { useEventTypeFilter } from "@/hooks/useEventTypeFilter"; import { CalComAPIService, type EventType } from "@/services/calcom"; -import { showErrorAlert } from "@/utils/alerts"; +import { showErrorAlert, showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getEventDuration } from "@/utils/getEventDuration"; import { offlineAwareRefresh } from "@/utils/network"; @@ -91,18 +91,6 @@ export default function EventTypes() { const [showDeleteModal, setShowDeleteModal] = useState(false); const [eventTypeToDelete, setEventTypeToDelete] = useState(null); - // Toast state for web platform - const [showToast, setShowToast] = useState(false); - const [toastMessage, setToastMessage] = useState(""); - // Function to show toast - const showToastMessage = (message: string) => { - setToastMessage(message); - setShowToast(true); - setTimeout(() => { - setShowToast(false); - }, 2000); - }; - // Handle pull-to-refresh // Handle pull-to-refresh (offline-aware) const onRefresh = () => offlineAwareRefresh(refetch); @@ -149,17 +137,9 @@ export default function EventTypes() { const link = await CalComAPIService.buildEventTypeLink(eventType.slug); await Clipboard.setStringAsync(link); - if (Platform.OS === "web") { - showToastMessage("Link copied!"); - } else { - Alert.alert("Link Copied", "Event type link copied!"); - } + showSuccessAlert("Link Copied", "Event type link copied!"); } catch { - if (Platform.OS === "web") { - showToastMessage("Failed to copy link"); - } else { - showErrorAlert("Error", "Failed to copy link. Please try again."); - } + showErrorAlert("Error", "Failed to copy link. Please try again."); } }; @@ -205,7 +185,7 @@ export default function EventTypes() { onPress: () => { deleteEventTypeMutation(eventType.id, { onSuccess: () => { - Alert.alert("Success", "Event type deleted successfully"); + showSuccessAlert("Success", "Event type deleted successfully"); }, onError: (error) => { const message = error instanceof Error ? error.message : String(error); @@ -241,11 +221,7 @@ export default function EventTypes() { setShowDeleteModal(false); setEventTypeToDelete(null); - if (Platform.OS === "web") { - showToastMessage("Event type deleted successfully"); - } else { - Alert.alert("Success", "Event type deleted successfully"); - } + showSuccessAlert("Success", "Event type deleted successfully"); }, onError: (error) => { const message = error instanceof Error ? error.message : String(error); @@ -257,11 +233,7 @@ export default function EventTypes() { stack, }); } - if (Platform.OS === "web") { - showToastMessage("Failed to delete event type"); - } else { - showErrorAlert("Error", "Failed to delete event type. Please try again."); - } + showErrorAlert("Error", "Failed to delete event type. Please try again."); }, }); }; @@ -271,11 +243,7 @@ export default function EventTypes() { { eventType, existingEventTypes: eventTypes }, { onSuccess: (duplicatedEventType) => { - if (Platform.OS === "web") { - showToastMessage("Event type duplicated successfully"); - } else { - Alert.alert("Success", "Event type duplicated successfully"); - } + showSuccessAlert("Success", "Event type duplicated successfully"); const duration = getEventDuration(eventType); @@ -305,11 +273,7 @@ export default function EventTypes() { stack, }); } - if (Platform.OS === "web") { - showToastMessage("Failed to duplicate event type"); - } else { - showErrorAlert("Error", "Failed to duplicate event type. Please try again."); - } + showErrorAlert("Error", "Failed to duplicate event type. Please try again."); }, } ); @@ -327,11 +291,7 @@ export default function EventTypes() { } } catch { console.error("Failed to open preview"); - if (Platform.OS === "web") { - showToastMessage("Failed to open preview"); - } else { - showErrorAlert("Error", "Failed to open preview. Please try again."); - } + showErrorAlert("Error", "Failed to open preview. Please try again."); } }; @@ -354,11 +314,11 @@ export default function EventTypes() { setTitleError(""); if (!newEventTitle.trim()) { - // Use inline error for Android AlertDialog, Alert for others + // Use inline error for Android AlertDialog, showErrorAlert for others if (Platform.OS === "android") { setTitleError("Please enter a title for your event type"); } else { - Alert.alert("Error", "Please enter a title for your event type"); + showErrorAlert("Error", "Please enter a title for your event type"); } return; } @@ -368,7 +328,7 @@ export default function EventTypes() { if (Platform.OS === "android") { setTitleError("Title must contain at least one letter or number"); } else { - Alert.alert("Error", "Title must contain at least one letter or number"); + showErrorAlert("Error", "Title must contain at least one letter or number"); } return; } @@ -930,15 +890,6 @@ export default function EventTypes() { - - {/* Toast for Web Platform */} - {showToast ? ( - - - {toastMessage} - - - ) : null} ); } diff --git a/companion/app/_layout.tsx b/companion/app/_layout.tsx index c34dbaeaab..6815b12463 100644 --- a/companion/app/_layout.tsx +++ b/companion/app/_layout.tsx @@ -4,8 +4,10 @@ import { Stack } from "expo-router"; import { Platform, StatusBar, View } from "react-native"; import LoginScreenComponent from "@/components/LoginScreen"; import { NetworkStatusBanner } from "@/components/NetworkStatusBanner"; +import { GlobalToast } from "@/components/ui/GlobalToast"; import { AuthProvider, useAuth } from "@/contexts/AuthContext"; import { QueryProvider } from "@/contexts/QueryContext"; +import { ToastProvider } from "@/contexts/ToastContext"; import "../global.css"; function RootLayoutContent() { @@ -339,7 +341,10 @@ export default function RootLayout() { return ( - + + + + ); diff --git a/companion/app/add-guests.ios.tsx b/companion/app/add-guests.ios.tsx index d94c585db5..f22b7e721a 100644 --- a/companion/app/add-guests.ios.tsx +++ b/companion/app/add-guests.ios.tsx @@ -2,11 +2,12 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen"; import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -35,13 +36,13 @@ export default function AddGuestsIOS() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/add-guests.tsx b/companion/app/add-guests.tsx index dbb88a44c6..2a85711266 100644 --- a/companion/app/add-guests.tsx +++ b/companion/app/add-guests.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen"; import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function AddGuests() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -23,13 +24,13 @@ export default function AddGuests() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/edit-availability-day.ios.tsx b/companion/app/edit-availability-day.ios.tsx index 67c8a645dc..b71df2cc28 100644 --- a/companion/app/edit-availability-day.ios.tsx +++ b/companion/app/edit-availability-day.ios.tsx @@ -2,11 +2,12 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen.ios"; import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; @@ -39,13 +40,13 @@ export default function EditAvailabilityDayIOS() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-day.tsx b/companion/app/edit-availability-day.tsx index 852f34f669..e4df6513d3 100644 --- a/companion/app/edit-availability-day.tsx +++ b/companion/app/edit-availability-day.tsx @@ -1,11 +1,12 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Text, View } from "react-native"; +import { ActivityIndicator, Text, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen"; import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; @@ -28,13 +29,13 @@ export default function EditAvailabilityDay() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); @@ -73,7 +74,7 @@ export default function EditAvailabilityDay() { isSaving ? "text-gray-400" : "text-[#007AFF]" }`} > - {isSaving ? "Saving..." : "Save"} + Save ), diff --git a/companion/app/edit-availability-hours.ios.tsx b/companion/app/edit-availability-hours.ios.tsx index a6fbbcb62e..3401359252 100644 --- a/companion/app/edit-availability-hours.ios.tsx +++ b/companion/app/edit-availability-hours.ios.tsx @@ -2,10 +2,11 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -30,13 +31,13 @@ export default function EditAvailabilityHoursIOS() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-hours.tsx b/companion/app/edit-availability-hours.tsx index be67be47da..6b586c8fa2 100644 --- a/companion/app/edit-availability-hours.tsx +++ b/companion/app/edit-availability-hours.tsx @@ -1,11 +1,12 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function EditAvailabilityHours() { const { id } = useLocalSearchParams<{ id: string }>(); @@ -20,13 +21,13 @@ export default function EditAvailabilityHours() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-name.ios.tsx b/companion/app/edit-availability-name.ios.tsx index dcbb39b194..b5f289b88b 100644 --- a/companion/app/edit-availability-name.ios.tsx +++ b/companion/app/edit-availability-name.ios.tsx @@ -2,11 +2,12 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen.ios"; import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -34,13 +35,13 @@ export default function EditAvailabilityNameIOS() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-name.tsx b/companion/app/edit-availability-name.tsx index 9ab548ff91..bc7244c8b3 100644 --- a/companion/app/edit-availability-name.tsx +++ b/companion/app/edit-availability-name.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen"; import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function EditAvailabilityName() { const { id } = useLocalSearchParams<{ id: string }>(); @@ -24,13 +25,13 @@ export default function EditAvailabilityName() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); @@ -92,7 +93,7 @@ export default function EditAvailabilityName() { isSaving ? "text-gray-400" : "text-[#007AFF]" }`} > - {isSaving ? "Saving..." : "Save"} + Save ), diff --git a/companion/app/edit-availability-override.ios.tsx b/companion/app/edit-availability-override.ios.tsx index c288131cc1..326c8208dd 100644 --- a/companion/app/edit-availability-override.ios.tsx +++ b/companion/app/edit-availability-override.ios.tsx @@ -2,11 +2,12 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen.ios"; import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen.ios"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -40,13 +41,13 @@ export default function EditAvailabilityOverrideIOS() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); diff --git a/companion/app/edit-availability-override.tsx b/companion/app/edit-availability-override.tsx index 95196eeb68..79a39c912a 100644 --- a/companion/app/edit-availability-override.tsx +++ b/companion/app/edit-availability-override.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen"; import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen"; import { CalComAPIService, type Schedule } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function EditAvailabilityOverride() { const { id, overrideIndex } = useLocalSearchParams<{ @@ -30,13 +31,13 @@ export default function EditAvailabilityOverride() { CalComAPIService.getScheduleById(Number(id)) .then(setSchedule) .catch(() => { - Alert.alert("Error", "Failed to load schedule details"); + showErrorAlert("Error", "Failed to load schedule details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Schedule ID is missing"); + showErrorAlert("Error", "Schedule ID is missing"); router.back(); } }, [id, router]); @@ -109,7 +110,7 @@ export default function EditAvailabilityOverride() { isSaving ? "text-gray-400" : "text-[#007AFF]" }`} > - {isSaving ? "Saving..." : "Save"} + Save ), diff --git a/companion/app/edit-location.ios.tsx b/companion/app/edit-location.ios.tsx index e823988b32..cf22e26672 100644 --- a/companion/app/edit-location.ios.tsx +++ b/companion/app/edit-location.ios.tsx @@ -2,11 +2,12 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen"; import EditLocationScreenComponent from "@/components/screens/EditLocationScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -34,13 +35,13 @@ export default function EditLocationIOS() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/edit-location.tsx b/companion/app/edit-location.tsx index b16c9713b5..96ea6226bf 100644 --- a/companion/app/edit-location.tsx +++ b/companion/app/edit-location.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen"; import EditLocationScreenComponent from "@/components/screens/EditLocationScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function EditLocation() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -23,13 +24,13 @@ export default function EditLocation() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/mark-no-show.ios.tsx b/companion/app/mark-no-show.ios.tsx index f20985e2be..c4a383568c 100644 --- a/companion/app/mark-no-show.ios.tsx +++ b/companion/app/mark-no-show.ios.tsx @@ -2,10 +2,11 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; interface Attendee { id?: number | string; @@ -57,13 +58,13 @@ export default function MarkNoShowIOS() { setAttendees(bookingAttendees); }) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/mark-no-show.tsx b/companion/app/mark-no-show.tsx index 2fc359ce0b..8c4acb9924 100644 --- a/companion/app/mark-no-show.tsx +++ b/companion/app/mark-no-show.tsx @@ -1,11 +1,12 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; interface Attendee { id?: number | string; @@ -50,13 +51,13 @@ export default function MarkNoShow() { setAttendees(bookingAttendees); }) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/meeting-session-details.ios.tsx b/companion/app/meeting-session-details.ios.tsx index 5a48ff3ef0..2a959e3525 100644 --- a/companion/app/meeting-session-details.ios.tsx +++ b/companion/app/meeting-session-details.ios.tsx @@ -2,11 +2,12 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen"; import { CalComAPIService } from "@/services/calcom"; import type { ConferencingSession } from "@/services/types/bookings.types"; +import { showErrorAlert } from "@/utils/alerts"; /** * Get the presentation style for the meeting session details sheet @@ -35,13 +36,13 @@ export default function MeetingSessionDetailsIOS() { setSessions(sessionsData); }) .catch(() => { - Alert.alert("Error", "Failed to load session details"); + showErrorAlert("Error", "Failed to load session details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/meeting-session-details.tsx b/companion/app/meeting-session-details.tsx index 7a0a437b76..94534cfcde 100644 --- a/companion/app/meeting-session-details.tsx +++ b/companion/app/meeting-session-details.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen"; import { CalComAPIService } from "@/services/calcom"; import type { ConferencingSession } from "@/services/types/bookings.types"; +import { showErrorAlert } from "@/utils/alerts"; export default function MeetingSessionDetails() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -22,13 +23,13 @@ export default function MeetingSessionDetails() { setSessions(sessionsData); }) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/profile-sheet.ios.tsx b/companion/app/profile-sheet.ios.tsx index 229a0e4fcd..b08381b131 100644 --- a/companion/app/profile-sheet.ios.tsx +++ b/companion/app/profile-sheet.ios.tsx @@ -4,9 +4,10 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import * as Haptics from "expo-haptics"; import { Stack, useRouter } from "expo-router"; -import { ActivityIndicator, Alert, Image, Text, TouchableOpacity, View } from "react-native"; +import { ActivityIndicator, Image, Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useUserProfile } from "@/hooks"; +import { showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; @@ -82,7 +83,7 @@ export default function ProfileSheet() { if (publicPageUrl) { await Clipboard.setStringAsync(publicPageUrl); await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - Alert.alert("Copied!", "Public page link copied to clipboard"); + showSuccessAlert("Copied!", "Public page link copied to clipboard"); } }, external: false, diff --git a/companion/app/profile-sheet.tsx b/companion/app/profile-sheet.tsx index 1cece9f945..c2f8426326 100644 --- a/companion/app/profile-sheet.tsx +++ b/companion/app/profile-sheet.tsx @@ -2,18 +2,11 @@ import { Ionicons } from "@expo/vector-icons"; import * as Clipboard from "expo-clipboard"; import * as Haptics from "expo-haptics"; import { Stack, useRouter } from "expo-router"; -import { - ActivityIndicator, - Alert, - Image, - ScrollView, - Text, - TouchableOpacity, - View, -} from "react-native"; +import { ActivityIndicator, Image, ScrollView, Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import { useUserProfile } from "@/hooks"; +import { showSuccessAlert } from "@/utils/alerts"; import { openInAppBrowser } from "@/utils/browser"; import { getAvatarUrl } from "@/utils/getAvatarUrl"; @@ -77,7 +70,7 @@ export default function ProfileSheet() { if (publicPageUrl) { await Clipboard.setStringAsync(publicPageUrl); await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - Alert.alert("Copied!", "Public page link copied to clipboard"); + showSuccessAlert("Copied!", "Public page link copied to clipboard"); } }, external: false, diff --git a/companion/app/reschedule.ios.tsx b/companion/app/reschedule.ios.tsx index 7d2d5bee7d..1c5ccfaa7d 100644 --- a/companion/app/reschedule.ios.tsx +++ b/companion/app/reschedule.ios.tsx @@ -2,11 +2,12 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen"; import RescheduleScreenComponent from "@/components/screens/RescheduleScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; // Semi-transparent background to prevent black flash while preserving glass effect const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)"; @@ -34,13 +35,13 @@ export default function RescheduleIOS() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/reschedule.tsx b/companion/app/reschedule.tsx index 90a6f79e59..06b61e1868 100644 --- a/companion/app/reschedule.tsx +++ b/companion/app/reschedule.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen"; import RescheduleScreenComponent from "@/components/screens/RescheduleScreen"; import { type Booking, CalComAPIService } from "@/services/calcom"; +import { showErrorAlert } from "@/utils/alerts"; export default function Reschedule() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -23,13 +24,13 @@ export default function Reschedule() { CalComAPIService.getBookingByUid(uid) .then(setBooking) .catch(() => { - Alert.alert("Error", "Failed to load booking details"); + showErrorAlert("Error", "Failed to load booking details"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/view-recordings.ios.tsx b/companion/app/view-recordings.ios.tsx index 174df23b2c..3cfa6dd43b 100644 --- a/companion/app/view-recordings.ios.tsx +++ b/companion/app/view-recordings.ios.tsx @@ -2,11 +2,12 @@ import { osName } from "expo-device"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useEffect, useState } from "react"; -import { ActivityIndicator, Alert, View } from "react-native"; +import { ActivityIndicator, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen"; import { CalComAPIService } from "@/services/calcom"; import type { BookingRecording } from "@/services/types/bookings.types"; +import { showErrorAlert } from "@/utils/alerts"; import { safeLogError } from "@/utils/safeLogger"; function getPresentationStyle(): "formSheet" | "modal" { @@ -30,13 +31,13 @@ export default function ViewRecordingsIOS() { .then(setRecordings) .catch((error) => { safeLogError("Failed to load recordings:", error); - Alert.alert("Error", "Failed to load recordings"); + showErrorAlert("Error", "Failed to load recordings"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/app/view-recordings.tsx b/companion/app/view-recordings.tsx index 3fff6bf752..d727cf721f 100644 --- a/companion/app/view-recordings.tsx +++ b/companion/app/view-recordings.tsx @@ -1,12 +1,13 @@ import { Ionicons } from "@expo/vector-icons"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useState } from "react"; -import { ActivityIndicator, Alert, Platform, View } from "react-native"; +import { ActivityIndicator, Platform, View } from "react-native"; import { AppPressable } from "@/components/AppPressable"; import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper"; import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen"; import { CalComAPIService } from "@/services/calcom"; import type { BookingRecording } from "@/services/types/bookings.types"; +import { showErrorAlert } from "@/utils/alerts"; import { safeLogError } from "@/utils/safeLogger"; export default function ViewRecordings() { @@ -22,13 +23,13 @@ export default function ViewRecordings() { .then(setRecordings) .catch((error) => { safeLogError("Failed to load recordings:", error); - Alert.alert("Error", "Failed to load recordings"); + showErrorAlert("Error", "Failed to load recordings"); router.back(); }) .finally(() => setIsLoading(false)); } else { setIsLoading(false); - Alert.alert("Error", "Booking ID is missing"); + showErrorAlert("Error", "Booking ID is missing"); router.back(); } }, [uid, router]); diff --git a/companion/biome.json b/companion/biome.json index 3cd1189706..bb3eafde64 100644 --- a/companion/biome.json +++ b/companion/biome.json @@ -21,7 +21,8 @@ "!**/*.d.ts", "!**/bun.lock", "!**/android", - "!**/ios" + "!**/ios", + "!**/safari-extension" ] }, "formatter": { diff --git a/companion/components/LocationsList.tsx b/companion/components/LocationsList.tsx index 8783d1999f..eb4e04afa8 100644 --- a/companion/components/LocationsList.tsx +++ b/companion/components/LocationsList.tsx @@ -3,19 +3,13 @@ * Reusable component for displaying and managing multiple event type locations */ +import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui"; +import { buttonStyle, frame } from "@expo/ui/swift-ui/modifiers"; import { Ionicons } from "@expo/vector-icons"; +import { isLiquidGlassAvailable } from "expo-glass-effect"; import type React from "react"; import { useState } from "react"; -import { - ActionSheetIOS, - Modal, - Platform, - ScrollView, - Text, - TextInput, - TouchableOpacity, - View, -} from "react-native"; +import { Modal, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native"; import type { LocationItem, LocationOptionGroup } from "@/types/locations"; import { createLocationItemFromOption, @@ -40,6 +34,12 @@ interface LocationsListProps { disabled?: boolean; /** Whether locations are loading */ loading?: boolean; + /** Whether to hide the internal add button */ + hideAddButton?: boolean; + /** External control for the add modal */ + showAddModal?: boolean; + /** Callback to change modal visibility */ + onShowAddModalChange?: (visible: boolean) => void; } function isLocationAlreadyAdded(locations: LocationItem[], optionValue: string): boolean { @@ -54,6 +54,94 @@ function isLocationAlreadyAdded(locations: LocationItem[], optionValue: string): }); } +const getSFSymbolForType = (type: string) => { + switch (type) { + case "address": + return "mappin.and.ellipse"; + case "link": + return "link"; + case "phone": + return "phone"; + default: + return "video"; + } +}; + +export const AddLocationTrigger = ({ + isHeader = false, + locationOptions, + locations, + onSelectOption, + disabled = false, + loading = false, + onPressFallback, +}: { + isHeader?: boolean; + locationOptions: LocationOptionGroup[]; + locations: LocationItem[]; + onSelectOption: (value: string, label: string) => void; + disabled?: boolean; + loading?: boolean; + onPressFallback?: () => void; +}) => { + const trigger = isHeader ? ( + + + + ) : ( + + {loading ? ( + Loading options... + ) : ( + <> + + Add Location + + )} + + ); + + if (Platform.OS !== "ios") { + return trigger; + } + + return ( + + + + {locationOptions.flatMap((group) => + group.options + .filter((opt) => !isLocationAlreadyAdded(locations, opt.value)) + .map((option) => ( +