From 2a919e722c6194e53087e0bd78399468ce480da8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 00:26:22 +0000 Subject: [PATCH] =?UTF-8?q?=E8=A9=A6=E9=A8=93=E7=9A=84=E6=A9=9F=E8=83=BD?= =?UTF-8?q?=E3=81=8B=E3=82=89ETA=E3=82=A2=E3=82=B7=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E6=89=8B=E5=8B=95=E3=83=88=E3=82=B0=E3=83=AB=E3=82=92?= =?UTF-8?q?=E5=BB=83=E6=AD=A2=E3=81=97=E3=83=AA=E3=83=A2=E3=83=BC=E3=83=88?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=81=AE=E3=81=BF=E3=81=A7=E8=87=AA=E5=8B=95?= =?UTF-8?q?=E6=9C=89=E5=8A=B9=E5=8C=96=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01T4aCJFU5HuP4vcYPpnZLpz --- assets/translations/en.json | 2 - assets/translations/ja.json | 2 - src/components/Permitted.tsx | 13 +----- src/constants/storage.ts | 1 - src/lib/remoteConfig.test.ts | 39 ++++------------- src/lib/remoteConfig.ts | 19 +++----- src/screens/ExperimentalSettings.test.tsx | 53 +++-------------------- src/screens/ExperimentalSettings.tsx | 49 +-------------------- src/store/atoms/experimental.ts | 6 --- 9 files changed, 21 insertions(+), 163 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index d0b693f0cc..d755ed5884 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -329,8 +329,6 @@ "portraitModeTitle": "Portrait Mode", "portraitModeDescription": "When enabled, the running screen switches to a layout optimized for portrait orientation while you hold your device upright.", "telemetryDescription": "Your device's geographic coordinates are sent to our analytics server. The information is used only for analytics.", - "etaAssistTitle": "Improve arrival detection", - "etaAssistDescription": "When enabled, in sections where GPS accuracy drops such as subways, the app uses the server's estimated arrival times (ETA) to assist arrival detection. GPS remains the source of truth for arrival and current location; ETA is only an aid.", "batterySettings": "Battery", "powerSavingLocationTitle": "Power-saving location mode", "powerSavingLocationDescription": "When enabled, location accuracy is lowered to a battery-friendly level and iOS pauses tracking automatically while you are stopped, further reducing battery drain and heat on long rides. The lower accuracy may delay or shift station detection and arrival announcements. The relaxed update frequency is already part of the default settings. It also turns on automatically while your device is in low-power mode.", diff --git a/assets/translations/ja.json b/assets/translations/ja.json index 507dfc8cca..1447780baf 100644 --- a/assets/translations/ja.json +++ b/assets/translations/ja.json @@ -330,8 +330,6 @@ "portraitModeTitle": "ポートレートモード", "portraitModeDescription": "有効にすると、走行画面で端末を縦向きにした際に、縦画面に最適化されたデザインで表示します。", "telemetryDescription": "お使いの端末の地理的座標を解析用サーバに送信します。送信された情報は解析以外に使用されません。", - "etaAssistTitle": "到着判定の改善", - "etaAssistDescription": "有効にすると、地下鉄などGPSの精度が落ちる区間で、サーバーの到着予測(ETA)を使って到着判定を補助します。到着や現在地はGPSが基準で、ETAはあくまで補助です。", "batterySettings": "バッテリー", "powerSavingLocationTitle": "省電力測位モード", "powerSavingLocationDescription": "有効にすると、測位精度を電池優先まで下げ、停車中はiOSが測位を自動休止して、長時間の乗車での電池消費と発熱をさらに減らします。精度の低下により駅の判定や到着案内が遅れたりずれたりする場合があります。位置情報の更新頻度の緩和は標準設定に組み込まれています。端末の省電力モード中は自動的に有効になります。", diff --git a/src/components/Permitted.tsx b/src/components/Permitted.tsx index dcc82fd75b..1bdba9e5a7 100644 --- a/src/components/Permitted.tsx +++ b/src/components/Permitted.tsx @@ -43,10 +43,7 @@ import { import { useTrainTypeModal } from '../hooks/useTrainTypeModal'; import { storage } from '../lib/storage'; import { THEME_PREFERENCE, type ThemePreference } from '../models/Theme'; -import { - etaAssistManualEnabledAtom, - portraitModeEnabledAtom, -} from '../store/atoms/experimental'; +import { portraitModeEnabledAtom } from '../store/atoms/experimental'; import navigationState, { autoModeEnabledAtom, isAppLatestAtom, @@ -78,7 +75,6 @@ const PermittedLayout: React.FC = ({ children }: Props) => { const setNotify = useSetAtom(notifyState); const setPictureInPicture = useSetAtom(pictureInPictureAtom); const setPortraitModeEnabled = useSetAtom(portraitModeEnabledAtom); - const setEtaAssistManualEnabled = useSetAtom(etaAssistManualEnabledAtom); const { enabled: pictureInPictureEnabled, active: pictureInPictureActive } = useAtomValue(pictureInPictureAtom); const isAppActive = useIsAppActive(); @@ -440,9 +436,6 @@ const PermittedLayout: React.FC = ({ children }: Props) => { const portraitModeEnabledStr = storage.getString( STORAGE_KEYS.PORTRAIT_MODE_ENABLED ); - const etaAssistManualEnabledStr = storage.getString( - STORAGE_KEYS.ETA_ASSIST_MANUAL_ENABLED - ); // NOTE: powerSavingLocationEnabledAtom はここでは復元しない。effect復元だと // 継続測位がデフォルト精度で一度起動してから再起動されるため、 // atom定義側(store/atoms/battery.ts)でMMKVから同期的に初期値を確定している。 @@ -555,9 +548,6 @@ const PermittedLayout: React.FC = ({ children }: Props) => { if (portraitModeEnabledStr) { setPortraitModeEnabled(portraitModeEnabledStr === 'true'); } - if (etaAssistManualEnabledStr) { - setEtaAssistManualEnabled(etaAssistManualEnabledStr === 'true'); - } }; loadSettings(); @@ -569,7 +559,6 @@ const PermittedLayout: React.FC = ({ children }: Props) => { setNotify, setPictureInPicture, setPortraitModeEnabled, - setEtaAssistManualEnabled, ]); useEffect(() => { diff --git a/src/constants/storage.ts b/src/constants/storage.ts index 2cd51385e4..f0f9c21a3d 100644 --- a/src/constants/storage.ts +++ b/src/constants/storage.ts @@ -36,7 +36,6 @@ export const STORAGE_KEYS = { WRONG_DIRECTION_NOTIFY_ENABLED: '@TrainLCD:wrongDirectionNotifyEnabled', PICTURE_IN_PICTURE_ENABLED: '@TrainLCD:pictureInPictureEnabled', PORTRAIT_MODE_ENABLED: '@TrainLCD:portraitModeEnabled', - ETA_ASSIST_MANUAL_ENABLED: '@TrainLCD:etaAssistManualEnabled', POWER_SAVING_LOCATION_ENABLED: '@TrainLCD:powerSavingLocationEnabled', } as const; diff --git a/src/lib/remoteConfig.test.ts b/src/lib/remoteConfig.test.ts index eade0a76d6..c97d57a76b 100644 --- a/src/lib/remoteConfig.test.ts +++ b/src/lib/remoteConfig.test.ts @@ -1,12 +1,9 @@ import { MAX_PERMIT_ACCURACY } from '~/constants/location'; -import { store } from '~/store'; -import { etaAssistManualEnabledAtom } from '~/store/atoms/experimental'; import { getEtaFallbackArrivalConfirmMarginSec, getEtaFallbackMaxDurationMin, getMaxPermitAccuracy, isEtaAssistEnabled, - isEtaAssistRemoteEnabled, isForceNotArrivedOnLowAccuracyEnabled, resetRemoteConfigCache, setupRemoteConfig, @@ -31,8 +28,6 @@ const mockRemoteConfig = (body: unknown, ok = true) => { afterEach(() => { jest.clearAllMocks(); resetRemoteConfigCache(); - // 手動トグルは共有ストアに残るため、テスト間の汚染を防ぐべく毎回戻す。 - store.set(etaAssistManualEnabledAtom, false); }); afterAll(() => { @@ -84,48 +79,30 @@ describe('isForceNotArrivedOnLowAccuracyEnabled', () => { }); }); -describe('isEtaAssistRemoteEnabled(マスタースイッチ)', () => { +describe('isEtaAssistEnabled(Remoteマスタースイッチのみで判定)', () => { it('falls back to false before setup', () => { - expect(isEtaAssistRemoteEnabled()).toBe(false); + expect(isEtaAssistEnabled()).toBe(false); }); - it('returns the remote boolean after setup', async () => { + it('RemoteがONなら自動的に有効(手動トグルなし)', async () => { mockRemoteConfig({ max_permit_accuracy: 1500, force_not_arrived_on_low_accuracy: true, eta_assist_enabled: true, }); await setupRemoteConfig(); - expect(isEtaAssistRemoteEnabled()).toBe(true); - }); - - it('falls back to false when the boolean is missing', async () => { - mockRemoteConfig({ max_permit_accuracy: 1500 }); - await setupRemoteConfig(); - expect(isEtaAssistRemoteEnabled()).toBe(false); - }); -}); - -describe('isEtaAssistEnabled(Remote AND 手動トグル)', () => { - it('Remoteと手動トグルの両方がONのときだけ有効', async () => { - mockRemoteConfig({ eta_assist_enabled: true }); - await setupRemoteConfig(); - // Remoteがtrueでも手動トグルOFFなら無効 - expect(isEtaAssistEnabled()).toBe(false); - // 手動トグルONで有効 - store.set(etaAssistManualEnabledAtom, true); expect(isEtaAssistEnabled()).toBe(true); }); - it('Remoteがfalseなら手動トグルONでも無効(マスターOFF)', async () => { - store.set(etaAssistManualEnabledAtom, true); - mockRemoteConfig({ eta_assist_enabled: false }); + it('RemoteがOFFなら無効(サーバー側キルスイッチ)', async () => { + mockRemoteConfig({ max_permit_accuracy: 1500, eta_assist_enabled: false }); await setupRemoteConfig(); expect(isEtaAssistEnabled()).toBe(false); }); - it('セットアップ前(Remote未取得=false)は手動トグルONでも無効', () => { - store.set(etaAssistManualEnabledAtom, true); + it('falls back to false when the boolean is missing', async () => { + mockRemoteConfig({ max_permit_accuracy: 1500 }); + await setupRemoteConfig(); expect(isEtaAssistEnabled()).toBe(false); }); }); diff --git a/src/lib/remoteConfig.ts b/src/lib/remoteConfig.ts index 07384eaa08..88bf5fe498 100644 --- a/src/lib/remoteConfig.ts +++ b/src/lib/remoteConfig.ts @@ -1,6 +1,4 @@ import { MAX_PERMIT_ACCURACY } from '~/constants/location'; -import { store } from '~/store'; -import { etaAssistManualEnabledAtom } from '~/store/atoms/experimental'; import { workerUrl } from './workerApi'; // Cloudflare Worker(/config/remote) 配信の設定キー。Worker 側のレスポンスキーと一致させる。 @@ -121,23 +119,18 @@ export const isForceNotArrivedOnLowAccuracyEnabled = (): boolean => { return FORCE_NOT_ARRIVED_ON_LOW_ACCURACY_FALLBACK; }; -// Remote Config が配信する ETA補助の許可(マスタースイッチ)を同期的に取得する。 -// setupRemoteConfig 完了後は取得済みのリモート値を、未設定・取得失敗時はフォールバック -// (false=既定無効)を返す。これが false のときは機能を提供せず、設定画面の手動トグルも -// 操作不可にする(ExperimentalSettings 側で disabled)。 -export const isEtaAssistRemoteEnabled = (): boolean => { +// ETA補助機能の実効的な有効/無効を同期的に取得する。以前は設定画面の手動トグルとの AND で +// 判定していたが、手動トグルを廃止し自動有効化に変更したため、Remote Config が配信する +// マスタースイッチ(eta_assist_enabled)のみで判定する。setupRemoteConfig 完了後は取得済みの +// リモート値を、未設定・取得失敗時はフォールバック(false=既定無効)を返す。これが false の +// ときは機能を提供しない(サーバー側キルスイッチ)。 +export const isEtaAssistEnabled = (): boolean => { if (cachedEtaAssistEnabled != null) { return cachedEtaAssistEnabled; } return ETA_ASSIST_ENABLED_FALLBACK; }; -// ETA補助機能の実効的な有効/無効を同期的に取得する。Remote Config が許可(マスターON)し、 -// かつ設定画面の手動トグルもONのときだけ有効。Remote が false のときは手動トグルの値に -// 関わらず無効。 -export const isEtaAssistEnabled = (): boolean => - isEtaAssistRemoteEnabled() && store.get(etaAssistManualEnabledAtom); - // ETAフォールバックの到着確定マージン(秒)を同期的に取得する。setupRemoteConfig 完了後は // 取得済みのリモート値を、未完了・未設定・取得失敗時はフォールバックを返す。 // 0 や負値・非数といった不正値もフォールバックへ倒す。 diff --git a/src/screens/ExperimentalSettings.test.tsx b/src/screens/ExperimentalSettings.test.tsx index a70d00fabf..1a3ac1a100 100644 --- a/src/screens/ExperimentalSettings.test.tsx +++ b/src/screens/ExperimentalSettings.test.tsx @@ -2,12 +2,8 @@ import { fireEvent, render } from '@testing-library/react-native'; import { createStore, Provider } from 'jotai'; import { Alert } from 'react-native'; import { STORAGE_KEYS } from '~/constants'; -import * as remoteConfigModule from '~/lib/remoteConfig'; import { storage } from '~/lib/storage'; -import { - etaAssistManualEnabledAtom, - portraitModeEnabledAtom, -} from '~/store/atoms/experimental'; +import { portraitModeEnabledAtom } from '~/store/atoms/experimental'; import tuningState from '~/store/atoms/tuning'; import ExperimentalSettingsScreen from './ExperimentalSettings'; @@ -28,12 +24,10 @@ jest.mock('~/translation', () => ({ const renderWithStore = ( portraitModeEnabled: boolean, - telemetryEnabled = false, - etaAssistManualEnabled = false + telemetryEnabled = false ) => { const store = createStore(); store.set(portraitModeEnabledAtom, portraitModeEnabled); - store.set(etaAssistManualEnabledAtom, etaAssistManualEnabled); store.set(tuningState, (prev) => ({ ...prev, telemetryEnabled })); const screen = render( @@ -99,47 +93,10 @@ describe('ExperimentalSettingsScreen', () => { alertSpy.mockRestore(); }); - it('Remote Config が許可していればETA補助をONにできる(atomとストレージへ保存)', () => { - jest - .spyOn(remoteConfigModule, 'isEtaAssistRemoteEnabled') - .mockReturnValue(true); - const { getByLabelText, store } = renderWithStore(false); - - fireEvent.press(getByLabelText('etaAssistTitle')); - - expect(store.get(etaAssistManualEnabledAtom)).toBe(true); - expect(storage.getString(STORAGE_KEYS.ETA_ASSIST_MANUAL_ENABLED)).toBe( - 'true' - ); - }); - - it('Remote Config が許可していればETA補助をOFFにできる(atomとストレージへ保存)', () => { - jest - .spyOn(remoteConfigModule, 'isEtaAssistRemoteEnabled') - .mockReturnValue(true); - const { getByLabelText, store } = renderWithStore(false, false, true); - - fireEvent.press(getByLabelText('etaAssistTitle')); + it('ETA補助トグルは廃止され表示されない(自動有効化)', () => { + const { queryByLabelText } = renderWithStore(false); - expect(store.get(etaAssistManualEnabledAtom)).toBe(false); - expect(storage.getString(STORAGE_KEYS.ETA_ASSIST_MANUAL_ENABLED)).toBe( - 'false' - ); - }); - - it('Remote Config が false のときはETA補助トグルを操作できない', () => { - jest - .spyOn(remoteConfigModule, 'isEtaAssistRemoteEnabled') - .mockReturnValue(false); - const { getByLabelText, store } = renderWithStore(false); - - // 操作不可(disabled)。押してもatomはONにならず、ONの永続化も走らない。 - fireEvent.press(getByLabelText('etaAssistTitle')); - - expect(store.get(etaAssistManualEnabledAtom)).toBe(false); - expect(storage.getString(STORAGE_KEYS.ETA_ASSIST_MANUAL_ENABLED)).not.toBe( - 'true' - ); + expect(queryByLabelText('etaAssistTitle')).toBeNull(); }); it('テレメトリをONにするとatomとストレージへ保存される', () => { diff --git a/src/screens/ExperimentalSettings.tsx b/src/screens/ExperimentalSettings.tsx index 692e991cde..2d39acd829 100644 --- a/src/screens/ExperimentalSettings.tsx +++ b/src/screens/ExperimentalSettings.tsx @@ -16,11 +16,7 @@ import FooterTabBar from '~/components/FooterTabBar'; import { SettingsHeader } from '~/components/SettingsHeader'; import { StatePanel } from '~/components/ToggleButton'; import Typography from '~/components/Typography'; -import { isEtaAssistRemoteEnabled } from '~/lib/remoteConfig'; -import { - etaAssistManualEnabledAtom, - portraitModeEnabledAtom, -} from '~/store/atoms/experimental'; +import { portraitModeEnabledAtom } from '~/store/atoms/experimental'; import { isLEDThemeAtom } from '~/store/atoms/theme'; import tuningState from '~/store/atoms/tuning'; import { translate } from '~/translation'; @@ -104,15 +100,8 @@ const ExperimentalSettingsScreen: React.FC = () => { const [portraitModeEnabled, setPortraitModeEnabled] = useAtom( portraitModeEnabledAtom ); - const [etaAssistManualEnabled, setEtaAssistManualEnabled] = useAtom( - etaAssistManualEnabledAtom - ); const [tuning, setTuning] = useAtom(tuningState); - // Remote Config(マスタースイッチ)が許可していない間は、手動トグルを操作不可にする。 - // 配信状態は起動時に確定する非リアクティブ値のため、レンダー時に一度参照すれば足りる。 - const etaAssistRemoteEnabled = isEtaAssistRemoteEnabled(); - const navigation = useNavigation(); const handleTogglePortraitMode = useCallback(() => { @@ -129,31 +118,6 @@ const ExperimentalSettingsScreen: React.FC = () => { } }, [portraitModeEnabled, setPortraitModeEnabled]); - const handleToggleEtaAssist = useCallback(() => { - // Remote Config が許可していないときは操作させない(UI側でも disabled)。 - if (!etaAssistRemoteEnabled) { - return; - } - const flag = !etaAssistManualEnabled; - setEtaAssistManualEnabled(flag); - try { - storage.set( - STORAGE_KEYS.ETA_ASSIST_MANUAL_ENABLED, - flag ? 'true' : 'false' - ); - } catch (error) { - // 保存に失敗したままだと次回起動時に設定が巻き戻るため、 - // UIと永続値の不整合を防ぐべくatom状態をロールバックする - setEtaAssistManualEnabled(!flag); - console.error('Failed to save ETA assist setting', error); - Alert.alert(translate('errorTitle'), translate('failedToSavePreference')); - } - }, [ - etaAssistManualEnabled, - setEtaAssistManualEnabled, - etaAssistRemoteEnabled, - ]); - const handleToggleTelemetry = useCallback(() => { const flag = !tuning.telemetryEnabled; setTuning((prev) => ({ ...prev, telemetryEnabled: flag })); @@ -203,17 +167,6 @@ const ExperimentalSettingsScreen: React.FC = () => { {translate('telemetryDescription')} - - - - - {translate('etaAssistDescription')} - {translate('experimentalSettingsNotice')} diff --git a/src/store/atoms/experimental.ts b/src/store/atoms/experimental.ts index 295d03dfe8..6e5f12659b 100644 --- a/src/store/atoms/experimental.ts +++ b/src/store/atoms/experimental.ts @@ -3,9 +3,3 @@ import { atom } from 'jotai'; // 試験的機能のオンオフ。フィールド単位のプリミティブatomとして公開し、 // 読み取りは必ずこちらを購読する(docs/state-management.md 参照)。 export const portraitModeEnabledAtom = atom(false); - -// 地下GPS喪失時のETA位置補完(R2フォールバック)を手動で有効化するトグル。 -// リモート設定(eta_assist_enabled)だけだと手動A/Bテストがしづらいため、設定画面の -// 試験的機能から個別に切り替えられるようにする。ONのときはリモート設定より優先して -// 有効化する(isEtaAssistEnabled 参照)。 -export const etaAssistManualEnabledAtom = atom(false);