課金機能の実装#203
Conversation
…imatch to version 10.2.4, and add undici and esbuild with minimum versions. Enhance vitest configuration for shared components by adding React aliases to prevent multiple instances. Introduce new authentication hooks and types for shared use across applications. Refactor calendar data hooks to remove unnecessary user authentication checks. Update SettingsClient to handle Stripe session verification after checkout. Add a link to the specific commercial transaction law in the About page and footer for compliance.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Walkthroughこのプルリクエストは、モバイルアプリにビデオアップロード・再生機能を追加し、プレミアム機能ゲーティング(画像/ビデオアップロード、スプリットタイム、練習タイム数制限)を実装。スキーマ更新により Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant VideoUploader as VideoUploader<br/>Component
participant Selector as ImagePicker<br/>(Expo)
participant Validator as Validator
participant API as Upload API
participant R2 as R2 Storage
participant Supabase as Supabase DB
User->>VideoUploader: Select/Record Video
VideoUploader->>Selector: Pick from camera/library
Selector-->>VideoUploader: Return video URI + metadata
VideoUploader->>Validator: Check file size & auth
alt isPremium = false & uploadState = idle
Validator-->>VideoUploader: Show PremiumBadge
else Valid & id exists
Validator-->>VideoUploader: Proceed to upload
VideoUploader->>API: Request presigned URLs<br/>(video + thumbnail)
API-->>VideoUploader: Return signed URLs
VideoUploader->>Selector: Generate thumbnail (0s frame)
Selector-->>VideoUploader: Thumbnail URI
par
VideoUploader->>R2: PUT video to signed URL<br/>(with progress)
VideoUploader->>R2: PUT thumbnail to signed URL
and
R2-->>VideoUploader: Upload complete
end
VideoUploader->>API: Confirm upload<br/>(videoPath, thumbnailPath)
API->>Supabase: Update record/practice_log<br/>with video paths
Supabase-->>API: Success
API-->>VideoUploader: onUploadComplete callback
VideoUploader->>VideoUploader: Render VideoPlayer<br/>with saved paths
else id missing
VideoUploader->>VideoUploader: Store pendingVideoUri<br/>Show "upload on save"
end
sequenceDiagram
participant User
participant VideoPlayer as VideoPlayer<br/>Component
participant URLHook as useVideoPresignedUrl<br/>Hook
participant API as Get Presigned URL API
participant Auth as Auth System
participant R2 as R2 Storage
participant ExpoVideo as expo-video<br/>Library
User->>VideoPlayer: Mount with videoPath
VideoPlayer->>URLHook: Call hook(videoPath)
URLHook->>Auth: Get access_token
Auth-->>URLHook: Return token
URLHook->>API: getPresignedUrl<br/>(videoPath, access_token)
API->>R2: Get signed streaming URL
R2-->>API: Presigned URL
API-->>URLHook: Return {videoUrl, thumbnailUrl}
URLHook-->>VideoPlayer: {videoUrl, thumbnailUrl, isLoading}
alt isLoading
VideoPlayer->>VideoPlayer: Render ActivityIndicator
else error exists
VideoPlayer->>VideoPlayer: Render error + retry button
else URLs ready
VideoPlayer->>VideoPlayer: Create expo-video player
VideoPlayer->>ExpoVideo: useVideoPlayer(videoUrl)
ExpoVideo-->>VideoPlayer: Player instance
VideoPlayer->>VideoPlayer: Show thumbnail overlay<br/>+ play button
User->>VideoPlayer: Tap play button
VideoPlayer->>ExpoVideo: player.replay()
ExpoVideo-->>VideoPlayer: Playback started
VideoPlayer->>VideoPlayer: Hide thumbnail
User->>VideoPlayer: Tap fullscreen
VideoPlayer->>ExpoVideo: enterFullscreen()
ExpoVideo-->>VideoPlayer: Fullscreen active
User->>VideoPlayer: Exit fullscreen
VideoPlayer->>ExpoVideo: Pause & seek back<br/>to thumbnail state
ExpoVideo-->>VideoPlayer: Paused, reset position
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The lockfile had a missing entry for workerd@1.20260310.1 after merging main into stripe, causing pnpm install --frozen-lockfile to fail in CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…date tests for authentication failure scenarios. The initial calendar items and monthly summary now default to empty states if not provided. Test cases updated to reflect API call failures when user authentication is lost.
- Integrated premium checks for image uploads and practice time limits in CompetitionBasicFormScreen, PracticeFormScreen, and RecordFormScreen. - Added PremiumBadge components to notify users of premium features in various forms. - Implemented validation logic for split time limits in RecordLogForm and RecordForm, ensuring free users are informed of their limits. - Updated image upload utility to enforce premium restrictions. - Enhanced user experience by providing clear messaging for premium features across the application.
…ser handling and loading states - Updated PracticeDetails to use userId instead of user directly for filtering practice logs in team practices. - Enhanced SettingsClient to refresh subscription on mount and added loading state handling for subscription settings. - Improved user experience in SubscriptionSettings by displaying a skeleton loader while subscription data is being fetched.
- Mocked useAuth in RecordForm tests to simulate user authentication without AuthProvider. - Updated useRecordForm tests to include isPremium flag, allowing for testing without free restrictions on split time additions.
…ring Supabase stack startup - Modified the Supabase stack startup command in both ci.yml and supabase.yml to exclude edge-runtime, enhancing stability for end-to-end tests. - Increased timeout for the startup process to 8 minutes to accommodate potential delays.
- Updated PremiumBadge component to link directly to the subscription page instead of settings. - Added DAILY_TOKEN_LIMIT to free plan limits for better token management. - Refined subscription type definitions to include a UserPlan type for better clarity. - Adjusted logic in premium utility functions to enforce stricter limits on split and practice times for free users. - Enhanced subscription status handling in the SubscriptionSettings component to display payment failure messages. - Updated Stripe webhook handlers to improve customer ID management and session validation. - Added a new pricing page with detailed plans and features for free and premium users. - Improved sitemap to include the new pricing page for better SEO.
コンフリクト解決: page.tsxはstripeの料金カードを採用、 PracticeMenuItemとRecordLogEntryはmainのレスポンシブクラスを採用しつつ stripeのisSplitTimeLimitReachedによるプレミアム制限を維持。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xml-parser to >=5.5.7 and flatted to >=3.4.2 for improved compatibility; modify path-expression-matcher to version 1.2.0. Enhance CI workflows by adding continue-on-error for stopping Supabase stack.
… version - Modified billing.spec.ts to create a new browser context for unauthenticated requests, ensuring accurate testing of subscription status API. - Updated the expected response status for invalid priceId in checkout API to allow for both 400 and 500 responses in CI environments. - Enhanced visibility checks in settings page tests to include subscription-related text. - Updated Stripe webhook handler to use the latest API version (2025-02-24.acacia) for improved compatibility.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 6
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/web/app/page.tsx (1)
139-145:⚠️ Potential issue | 🟡 Minor
sm:whitespace-nowrapが中間幅で本文をはみ出させます日本語の長文を
smから折り返し禁止にしているので、640px 前後の幅で各行がコンテナに収まらず横スクロールや見切れが起きやすいです。通常の折り返しのままにするか、必要ならtext-balanceなどで整えた方が安全です。📱 提案差分
- <p className="text-lg text-gray-600 sm:whitespace-nowrap"> + <p className="text-lg text-gray-600"> - <p className="text-lg text-gray-600 sm:whitespace-nowrap"> + <p className="text-lg text-gray-600"> - <p className="text-lg text-gray-600 sm:whitespace-nowrap"> + <p className="text-lg text-gray-600">As per coding guidelines, 'Implement responsive design in UI components'.
Also applies to: 193-201, 249-253
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/page.tsx` around lines 139 - 145, The paragraph element using className "text-lg text-gray-600 sm:whitespace-nowrap" prevents wrapping at small/medium screens and causes overflow; remove the responsive nowrap utility (sm:whitespace-nowrap) from the <p> element(s) in page.tsx (and the other occurrences noted) or replace it with a responsive-friendly alternative such as a balancing utility (e.g., add a "text-balance" class) so the Japanese copy wraps naturally and doesn't overflow the container.apps/mobile/screens/CompetitionBasicFormScreen.tsx (1)
1-1:⚠️ Potential issue | 🟡 Minor
"use client"ディレクティブは React Native ファイルでは不要です。
"use client"は Next.js App Router のディレクティブであり、React Native/Expo モバイルアプリには適用されません。このディレクティブは誤って追加された可能性があります。🔧 修正案
-"use client"; - import React, { useState, useEffect, useCallback, useRef } from "react";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/screens/CompetitionBasicFormScreen.tsx` at line 1, Remove the unnecessary Next.js directive "use client" from the top of CompetitionBasicFormScreen.tsx; this is not applicable to React Native/Expo files—open the CompetitionBasicFormScreen component file, delete the standalone "use client" line (the exact string shown in the diff), and ensure no other Next.js-only directives remain at the top of the file.apps/mobile/screens/PracticeTimeFormScreen.tsx (1)
190-215:⚠️ Potential issue | 🟡 Minor無料枠の最後の入力が
nextのままです。無料ユーザーで上限より多いセルがある場合、最後に編集できるセルでも
returnKeyTypeが"next"のままで、focusNextInput()は次の disabled セルへ進もうとします。上限到達時の Enter でフォーカス遷移が詰まるので、最後の編集可能セルは"done"にしてください。💡 修正例
- const isLastInput = globalIndex === setCount * repCount - 1; + const lastEditableIndex = isPremium + ? setCount * repCount - 1 + : Math.min(setCount * repCount, FREE_PLAN_LIMITS.PRACTICE_TIMES_PER_LOG) - 1; + const isLastInput = globalIndex === lastEditableIndex; @@ onSubmitEditing={() => { handleTimeConfirm(timeEntry.id, timeEntry.displayValue || ""); - focusNextInput(globalIndex); + if (!isLastInput) { + focusNextInput(globalIndex); + } }}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/screens/PracticeTimeFormScreen.tsx` around lines 190 - 215, The returnKeyType is computed only by isLastInput, so for free users the last editable cell (before disabled cells) still shows "next" and focusNextInput() moves into disabled inputs; compute the last editable index and use that to set returnKeyType and whether to call focusNextInput. Specifically, derive totalCount = setCount * repCount, compute lastEditableIndex = isPremium ? totalCount - 1 : Math.min(totalCount, FREE_PLAN_LIMITS.PRACTICE_TIMES_PER_LOG) - 1, replace use of isLastInput in returnKeyType with (globalIndex === lastEditableIndex) and guard the onSubmitEditing call to only call focusNextInput when globalIndex < lastEditableIndex (or when not isDisabledByLimit).apps/web/components/forms/record-log/hooks/useRecordLogForm.ts (1)
106-116:⚠️ Potential issue | 🟠 Major編集初期化で既存の
videoThumbnailPathを落としています。
editData.videoPathは引き継いでいるのに、サムネイルだけ常にnullで初期化されています。これだと編集画面で既存サムネイルが表示されず、再保存時にvideo_thumbnail_pathを消してしまう可能性があります。差分案
note: editData.note || "", videoPath: editData.videoPath ?? null, - videoThumbnailPath: null, + videoThumbnailPath: editData.videoThumbnailPath ?? null, reactionTime: editData.reactionTime?.toString() || "",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/forms/record-log/hooks/useRecordLogForm.ts` around lines 106 - 116, The form initialization is clearing existing thumbnails by always setting videoThumbnailPath to null; update the setFormDataList initialization to preserve editData.videoThumbnailPath when present (use editData.videoThumbnailPath ?? null) instead of unconditionally null so the existing thumbnail is shown and not removed on save; locate the setFormDataList call in useRecordLogForm and change the videoThumbnailPath assignment to read from editData (same pattern used for videoPath) while leaving null fallback.
🟠 Major comments (31)
apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsx-131-135 (1)
131-135:⚠️ Potential issue | 🟠 Majorチーム練習時に
userId未確定だと他メンバーのログを表示してしまいます。Line 131 の分岐で、
isTeamPracticeかつuserId未取得時に全ログへフォールバックしており、一時的でも表示漏えいが起きます。userIdがない間は空配列にするか、読み込み継続にしてください。修正案(フォールバックを空配列に変更)
- const filteredLogs = - isTeamPractice && userId - ? (practiceData.practice_logs || []).filter( - (log: PracticeLogFromDB) => log.user_id === userId, - ) - : practiceData.practice_logs || []; + const allLogs = practiceData.practice_logs || []; + const filteredLogs = isTeamPractice + ? userId + ? allLogs.filter((log: PracticeLogFromDB) => log.user_id === userId) + : [] + : allLogs;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsx around lines 131 - 135, The current ternary in PracticeDetails that uses isTeamPractice && userId ? ... : practiceData.practice_logs returns all logs when isTeamPractice is true but userId is falsy; change the fallback so that when isTeamPractice is true and userId is not present you return an empty array (or a loading state) instead of practiceData.practice_logs to avoid showing other members' logs—update the expression that derives the filtered logs (referencing isTeamPractice, userId, and practiceData.practice_logs / PracticeLogFromDB inside PracticeDetails) to explicitly check for userId and return [] when missing.apps/web/app/pricing/page.tsx-43-47 (1)
43-47:⚠️ Potential issue | 🟠 Major比較表の可否がスクリーンリーダーで判別できません
CellContentの boolean 分岐がアイコンだけを返しているので、料金比較の主情報が読み上げでは空セルになります。sr-onlyの文言を添えて可否をテキストでも持たせたいです。♿ 提案差分
function CellContent({ value }: { value: string | boolean }) { if (typeof value === "string") { return <span className="text-sm text-gray-700">{value}</span>; } - return value ? CHECK : CROSS; + return ( + <span className="inline-flex items-center justify-center"> + <span className="sr-only">{value ? "利用可能" : "利用不可"}</span> + {value ? CHECK : CROSS} + </span> + ); }As per coding guidelines, 'Set
aria-labelattributes appropriately for accessibility'.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/pricing/page.tsx` around lines 43 - 47, CellContent returns only icons for boolean values, which is inaccessible to screen readers; update the boolean branch in CellContent to include an accessible text node (e.g., a span with className "sr-only" containing a localized status like "Available" / "Not available") and ensure the visual icon constants CHECK and CROSS are aria-hidden (or the wrapper element carries an appropriate aria-label) so screen readers announce the status while sighted users see the icon; modify the boolean branch to render the icon plus the sr-only span (or add aria-label on the wrapper) and keep string handling unchanged.apps/web/app/page.tsx-363-375 (1)
363-375:⚠️ Potential issue | 🟠 MajorFreeプランの不可項目が肯定アイコンで表示されています
画像・動画アップロード: 不可まで同じチェックアイコンで描画しているので、一覧を流し見したときに利用可能と誤認されます。/pricing側の比較表とも表現がズレています。💳 提案差分
- <ul className="space-y-3 mb-6 flex-1"> - {[ - "Split-time: 最大3個/記録", - "PracticeTime: 最大18個/練習ログ", - "画像・動画アップロード: 不可", - ].map((feature) => ( - <li key={feature} className="flex items-start gap-2 text-sm text-gray-700"> - <svg className="h-5 w-5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> - <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> - </svg> - {feature} - </li> - ))} - </ul> + <ul className="space-y-3 mb-6 flex-1"> + {[ + { label: "Split-time: 最大3個/記録", available: true }, + { label: "PracticeTime: 最大18個/練習ログ", available: true }, + { label: "画像・動画アップロード: 不可", available: false }, + ].map(({ label, available }) => ( + <li key={label} className="flex items-start gap-2 text-sm text-gray-700"> + <svg + className={`h-5 w-5 shrink-0 ${available ? "text-gray-400" : "text-gray-300"}`} + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + strokeWidth={2} + aria-hidden="true" + > + {available ? ( + <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> + ) : ( + <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> + )} + </svg> + {label} + </li> + ))} + </ul>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/page.tsx` around lines 363 - 375, The list shows an availability check icon for every entry including the "画像・動画アップロード: 不可" item; update the map so unavailable items render a negative icon instead. Replace the plain string array in page.tsx with objects (e.g. {label, available}) or detect "不可" in the mapped value, and inside the map render a different SVG/class when available is false (or label includes "不可") so the "画像・動画アップロード: 不可" list item shows an X/disabled icon and/or muted color while other items keep the check icon.apps/web/app/pricing/page.tsx-293-318 (1)
293-318:⚠️ Potential issue | 🟠 Major料金ページに法務導線付きフッター(StaticFooter)が欠けています
検証結果:
/pricingページはStaticFooterをインポート・描画していません。apps/web/app/pricing/page.tsxはホームページのapps/web/app/page.tsxのようなStaticFooterが見当たらず、ページが CTA で終わっています。親レイアウトの確認:
apps/web/app/pricing/にはlayout.tsxがないapps/web/app/layout.tsx(ルートレイアウト)にはStaticFooterがない- pricing ページは root layout のみを継承するため、フッターが描画されていない
対応方法:
apps/web/app/pricing/page.tsxの最後に<StaticFooter />をインポート・追加して、特定商取引法へのリンクを含むフッターを表示してください。期待値は「料金ページでも法務リンクに到達できること」です。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/pricing/page.tsx` around lines 293 - 318, The pricing page is missing the StaticFooter component; import StaticFooter at the top of apps/web/app/pricing/page.tsx and render <StaticFooter /> at the end of the page JSX (e.g., immediately after the closing </section> / before the outer </div>), ensuring the footer with legal/terms links appears on the /pricing page; reference the StaticFooter symbol in the import and insertion so the component is included in the page output.apps/web/components/forms/practice-log/types.ts-22-23 (1)
22-23:⚠️ Potential issue | 🟠 Major動画フィールドが送信型まで届いていません。
PracticeMenuとPracticeLogEditDataには動画パスを持たせていますが、onSubmitの契約であるPracticeLogSubmitDataにはまだありません。このままだとvideoPath/videoThumbnailPathが送信直前で型から脱落し、保存処理へ渡せません。🛠️ 最小修正案
export interface PracticeLogSubmitData { style: string; swimCategory: "Swim" | "Pull" | "Kick"; distance: number; reps: number; sets: number; circleTime: number | null; note: string; tags: Tag[]; times: TimeEntry[]; + videoPath?: string | null; + videoThumbnailPath?: string | null; }Also applies to: 49-50
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/forms/practice-log/types.ts` around lines 22 - 23, PracticeLogSubmitData is missing the optional video fields so videoPath/videoThumbnailPath are dropped before onSubmit; update the PracticeLogSubmitData type to include videoPath?: string | null and videoThumbnailPath?: string | null (matching PracticeMenu and PracticeLogEditData) so the onSubmit contract carries the video fields through to save, and verify the same additions are applied where PracticeLogSubmitData is referenced (e.g., form submit handlers).apps/web/components/forms/record-log/types.ts-29-30 (1)
29-30:⚠️ Potential issue | 🟠 Major
RecordLogEditDataにvideoThumbnailPathフィールドを追加し、フォーム初期化時に既存サムネイルパスを保持してください。
RecordLogFormState/RecordLogFormDataにはvideoThumbnailPathが定義されていますが、編集入力データ型のRecordLogEditDataに欠けています。useRecordLogForm.tsの初期化(115行目)でvideoThumbnailPath: nullと硬定義されているため、既存動画を再アップロードせず編集保存する場合、サムネイルパスが失われます。修正が必要な場所:
types.tsのRecordLogEditDataにvideoThumbnailPath?: string | null;を追加useRecordLogForm.ts115行目をvideoThumbnailPath: editData.videoThumbnailPath ?? null,に変更同じ問題は
useRecordForm.ts(競技記録編集)にも存在します。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/forms/record-log/types.ts` around lines 29 - 30, RecordLogEditData is missing the optional videoThumbnailPath field which causes existing thumbnail paths to be dropped during form initialization in useRecordLogForm; add videoThumbnailPath?: string | null to RecordLogEditData in types.ts and update the initializer in useRecordLogForm (where form fields are set) to use editData.videoThumbnailPath ?? null instead of a hard-coded null; apply the same change to the analogous useRecordForm implementation to preserve thumbnails for competition records as well.apps/shared/hooks/useAuthState.ts-24-50 (1)
24-50:⚠️ Potential issue | 🟠 Major
getSession()のフォールバックが最新の認証状態を上書きできます。Line 27-32 の
onAuthStateChangeと Line 35-50 のgetSession()が独立に同じ state を更新しているので、その間に SIGNED_OUT / SIGNED_IN が起きると、後から解決した fallback が古い session を戻せます。初回取得結果は「まだ別イベントが反映されていないときだけ」適用するガードが必要です。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/shared/hooks/useAuthState.ts` around lines 24 - 50, The fallback getSession() can overwrite a newer auth state because both onAuthStateChange (supabase.auth.onAuthStateChange) and getSession() independently call setSession/setUser/setLoading; fix by introducing a small guard flag (e.g., authEventHandledRef) that you set to true inside the onAuthStateChange callback and check before applying the initialSession in the getSession().then/catch handlers so you only apply the fallback when no auth event has already fired; ensure the flag is a ref or similar persistent value (not local variable) so onAuthStateChange and the getSession handlers see the same state.apps/shared/hooks/useAuthState.ts-1-3 (1)
1-3:⚠️ Potential issue | 🟠 Major
'use client'ディレクティブを追加してください。この shared hook は
useState/useEffectを直接使用しているため、Line 1 に'use client'ディレクティブが必須です。App Router で使用する際の境界が曖昧になるため、必ず追加してください。修正例
+"use client"; + import { useState, useEffect } from "react"; import type { SupabaseClient, User, Session } from "@supabase/supabase-js"; import type { BaseAuthState } from "../types/auth";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/shared/hooks/useAuthState.ts` around lines 1 - 3, Add the React client directive to the top of the module: insert 'use client' as the very first statement in apps/shared/hooks/useAuthState.ts (before any imports) because this file uses useState/useEffect and defines the useAuthState hook; this ensures the hook runs on the client and clarifies App Router boundaries.supabase/migrations/20260318000000_drop_video_url.sql-1-2 (1)
1-2:⚠️ Potential issue | 🟠 Major
video_urlのドロップ前に backfill と残存参照の処理が必須です。検査結果:
20260317移行ファイルはカラム追加のみで、video_url→ 新カラムへのデータコピーがありませんtest-data-2025.sqlがvideo_urlをまだ参照しているため、このドロップ後は INSERT が失敗します- 本番環境にデータが存在する場合、アップデート処理なしでは永続的に失われます
必要な修正
- 20260317 と 20260318 の間に backfill 移行を追加:
UPDATE records SET video_path = video_url ... WHERE video_url IS NOT NULLtest-data-2025.sqlの INSERT ステートメントからvideo_url参照を削除🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/migrations/20260318000000_drop_video_url.sql` around lines 1 - 2, Add a backfill migration between the 20260317 and 20260318 migrations that copies data from the old column to the new one (e.g., run UPDATE records SET video_path = video_url WHERE video_url IS NOT NULL) so existing production rows are preserved before the drop; then remove all references to video_url from test-data-2025.sql INSERTs so tests won’t fail after the drop (keep the existing ALTER TABLE ... DROP COLUMN IF EXISTS video_url migration as-is but only apply it after the backfill and after the Step 7 code deploy).apps/mobile/screens/PracticeFormScreen.tsx-594-604 (1)
594-604:⚠️ Potential issue | 🟠 Majorプレミアム制御が UI 非表示だけで、送信経路はまだ通ります。
Line 594 の表示切り替えで uploader を隠しても、
handleSave/handleContinueToLog側はnewImageFiles.length > 0ならそのままuploadImages(...)を呼びます。購読状態が画面表示中に変わった場合や state が残った場合に無料会員でも送信できるので、送信直前でもisPremiumを再判定してキューを破棄/ブロックした方が安全です。🔒 対応イメージ
+ useEffect(() => { + if (!isPremium) { + setNewImageFiles([]); + setDeletedImageIds([]); + } + }, [isPremium]); + const handleSave = async () => { + if (!isPremium && newImageFiles.length > 0) { + Alert.alert("プレミアム限定", PREMIUM_MESSAGES.image_upload, [{ text: "OK" }]); + return; + } // 二重送信防止 if (isSubmittingRef.current) return; ... }; const handleContinueToLog = async () => { + if (!isPremium && newImageFiles.length > 0) { + Alert.alert("プレミアム限定", PREMIUM_MESSAGES.image_upload, [{ text: "OK" }]); + return; + } // 二重送信防止 if (isSubmittingRef.current) return; ... };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/screens/PracticeFormScreen.tsx` around lines 594 - 604, The UI currently hides ImageUploader when isPremium is false but handleSave and handleContinueToLog still call uploadImages when newImageFiles.length > 0; modify those save/continue paths (functions handleSave and handleContinueToLog) to re-check isPremium immediately before any upload step and if not premium, skip/abort the image upload (e.g., set/clear newImageFiles or prevent calling uploadImages) and proceed without images (or return early), ensuring the upload queue cannot be triggered when isPremium is false even if state lingers; reference isPremium, newImageFiles, uploadImages, ImageUploader in your changes.supabase/functions/revenucat-webhook/index.ts-1-3 (1)
1-3:⚠️ Potential issue | 🟠 MajorRevenueCat のウェブフック設定の綴りを統一してください。
ディレクトリ名とコード内で
revenucatを使用していますが、公式サービスは「RevenueCat」(revenuecat.com)です。Supabase Edge Functions はディレクトリ名がそのままエンドポイント名になるため、RevenueCat の外部ダッシュボード側でrevenuecat-webhookを設定している場合、現在のrevenucat-webhookエンドポイントへのリクエストが 404 になります。以下の箇所をマージ前に統一してください:
- ディレクトリ名:
supabase/functions/revenucat-webhook/→revenuecat-webhook/- インポートパス、ファイル名、データベース migration の
provider値も合わせて統一🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/revenucat-webhook/index.ts` around lines 1 - 3, ディレクトリ名とインポートで綴りが間違っているためエンドポイントが一致しません;rename the function directory from revenucat-webhook to revenuecat-webhook, update the import path string ("../_shared/revenucat-webhook-handler.ts") to use revenuecat spelling, and rename any shared file names and migration `provider` values that use "revenucat" to "revenuecat" so createRevenueCatWebhookHandler and its handler file, import, and DB migration all use the same correct "revenuecat" spelling.apps/mobile/package.json-63-63 (1)
63-63:⚠️ Potential issue | 🟠 Major
reactとreact-test-rendererのバージョンを揃えてください。Line 63 で
reactを19.1.4に上げていますが、Line 85 のreact-test-rendererは19.2.4のままです。Renderer と core の minor がずれると、@testing-library/react-native配下で peer 警告や不安定なテスト失敗の原因になります。react-test-rendererを19.1.4に更新してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/package.json` at line 63, package.json の依存不整合を修正してください: 現在 "react" が "19.1.4" に上がっている一方で "react-test-renderer" が "19.2.4" のままなので、package.json の dependencies/devDependencies 内の "react-test-renderer" エントリを "19.1.4" に揃えて更新し、依存整合後にロックファイルを再生成して(npm/yarn/pnpm のいずれかで)テストを再実行してください.package.json-113-114 (1)
113-114:⚠️ Potential issue | 🟠 Majorexpo パッケージのバージョンが大幅に古い状態です。更新を検討してください。
expo-video@3.0.16とexpo-video-thumbnails@10.0.8は npm に存在する有効なバージョンですが、現在の最新バージョンは両方とも55.0.10です。これらのパッケージは 50+ メジャーバージョン以上遅れており、セキュリティ更新やバグ修正が失われている可能性があります。最新バージョンへの更新を検討してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 113 - 114, The expo-video and expo-video-thumbnails dependencies are pinned to very old versions; update the package.json entries for "expo-video" and "expo-video-thumbnails" from "~3.0.16" and "~10.0.8" to the current release "55.0.10", then run your package manager (npm/yarn/pnpm) to install and rebuild; after updating, run the app and relevant tests to verify there are no breaking API changes from the upgraded "expo-video" and "expo-video-thumbnails" packages.apps/mobile/screens/PracticeLogFormScreen.tsx-64-66 (1)
64-66:⚠️ Potential issue | 🟠 Major動画 state が menu 単位になっていません。
VideoUploaderは各メニュー内で描画されているのに、保持しているexistingVideoPath/existingThumbnailPathは画面全体で 1 組だけです。これだと 2 件目のメニューでアップロード/削除した内容が他のメニューにも波及しますし、新規作成時はどの menu の動画かも区別できません。menu.idごとに動画 state を持たせて、保存後に対応する practice log へ紐付ける形にしないと誤表示・誤保存が起きます。💡 方向性の例
- const [existingVideoPath, setExistingVideoPath] = useState<string | null>(null); - const [existingThumbnailPath, setExistingThumbnailPath] = useState<string | null>(null); + const [videosByMenuId, setVideosByMenuId] = useState< + Record<string, { videoPath: string | null; thumbnailPath: string | null }> + >({}); ... - <VideoUploader + <VideoUploader type="practice-log" - id={practiceLogId} - existingVideoPath={existingVideoPath} - existingThumbnailPath={existingThumbnailPath} + id={isEditMode && menu.id === practiceLogId ? practiceLogId : undefined} + existingVideoPath={videosByMenuId[menu.id]?.videoPath ?? null} + existingThumbnailPath={videosByMenuId[menu.id]?.thumbnailPath ?? null} isPremium={isPremium} onUploadComplete={(vPath, tPath) => { - setExistingVideoPath(vPath); - setExistingThumbnailPath(tPath); + setVideosByMenuId((prev) => ({ + ...prev, + [menu.id]: { videoPath: vPath, thumbnailPath: tPath }, + })); }} onDelete={() => { - setExistingVideoPath(null); - setExistingThumbnailPath(null); + setVideosByMenuId((prev) => ({ + ...prev, + [menu.id]: { videoPath: null, thumbnailPath: null }, + })); }} />Also applies to: 767-783
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/screens/PracticeLogFormScreen.tsx` around lines 64 - 66, The current global existingVideoPath/existingThumbnailPath state in PracticeLogFormScreen causes cross-menu interference; replace them with a per-menu map keyed by menu.id (e.g. existingVideoByMenu: Record<string, string|null> and existingThumbnailByMenu: Record<string, string|null>), update the setters (setExistingVideoByMenu/setExistingThumbnailByMenu) to only change the entry for the target menu.id, and pass the menu-specific values and setters into VideoUploader instead of the single global values; finally, when saving the form ensure you iterate menus and attach each menu.id's video/thumbnail from your maps to the corresponding practice log entry so uploads/deletes are isolated per menu.apps/mobile/components/calendar/DayDetailModal/components/RecordDetail.tsx-341-345 (1)
341-345:⚠️ Potential issue | 🟠 Major動画付き記録の編集データが不完全です。
ここで動画を表示するようになった一方で、同ファイルの
onEditRecord用calendarItem.metadata.recordにはvideo_path/video_thumbnail_pathを詰めていません。いまのままだと動画付き記録を編集したときに既存動画がフォームへ渡らず、再保存で外れる可能性があります。💡 修正例
metadata: { record: { time: fullRecord.time, is_relaying: fullRecord.is_relaying || false, reaction_time: fullRecord.reaction_time ?? null, + video_path: fullRecord.video_path ?? null, + video_thumbnail_path: fullRecord.video_thumbnail_path ?? null, style: {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/components/calendar/DayDetailModal/components/RecordDetail.tsx` around lines 341 - 345, The edit flow doesn’t include existing video fields into calendarItem.metadata.record, so when a user edits a video record the video_path/video_thumbnail_path are lost; update the onEditRecord logic that constructs calendarItem.metadata.record to copy record.videoPath -> video_path and record.videoThumbnailPath -> video_thumbnail_path (ensure naming matches backend shape) so the VideoPlayer-backed record retains its paths when opening the edit form and resaving.apps/web/e2e/src/tests/billing.spec.ts-37-42 (1)
37-42:⚠️ Potential issue | 🟠 Major500 を成功扱いにしないでください。
ここで
500を許容すると、Stripe 設定漏れやサーバー例外でもテストが通ってしまい、課金 API の回帰を検知できません。CI で前提環境が揃わないケースを考慮するなら、この spec 自体を skip するか、Stripe をモックした上で期待値を4xxに固定した方が安全です。Also applies to: 61-65
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/e2e/src/tests/billing.spec.ts` around lines 37 - 42, The test TC-BILLING-002 currently accepts a 500 status which masks server/configuration errors; update the test to not treat 500 as a valid success: either skip the test when Stripe env vars are missing (detect missing env in the test setup and call test.skip) or replace the live call with a mocked Stripe interaction and assert only 4xx responses (e.g., expect(res.status()).toBe(400)). Apply the same change to the related test at the other occurrence (the block around lines 61-65) so neither test accepts 500.apps/shared/types/ui.ts-95-96 (1)
95-96:⚠️ Potential issue | 🟠 Majorshared UI 型の命名がまた分岐しています。
apps/shared/types/ui.tsでvideo_path/video_thumbnail_pathを追加すると、同じ PR で増えているvideoPath系と型契約が食い違います。このファイル内にもRecordFormData.videoUrlが残っているので、呼び出し側にvideoUrl/videoPath/video_pathの 3 系統が混在します。DB 由来の snake_case は変換レイヤーで吸収し、UI 型は camelCase に揃えた方が安全です。As per coding guidelines "UI type definitions should use camelCase properties (defined in `apps/shared/types/ui.ts` and `apps/web/stores/types.ts`)".💡 方向性
- video_path?: string; - video_thumbnail_path?: string; + videoPath?: string; + videoThumbnailPath?: string;他の
record型定義とisRecordMetadata()の戻り型も同じ命名に揃えてください。Also applies to: 170-171, 450-451
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/shared/types/ui.ts` around lines 95 - 96, The UI types in apps/shared/types/ui.ts must use camelCase: replace video_path and video_thumbnail_path with videoPath and videoThumbnailPath in the Record / RecordFormData type definitions and ensure RecordFormData.videoUrl aligns with the videoPath naming; update any related record type aliases and the isRecordMetadata() return type to the same camelCase properties so callers only see videoUrl/videoPath (not snake_case), and keep snake_case -> camelCase conversion in the DB/serialization layer instead of in UI types.apps/web/app/api/stripe/portal/route.ts-22-26 (1)
22-26:⚠️ Potential issue | 🟠 Major
user_subscriptionsテーブルに行が存在しないユーザーではキャッシュの書き戻しが失敗します。
single()でデータが見つからない場合、subscriptionは null になります。その後のフォールバック処理は動作しますが、49–52 行目のupdate()は対象行が存在しないため無操作に終わります。update()は行がないときでもエラーを返さず(error: null)、結果としてstripe_customer_idがキャッシュされません。以後のリクエストでも毎回 Stripe Search にフォールバックするため、不要な API 呼び出しが発生します。50–52 行目を
upsert()に変更してください:修正案
await supabase .from("user_subscriptions") .upsert({ id: user.id, stripe_customer_id: customerId }) .eq("id", user.id);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/api/stripe/portal/route.ts` around lines 22 - 26, The current query reads subscription with const { data: subscription } = await supabase.from("user_subscriptions").select("stripe_customer_id").eq("id", user.id).single(), but later the code calls supabase.from("user_subscriptions").update(...) which is a no-op when the row doesn't exist; replace that update call with an upsert so the stripe_customer_id is written back for users without an existing row. Specifically, locate the supabase.from("user_subscriptions").update(...) call in route.ts (the block that writes customerId back) and change the call to use upsert({ id: user.id, stripe_customer_id: customerId }) so the record is inserted or updated atomically.apps/mobile/screens/RecordLogFormScreen.tsx-75-77 (1)
75-77:⚠️ Potential issue | 🟠 Major複数フォーム時の動画状態管理に問題があります。
formDataListは複数のレコードフォームを扱えますが、動画の状態(existingVideoPath,existingThumbnailPath)は単一のuseStateで管理されています。複数フォームがある場合、すべてのフォームで同じ動画が共有されてしまいます。新規作成モード(
!recordId)では複数フォームが存在する可能性がありますが、動画アップロードはid={recordId}でrecordIdが undefined になるため、アップロード自体は機能しない可能性があります。🔧 修正案
動画状態を
formDataListに含めるか、編集モード(単一フォーム)でのみ動画アップロードを有効にすることを検討してください:{/* 動画 */} <View style={styles.field}> <Text style={styles.label}>動画</Text> + {recordId ? ( <VideoUploader type="record" id={recordId} existingVideoPath={existingVideoPath} existingThumbnailPath={existingThumbnailPath} isPremium={isPremium} onUploadComplete={(vPath, tPath) => { setExistingVideoPath(vPath); setExistingThumbnailPath(tPath); }} onDelete={() => { setExistingVideoPath(null); setExistingThumbnailPath(null); }} /> + ) : ( + <Text style={styles.hintText}>記録保存後に動画を追加できます</Text> + )} </View>Also applies to: 728-745
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/screens/RecordLogFormScreen.tsx` around lines 75 - 77, The video state (existingVideoPath, existingThumbnailPath) is currently global and will be shared across multiple entries in formDataList; change this by moving video/thumbnail fields into each form entry inside formDataList (e.g., add videoPath and thumbnailPath properties), initialize them when creating entries, and update all handlers and components that read or set existingVideoPath/existingThumbnailPath to accept an entry identifier (index or the entry's id) and update the corresponding formDataList[index].videoPath / .thumbnailPath; alternatively, if you only want uploads in edit mode, gate the upload UI/handlers behind recordId (only enable when recordId is truthy) and keep current single-state approach for the single-form edit case.apps/web/app/(authenticated)/teams/[teamId]/competitions/[competitionId]/records/_client/RecordClient.tsx-623-673 (1)
623-673:⚠️ Potential issue | 🟠 Major動画アップロード時のID参照が不正確
動画アップロード処理で
mr.idを使用していますが、これは新規作成時にはフォーム内部で生成された一時的なIDです。レコード挿入後に返されるnewRecord.idを使用する必要があります。現在のコードでは、新規作成した
validRecordsループと動画アップロードのstyleEntriesループが分離しているため、新しいレコードIDとの紐付けができていません。また、Lines 641-643 の PUT リクエストでレスポンスステータスのチェックがなく、アップロード失敗時にも後続処理が継続してしまいます。
🔧 修正案: レコード作成と動画アップロードを連携させる
// 新規レコードを作成 for (const record of validRecords) { + // 対応する styleEntry と memberRecord を特定 + const matchingEntry = styleEntries.find(e => e.styleId === record.styleId); + const matchingMr = matchingEntry?.memberRecords.find(mr => mr.memberUserId === record.memberUserId); + const { data: newRecord, error: recordError } = await supabase .from("records") .insert({ // ... existing insert code }) .select("id") .single(); if (recordError) { console.error(`Record作成エラー (${record.memberName}):`, recordError); hasError = true; continue; } // ... split times handling ... + // 動画アップロード(レコード作成直後に対応するIDで実行) + if (matchingMr?.videoFile && newRecord) { + try { + const uploadUrlRes = await fetch("/api/storage/videos/upload-url", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ type: "record", id: newRecord.id, contentType: "video/mp4" }), + }); + if (!uploadUrlRes.ok) continue; + // ... rest of upload logic using newRecord.id ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(authenticated)/teams/[teamId]/competitions/[competitionId]/records/_client/RecordClient.tsx around lines 623 - 673, The upload loop uses the temporary form-generated mr.id instead of the persisted record ID returned after inserting validRecords, and it also lacks response checks on the PUT uploads; fix by creating a mapping from the temporary id to the returned newRecord.id during the record-creation step that processes validRecords (capture the API response for each created record and store {tempId -> newId}), then in the styleEntries -> memberRecords loop replace usage of mr.id with the mapped newId (skip if no mapping), and after fetching videoUploadUrl/thumbnailUploadUrl check response.ok and also check the PUT upload responses (videoUploadRes.ok and thumbnailUploadRes.ok) before proceeding to confirm and team-assign; reference symbols: validRecords, styleEntries, memberRecords, mr.id, videoUploadUrl, thumbnailUploadUrl, vPath/tPath, and the fetch calls to "/api/storage/videos/upload-url", the PUT to videoUploadUrl/thumbnailUploadUrl, "/api/storage/videos/confirm", and "/api/storage/videos/team-assign".apps/web/components/video/VideoPlayer.tsx-118-127 (1)
118-127:⚠️ Potential issue | 🟠 MajorPresigned URL でのダウンロード属性が機能しない
download="video.mp4"属性は、Cloudflare R2 の presigned URL(異なるオリジン)では期待通りに動作しません。generateVideoGetUrl()がResponseContentDispositionパラメータなしで署名付き URL を生成しているため、ブラウザがdownload属性を無視してナビゲーション(新しいタブで開く)として扱います。確実にダウンロードさせるには、
GetObjectCommandにResponseContentDisposition: 'attachment; filename="video.mp4"'を指定するか、代替手段として Blob 経由のダウンロード実装を検討してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/video/VideoPlayer.tsx` around lines 118 - 127, The presigned URL download is ignored by the browser because generateVideoGetUrl() signs the R2 GetObjectCommand without setting ResponseContentDisposition, so update the presigning logic in generateVideoGetUrl (the function that builds the GetObjectCommand) to include ResponseContentDisposition: 'attachment; filename="video.mp4"' so the browser treats the URL as a download; alternatively, if you prefer a client-side fallback, change the VideoPlayer component (where canDownload and presignedUrl are used) to fetch the presigned URL, convert the response to a Blob, create an object URL, and trigger an anchor click with download to force download when the presigned ResponseContentDisposition cannot be set.supabase/functions/_shared/stripe-webhook-handler.ts-137-165 (1)
137-165:⚠️ Potential issue | 🟠 Major処理前に
processedを確定すると、一時障害でイベントが永久に取りこぼれます。
processed_webhook_eventsへの INSERT をハンドラ実行前に確定しているので、その後の Stripe / Supabase 失敗で 500 を返しても次回配信はduplicate扱いになります。課金自体は Stripe 側で既に冪等なので、ここはprocessing/processedの二段階管理か、成功後マークにしないと購読状態が不整合になりやすいです。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_shared/stripe-webhook-handler.ts` around lines 137 - 165, The current tryMarkAsProcessed flow marks events as processed up-front causing permanent drops on transient handler failures; change to a two-phase idempotency record in the processed_webhook_events lifecycle: instead of inserting a final processed row before handling in tryMarkAsProcessed, either (A) insert a row with a status field like "processing" (and a processed_at nullable) using a unique constraint on event_id, then update that row to status "processed" and set processed_at only after the handler succeeds (and treat existing "processing" rows older than a short TTL as stale), or (B) avoid pre-inserting entirely and insert the processed row only after successful handling; update code paths that call tryMarkAsProcessed to use the new semantics and ensure duplicate detection still checks event_id uniqueness (handle error.code "23505" as duplicate), and add cleanup/TTL logic for stale "processing" entries if you implement option A.apps/web/app/api/storage/videos/upload-url/route.ts-65-92 (1)
65-92:⚠️ Potential issue | 🟠 Majorこの所有者チェックでは、チーム管理者の代理アップロードが常に 403 になります。
PracticeLogClientの新フローは、管理者が他メンバーのpractice_logsを保存した後にこの API で署名 URL を取りに来ます。ここでpractice_logs.user_id === user.idを要求すると、所有者がメンバー本人である以上、代理アップロードは全件失敗します。管理者向けフローは別認可にするか、チーム管理者検証付きの一時アップロードモードを追加した方がよいです。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/api/storage/videos/upload-url/route.ts` around lines 65 - 92, The owner-check logic in the route (the branches that compare record.user_id === user.id and log.user_id === user.id) incorrectly blocks team-admin delegated uploads; update the authorization to also allow admin agents: when type === "record" or "practice_log" (practice_logs.user_id), after fetching the target row (record or log), permit the request if either user.id matches OR the requesting user is a team admin for the target owner (call an existing helper like isTeamAdminFor(user.id, targetOwnerId) or verify the team_admin relationship), or alternatively accept an explicit delegatedUpload flag from PracticeLogClient that triggers a different admin-validated path; ensure you reference/modify the owner checks around record.user_id and log.user_id to include that admin/delegated condition and return 403 only if neither condition is satisfied.apps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx-436-477 (1)
436-477:⚠️ Potential issue | 🟠 Major各 fetch レスポンスの
okを確認してください。
fetchは 4xx/5xx でも例外にならないため、PUT/confirm/team-assignの失敗がcatchに入らず、そのまま成功扱いでリダイレクトされます。これにより、ユーザーには保存成功に見えても、実際には動画のアップロードや確認処理が失敗している可能性があります。各fetchのレスポンスのokを確認し、失敗時には適切にエラーハンドリングを行ってください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx around lines 436 - 477, The upload flow in PracticeLogClient.tsx currently ignores non-2xx fetch responses for the PUTs to videoUploadUrl/thumbnailUploadUrl and the POSTs to "/api/storage/videos/confirm" and "/api/storage/videos/team-assign", so failures won't surface to the outer catch; update the logic in the method handling the upload (the block that obtains uploadUrlRes and then does fetch(videoUploadUrl...), fetch(thumbnailUploadUrl...), fetch("/api/storage/videos/confirm"...), and fetch("/api/storage/videos/team-assign"...)) to await each fetch response, check response.ok, and on failure read response.text() (or json()) and throw or return an Error with that message so the outer catch can handle it (or mark the specific member upload failed and continue), ensuring any non-ok response is logged and prevents a false-success redirect.supabase/functions/_shared/stripe-webhook-handler.ts-182-197 (1)
182-197:⚠️ Potential issue | 🟠 Major
customer.subscription.createdでは、失敗した初回決済(incomplete)を含む非 active 状態を単純にactiveに統一しないでください。Stripe の
subscription.status値の意味を正確に理解する必要があります:
incomplete: 初回決済失敗(まだ支払い未確認、有効期限は約23時間)incomplete_expired: 初回決済期限切れ(終端状態)active: 有効な購読状態past_due: 最新請求書未払い(リトライ中)canceled/unpaid/paused: premium 権限を付与すべきでない状態現在のコードは
trialing以外をすべてactiveに潰すため、決済失敗したincomplete状態のサブスクリプションにも premium アクセス権を与えてしまいます。Stripe の仕様に従い、activeとtrialingのみを許容し、その他のステータスは保持するか明示的に制限してください。コードスニペット
const status = subscription.status === "trialing" ? "trialing" : "active";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_shared/stripe-webhook-handler.ts` around lines 182 - 197, The current line that forces any non-"trialing" status to "active" is incorrect; change the logic that sets the local status variable (the line defining status) to preserve subscription.status instead of coercing to "active" and ensure premium access is granted only when status is explicitly "active" or "trialing" (and not for "incomplete", "incomplete_expired", "past_due", "canceled", etc.); update the upsertData construction (the upsertData object and its plan field) so plan is set to "premium" only when the preserved status is "active" or "trialing" and otherwise set to a non-premium value (e.g., null/"free") or leave it unset.apps/web/lib/cropVideo.ts-89-94 (1)
89-94:⚠️ Potential issue | 🟠 Major
VideoEncoderのエラーハンドラーで throw しても Promise が reject されません。エラーコールバック内で
throwしても、外側の Promise を reject しないため、エラーが適切に伝播しません。エラーを追跡して Promise を reject するか、別の方法でハンドリングする必要があります。🐛 提案する修正
+ let encoderError: Error | null = null; + const encoder = new VideoEncoder({ output: (chunk, meta) => muxer.addVideoChunk(chunk, meta ?? {}), error: (e) => { - throw e; + encoderError = e; }, });また、フレーム処理ループ内でエラーをチェックするか、Promise を reject するロジックを追加してください。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/lib/cropVideo.ts` around lines 89 - 94, The VideoEncoder error callback currently just throws inside error: (e) => { throw e; } which does not reject the surrounding Promise or propagate the error; update the encoder creation to capture the external Promise's reject (or set an async error handler) and call that reject inside the error callback (e.g., store a reject function used by the Promise that wraps your encoding flow), or set an error flag (e.g., encoderError) that the frame-processing loop inspects and then rejects the Promise or aborts processing; reference the VideoEncoder instance named encoder and the error callback, and ensure any place that awaits the encoding (the Promise wrapping muxer.addVideoChunk/output processing) is rejected when encoder.error runs.apps/web/lib/r2-video.ts-16-24 (1)
16-24:⚠️ Potential issue | 🟠 Major環境変数の非 null アサーション(
!)はランタイムエラーのリスクがあります。
getS3Client()はR2_ACCESS_KEY_IDとR2_SECRET_ACCESS_KEYが undefined の場合でも S3Client を作成しようとしますが、認証に失敗します。isVideoR2Enabled()と同様のチェックを追加するか、明示的なエラーを投げることを推奨します。🛡️ 提案する修正
const getS3Client = () => { + if (!process.env.R2_ACCOUNT_ID || !process.env.R2_ACCESS_KEY_ID || !process.env.R2_SECRET_ACCESS_KEY) { + throw new Error("R2環境変数が設定されていません"); + } return new S3Client({ region: "auto", endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { - accessKeyId: process.env.R2_ACCESS_KEY_ID!, - secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, + accessKeyId: process.env.R2_ACCESS_KEY_ID, + secretAccessKey: process.env.R2_SECRET_ACCESS_KEY, }, }); +};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/lib/r2-video.ts` around lines 16 - 24, getS3Client currently uses non-null assertions on process.env.R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY which can cause runtime failures; update getS3Client to validate those env vars (or call isVideoR2Enabled()) before creating the S3Client and throw a clear error if they are missing. Locate getS3Client and add explicit checks for process.env.R2_ACCESS_KEY_ID and process.env.R2_SECRET_ACCESS_KEY, and if either is undefined throw a descriptive Error (e.g., "R2 credentials missing: set R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY") instead of using "!"; then pass the validated values into the S3Client constructor. Ensure any calling code either guards with isVideoR2Enabled() or handles the thrown error.apps/web/components/video/VideoUploader.tsx-74-80 (1)
74-80:⚠️ Potential issue | 🟠 Major
uploadUrlRes.json()が2回呼ばれる可能性があります。Line 75 で
uploadUrlRes.json()を呼び、Line 79-80 でも再度呼んでいます。Response body は一度しか読めないため、エラー時に body を消費した後、成功パスで再度読もうとするとエラーになります。🐛 提案する修正
const uploadUrlRes = await fetch("/api/storage/videos/upload-url", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type, id: targetId, contentType: "video/mp4" }), }); + const uploadUrlData = await uploadUrlRes.json() as UploadUrlResponse | { error?: string; message?: string }; + if (!uploadUrlRes.ok) { - const data = await uploadUrlRes.json() as { error?: string; message?: string }; - throw new Error(data.message ?? data.error ?? "アップロードURLの取得に失敗しました"); + const errorData = uploadUrlData as { error?: string; message?: string }; + throw new Error(errorData.message ?? errorData.error ?? "アップロードURLの取得に失敗しました"); } - const { videoUploadUrl, thumbnailUploadUrl, videoPath: vPath, thumbnailPath: tPath } = - await uploadUrlRes.json() as UploadUrlResponse; + const { videoUploadUrl, thumbnailUploadUrl, videoPath: vPath, thumbnailPath: tPath } = + uploadUrlData as UploadUrlResponse;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/video/VideoUploader.tsx` around lines 74 - 80, The response body is being consumed twice via uploadUrlRes.json(); read the JSON once into a variable and reuse it: await the body (e.g., const body = await uploadUrlRes.json()), then check uploadUrlRes.ok and throw using body.message/body.error if needed, otherwise destructure videoUploadUrl, thumbnailUploadUrl, videoPath (vPath) and thumbnailPath (tPath) from that same body (reference symbols: uploadUrlRes, UploadUrlResponse, videoUploadUrl, thumbnailUploadUrl, videoPath/vPath, thumbnailPath/tPath).supabase/functions/_shared/revenucat-webhook-handler.ts-258-266 (1)
258-266:⚠️ Potential issue | 🟠 Major
update()が affected_rows をチェックせず、行が存在しない場合のエラーを検出できません現在のコードでは、
user_subscriptionsテーブルに該当ユーザーの行が存在しない場合、update()は成功しますが行は更新されません(affected rows = 0)。特に以下のイベントタイプでこの問題が顕在化します:
- RENEWAL, PRODUCT_CHANGE, UNCANCELLATION, CANCELLATION: userId の存在確認がなく、update が失敗してもエラーが報告されません(hasActiveStripeSubscription のチェックがスキップされるため)
- INITIAL_PURCHASE: hasActiveStripeSubscription が
.single()を使用しているため、行がない場合は例外が発生し、結果的にエラーハンドリングされます解決方法として、Stripe ハンドラー(stripe-webhook-handler.ts:201)と同様に
upsert()の使用を検討してください。または最低限、affected_rows をチェックして、更新対象の行が存在しない場合は明示的にエラーを返してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/functions/_shared/revenucat-webhook-handler.ts` around lines 258 - 266, The update path using supabase.from("user_subscriptions").update(update).eq("id", userId) does not check affected_rows so a no-op update (zero rows) is treated as success; modify the handler to either use upsert() like the Stripe handler or after the update inspect the response (e.g., check count/affected_rows or returned data length) and treat zero affected rows as an error—return the JSON error and log accordingly so downstream checks (hasActiveStripeSubscription) still run when the row is missing.apps/mobile/utils/videoUpload.ts-217-226 (1)
217-226:⚠️ Potential issue | 🟠 Major
confirmUpload失敗時にアップロード済みファイルが孤立します。R2 への PUT が成功した後に DB 確定が失敗すると、未参照オブジェクトが残り続けます。
uploadVideo全体をtry/catchで包み、失敗時の補償削除(またはサーバー側のクリーンアップジョブ)を追加してください。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/utils/videoUpload.ts` around lines 217 - 226, Wrap the whole upload flow in uploadVideo (the section that runs uploadVideoToR2 and uploadThumbnailToR2 and then calls confirmUpload) in a try/catch: perform the parallel uploads and call confirmUpload inside the try; in the catch, attempt compensating deletes for the uploaded objects identified by videoPath and thumbnailPath (use your R2 delete helper or call the same R2 client used by uploadVideoToR2/uploadThumbnailToR2), log any delete failures, then rethrow or return a clear failure; ensure progress callbacks still reach 100/cleanup in finally as appropriate.apps/mobile/utils/videoUpload.ts-8-8 (1)
8-8:⚠️ Potential issue | 🟠 Major環境変数未設定時に本番APIへフォールバックしないでください。
webApiUrl未設定でhttps://swim-hub.appに送信されるため、開発/検証環境から本番データへ誤アクセスするリスクがあります。未設定時は即エラーにしてください。💡 修正案
-const WEB_API_URL = Constants.expoConfig?.extra?.webApiUrl || "https://swim-hub.app"; +const webApiUrl = Constants.expoConfig?.extra?.webApiUrl; +if (!webApiUrl) { + throw new Error("expo.extra.webApiUrl が未設定です"); +} +const WEB_API_URL = webApiUrl;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/mobile/utils/videoUpload.ts` at line 8, The WEB_API_URL constant currently falls back to "https://swim-hub.app" when Constants.expoConfig?.extra?.webApiUrl is missing; change this so that if webApiUrl is undefined/null you throw a clear runtime error instead of using the production URL. Replace the fallback assignment for WEB_API_URL in apps/mobile/utils/videoUpload.ts with logic that validates Constants.expoConfig?.extra?.webApiUrl and throws a descriptive Error (or asserts) when absent, and update any call sites that assume WEB_API_URL is always present accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f10ccae6-5b4c-471e-bece-3bb5a2e8fb7d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (113)
.github/workflows/ci.yml.github/workflows/supabase.ymlapps/mobile/components/calendar/DayDetailModal/components/RecordDetail.tsxapps/mobile/components/calendar/DayDetailModal/types.tsapps/mobile/components/practices/PracticeLogItem.tsxapps/mobile/components/shared/PremiumBadge.tsxapps/mobile/components/shared/VideoPlayer.tsxapps/mobile/components/shared/VideoUploader.tsxapps/mobile/components/shared/index.tsapps/mobile/hooks/useVideoPresignedUrl.tsapps/mobile/package.jsonapps/mobile/screens/CompetitionBasicFormScreen.tsxapps/mobile/screens/PracticeFormScreen.tsxapps/mobile/screens/PracticeLogFormScreen.tsxapps/mobile/screens/PracticeTimeFormScreen.tsxapps/mobile/screens/RecordDetailScreen.tsxapps/mobile/screens/RecordFormScreen.tsxapps/mobile/screens/RecordLogFormScreen.tsxapps/mobile/utils/imageUpload.tsapps/mobile/utils/videoUpload.tsapps/shared/__mocks__/supabase.tsapps/shared/__tests__/api/records.test.tsapps/shared/__tests__/hooks/queries/records.test.tsapps/shared/__tests__/hooks/useRecords.test.tsxapps/shared/__tests__/premium.test.tsapps/shared/api/records.tsapps/shared/constants/premium.tsapps/shared/hooks/index.tsapps/shared/hooks/queries/records.tsapps/shared/hooks/useAuthState.tsapps/shared/types/auth.tsapps/shared/types/practice.tsapps/shared/types/record.tsapps/shared/types/supabase-schema.tsapps/shared/types/ui.tsapps/shared/utils/premium.tsapps/shared/utils/validators.tsapps/shared/vitest.config.tsapps/web/.wrangler/state/v3/r2/miniflare-R2BucketObject/0a1b22727b76c386299eed302e8436724750ce227c75ac3307458277e1de66e7.sqliteapps/web/__tests__/components/forms/RecordForm.test.tsxapps/web/__tests__/contexts/CalendarProvider.test.tsxapps/web/__tests__/hooks/useRecordForm.test.tsapps/web/app/(authenticated)/competition/_client/CompetitionClient.tsxapps/web/app/(authenticated)/dashboard/_client/FormModals.tsxapps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsxapps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsxapps/web/app/(authenticated)/dashboard/_hooks/useCalendarData.tsapps/web/app/(authenticated)/dashboard/_hooks/useCalendarHandlers.tsapps/web/app/(authenticated)/dashboard/_hooks/useDashboardHandlers.tsapps/web/app/(authenticated)/dashboard/_providers/CalendarProvider.tsxapps/web/app/(authenticated)/settings/_client/SettingsClient.tsxapps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsxapps/web/app/(authenticated)/teams/[teamId]/competitions/[competitionId]/records/_client/RecordClient.tsxapps/web/app/(authenticated)/teams/[teamId]/competitions/[competitionId]/records/_server/RecordDataLoader.tsxapps/web/app/(unauthenticated)/about/page.tsxapps/web/app/api/storage/images/competition/route.tsapps/web/app/api/storage/images/practice/route.tsapps/web/app/api/storage/videos/confirm/route.tsapps/web/app/api/storage/videos/practice-log/route.tsapps/web/app/api/storage/videos/presigned-url/route.tsapps/web/app/api/storage/videos/record/route.tsapps/web/app/api/storage/videos/team-assign/route.tsapps/web/app/api/storage/videos/upload-url/route.tsapps/web/app/api/stripe/checkout/route.tsapps/web/app/api/stripe/portal/route.tsapps/web/app/api/stripe/verify-session/route.tsapps/web/app/api/subscription/status/route.tsapps/web/app/page.tsxapps/web/app/pricing/page.tsxapps/web/app/sitemap.tsapps/web/components/forms/CompetitionBasicForm.tsxapps/web/components/forms/PracticeBasicForm.tsxapps/web/components/forms/practice-log/PracticeLogForm.tsxapps/web/components/forms/practice-log/hooks/usePracticeLogForm.tsapps/web/components/forms/practice-log/types.tsapps/web/components/forms/record-log/RecordLogForm.tsxapps/web/components/forms/record-log/components/RecordLogEntry.tsxapps/web/components/forms/record-log/hooks/useRecordLogForm.tsapps/web/components/forms/record-log/types.tsapps/web/components/forms/record/RecordForm.tsxapps/web/components/forms/record/components/RecordSetItem.tsxapps/web/components/forms/record/hooks/useRecordForm.tsapps/web/components/forms/record/types.tsapps/web/components/layout/Footer.tsxapps/web/components/settings/SubscriptionSettings.tsxapps/web/components/ui/PremiumBadge.tsxapps/web/components/video/TeamVideoUploader.tsxapps/web/components/video/VideoEditor.tsxapps/web/components/video/VideoPlayer.tsxapps/web/components/video/VideoUploader.tsxapps/web/components/video/index.tsapps/web/contexts/AuthProvider.tsxapps/web/e2e/playwright/.auth/user.jsonapps/web/e2e/src/tests/billing.spec.tsapps/web/lib/auth-api.tsapps/web/lib/cropVideo.tsapps/web/lib/r2-video.tsapps/web/lib/r2.tsapps/web/lib/stripe.tsapps/web/package.jsonapps/web/stores/form/competitionRecordStore.tsapps/web/stores/types.tsapps/web/wrangler.jsoncpackage.jsonsupabase/functions/_shared/revenucat-webhook-handler.tssupabase/functions/_shared/stripe-webhook-handler.tssupabase/functions/revenucat-webhook/index.tssupabase/functions/stripe-webhook/index.tssupabase/migrations/20260312000001_create_processed_webhook_events.sqlsupabase/migrations/20260317000000_add_video_paths.sqlsupabase/migrations/20260318000000_drop_video_url.sqlsupabase/migrations/20260320000001_cleanup_webhook_events.sqlsupabase/migrations/20260320000002_add_stripe_customer_id.sql
- Updated text styles in the Home component for better readability. - Implemented timeout handling in CompetitionDetails and PracticeDetails components to improve user experience during data loading. - Added error handling for goal deletion and editing in GoalsClient to ensure robust cache invalidation. - Refactored PracticeLogClient to build a logId map for better data association. - Enhanced subscription verification logic in the Stripe API to ensure valid subscription statuses. - Improved AuthGuard to handle hydration state more effectively, ensuring smoother user experience during authentication. - Updated E2E tests to utilize supabaseLogin for consistent login handling and improved data loading checks.
| const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "http://127.0.0.1:54321"; | ||
| const supabaseAnonKey = | ||
| process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || | ||
| "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"; |
Check warning
Code scanning / Trivy
JWT token Medium
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 20
♻️ Duplicate comments (1)
apps/web/app/api/storage/videos/team-assign/route.ts (1)
75-93:⚠️ Potential issue | 🟠 Major
practice-log側でもtargetUserIdとの一致を確認してください。ここは
practice_id -> teamIdしか見ていないため、同じチーム内なら別メンバーのpractice_logs.idでも通ります。すると別ユーザーのログ行を更新しつつ、保存先パスだけtargetUserId配下にできてしまい、行データと動画の所属がずれます。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/api/storage/videos/team-assign/route.ts` around lines 75 - 93, 現在のチェックは practice_logs → practices の team_id 一致しか行っておらず、同チームの別ユーザーの practice_logs.id を使えてしまう問題があります。最初の supabase クエリで取得している practiceLog(変数 practiceLog、変数 sourceId を使用)に対して targetUserId と一致する条件を追加し、たとえば .eq("user_id", targetUserId) のようにして取得時点で user 所有を検証し、必要であれば存在しない場合は 403 を返すように修正してください(practiceLogs クエリと後続の practice クエリの両方を参照している箇所を更新)。
🧹 Nitpick comments (1)
apps/web/app/(authenticated)/goals/_client/GoalsClient.tsx (1)
67-69: キャッシュ無効化失敗時に、ユーザー向けエラー表示が不足しています。現在はコンソール出力のみで、画面上では失敗が判別できません。日本語の通知(toast/inline/alert)を追加してください。
💡 最小差分例
} catch (e) { console.error("キャッシュ無効化エラー:", e); + alert("画面の更新に失敗しました。再読み込みしてもう一度お試しください。"); } finally {As per coding guidelines, "Implement error state handling in components" and "Display error messages in Japanese".
Also applies to: 98-100
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(authenticated)/goals/_client/GoalsClient.tsx around lines 67 - 69, The catch blocks in GoalsClient.tsx that currently only call console.error (e.g., the catch at the invalidate cache call around "catch (e) { console.error(...); }" and the similar one at lines ~98-100) need to show a Japanese user-facing error; replace or augment the console.error with a call to the app's notification mechanism (e.g., toast.error or setting an error state/Alert) with a concise Japanese message like "キャッシュを無効化できませんでした。もう一度お試しください。" and ensure the toast/Alert component is imported and used consistently; update both catch sites in the GoalsClient component (the invalidate cache catch and the other catch at 98-100) so users see the error on-screen.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/web/app/`(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx:
- Around line 81-83: loadRecords の開始時に actualRecords と competitionImages
を必ずクリアし、単に setLoading(false) を呼ぶだけのタイムアウト処理をやめて専用のタイムアウト状態(例: timedOut または
loadError)を追加して管理してください; 具体的には loadRecords 内で最初に actualRecords = [] と
competitionImages = [] をクリアし、setLoading(true) をセットし、setTimeout 側では
setTimedOut(true) と setLoading(false) を組み合わせてタイムアウトが発生したことを示すようにし、成功時には timedOut
を false に戻してタイムアウトのフラグで UI(「大会記録を追加」表示など)を分岐させるようにして下さい; また timeoutId
のクリアとコンポーネントのクリーンアップ(unmount 時の clearTimeout)を忘れずに実装し、同様の修正を CompetitionDetails
コンポーネントの他の load 部分(loadRecords を呼ぶ箇所)にも適用してください.
In
`@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx:
- Around line 448-489: The upload flow currently ignores non-ok responses
(uploadUrlRes, PUT to videoUploadUrl/thumbnailUploadUrl, confirm and team-assign
fetches) causing silent failures; update the upload loop in PracticeLogClient
(references: uploadUrlRes, videoUploadUrl, thumbnailUploadUrl, confirmFormData,
and the fetch calls to "/api/storage/videos/confirm" and
"/api/storage/videos/team-assign") to explicitly check response.ok for each
network call, throw or collect a descriptive error including
logId/memberId/teamId and response status/body when a call fails, and
surface/aggregate those errors to the caller or UI instead of continuing
silently. Ensure you validate the PUT responses for both video and thumbnail,
the confirm POST response, and the team-assign POST response and stop/rollback
further steps on failure.
In `@apps/web/app/api/storage/videos/team-assign/route.ts`:
- Around line 16-25: Don't trust tempVideoPath/tempThumbnailPath from the
request body: stop passing those raw values into
copyVideoInR2/deleteVideosFromR2 and avoid deleting before DB updates; instead,
resolve the server-issued temporary paths on the server (e.g., lookup the temp
object keys associated with sourceId/teamId or generate/verify the temp names
inside the route handler) and use those verified keys for copyVideoInR2; perform
deleteVideosFromR2 only after the DB update/transaction that assigns the video
to targetUserId/teamId has succeeded (and handle rollback/error so temp objects
remain intact on failure). Ensure the changes touch the route handler logic that
deserializes body (type, sourceId, targetUserId, teamId) and the code paths that
call copyVideoInR2 and deleteVideosFromR2 so they use server-verified temp paths
and post-commit deletion.
In `@apps/web/app/api/stripe/verify-session/route.ts`:
- Around line 91-109: The Supabase update call on table user_subscriptions using
supabase.from("user_subscriptions").update(...) only checks for error but not
whether a row was actually updated; change the call to append
.select("id").single() and then verify both error and that data is present (if
no data or null, return a 500 with the existing message). Also replace
updated_at: new Date().toISOString() with date-fns formatted timestamp (e.g.,
formatISO(new Date())) to follow the guideline; keep the rest of the fields and
error logging as-is.
In `@apps/web/components/profile/ProfileEditModal.tsx`:
- Line 80: Replace the direct use of err.message in the UI: in ProfileEditModal
where setError(err instanceof Error ? err.message : "プロフィールの更新に失敗しました") is
called, keep logging the raw error (e.g., processLogger.error(err) or
console.error) but set a Japanese fixed or mapped user-facing message via
setError (for example a constant "プロフィールの更新に失敗しました" or use a mapper function
that converts known error types to Japanese messages); if desired, implement a
small map function (e.g., mapProfileErrorToJapanese) and use it before calling
setError to avoid exposing raw English/internal messages.
- Around line 62-75: The current Promise.race usage (TIMEOUT, timeoutPromise,
and onUpdate({...})) only treats the timeout as a failure but does not cancel
the ongoing update, causing concurrent updates when isUpdating is cleared;
modify the code to either (A) implement true cancellation: create an
AbortController, pass its signal through onUpdate (and ensure MyPageClient
update accepts and honors an AbortSignal to abort the Supabase request) and call
controller.abort() when the timeout elapses, or (B) remove Promise.race and
instead await onUpdate without cancellation but show a timeout notification to
the user (keep isUpdating true until the real update resolves/rejects); update
the call sites that reference TIMEOUT/timeoutPromise and ensure onUpdate and the
underlying update function accept and check an AbortSignal if you choose option
A.
In `@apps/web/contexts/AuthProvider.tsx`:
- Around line 450-458: The code currently sets loading: false immediately when
updating auth state in the session handling block (see setAuthState usage),
which causes premium checks to treat subscription:null as non-premium; instead
preserve loading:true until the async subscription fetch completes (or introduce
a separate subscriptionLoading flag) and only set loading:false (or
subscriptionLoading:false) after subscription is fetched and set into
authState.subscription; apply the same change to the initial session
initialization path as well (the other setAuthState sites in the file mentioned
around the later blocks) so UI components that use premium check utilities wait
for subscription resolution before deciding premium vs free UI.
In `@apps/web/e2e/src/config/config.ts`:
- Around line 11-15: The fallback candidate in the candidates array uses
path.resolve(__dirname, "../../../..") which points to apps instead of apps/web;
update that entry to path.resolve(__dirname, "../../../") so the candidates
array (variable name: candidates) correctly includes the apps/web directory when
resolving .env files from config.ts's __dirname context.
In `@apps/web/e2e/src/config/global-setup.ts`:
- Around line 17-34: The upsert call in global-setup.ts currently ignores
Supabase's returned error object so failures can be masked; update the call to
capture the result (e.g., const { data, error } = await
supabase.from("user_subscriptions").upsert(..., { onConflict: "id" })), check if
error is truthy and rethrow it (throw error) so the outer try/catch surfaces the
failure, and only log the success message ("✅ テストユーザーを Premium プランに設定しました")
after confirming no error; apply this check around the upsert call in the same
function.
In `@apps/web/e2e/src/tests/attendance.spec.ts`:
- Around line 125-131: The test currently treats "読み込み中" as an acceptable
success condition, which masks a permanently stuck loading state; remove "読み込み中"
from the success OR checks in the attendance test (the code using
page.textContent("body") / pageText includes(...)) and instead first wait for
the loading indicator to disappear (e.g., await
page.waitForSelector('text=読み込み中', { state: 'detached' }) or wait for the
specific loading locator used in the app to be hidden), then read page content
and assert that it contains "直近の出欠" or "表示できる月がありません" or "イベントがありません"; apply the
same change to the other occurrences flagged (the other two similar checks).
In `@apps/web/e2e/src/tests/billing-e2e-manual.spec.ts`:
- Around line 203-204: The test currently records failures into testResults
(const testResults: Record<number, { status: "OK" | "NG"; note: string }> = {})
but never fails the test run; update every place that sets testResults[no] = {
status: "NG", ... } so it also fails the test immediately by throwing an Error
or calling Playwright's assertion (e.g., throw new Error(`Billing check failed
for ${no}: ${note}`) or expect(false, note)), ensuring CI detects the failure;
look for all updates to testResults (including the block around the shown
declaration and the other occurrences noted at 1347-1353) and add the
throw/expect call right after setting the NG entry.
In `@apps/web/e2e/src/tests/goals.spec.ts`:
- Around line 28-34: In createAdminClient, remove the hard-coded SUPABASE
service-role JWT and only read process.env.SUPABASE_SERVICE_ROLE_KEY; if that
env var is not set, fail fast (throw an error or stop the test/seed run) instead
of using a default, and ensure the createClient call still uses the retrieved
serviceKey and auth options; this prevents accidental leaking of admin
credentials.
- Around line 173-189: The test currently only logs goalCount and goalCountRetry
but never asserts that seeded goals exist; update the test around the goalItems
locator (selector '[class*="cursor-pointer"][class*="rounded-lg"]') to include
assertions: when goalCount > 0 assert goalCount > 0, and in the retry branch
after computing goalCountRetry assert goalCountRetry > 0 (use your test
framework's expect to fail the test if no goals are present), keeping the
existing waits (page.waitForTimeout) and logs.
In `@apps/web/e2e/src/tests/members.spec.ts`:
- Around line 148-159: The use of locator.isVisible({ timeout }) is incorrect
because isVisible ignores timeout and returns immediately; in the block that
defines hasTeamMemberManagement (using
page.locator('[data-testid="team-member-management"]') and locator.isVisible)
replace that pattern with an explicit wait such as await
expect(page.locator('[data-testid="team-member-management"]')).toBeVisible({
timeout: 15000 }) or await
page.locator('[data-testid="team-member-management"]').waitFor({ state:
"visible", timeout: 15000 }) and remove the .catch() boolean fallback logic;
apply the same replacement for the other similar checks in this file (the blocks
around the variables/locators at the other reported sections) so each visibility
check actually waits up to 15s instead of returning immediately.
In `@apps/web/e2e/src/tests/personal/competition.spec.ts`:
- Around line 40-47: The test currently hardcodes a Supabase service-role JWT in
the serviceKey constant and proceeds to call createClient; remove the embedded
JWT and require SUPABASE_SERVICE_ROLE_KEY to be set in the environment, i.e.
make serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY and if it's
undefined/empty (check the serviceKey variable) abort the test/seed startup
early (throw/return/skip) before calling createClient; update references to
supabaseUrl/serviceKey and ensure createClient(supabaseUrl, serviceKey, ...) is
only invoked when the env var is present.
- Around line 50-53: The test seeds the user by hardcoding
"e2e-test@swimhub.com" which can differ from the account used by
supabaseLogin/EnvConfig; change the lookup to use the configured E2E email (use
the same EnvConfig or E2E_EMAIL used by supabaseLogin) when finding the user
(replace the hardcoded string in the find predicate), and ensure testUser is
validated (fail early or create/seed) if that configured email is not present;
refer to supabase.auth.admin.listUsers, supabaseLogin(page), EnvConfig/E2E_EMAIL
and the testUser variable when making the change.
In `@apps/web/e2e/src/tests/personal/practice.spec.ts`:
- Around line 496-504: The test (TC-PRACTICE-004) currently depends on prior
test state by accepting either 2x2 or 3x3 inputs via the hardcoded times array;
make the test deterministic by explicitly seeding the required practice log
shape at test setup (e.g., add a setup step or call a helper like
seedPracticeLogs/seedPracticeConfig to create exactly N sets × M reps expected
by this spec) and then derive the times array from that seeded shape (or assert
against the seeded size). Update the test setup (beforeEach or the start of the
TC-PRACTICE-004 test) to create the practice data programmatically so the code
paths that reference the times array and the form inputs always see the same
number of time input fields. Ensure any helper name used (seedPracticeLogs,
createPracticeConfig, or similar) is referenced where you add the call so
reviewers can locate the change.
- Around line 29-35: The code in createAdminClient embeds a hard-coded service
role JWT in the serviceKey fallback (const serviceKey) which must be removed;
instead require SUPABASE_SERVICE_ROLE_KEY from environment and if it's not set,
make setup/seeding skip or fail early (e.g., throw or return a sentinel from the
routine that calls createAdminClient) so high-privilege keys are never
committed. Update createAdminClient to only read
process.env.SUPABASE_SERVICE_ROLE_KEY (no default literal), ensure callers check
for its presence and handle missing key by aborting or skipping seed operations,
and add a clear error/log message when the env var is absent.
In `@apps/web/e2e/src/tests/schedule.spec.ts`:
- Around line 307-315: The code uses locator.isVisible({ timeout: 15000 }) which
ignores the timeout and does not wait; replace calls like
practicesHeader.isVisible({ timeout: 15000 }) with an explicit wait such as
await expect(practicesHeader).toBeVisible({ timeout: 15000 }) or await
practicesHeader.waitFor({ state: "visible", timeout: 15000 }) and keep the same
branching logic, and make the same replacement for the other occurrences flagged
(the similar locator variables used around the other comment ranges).
In `@apps/web/e2e/src/utils/supabase-login.ts`:
- Line 2: EnvConfig.getTestEnvironment() is coupling credential resolution to
E2E_BASE_URL and can throw/fallback when baseUrl is missing; change the login
utility to read E2E_EMAIL and E2E_PASSWORD directly (or via a dedicated
EnvConfig.getCredentials() helper) instead of calling
EnvConfig.getTestEnvironment(), leaving Playwright's baseURL management
separate; update the code paths that currently call
EnvConfig.getTestEnvironment() in this file (and the related block around lines
24-32) to obtain only credentials and not depend on baseUrl so tests don't
silently fall back to defaults or skip.
---
Duplicate comments:
In `@apps/web/app/api/storage/videos/team-assign/route.ts`:
- Around line 75-93: 現在のチェックは practice_logs → practices の team_id
一致しか行っておらず、同チームの別ユーザーの practice_logs.id を使えてしまう問題があります。最初の supabase クエリで取得している
practiceLog(変数 practiceLog、変数 sourceId を使用)に対して targetUserId と一致する条件を追加し、たとえば
.eq("user_id", targetUserId) のようにして取得時点で user 所有を検証し、必要であれば存在しない場合は 403
を返すように修正してください(practiceLogs クエリと後続の practice クエリの両方を参照している箇所を更新)。
---
Nitpick comments:
In `@apps/web/app/`(authenticated)/goals/_client/GoalsClient.tsx:
- Around line 67-69: The catch blocks in GoalsClient.tsx that currently only
call console.error (e.g., the catch at the invalidate cache call around "catch
(e) { console.error(...); }" and the similar one at lines ~98-100) need to show
a Japanese user-facing error; replace or augment the console.error with a call
to the app's notification mechanism (e.g., toast.error or setting an error
state/Alert) with a concise Japanese message like
"キャッシュを無効化できませんでした。もう一度お試しください。" and ensure the toast/Alert component is
imported and used consistently; update both catch sites in the GoalsClient
component (the invalidate cache catch and the other catch at 98-100) so users
see the error on-screen.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2cfea1b5-6ef1-4a12-99c1-0b397722c823
📒 Files selected for processing (27)
apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsxapps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsxapps/web/app/(authenticated)/goals/_client/GoalsClient.tsxapps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsxapps/web/app/api/storage/videos/team-assign/route.tsapps/web/app/api/stripe/verify-session/route.tsapps/web/app/page.tsxapps/web/components/auth/AuthGuard.tsxapps/web/components/forms/CompetitionBasicForm.tsxapps/web/components/profile/ProfileEditModal.tsxapps/web/components/settings/SubscriptionSettings.tsxapps/web/contexts/AuthProvider.tsxapps/web/e2e/playwright/.auth/user.jsonapps/web/e2e/src/config/config.tsapps/web/e2e/src/config/global-setup.tsapps/web/e2e/src/config/playwright.config.tsapps/web/e2e/src/tests/attendance.spec.tsapps/web/e2e/src/tests/billing-e2e-manual.spec.tsapps/web/e2e/src/tests/goals.spec.tsapps/web/e2e/src/tests/members.spec.tsapps/web/e2e/src/tests/mypage.spec.tsapps/web/e2e/src/tests/personal/competition.spec.tsapps/web/e2e/src/tests/personal/practice.spec.tsapps/web/e2e/src/tests/schedule.spec.tsapps/web/e2e/src/tests/teams.spec.tsapps/web/e2e/src/utils/supabase-login.tsapps/web/lib/supabase-auth/middleware.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/web/e2e/playwright/.auth/user.json
- apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/PracticeSection/PracticeDetails.tsx
- apps/web/app/page.tsx
- apps/web/components/forms/CompetitionBasicForm.tsx
| const timeoutId = setTimeout(() => { | ||
| setLoading(false); | ||
| }, 15000); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find apps/web -name "CompetitionDetails.tsx" -type fRepository: ryuuhei0729/swim-hub
Length of output: 182
🏁 Script executed:
git ls-files apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/Repository: ryuuhei0729/swim-hub
Length of output: 525
🏁 Script executed:
cat -n apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx | head -250Repository: ryuuhei0729/swim-hub
Length of output: 10361
🏁 Script executed:
cat -n apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx | sed -n '250,360p'Repository: ryuuhei0729/swim-hub
Length of output: 5206
🏁 Script executed:
cat -n apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx | sed -n '290,340p'Repository: ryuuhei0729/swim-hub
Length of output: 2280
🏁 Script executed:
# Check if there are any other places where actualRecords/competitionImages are reset
grep -n "setActualRecords\|setCompetitionImages" apps/web/app/\(authenticated\)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsxRepository: ryuuhei0729/swim-hub
Length of output: 387
タイムアウト時のloading状態の扱いを改善してください。
loadRecords() の開始時に actualRecords と competitionImages をリセットしていないため、15秒でタイムアウトが発火した場合、データ取得がまだ完了していないのに loading=false になってしまいます。その結果、前回の大会データが画面に残ったままになるか、初回表示では「大会記録を追加」ボタンが誤って表示されます。
タイムアウト時の状態をより明確に区別するために、専用の loadError または timedOut 状態を追加し、 actualRecords をロード開始時にクリアすることを推奨します。
修正案
+ const [loadError, setLoadError] = useState<string | null>(null);
useEffect(() => {
- const timeoutId = setTimeout(() => {
- setLoading(false);
- }, 15000);
+ const timeoutId = window.setTimeout(() => {
+ setLoadError("読み込みがタイムアウトしました。再度お試しください。");
+ setLoading(false);
+ }, 15000);
const loadRecords = async () => {
try {
setLoading(true);
+ setLoadError(null);
+ setActualRecords([]);
+ setCompetitionImages([]);コーディングガイドラインの「Display appropriate loading states in components」および「Implement error state handling in components」に対応してください。
適用箇所:198-200行目、210-213行目も同様の修正が必要です。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/web/app/`(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx
around lines 81 - 83, loadRecords の開始時に actualRecords と competitionImages
を必ずクリアし、単に setLoading(false) を呼ぶだけのタイムアウト処理をやめて専用のタイムアウト状態(例: timedOut または
loadError)を追加して管理してください; 具体的には loadRecords 内で最初に actualRecords = [] と
competitionImages = [] をクリアし、setLoading(true) をセットし、setTimeout 側では
setTimedOut(true) と setLoading(false) を組み合わせてタイムアウトが発生したことを示すようにし、成功時には timedOut
を false に戻してタイムアウトのフラグで UI(「大会記録を追加」表示など)を分岐させるようにして下さい; また timeoutId
のクリアとコンポーネントのクリーンアップ(unmount 時の clearTimeout)を忘れずに実装し、同様の修正を CompetitionDetails
コンポーネントの他の load 部分(loadRecords を呼ぶ箇所)にも適用してください.
| const { error } = await supabase | ||
| .from("user_subscriptions") | ||
| .update({ | ||
| plan: "premium", | ||
| status, | ||
| provider: "stripe", | ||
| provider_subscription_id: subscription.id, | ||
| premium_expires_at: unixToISO(subscription.current_period_end), | ||
| current_period_start: unixToISO(subscription.current_period_start), | ||
| cancel_at_period_end: subscription.cancel_at_period_end, | ||
| trial_start: unixToISO(subscription.trial_start), | ||
| trial_end: unixToISO(subscription.trial_end), | ||
| updated_at: new Date().toISOString(), | ||
| }) | ||
| .eq("id", userId); | ||
|
|
||
| if (error) { | ||
| console.error("Supabase update error:", error); | ||
| return NextResponse.json({ error: "サブスクリプションの更新に失敗しました" }, { status: 500 }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/app/api/stripe/verify-session/route.ts | head -150Repository: ryuuhei0729/swim-hub
Length of output: 4880
🏁 Script executed:
# Check if date-fns is used elsewhere in the codebase for timestamp creation
fd -e ts -e tsx apps/web | xargs rg "new Date\(\)\.toISOString\(\)" -A 2 -B 2 | head -40Repository: ryuuhei0729/swim-hub
Length of output: 3235
🏁 Script executed:
# Check date-fns usage patterns in the codebase
fd -e ts -e tsx apps/web | xargs rg "formatISO|toISOString|format.*date" -l | head -10Repository: ryuuhei0729/swim-hub
Length of output: 870
🏁 Script executed:
# Check if there are examples of how Supabase updates are handled elsewhere
rg "\.update\(" apps/web -A 3 -B 1 | head -50Repository: ryuuhei0729/swim-hub
Length of output: 2672
🏁 Script executed:
# Check Supabase documentation behavior patterns and check if select() is actually used with updates
rg "\.update\(" apps/web --context 5 | grep -A 5 "\.select\(\)"Repository: ryuuhei0729/swim-hub
Length of output: 46
🏁 Script executed:
# Check if upsert is used elsewhere in the codebase for similar patterns
rg "\.upsert\(" apps/web -A 3 | head -50Repository: ryuuhei0729/swim-hub
Length of output: 636
🏁 Script executed:
# Check for any row count verification patterns in the codebase
rg "\.single\(\)" apps/web -B 2 -A 2 | head -30Repository: ryuuhei0729/swim-hub
Length of output: 1570
🏁 Script executed:
# Check the Supabase Firebase Edge Function for similar patterns
cat -n supabase/functions/_shared/stripe-webhook-handler.ts | grep -A 20 "\.update\(\|\.upsert\("Repository: ryuuhei0729/swim-hub
Length of output: 87
🏁 Script executed:
# Verify the exact behavior: look for how update() is typically handled
rg "\.update\(" supabase/functions/_shared --context 7 | head -60Repository: ryuuhei0729/swim-hub
Length of output: 4954
🏁 Script executed:
# Check the package.json to confirm date-fns is available
cat apps/web/package.json | grep -A 2 -B 2 "date-fns"Repository: ryuuhei0729/swim-hub
Length of output: 252
🏁 Script executed:
# Check if there's a specific pattern for how updated_at timestamps are typically set in the codebase
rg "updated_at" apps/web --context 2 | grep -A 2 -B 2 "new Date\|formatISO\|fromNow" | head -30Repository: ryuuhei0729/swim-hub
Length of output: 1604
🏁 Script executed:
# Check if there's a pattern for how row count verification should work in Supabase operations
rg "count\(\)" apps/web --context 2 | head -20Repository: ryuuhei0729/swim-hub
Length of output: 1436
🏁 Script executed:
# Check database schema for user_subscriptions to understand if row is guaranteed to exist
fd . --full-path "supabase/migrations" -name "*.sql" | head -10Repository: ryuuhei0729/swim-hub
Length of output: 294
🏁 Script executed:
# Find migration files related to user_subscriptions
find supabase -name "*.sql" | xargs grep -l "user_subscriptions" | head -5Repository: ryuuhei0729/swim-hub
Length of output: 314
🏁 Script executed:
# Check if there's an initialization trigger or constraint that ensures user_subscriptions row exists for each user
cat supabase/migrations/*.sql 2>/dev/null | grep -A 10 "user_subscriptions" | head -50Repository: ryuuhei0729/swim-hub
Length of output: 1918
🏁 Script executed:
# Check the migration that creates user_subscriptions for new users
cat supabase/migrations/20260313000000_handle_new_user_add_subscription.sqlRepository: ryuuhei0729/swim-hub
Length of output: 1092
🏁 Script executed:
# Verify the exact coding guideline on date-fns and when to use it
rg "date-fns" --context 3 < /dev/null || echo "Checking instructions..."
cat /dev/nullRepository: ryuuhei0729/swim-hub
Length of output: 50376
🏁 Script executed:
# Check how other API routes handle Supabase updates for critical operations
rg "api/stripe" apps/web --context 1 -lRepository: ryuuhei0729/swim-hub
Length of output: 232
UPDATE後の行数検証がありません
Supabase の update() は既定ではエラー情報のみ返すため、この実装では premium 状態が実際に保存されたか確認できません。重要な購読更新処理なので、.select("id").single() を追加して少なくとも 1 行が更新されたことを確認してください。
また、同時に updated_at: new Date().toISOString() は date-fns を使用してください(ガイドラインに従う)。
💡 修正例
const { error } = await supabase
.from("user_subscriptions")
.update({
plan: "premium",
status,
provider: "stripe",
provider_subscription_id: subscription.id,
premium_expires_at: unixToISO(subscription.current_period_end),
current_period_start: unixToISO(subscription.current_period_start),
cancel_at_period_end: subscription.cancel_at_period_end,
trial_start: unixToISO(subscription.trial_start),
trial_end: unixToISO(subscription.trial_end),
updated_at: new Date().toISOString(),
})
.eq("id", userId);
- if (error) {
+ if (error || !data || data.length === 0) {
console.error("Supabase update error:", error);
return NextResponse.json({ error: "サブスクリプションの更新に失敗しました" }, { status: 500 });
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/api/stripe/verify-session/route.ts` around lines 91 - 109, The
Supabase update call on table user_subscriptions using
supabase.from("user_subscriptions").update(...) only checks for error but not
whether a row was actually updated; change the call to append
.select("id").single() and then verify both error and that data is present (if
no data or null, return a 500 with the existing message). Also replace
updated_at: new Date().toISOString() with date-fns formatted timestamp (e.g.,
formatISO(new Date())) to follow the guideline; keep the rest of the fields and
error logging as-is.
| // タイムアウト付きで更新を実行 | ||
| const TIMEOUT = 15000; | ||
| const timeoutPromise = new Promise<never>((_, reject) => | ||
| setTimeout(() => reject(new Error("更新がタイムアウトしました")), TIMEOUT) | ||
| ); | ||
| await Promise.race([ | ||
| onUpdate({ | ||
| name: formData.name.trim(), | ||
| birthday, | ||
| gender: formData.gender, | ||
| bio: formData.bio.trim() || null, | ||
| }), | ||
| timeoutPromise, | ||
| ]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd ProfileEditModal.tsxRepository: ryuuhei0729/swim-hub
Length of output: 164
🏁 Script executed:
find apps/web/components/profile -name "*.tsx" -o -name "*.ts" | head -20Repository: ryuuhei0729/swim-hub
Length of output: 298
🏁 Script executed:
find apps/web/app -path "*mypage*" -name "*Client*.tsx" | head -20Repository: ryuuhei0729/swim-hub
Length of output: 124
🏁 Script executed:
wc -l apps/web/components/profile/ProfileEditModal.tsxRepository: ryuuhei0729/swim-hub
Length of output: 116
🏁 Script executed:
cat -n apps/web/components/profile/ProfileEditModal.tsxRepository: ryuuhei0729/swim-hub
Length of output: 9881
🏁 Script executed:
wc -l apps/web/app/\(authenticated\)/mypage/_client/MyPageClient.tsxRepository: ryuuhei0729/swim-hub
Length of output: 128
🏁 Script executed:
sed -n '90,140p' apps/web/app/\(authenticated\)/mypage/_client/MyPageClient.tsx | cat -nRepository: ryuuhei0729/swim-hub
Length of output: 2227
🏁 Script executed:
sed -n '1,50p' apps/web/app/\(authenticated\)/mypage/_client/MyPageClient.tsx | cat -nRepository: ryuuhei0729/swim-hub
Length of output: 1914
🏁 Script executed:
rg "queryClient|useCallback|handleProfileUpdate" apps/web/app/\(authenticated\)/mypage/_client/MyPageClient.tsx -A 2 -B 1Repository: ryuuhei0729/swim-hub
Length of output: 1684
Promise.race のタイムアウトだけでは更新が止まらず、再送が競合する可能性があります。
Promise.race は負けた onUpdate(...) を中断しないため、15秒後にタイムアウトで失敗扱いになっても、MyPageClient.tsx の Supabase 更新処理は裏で継続します。finally で isUpdating が false に戻るため、ユーザーが再送すると古い更新と新しい更新が同時実行され、どちらが先に完了するかで最終的なプロフィール状態が決まります。真のキャンセル処理を実装するか、タイムアウトを通知に変更する方が安全です。
💡 安全側の最小修正例
- const timeoutPromise = new Promise<never>((_, reject) =>
- setTimeout(() => reject(new Error("更新がタイムアウトしました")), TIMEOUT)
- );
- await Promise.race([
- onUpdate({
- name: formData.name.trim(),
- birthday,
- gender: formData.gender,
- bio: formData.bio.trim() || null,
- }),
- timeoutPromise,
- ]);
+ const timeoutId = window.setTimeout(() => {
+ setError(
+ "更新に時間がかかっています。通信状況をご確認のうえ、そのままお待ちください。",
+ );
+ }, TIMEOUT);
+ try {
+ await onUpdate({
+ name: formData.name.trim(),
+ birthday,
+ gender: formData.gender,
+ bio: formData.bio.trim() || null,
+ });
+ } finally {
+ window.clearTimeout(timeoutId);
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/components/profile/ProfileEditModal.tsx` around lines 62 - 75, The
current Promise.race usage (TIMEOUT, timeoutPromise, and onUpdate({...})) only
treats the timeout as a failure but does not cancel the ongoing update, causing
concurrent updates when isUpdating is cleared; modify the code to either (A)
implement true cancellation: create an AbortController, pass its signal through
onUpdate (and ensure MyPageClient update accepts and honors an AbortSignal to
abort the Supabase request) and call controller.abort() when the timeout
elapses, or (B) remove Promise.race and instead await onUpdate without
cancellation but show a timeout notification to the user (keep isUpdating true
until the real update resolves/rejects); update the call sites that reference
TIMEOUT/timeoutPromise and ensure onUpdate and the underlying update function
accept and check an AbortSignal if you choose option A.
- Changed loading state from "networkidle" to "domcontentloaded" in openDateModal function for better synchronization. - Updated date selection logic to ensure visibility checks are more robust, enhancing test reliability.
- Added detailed error messages for video and thumbnail upload failures, including member names and HTTP status codes. - Implemented an alert to notify users of any upload errors after processing all video files. - Improved error handling in the subscription verification logic to ensure proper feedback when no subscription record is found.
- Add load error state and timeout notification in CompetitionDetails - Validate server-side paths and reorder delete-after-DB-update in team-assign route - Replace Promise.race with proper timeout handling in ProfileEditModal - Keep auth loading state until subscription fetch completes in AuthProvider - Fix E2E config path resolution, add upsert error handling in global-setup - Remove "読み込み中" from success conditions in attendance tests - Add recordResult helper to fail tests on NG in billing E2E - Add missing assertion in goals spec, use env vars for E2E credentials - Decouple supabase-login from EnvConfig.getTestEnvironment() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
- Simplified date selection logic in openDateModal by removing unnecessary visibility checks. - Added skip condition for Stripe tests when STRIPE_SECRET_KEY is not set, improving test reliability. - Updated trial badge and plan management tests to use more efficient locator methods for better performance.
There was a problem hiding this comment.
Actionable comments posted: 14
♻️ Duplicate comments (3)
apps/web/e2e/src/tests/personal/practice.spec.ts (1)
497-502:⚠️ Potential issue | 🟠 MajorTC-PRACTICE-004 が前テストの更新結果に依存したままです。
ここで 2×2 と 3×3 の両方を許容している限り、単体実行や retry で期待する入力マトリクスが変わり続けます。TC-PRACTICE-004 の先頭で必要な
practice_log形状をシードし直して、このテスト単体で固定した前提を持たせた方がよいです。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/e2e/src/tests/personal/practice.spec.ts` around lines 497 - 502, TC-PRACTICE-004 currently relies on prior test state; seed the required practice_log shape at the start of that test (or in test.beforeEach for the TC-PRACTICE-004 describe/block) so the test always runs with a fixed matrix (e.g., create a practice_log with the expected sets×reps and timestamps or clear-and-create the record used by the UI). Specifically, add a setup step that POSTs/creates the practice_log fixture or calls the existing helper that sets sets/reps to the exact dimensions your assertions expect, then proceed to interact with the time input fields—this removes dependency on TC-PRACTICE-003 and stabilizes retries. Ensure you reference and update the practice_log used by the TC-PRACTICE-004 test block rather than reading whatever previous state exists.apps/web/app/api/storage/videos/team-assign/route.ts (1)
74-94:⚠️ Potential issue | 🔴 Critical
practice-log側でもtargetUserIdを更新条件に含めてください。この分岐は
practice_logs.idとpractices.team_idしか見ていないので、同じチーム内の別メンバーのpractice_logs.idを渡すと、その行に動画パスを書き換えられます。user_id === targetUserIdの検証を追加して、更新クエリ側でも同じ条件を掛けておかないと誤紐付けを防ぎきれません。修正イメージ
- const { data: practiceLog } = await supabase + const { data: practiceLog, error: practiceLogError } = await supabase .from("practice_logs") - .select("id, practice_id") + .select("id, user_id, practice_id") .eq("id", sourceId) + .eq("user_id", targetUserId) .maybeSingle(); + + if (practiceLogError) { + console.error("練習ログ確認エラー:", practiceLogError); + return NextResponse.json({ error: "練習ログの確認に失敗しました" }, { status: 500 }); + }const { error } = await supabase .from("practice_logs") .update({ video_path: finalVideoPath, video_thumbnail_path: finalThumbnailPath }) - .eq("id", sourceId); + .eq("id", sourceId) + .eq("user_id", targetUserId);Also applies to: 129-132
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/api/storage/videos/team-assign/route.ts` around lines 74 - 94, The current checks for practice_logs only filter by practice_logs.id and practices.team_id, so add the targetUserId equality to the practice_logs query and to the subsequent update condition: when querying practice_logs (variable practiceLog) include .eq("user_id", targetUserId) and when validating/updating ensure the update query for the video path also includes .eq("user_id", targetUserId) so only the target user's practice_log row can be read/modified (referencing practiceLog, practice, and targetUserId).apps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx (1)
420-447:⚠️ Potential issue | 🟠 Major
log_ids不足時に動画を黙って捨てないでください。動画が選択されているのに
result.log_idsが無い、または件数が足りない場合、ここはcontinueで抜けるだけなので、保存成功に見えたまま一部の動画だけ落ちます。選択動画数とlog_idsの件数を先に突き合わせて、不足分はvideoUploadErrorsに積んで明示したほうが安全です。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx around lines 420 - 447, The code currently silently skips videos when result.log_ids is missing/short; before building logIdMap (or immediately after obtaining logIds), compute the expected number of log entries (sum of targetMembers across menus with videoFiles) and compare to logIds.length, and for any missing entries push descriptive messages into videoUploadErrors (use the same keys like `${mi}_${member.user_id}` or count mismatches) so missing thumbnails/logIds are reported instead of being dropped; update the later loop that looks up logId (where logIdMap.get(`${i}_${memberId}`) is used) to record an error into videoUploadErrors when logId is undefined rather than just continue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx:
- Around line 69-70: The submit flow currently only processes new files in
menu.videoFiles / videoThumbnails and calls replace_practice_logs which
recreates logs, so existing video_path / video_thumbnail_path on existing logs
get dropped; fix by preserving and reassigning existing paths during
submit—e.g., when building the payload in PracticeLogClient, read existing
log.video_path and log.video_thumbnail_path into the form state (or include them
in the menu item shape) and copy them into the replacement entries if no new
File/Blob is provided, or alternately detect logs that already have
video_path/video_thumbnail_path and avoid doing a full replace_practice_logs for
those entries so their paths are retained. Ensure you update the codepaths that
reference menu.videoFiles, videoThumbnails, replace_practice_logs, video_path,
and video_thumbnail_path.
In `@apps/web/app/api/storage/videos/team-assign/route.ts`:
- Around line 37-55: The Supabase queries (adminMembership, targetMembership,
record, practiceLog, practice) currently only read the data field and ignore the
returned error; after each .select(...).maybeSingle() call (for adminMembership
and targetMembership) and the other Supabase queries later, check the returned
error first and if present return a NextResponse.json with a 500 status
(including an appropriate error message), otherwise proceed to use the data;
update the blocks referencing adminMembership, targetMembership, record,
practiceLog and practice to destructure both data and error from the query
result and branch on error before treating null data as “not found” or
“forbidden.”
In `@apps/web/e2e/src/config/config.ts`:
- Around line 17-19: The dotenv loading loop over candidates currently calls
dotenv.config for each dir which doesn't overwrite existing process.env, so
earlier-loaded root .env wins; update the logic in the loop that iterates over
candidates to enforce the intended precedence by either (A) loading only the
first matching candidate (break after the successful dotenv.config) or (B)
calling dotenv.config with { override: true } for the later files so
apps/web/.env.local can replace root values; change the calls to dotenv.config({
path: path.resolve(dir, ".env.local"), override: true }) / dotenv.config({ path:
path.resolve(dir, ".env"), override: true }) or implement the early-exit
behavior on the first found candidate to ensure consistent env priority (modify
the loop that references candidates and the two dotenv.config calls).
In `@apps/web/e2e/src/tests/billing-e2e-manual.spec.ts`:
- Around line 738-745: The test computes is409 from the API call but doesn't use
it in the pass/fail decision, so a broken duplicate-purchase guard could be
missed; update the recordResult call (the condition argument currently using
!hasSelectBtn && hasManage) to also require is409 (i.e., !hasSelectBtn &&
hasManage && is409) and keep the diagnostic message that includes apiStatus;
locate the call to page.request.post("/api/stripe/checkout") / apiResponse /
is409 and modify the recordResult invocation to include is409 in the boolean
condition.
- Around line 30-36: getAdminClient currently returns a client even when
SUPABASE_SERVICE_ROLE_KEY is empty and createPremiumUser ignores Supabase
errors, causing failed premium seeds to fall back to free users; update
getAdminClient to validate SUPABASE_SERVICE_ROLE_KEY and throw a clear error if
unset, and wrap Supabase calls in createPremiumUser (and any code touching
user_subscriptions upsert) with try-catch, checking the upsert response.error
and throwing if non-null so failures stop the test flow immediately rather than
returning a free user.
- Around line 791-798: The test currently considers the scenario successful
using only stillOnStripe; update the assertion to require both an error message
and that the page is still on Stripe by combining hasError and stillOnStripe
(e.g., passCondition = hasError && stillOnStripe) before calling recordResult;
reference the existing variables errorMsg, hasError, stillOnStripe and
recordResult so you locate the change point and update the recorded message to
include both flags.
- Around line 9-12: newCleanPage creates a BrowserContext without baseURL so
subsequent relative navigations (e.g., page.goto("/pricing") and
page.goto("/dashboard")) fail; update newCleanPage to call browser.newContext
with the baseURL set (e.g., browser.newContext({ baseURL:
"http://localhost:3000" })) and likewise change any other manual
browser.newContext() usages in this file (including the anonymous contexts used
around page.goto calls) to use the same baseURL or refactor into a shared helper
that centralizes newContext({ baseURL: ... }) so all relative page.goto calls
work.
In `@apps/web/e2e/src/tests/goals.spec.ts`:
- Around line 281-314: The test currently treats missing edit buttons or dialogs
as a skipped-success by logging and returning; instead assert failures so CI
catches regressions: remove the console.log(...) + return branches that bypass
checks for editButtonVisible/pageEditVisible and dialogVisible, and replace them
with explicit assertions (e.g., using expect) referencing the existing
locators/vars (firstGoalCard, editButton, pageEditButton, dialogVisible,
goalItems) so the test fails when the edit button or dialog is not present or
visible rather than silently returning.
- Around line 66-77: The beforeAll seeding currently returns on Supabase errors
or missing user which leaves the suite green; wrap Supabase calls (e.g.,
supabase.auth.admin.listUsers and any user insert) inside try-catch and on any
caught error call throw new Error(...) (or use Jest's fail) so beforeAll
rejects, and explicitly throw when testUser is not found (instead of return) to
fail the suite; update references in this file to ensure listUsers results and
insertion failures propagate by throwing with a clear message.
In `@apps/web/e2e/src/tests/members.spec.ts`:
- Around line 148-159: The current early-return when team-member-management
isn't found (the hasTeamMemberManagement check using
page.locator('[data-testid="team-member-management"]')) lets CI pass silently if
the UI is broken; instead, only skip when the team truly doesn't exist (the
prior teamId/tab navigation check), and replace the console.log(...) + return
with an explicit assertion like
expect(page.locator('[data-testid="team-member-management"]')).toBeVisible() so
failures are caught; update the same pattern where used elsewhere (the other
hasTeamMemberManagement-like blocks) to assert visibility rather than quietly
returning.
In `@apps/web/e2e/src/tests/personal/competition.spec.ts`:
- Around line 50-55: The beforeAll is silently returning when seeding or
resolving the test user fails (see supabase.auth.admin.listUsers(), testUser
variable), which causes tests to incorrectly pass; wrap the Supabase calls in
try-catch and, instead of returning, throw or call the test runner's fail (or
expect) with a clear error message including testEmail and context (e.g., "E2E
seed: user not found" or the caught error), and apply the same change to the
other similar blocks at the lines referenced so the suite fails fast on seed or
insert errors.
In `@apps/web/e2e/src/tests/personal/practice.spec.ts`:
- Around line 70-81: The beforeAll seed logic (in practice.spec.ts) currently
swallows Supabase errors by logging and returning, so replace those early
returns with explicit test failures and add try/catch around Supabase calls
(e.g., supabase.auth.admin.listUsers, any insert/update calls referenced at
lines ~137-140, ~190-193, ~221-224) inside the beforeAll; on catch or when
required data (testUser) is missing, call the test framework fail/assertion (or
throw an Error) with a descriptive message instead of console.error+return so
the suite fails fast when seeding fails. Ensure you update all occurrences where
console.error(...); return; is used during seeding to use the same try/catch +
throw/fail behavior.
In `@apps/web/e2e/src/tests/schedule.spec.ts`:
- Around line 307-314: 現在のチェック treats missing UI as acceptable by falling back
to URL check using practicesHeader, isHeaderVisible and expect(page.url()),
which masks regressions; replace this fallback with hard assertions so the test
fails if required UI is absent. Specifically, in the block using practicesHeader
locator and isHeaderVisible (and similar blocks at the other ranges), remove the
silent catch/console.log and instead assert visibility of the header
(practicesHeader.waitFor / expect(practicesHeader).toBeVisible()) and assert
presence/visibility of the schedule action buttons (e.g., locate and expect the
"練習記録追加" and "大会追加" buttons to be visible) so the test fails immediately when
those UI elements are missing rather than passing on URL-only checks. Ensure any
thrown errors are allowed to surface (do not swallow with .catch(() => false) or
return) so CI fails on UI regressions.
In `@apps/web/e2e/src/utils/supabase-login.ts`:
- Around line 84-119: The cookie chunking uses standard base64 and a uniform
`base64-` prefix which doesn't match `@supabase/ssr`; change to base64url (replace
+/ with -_ and strip padding) and emit the library's metadata on the first
chunk: compute encodedSession as base64url (e.g.,
Buffer.from(sessionData).toString('base64') then replace and strip '=' or use
base64url), set the first cookie value to
`base64l-<originalLength>-<firstChunkData>` (use cookie name cookieBaseName for
single or first chunk) and subsequent chunks as the raw chunk data (no `base64-`
prefix) with names cookieBaseName.1, cookieBaseName.2, etc.; update the code
paths that build chunks (encodedSession, chunkSize, chunks) and the
page.context().addCookies calls to follow this format so `@supabase/ssr` can
reconstruct the session.
---
Duplicate comments:
In
`@apps/web/app/`(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsx:
- Around line 420-447: The code currently silently skips videos when
result.log_ids is missing/short; before building logIdMap (or immediately after
obtaining logIds), compute the expected number of log entries (sum of
targetMembers across menus with videoFiles) and compare to logIds.length, and
for any missing entries push descriptive messages into videoUploadErrors (use
the same keys like `${mi}_${member.user_id}` or count mismatches) so missing
thumbnails/logIds are reported instead of being dropped; update the later loop
that looks up logId (where logIdMap.get(`${i}_${memberId}`) is used) to record
an error into videoUploadErrors when logId is undefined rather than just
continue.
In `@apps/web/app/api/storage/videos/team-assign/route.ts`:
- Around line 74-94: The current checks for practice_logs only filter by
practice_logs.id and practices.team_id, so add the targetUserId equality to the
practice_logs query and to the subsequent update condition: when querying
practice_logs (variable practiceLog) include .eq("user_id", targetUserId) and
when validating/updating ensure the update query for the video path also
includes .eq("user_id", targetUserId) so only the target user's practice_log row
can be read/modified (referencing practiceLog, practice, and targetUserId).
In `@apps/web/e2e/src/tests/personal/practice.spec.ts`:
- Around line 497-502: TC-PRACTICE-004 currently relies on prior test state;
seed the required practice_log shape at the start of that test (or in
test.beforeEach for the TC-PRACTICE-004 describe/block) so the test always runs
with a fixed matrix (e.g., create a practice_log with the expected sets×reps and
timestamps or clear-and-create the record used by the UI). Specifically, add a
setup step that POSTs/creates the practice_log fixture or calls the existing
helper that sets sets/reps to the exact dimensions your assertions expect, then
proceed to interact with the time input fields—this removes dependency on
TC-PRACTICE-003 and stabilizes retries. Ensure you reference and update the
practice_log used by the TC-PRACTICE-004 test block rather than reading whatever
previous state exists.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: deccc478-bde8-43ac-b41e-6b39d68a1252
📒 Files selected for processing (16)
apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsxapps/web/app/(authenticated)/teams-admin/[teamId]/practices/[practiceId]/logs/_client/PracticeLogClient.tsxapps/web/app/api/storage/videos/team-assign/route.tsapps/web/app/api/stripe/verify-session/route.tsapps/web/components/profile/ProfileEditModal.tsxapps/web/contexts/AuthProvider.tsxapps/web/e2e/src/config/config.tsapps/web/e2e/src/config/global-setup.tsapps/web/e2e/src/tests/attendance.spec.tsapps/web/e2e/src/tests/billing-e2e-manual.spec.tsapps/web/e2e/src/tests/goals.spec.tsapps/web/e2e/src/tests/members.spec.tsapps/web/e2e/src/tests/personal/competition.spec.tsapps/web/e2e/src/tests/personal/practice.spec.tsapps/web/e2e/src/tests/schedule.spec.tsapps/web/e2e/src/utils/supabase-login.ts
✅ Files skipped from review due to trivial changes (1)
- apps/web/e2e/src/tests/attendance.spec.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- apps/web/e2e/src/config/global-setup.ts
- apps/web/components/profile/ProfileEditModal.tsx
- apps/web/contexts/AuthProvider.tsx
- apps/web/app/(authenticated)/dashboard/_components/DayDetailModal/components/CompetitionSection/CompetitionDetails.tsx
- apps/web/app/api/stripe/verify-session/route.ts
…dSetItem and RecordLogEntry components - Updated RecordSetItem and RecordLogEntry components to include data-testid attributes for the premium badge, enhancing testability. - Adjusted E2E tests to verify the presence of the premium badge using both data-testid and text search methods for improved reliability.
…onents - Added useEffect hooks in CalendarView and DatePicker to set current year and month on the client side, preventing hydration mismatches. - Streamlined date formatting in TeamCompetitionForm and TeamPracticeForm for consistency. - Improved loading state handling in AuthGuard to ensure proper rendering during authentication checks. - Updated E2E tests to ensure robust handling of record saving and editing scenarios.
- Added multiple versions of picomatch (2.3.2, 3.0.2, 4.0.4) to package.json and pnpm-lock.yaml for improved compatibility. - Enhanced metadata across various pages by adding canonical URLs to improve SEO and page indexing.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/components/forms/record-log/components/RecordLogEntry.tsx (1)
363-371:⚠️ Potential issue | 🟡 Minorアクセシビリティ改善:
aria-labelを追加してください。削除ボタンにはアイコンのみが表示されており、スクリーンリーダーユーザーにはボタンの目的が伝わりません。
♿ 修正案
<button type="button" onClick={() => onRemoveSplitTime(originalIndex)} className="p-2 text-red-600 hover:text-red-700" disabled={isLoading} data-testid={`record-split-remove-button-${sectionIndex}-${splitIndex + 1}`} + aria-label={`スプリットタイム ${splitIndex + 1} を削除`} > <TrashIcon className="h-5 w-5" /> </button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/forms/record-log/components/RecordLogEntry.tsx` around lines 363 - 371, The delete button in RecordLogEntry.tsx lacks an accessible name for screen readers; update the <button> element (used with onRemoveSplitTime, originalIndex, sectionIndex, splitIndex, isLoading) to include a descriptive aria-label such as "Remove split {splitIndex + 1}" (or a localized equivalent) so the button's purpose is exposed to assistive technologies while preserving existing props (type, onClick, disabled, data-testid).
🧹 Nitpick comments (3)
apps/web/components/forms/record-log/components/RecordLogEntry.tsx (1)
391-399: 冗長なnullish coalescingを削除してください。
isPremiumは関数シグネチャで既にデフォルト値falseが設定されているため(75行目)、isPremium ?? falseは冗長です。♻️ 修正案
<VideoUploader type="record" id={recordId && isDbUuid(recordId) ? recordId : undefined} existingVideoPath={videoPath ?? undefined} existingThumbnailPath={videoThumbnailPath ?? undefined} - isPremium={isPremium ?? false} + isPremium={isPremium} onUploadComplete={(vPath, tPath) => onVideoPathChange(vPath, tPath)} onDelete={onVideoDelete} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/forms/record-log/components/RecordLogEntry.tsx` around lines 391 - 399, Remove the redundant nullish coalescing for the isPremium prop passed to the VideoUploader component: since the parent component's function signature already provides a default value for isPremium (false), change the prop from isPremium ?? false to simply isPremium in the VideoUploader JSX instance to avoid unnecessary fallback logic.apps/web/components/forms/record/components/RecordSetItem.tsx (1)
317-317: 冗長な nullish coalescing の削除を検討してください。
isPremiumは line 55 ですでにデフォルト値falseが設定されているため、isPremium ?? falseは冗長です。♻️ 修正案
- isPremium={isPremium ?? false} + isPremium={isPremium}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/forms/record/components/RecordSetItem.tsx` at line 317, The prop passing uses redundant nullish coalescing: change the call site that sets isPremium={isPremium ?? false} in the RecordSetItem component to simply isPremium={isPremium} since isPremium already has a default of false in the component's props (defined where isPremium default is set). Remove the "?? false" to eliminate redundancy without changing behavior.apps/web/components/team/TeamCompetitionForm.tsx (1)
29-36: フォーム初期値の重複を1箇所に集約してください。初期
formDataと成功時/クローズ時リセットで同じオブジェクトが3回定義されており、将来の項目追加時に差分漏れが発生しやすいです。createInitialFormData()の関数化を推奨します。Also applies to: 79-86, 99-106
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/components/team/TeamCompetitionForm.tsx` around lines 29 - 36, Extract the initial form object into a single factory function createInitialFormData() that returns { title: "", date: format(new Date(), "yyyy-MM-dd"), endDate: "", place: "", poolType: 0, note: "" }; use createInitialFormData() as the argument to useState to initialize formData and call setFormData(createInitialFormData()) wherever the form is reset (e.g., on successful submit and on close) so the initial shape is defined in one place and stays consistent with future changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/components/team/TeamCompetitionForm.tsx`:
- Line 31: The direct calls to format(new Date(), "yyyy-MM-dd") in
TeamCompetitionForm.tsx must be replaced with a safe helper that validates the
Date before formatting; create or use a getTodayDate() helper which constructs
new Date(), checks isValid(date) from date-fns, and returns the formatted string
(or a sensible fallback) and then replace every occurrence of format(new Date(),
"yyyy-MM-dd") in TeamCompetitionForm (the initial form default and the other two
spots where that exact call appears) with getTodayDate(). Ensure you reference
and import the helper where needed and keep the formatting string "yyyy-MM-dd"
inside the helper so callers no longer call format() directly on a raw Date.
In `@apps/web/components/ui/DatePicker.tsx`:
- Around line 85-87: Guard against invalid dates by validating defaultMonth with
date-fns isValid before calling startOfMonth or using it to initialize
currentMonth; update the code paths that call startOfMonth(defaultMonth) (and
the conditional that falls back to new Date(0)) to first check
isValid(defaultMonth) and only use startOfMonth(defaultMonth) when valid,
otherwise fall back to startOfMonth(new Date(0)) and ensure any later checks
(the conditional around defaultMonth at the later branch) also use
isValid(defaultMonth) to decide fallback.
---
Outside diff comments:
In `@apps/web/components/forms/record-log/components/RecordLogEntry.tsx`:
- Around line 363-371: The delete button in RecordLogEntry.tsx lacks an
accessible name for screen readers; update the <button> element (used with
onRemoveSplitTime, originalIndex, sectionIndex, splitIndex, isLoading) to
include a descriptive aria-label such as "Remove split {splitIndex + 1}" (or a
localized equivalent) so the button's purpose is exposed to assistive
technologies while preserving existing props (type, onClick, disabled,
data-testid).
---
Nitpick comments:
In `@apps/web/components/forms/record-log/components/RecordLogEntry.tsx`:
- Around line 391-399: Remove the redundant nullish coalescing for the isPremium
prop passed to the VideoUploader component: since the parent component's
function signature already provides a default value for isPremium (false),
change the prop from isPremium ?? false to simply isPremium in the VideoUploader
JSX instance to avoid unnecessary fallback logic.
In `@apps/web/components/forms/record/components/RecordSetItem.tsx`:
- Line 317: The prop passing uses redundant nullish coalescing: change the call
site that sets isPremium={isPremium ?? false} in the RecordSetItem component to
simply isPremium={isPremium} since isPremium already has a default of false in
the component's props (defined where isPremium default is set). Remove the "??
false" to eliminate redundancy without changing behavior.
In `@apps/web/components/team/TeamCompetitionForm.tsx`:
- Around line 29-36: Extract the initial form object into a single factory
function createInitialFormData() that returns { title: "", date: format(new
Date(), "yyyy-MM-dd"), endDate: "", place: "", poolType: 0, note: "" }; use
createInitialFormData() as the argument to useState to initialize formData and
call setFormData(createInitialFormData()) wherever the form is reset (e.g., on
successful submit and on close) so the initial shape is defined in one place and
stays consistent with future changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5b625bfe-6a6c-4532-904e-8bd9bc68b013
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (21)
apps/web/app/(authenticated)/dashboard/_components/CalendarView.tsxapps/web/app/(unauthenticated)/about/page.tsxapps/web/app/(unauthenticated)/blog/page.tsxapps/web/app/(unauthenticated)/blog/tag/[tag]/page.tsxapps/web/app/(unauthenticated)/contact/layout.tsxapps/web/app/(unauthenticated)/login/layout.tsxapps/web/app/(unauthenticated)/privacy/page.tsxapps/web/app/(unauthenticated)/reset-password/layout.tsxapps/web/app/(unauthenticated)/signup/layout.tsxapps/web/app/(unauthenticated)/support/page.tsxapps/web/app/(unauthenticated)/terms/page.tsxapps/web/app/(unauthenticated)/tokushoho/page.tsxapps/web/app/(unauthenticated)/update-password/layout.tsxapps/web/components/auth/AuthGuard.tsxapps/web/components/forms/record-log/components/RecordLogEntry.tsxapps/web/components/forms/record/components/RecordSetItem.tsxapps/web/components/team/TeamCompetitionForm.tsxapps/web/components/team/TeamPracticeForm.tsxapps/web/components/ui/DatePicker.tsxapps/web/e2e/src/tests/billing-e2e-manual.spec.tspackage.json
✅ Files skipped from review due to trivial changes (11)
- apps/web/app/(unauthenticated)/update-password/layout.tsx
- apps/web/app/(unauthenticated)/blog/page.tsx
- apps/web/app/(unauthenticated)/reset-password/layout.tsx
- apps/web/app/(unauthenticated)/contact/layout.tsx
- apps/web/app/(unauthenticated)/login/layout.tsx
- apps/web/app/(unauthenticated)/signup/layout.tsx
- apps/web/app/(unauthenticated)/terms/page.tsx
- apps/web/components/team/TeamPracticeForm.tsx
- apps/web/app/(unauthenticated)/privacy/page.tsx
- apps/web/app/(unauthenticated)/tokushoho/page.tsx
- apps/web/app/(unauthenticated)/support/page.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/web/components/auth/AuthGuard.tsx
- package.json
- apps/web/e2e/src/tests/billing-e2e-manual.spec.ts
| title: "", | ||
| date: format(new Date(), "yyyy-MM-dd"), // ローカル日付のYYYY-MM-DD形式 | ||
| endDate: "", // 終了日(複数日開催の場合) | ||
| date: format(new Date(), "yyyy-MM-dd"), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/components/team/TeamCompetitionForm.tsx | head -110Repository: ryuuhei0729/swim-hub
Length of output: 3614
format() の前に isValid() チェックを入れてください。
Line 31、81、101 で format(new Date(), "yyyy-MM-dd") を直接呼んでおり、日付妥当性チェックのガイドラインに未準拠です。getTodayDate() ヘルパーで集約すると安全性と一貫性が上がります。
修正案(diff)
-import { format, parseISO } from "date-fns";
+import { format, isValid, parseISO } from "date-fns";
+
+const getTodayDate = () => {
+ const now = new Date();
+ return isValid(now) ? format(now, "yyyy-MM-dd") : "";
+};
const [formData, setFormData] = useState({
title: "",
- date: format(new Date(), "yyyy-MM-dd"),
+ date: getTodayDate(),
endDate: "",
place: "",
poolType: 0,
note: "",
});
setFormData({
title: "",
- date: format(new Date(), "yyyy-MM-dd"),
+ date: getTodayDate(),
endDate: "",
place: "",
poolType: 0,
note: "",
});
const handleClose = () => {
if (!loading) {
onClose();
setFormData({
title: "",
- date: format(new Date(), "yyyy-MM-dd"),
+ date: getTodayDate(),
endDate: "",
place: "",
poolType: 0,
note: "",
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/components/team/TeamCompetitionForm.tsx` at line 31, The direct
calls to format(new Date(), "yyyy-MM-dd") in TeamCompetitionForm.tsx must be
replaced with a safe helper that validates the Date before formatting; create or
use a getTodayDate() helper which constructs new Date(), checks isValid(date)
from date-fns, and returns the formatted string (or a sensible fallback) and
then replace every occurrence of format(new Date(), "yyyy-MM-dd") in
TeamCompetitionForm (the initial form default and the other two spots where that
exact call appears) with getTodayDate(). Ensure you reference and import the
helper where needed and keep the formatting string "yyyy-MM-dd" inside the
helper so callers no longer call format() directly on a raw Date.
| if (defaultMonth) return startOfMonth(defaultMonth); | ||
| // SSR時はエポック起点を仮設定し、useEffectでクライアント側の現在月に更新 | ||
| return startOfMonth(new Date(0)); |
There was a problem hiding this comment.
defaultMonth 未検証で Invalid Date が state に入り、描画時に例外化する可能性があります。
Line 85 で defaultMonth を無条件で startOfMonth に渡しており、不正な Date が来ると currentMonth が壊れます。さらに Line 92 の条件も defaultMonth の妥当性を見ないため、フォールバックに入れません。isValid でガードしてください。
修正案(例)
const [currentMonth, setCurrentMonth] = useState<Date>(() => {
if (value) {
const date = typeof value === "string" ? parseISO(value) : value;
if (isValid(date)) return startOfMonth(date);
}
- if (defaultMonth) return startOfMonth(defaultMonth);
+ if (defaultMonth && isValid(defaultMonth)) return startOfMonth(defaultMonth);
// SSR時はエポック起点を仮設定し、useEffectでクライアント側の現在月に更新
return startOfMonth(new Date(0));
});
// クライアント側でのみ現在月を設定(Hydration Mismatch回避)
useEffect(() => {
- if (!value && !defaultMonth) {
+ if (!value && (!defaultMonth || !isValid(defaultMonth))) {
setCurrentMonth(startOfMonth(new Date()));
}
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [value, defaultMonth]);Based on learnings: Applies to **/*.{ts,tsx,js,jsx} : Always check date validity with isValid() before calling format() from date-fns.
Also applies to: 90-95
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/components/ui/DatePicker.tsx` around lines 85 - 87, Guard against
invalid dates by validating defaultMonth with date-fns isValid before calling
startOfMonth or using it to initialize currentMonth; update the code paths that
call startOfMonth(defaultMonth) (and the conditional that falls back to new
Date(0)) to first check isValid(defaultMonth) and only use
startOfMonth(defaultMonth) when valid, otherwise fall back to startOfMonth(new
Date(0)) and ensure any later checks (the conditional around defaultMonth at the
later branch) also use isValid(defaultMonth) to decide fallback.
…yout - Updated practices, records, and teams queries to ensure proper handling of undefined or non-array data. - Refactored layout components to adjust the order of AuthProvider and QueryProvider for better context management. - Replaced dynamic imports with static imports for several components to enhance performance and reduce bundle size. - Improved loading state management in dashboard hooks and forms for better user experience.
…ization - Changed loading state from "networkidle" to "domcontentloaded" in multiple E2E test scenarios to enhance synchronization and reliability during page interactions. - Ensured consistent loading behavior across various components, particularly in the billing and competition features.
- Added RevenueCat API key to app configuration for subscription management. - Updated app version to 1.4.0 in app.json. - Removed unused React and Expo dependencies from package.json and pnpm-lock.yaml. - Enhanced mobile navigation by adding a Paywall screen and integrating it into the Settings screen for premium upgrades. - Improved holiday utility to dynamically load holiday data for better performance. - Adjusted Metro configuration for better module resolution in a monorepo setup.
| anonKey: | ||
| 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0', | ||
| "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0", |
Check warning
Code scanning / Trivy
JWT token Medium
| serviceRoleKey: | ||
| 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU', | ||
| } | ||
| "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU", |
Check warning
Code scanning / Trivy
JWT token Medium
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/mobile/app.config.js`:
- Around line 36-38: The current defaulting of webApiUrl to
"https://swim-hub.app" causes non-production runs to accidentally hit
production; change the logic around the webApiUrl/native environment detection
so that webApiUrl is only defaulted to the production URL when
process.env.EXPO_PUBLIC_ENVIRONMENT === "production" (use the existing
environment variable), and for any non-production environment
(development/staging) fail-fast instead (throw an error or exit) if webApiUrl is
not provided; look for the webApiUrl variable and the environment usage in the
app.config.js export and implement the conditional fallback + fail-fast behavior
there.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9aa5b9ff-b417-4763-960e-76b8541f7c6a
📒 Files selected for processing (18)
.gitignoreapps/mobile/app.config.jsapps/mobile/app.jsonapps/mobile/components/shared/PremiumBadge.tsxapps/mobile/contexts/AuthProvider.tsxapps/mobile/eas.jsonapps/mobile/lib/revenucat.tsapps/mobile/metro.config.jsapps/mobile/navigation/MainStack.tsxapps/mobile/navigation/types.tsapps/mobile/package.jsonapps/mobile/screens/PaywallScreen.tsxapps/mobile/screens/SettingsScreen.tsxapps/mobile/scripts/ensure-react-version.jsapps/shared/hooks/queries/practices.tsapps/shared/hooks/queries/records.tsapps/shared/hooks/queries/teams.tsapps/shared/utils/holiday.ts
✅ Files skipped from review due to trivial changes (2)
- .gitignore
- apps/mobile/components/shared/PremiumBadge.tsx
| webApiUrl: webApiUrl || "https://swim-hub.app", | ||
| revenuecatIosApiKey: revenuecatIosApiKey, | ||
| environment: process.env.EXPO_PUBLIC_ENVIRONMENT || "development", |
There was a problem hiding this comment.
非本番で webApiUrl 未設定時に本番APIへフォールバックします
Line 36 の固定フォールバックにより、開発/検証環境の設定漏れで本番APIへ接続するリスクがあります。非本番は fail-fast にしてください。
💡 修正例
const webApiUrl = process.env.EXPO_PUBLIC_WEB_API_URL;
+const environment = process.env.EXPO_PUBLIC_ENVIRONMENT || "development";
+
+if (!webApiUrl && environment !== "production") {
+ throw new Error("EXPO_PUBLIC_WEB_API_URL is required outside production");
+}
module.exports = {
...baseConfig.expo,
extra: {
@@
- webApiUrl: webApiUrl || "https://swim-hub.app",
+ webApiUrl: webApiUrl || "https://swim-hub.app",
revenuecatIosApiKey: revenuecatIosApiKey,
- environment: process.env.EXPO_PUBLIC_ENVIRONMENT || "development",
+ environment: environment,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/mobile/app.config.js` around lines 36 - 38, The current defaulting of
webApiUrl to "https://swim-hub.app" causes non-production runs to accidentally
hit production; change the logic around the webApiUrl/native environment
detection so that webApiUrl is only defaulted to the production URL when
process.env.EXPO_PUBLIC_ENVIRONMENT === "production" (use the existing
environment variable), and for any non-production environment
(development/staging) fail-fast instead (throw an error or exit) if webApiUrl is
not provided; look for the webApiUrl variable and the environment usage in the
app.config.js export and implement the conditional fallback + fail-fast behavior
there.
…ent structure - Removed direct usage of Expo Constants for environment variables, replacing them with a centralized env module for better maintainability. - Updated CalendarHeader component to improve loading state handling and button arrangement. - Adjusted various components to utilize the new env module for accessing configuration values, enhancing consistency across the application. - Improved AppState management in QueryProvider to ensure proper refetching behavior when the app returns from the background.
- Add scripts/** to eslint ignores for apps/mobile (fixes lint parse error) - Update Next.js 16.1.6 → 16.2.2 (fixes 5 CVEs including CSRF bypass, HTTP smuggling) - Update undici override >=7.24.0 → >=7.24.7 (fixes 5 WebSocket DoS CVEs) - Add overrides for node-forge, @xmldom/xmldom, path-to-regexp (fixes 6 high vulnerabilities) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Removed conditional rendering of the loading indicator in the Email login button, streamlining the component structure. - Ensured consistent button content display for improved user experience.
The xmldom >=0.8.12 override introduced a breaking change where DOMParser.parseFromString() requires an explicit mimeType argument. @expo/plist calls it without mimeType, causing iOS build failure. This vulnerability must be fixed upstream by Expo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: multiple React instances (19.1.0 in root/shared, 19.2.3 in web) caused renderHook to return null, failing 375/599 tests. Changes: - Unify React to 19.2.3 in all packages (web, mobile, shared) - Upgrade vitest 1.6.1 → 3.2.4, @vitest/coverage-v8 1.6.0 → 3.2.4 - Upgrade jsdom 24.1.3 → 26.x for React 19 compatibility - Simplify vitest.config.ts (remove unnecessary aliases and base config indirection) - Add IS_REACT_ACT_ENVIRONMENT flag in vitest.setup.ts - Fix Mock<T> generics for vitest 3.x in useImageUpload.test.ts Result: 24/24 test files pass, 599/599 tests pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…st scrolling logic - Removed outdated text from iOSCalendarSyncSettings to streamline user information. - Enhanced scrolling synchronization in TeamMemberList by clarifying comments and disabling unnecessary scroll event handling for improved performance.
…nt and legal links - Added a cancellation note and legal links (Terms of Service and Privacy Policy) to the PaywallScreen. - Implemented a restore purchases feature in the SettingsScreen, including a loading state and a link to manage subscriptions. - Improved user experience by providing clear access to legal information and subscription management options.
- Increased safety timeout for loading state from 10 seconds to 15 seconds to ensure proper session initialization. - Simplified onAuthStateChange handling by removing redundant session fetching and improving asynchronous subscription management. - Ensured that loading state is cleared immediately upon session change, while handling RevenueCat login and subscription fetching asynchronously to avoid blocking UI updates. - Added error handling for RevenueCat login and subscription fetching to improve robustness.
- Upgraded Expo and related packages to version 55.0.11 for enhanced features and performance. - Updated React and React DOM to version 19.2.4 across the monorepo to ensure consistency. - Enhanced AuthProvider with improved session management and error handling. - Refactored app configuration files for better structure and maintainability. - Added new environment variables for Supabase integration in eas.json. - Improved module resolution in Metro configuration for better compatibility in a monorepo setup.
| "EXPO_PUBLIC_ENVIRONMENT": "production", | ||
| "EXPO_PUBLIC_REVENUCAT_IOS_API_KEY": "appl_YadgyFdSryuxgGOGiXAaabdkYNl", | ||
| "EXPO_PUBLIC_SUPABASE_URL": "https://kolhxtkxlkkorcfzyadk.supabase.co", | ||
| "EXPO_PUBLIC_SUPABASE_ANON_KEY": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtvbGh4dGt4bGtrb3JjZnp5YWRrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY4MDI2NzYsImV4cCI6MjA3MjM3ODY3Nn0.yOKJ7Vt3lZf_-b0vuT_re-RBUlH0QgPD811f7LzZRE4", |
Check warning
Code scanning / Trivy
JWT token Medium
- Removed fullscreen and picture-in-picture options from VideoPlayer for a simplified user experience. - Replaced benefits list with a PlanComparisonTable in PaywallScreen to enhance subscription clarity and presentation. - Updated styles to reflect the new comparison section layout.
- Changed the icon type in Sidebar from ComponentType<SVGProps<SVGSVGElement>> to React.ElementType for improved flexibility. - Refactored the loading animation styles in LoadingSpinner to use dangerouslySetInnerHTML for better performance and cleaner code.
…ption handling - Updated AuthProvider to fetch subscription information directly from Supabase, addressing token expiration issues and simplifying error handling. - Improved subscription state management in AuthProvider, ensuring proper updates on user changes. - Added a check in PaywallScreen to display a message for users already on the Premium plan, enhancing user experience and clarity.
…imatch to version 10.2.4, and add undici and esbuild with minimum versions. Enhance vitest configuration for shared components by adding React aliases to prevent multiple instances. Introduce new authentication hooks and types for shared use across applications. Refactor calendar data hooks to remove unnecessary user authentication checks. Update SettingsClient to handle Stripe session verification after checkout. Add a link to the specific commercial transaction law in the About page and footer for compliance.
概要
変更内容
テスト
チェックリスト
スクリーンショット
関連Issue
Closes #
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Chores