refactor: remove map-screen MOCK switch; settings toggle controls mock directly

The separate runtime MOCK switch on the map screen is removed.
Now the admin-only settings toggle (MOCK_API_FEATURE_ENABLED) is the
single control point — turning it on activates mock mode immediately,
turning it off deactivates it.

- useTrainMenu: remove mockApiEnabled/setMockApiEnabled state;
  mockApiConfig now derives from mockApiFeatureEnabled alone
- WebView: use mockApiFeatureEnabled for key prop (triggers reload)
- Apps.tsx: remove MockApiToggle import and JSX usage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
harukin-expo-dev-env
2026-05-01 12:35:36 +00:00
parent a809d287a2
commit a35956848a
9 changed files with 240 additions and 63 deletions

View File

@@ -130,11 +130,9 @@ export default ({ route }) => {
allowsBackForwardNavigationGestures
ref={webViewRef}
onLoadStart={() => {
setIsLoading(true);
isLoadingRef.current = true;
setHasError(false);
hasErrorRef.current = false;
lastPongAt.current = Date.now();
maxBodyLenRef.current = 0;
blankCountRef.current = 0;
}}

View File

@@ -22,7 +22,6 @@ import { AppsWebView } from "./Apps/WebView";
import { NewMenu } from "./Apps/NewMenu";
import { MapsButton } from "./Apps/MapsButton";
import { ReloadButton } from "./Apps/ReloadButton";
import { MockApiToggle } from "./Apps/MockApiToggle";
import { useStationList } from "../stateBox/useStationList";
import { FixedPositionBox } from "./Apps/FixedPositionBox";
@@ -123,9 +122,6 @@ export default function Apps() {
{mapSwitch == "true" ? (
<>
<MockApiToggle
right={isLandscape && trainInfo.trainNum ? (width / 100) * 40 : 0}
/>
<ReloadButton
onPress={() => Updates.reloadAsync()}
right={isLandscape && trainInfo.trainNum ? (width / 100) * 40 : 0}

View File

@@ -36,7 +36,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
setTrainInfo,
injectJavascript,
injectJavascriptBeforeContentLoaded,
mockApiEnabled,
mockApiFeatureEnabled,
} = useTrainMenu();
const { remountKey, remount, processHandlers } = useWebViewRemount();
var urlcache = "";
@@ -155,7 +155,7 @@ export const AppsWebView = ({ openStationACFromEachTrainInfo }) => {
return (
<WebView
key={(isDark ? 'dark' : 'light') + (mockApiEnabled ? '-mock' : '-live') + '-' + remountKey}
key={(isDark ? 'dark' : 'light') + (mockApiFeatureEnabled ? '-mock' : '-live') + '-' + remountKey}
ref={webview}
source={{ uri: "https://train.jr-shikoku.co.jp/sp.html" }}
originWhitelist={[

View File

@@ -10,9 +10,14 @@ import { useTrainMenu } from "@/stateBox/useTrainMenu";
import { useThemeColors } from "@/lib/theme";
import {
DEFAULT_JR_DATA_SYSTEM_ENV,
getJrDataSystemTrack,
getJrDataSystemUiVariant,
JR_DATA_SYSTEM_ENV_OPTIONS,
JrDataSystemTrack,
JrDataSystemUiVariant,
JrDataSystemEnvironmentKey,
normalizeJrDataSystemEnvironment,
resolveJrDataSystemEnvironment,
} from "@/lib/jrDataSystemEnvironment";
const HUB_LOGO_PNG = require("@/assets/relationLogo/unyohub_logo.webp");
@@ -172,6 +177,21 @@ export const DataSourceSettings = () => {
const [useElesite, setUseElesite] = useState(false);
const [jrDataSystemEnv, setJrDataSystemEnv] =
useState<JrDataSystemEnvironmentKey>(DEFAULT_JR_DATA_SYSTEM_ENV);
const [jrDataSystemTrack, setJrDataSystemTrack] =
useState<JrDataSystemTrack>(
getJrDataSystemTrack(DEFAULT_JR_DATA_SYSTEM_ENV),
);
const [jrDataSystemUiVariant, setJrDataSystemUiVariant] =
useState<JrDataSystemUiVariant>(
getJrDataSystemUiVariant(DEFAULT_JR_DATA_SYSTEM_ENV),
);
const applyJrDataSystemEnv = (env: JrDataSystemEnvironmentKey) => {
setJrDataSystemEnv(env);
setJrDataSystemTrack(getJrDataSystemTrack(env));
setJrDataSystemUiVariant(getJrDataSystemUiVariant(env));
AS.setItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV, env);
};
useEffect(() => {
AS.getItem(STORAGE_KEYS.USE_UNYOHUB).then((value) => {
@@ -182,10 +202,17 @@ export const DataSourceSettings = () => {
});
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
.then((value) => {
setJrDataSystemEnv(normalizeJrDataSystemEnvironment(value));
const env = normalizeJrDataSystemEnvironment(value);
setJrDataSystemEnv(env);
setJrDataSystemTrack(getJrDataSystemTrack(env));
setJrDataSystemUiVariant(getJrDataSystemUiVariant(env));
})
.catch(() => {
setJrDataSystemEnv(DEFAULT_JR_DATA_SYSTEM_ENV);
setJrDataSystemTrack(getJrDataSystemTrack(DEFAULT_JR_DATA_SYSTEM_ENV));
setJrDataSystemUiVariant(
getJrDataSystemUiVariant(DEFAULT_JR_DATA_SYSTEM_ENV),
);
});
}, []);
@@ -199,9 +226,18 @@ export const DataSourceSettings = () => {
AS.setItem(STORAGE_KEYS.USE_ELESITE, value.toString());
};
const handleSelectJrDataSystemEnv = (value: JrDataSystemEnvironmentKey) => {
setJrDataSystemEnv(value);
AS.setItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV, value);
const handleSelectJrDataSystemTrack = (value: JrDataSystemTrack) => {
setJrDataSystemTrack(value);
const normalizedVariant = value === "experimental" ? "release" : jrDataSystemUiVariant;
setJrDataSystemUiVariant(normalizedVariant);
const env = resolveJrDataSystemEnvironment(value, normalizedVariant);
applyJrDataSystemEnv(env);
};
const handleSelectJrDataSystemUiVariant = (value: JrDataSystemUiVariant) => {
setJrDataSystemUiVariant(value);
const env = resolveJrDataSystemEnvironment(jrDataSystemTrack, value);
applyJrDataSystemEnv(env);
};
return (
@@ -272,15 +308,19 @@ export const DataSourceSettings = () => {
</View>
)}
{showDebugSelector && (
<View style={[styles.debugSection, { backgroundColor: colors.surface, borderColor: colors.borderSecondary }]}>
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>デバッグ: 投稿システム接続先</Text>
<View style={[styles.debugSection, { backgroundColor: colors.surface, borderColor: colors.borderSecondary }]}>
<Text style={[styles.debugTitle, { color: colors.textPrimary }]}>稿</Text>
<Text style={[styles.debugDescription, { color: colors.textSecondary }]}>
稿 / ChatGPT案 / Claude案で切り替えま
/
</Text>
<Text style={[styles.debugCurrentText, { color: colors.textTertiary }]}></Text>
<View style={styles.debugOptionRow}>
{JR_DATA_SYSTEM_ENV_OPTIONS.map((option) => {
const selected = jrDataSystemEnv === option.key;
{[
{ key: "production" as const, label: "本番", caption: "一般公開向け" },
{ key: "experimental" as const, label: "実験", caption: "毎日リセット" },
].map((option) => {
const selected = jrDataSystemTrack === option.key;
return (
<TouchableOpacity
key={option.key}
@@ -295,7 +335,57 @@ export const DataSourceSettings = () => {
: colors.borderSecondary,
},
]}
onPress={() => handleSelectJrDataSystemEnv(option.key)}
onPress={() => handleSelectJrDataSystemTrack(option.key)}
activeOpacity={0.8}
>
<Text
style={[
styles.debugOptionTitle,
{ color: selected ? fixed.textOnPrimary : colors.textPrimary },
]}
>
{option.label}
</Text>
<Text
style={[
styles.debugOptionCaption,
{ color: selected ? fixed.textOnPrimary : colors.textTertiary },
]}
>
{option.caption}
</Text>
</TouchableOpacity>
);
})}
</View>
<Text style={[styles.debugCurrentText, { color: colors.textTertiary }]}>UIバージョン</Text>
<View style={styles.debugOptionRow}>
{[
{ key: "release" as const, label: "リリース", caption: "安定版" },
{ key: "beta" as const, label: "ベータ", caption: "夜間ビルド" },
].map((option) => {
const selected = jrDataSystemUiVariant === option.key;
const disabled = jrDataSystemTrack === "experimental";
return (
<TouchableOpacity
key={option.key}
style={[
styles.debugOptionButton,
{
opacity: disabled ? 0.45 : 1,
backgroundColor: selected
? fixed.primary
: colors.backgroundTertiary,
borderColor: selected
? fixed.primary
: colors.borderSecondary,
},
]}
onPress={() => {
if (disabled) return;
handleSelectJrDataSystemUiVariant(option.key);
}}
activeOpacity={0.8}
>
<Text
@@ -320,7 +410,6 @@ export const DataSourceSettings = () => {
</View>
<Text style={[styles.debugCurrentText, { color: colors.textTertiary }]}>: {JR_DATA_SYSTEM_ENV_OPTIONS.find((option) => option.key === jrDataSystemEnv)?.baseUrl}</Text>
</View>
)}
</ScrollView>
</View>
);

View File

@@ -91,7 +91,7 @@ export const STORAGE_KEYS = {
/** えれサイト使用設定 */
USE_ELESITE: 'useElesite',
/** 投稿システム接続先(デバッグ用) */
/** 投稿システム接続先 */
JR_DATA_SYSTEM_ENV: 'jrDataSystemEnv',
/** えれサイトデータ */

View File

@@ -1,27 +1,42 @@
export const BACKEND_API_BASE_URLS = {
production: "https://jr-shikoku-backend-api-v1.haruk.in",
experimental: "https://jr-shikoku-backend-api-v1-beta.haruk.in",
} as const;
export const JR_DATA_SYSTEM_ENVS = {
production: {
label: "本番",
caption: "現在の本番環境",
baseUrl: "https://jr-shikoku-data-system.pages.dev",
production_release: {
label: "本番 / リリース",
caption: "一般公開向け運用",
baseUrl: "https://shikoku-railinfo.haruk.in",
track: "production",
uiVariant: "release",
backendApiBaseUrl: BACKEND_API_BASE_URLS.production,
},
// chatgpt: {
// label: "ChatGPT",
// caption: "experiment-ux-refactoring-co-3crz",
// baseUrl:
// "https://experiment-ux-refactoring-co-3crz.jr-shikoku-data-system.pages.dev",
// },
claude: {
label: "Claude",
caption: "experiment-ux-refactoring-co-6cw7",
baseUrl:
"https://experiment-ux-refactoring-co-6cw7.jr-shikoku-data-system.pages.dev",
production_beta: {
label: "本番 / ベータ",
caption: "UI検証向け運用",
baseUrl: "https://nightly.shikoku-railinfo.haruk.in",
track: "production",
uiVariant: "beta",
backendApiBaseUrl: BACKEND_API_BASE_URLS.production,
},
experimental: {
label: "実験 / 実験場",
caption: "毎日リセットされる実験環境",
baseUrl: "https://experimental.shikoku-railinfo.haruk.in",
track: "experimental",
uiVariant: "release",
backendApiBaseUrl: BACKEND_API_BASE_URLS.experimental,
},
} as const;
export type JrDataSystemEnvironmentKey = keyof typeof JR_DATA_SYSTEM_ENVS;
export const DEFAULT_JR_DATA_SYSTEM_ENV: JrDataSystemEnvironmentKey =
"production";
"production_release";
export type JrDataSystemTrack = "production" | "experimental";
export type JrDataSystemUiVariant = "release" | "beta";
export const JR_DATA_SYSTEM_ENV_OPTIONS = (
Object.entries(JR_DATA_SYSTEM_ENVS) as [
@@ -36,12 +51,56 @@ export const JR_DATA_SYSTEM_ENV_OPTIONS = (
export const normalizeJrDataSystemEnvironment = (
value: unknown,
): JrDataSystemEnvironmentKey => {
// Backward compatibility for legacy keys.
if (value === "production") return "production_release";
if (value === "chatgpt" || value === "claude") return "production_beta";
if (typeof value === "string" && value in JR_DATA_SYSTEM_ENVS) {
return value as JrDataSystemEnvironmentKey;
}
return DEFAULT_JR_DATA_SYSTEM_ENV;
};
export const resolveJrDataSystemEnvironment = (
track: JrDataSystemTrack,
uiVariant: JrDataSystemUiVariant,
): JrDataSystemEnvironmentKey => {
if (track === "experimental") {
return "experimental";
}
return uiVariant === "beta" ? "production_beta" : "production_release";
};
export const getJrDataSystemTrack = (
environment: unknown,
): JrDataSystemTrack => {
const envKey = normalizeJrDataSystemEnvironment(environment);
return JR_DATA_SYSTEM_ENVS[envKey].track;
};
export const getJrDataSystemUiVariant = (
environment: unknown,
): JrDataSystemUiVariant => {
const envKey = normalizeJrDataSystemEnvironment(environment);
return JR_DATA_SYSTEM_ENVS[envKey].uiVariant;
};
export const getBackendApiBaseUrl = (environment: unknown): string => {
const envKey = normalizeJrDataSystemEnvironment(environment);
return JR_DATA_SYSTEM_ENVS[envKey].backendApiBaseUrl;
};
export const rewriteBackendApiUrl = (url: string, environment: unknown): string => {
if (typeof url !== "string" || url.length === 0) return url;
const target = getBackendApiBaseUrl(environment);
for (const base of Object.values(BACKEND_API_BASE_URLS)) {
if (url.startsWith(base)) {
return url.replace(base, target);
}
}
return url;
};
export const rewriteJrDataSystemUrl = (
uri: string,
environment: unknown,
@@ -51,14 +110,20 @@ export const rewriteJrDataSystemUrl = (
}
const envKey = normalizeJrDataSystemEnvironment(environment);
if (envKey === DEFAULT_JR_DATA_SYSTEM_ENV) {
return uri;
}
const productionBaseUrl = JR_DATA_SYSTEM_ENVS.production.baseUrl;
const targetBaseUrl = JR_DATA_SYSTEM_ENVS[envKey].baseUrl;
return uri.startsWith(productionBaseUrl)
? uri.replace(productionBaseUrl, targetBaseUrl)
: uri;
const knownBaseUrls = [
"https://jr-shikoku-data-system.pages.dev",
JR_DATA_SYSTEM_ENVS.production_release.baseUrl,
JR_DATA_SYSTEM_ENVS.production_beta.baseUrl,
JR_DATA_SYSTEM_ENVS.experimental.baseUrl,
];
for (const baseUrl of knownBaseUrls) {
if (uri.startsWith(baseUrl)) {
return uri.replace(baseUrl, targetBaseUrl);
}
}
return uri;
};

View File

@@ -1,4 +1,4 @@
import React, { Ref, useRef, useState, useEffect } from "react";
import React, { Ref, useRef, useState, useEffect, useCallback } from "react";
import {
View,
Platform,
@@ -6,11 +6,12 @@ import {
StyleProp,
ViewStyle,
Linking,
AppState,
} from "react-native";
import { WebView } from "react-native-webview";
import Constants from "expo-constants";
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { useNavigation, useFocusEffect } from "@react-navigation/native";
import { useThemeColors } from "@/lib/theme";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useWebViewRemount } from "@/lib/useWebViewRemount";
@@ -19,6 +20,31 @@ export default function tndView() {
const { navigate, addListener, isFocused } = useNavigation();
const { fixed } = useThemeColors();
const { top } = useSafeAreaInsets();
// タブ切り替え時の自動復帰: バックグラウンドから戻ったとき remount
const bgAtRef = useRef<number | null>(null);
useEffect(() => {
const sub = AppState.addEventListener("change", (state) => {
if (state.match(/inactive|background/)) {
bgAtRef.current = Date.now();
} else if (state === "active" && bgAtRef.current !== null) {
// 10秒超はuseWebViewRemount側が処理するのでここではクリアのみ
if (Date.now() - bgAtRef.current > 10_000) bgAtRef.current = null;
}
});
return () => sub.remove();
}, []);
// タブにフォーカスが来たとき、3秒以上バックグラウンドだった場合 remount
useFocusEffect(
useCallback(() => {
if (bgAtRef.current !== null) {
const elapsed = Date.now() - bgAtRef.current;
bgAtRef.current = null;
if (elapsed > 3_000) remount();
}
}, [remount])
);
const jsa = `
document.querySelector('.sitettl').style.display = 'none';
document.querySelector('.attention').style.display = 'none';

View File

@@ -10,6 +10,10 @@ import React, {
useState,
} from "react";
import { API_ENDPOINTS, STORAGE_KEYS } from "@/constants";
import {
getBackendApiBaseUrl,
BACKEND_API_BASE_URLS,
} from "@/lib/jrDataSystemEnvironment";
const initialState = {
allTrainDiagram: {},
setAllTrainDiagram: (e) => {},
@@ -46,6 +50,16 @@ export const AllTrainDiagramProvider: FC<Props> = ({ children }) => {
setKeyList(Object.keys(allTrainDiagram));
else setKeyList([]);
}, [allTrainDiagram]);
const [backendApiBaseUrl, setBackendApiBaseUrl] = React.useState(
BACKEND_API_BASE_URLS.production,
);
React.useEffect(() => {
AS.getItem(STORAGE_KEYS.JR_DATA_SYSTEM_ENV)
.then((value) => setBackendApiBaseUrl(getBackendApiBaseUrl(value)))
.catch(() => {});
}, []);
const getTrainDiagram = () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
@@ -92,7 +106,7 @@ export const AllTrainDiagramProvider: FC<Props> = ({ children }) => {
const getCustomTrainData = () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
fetch("https://jr-shikoku-backend-api-v1.haruk.in/train-data", { signal: controller.signal })
fetch(`${backendApiBaseUrl}/train-data`, { signal: controller.signal })
.then((res) => res.json())
.then((res) => {
setAllCustomTrainData(res.data);
@@ -111,7 +125,7 @@ export const AllTrainDiagramProvider: FC<Props> = ({ children }) => {
const getTodayOperation = () => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000);
fetch(API_ENDPOINTS.OPERATION_LOGS, { signal: controller.signal })
fetch(`${backendApiBaseUrl}/operation-logs`, { signal: controller.signal })
.then((res) => res.json())
.then((res) => {
if (res.data === null) {

View File

@@ -55,9 +55,6 @@ const initialState = {
injectJavascript: "",
/** injectedJavaScriptBeforeContentLoaded用XHRインターセプター */
injectJavascriptBeforeContentLoaded: "",
/** モックAPIが有効かどうか走行位置画面のスイッチ */
mockApiEnabled: false,
setMockApiEnabled: (e: boolean) => {},
/** WebView内の公式サイトAPIに流し込むモック列車位置データ */
mockTrainPositions: null as TrainEntry[] | null,
setMockTrainPositions: (e: TrainEntry[] | null) => {},
@@ -134,7 +131,6 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
const [useEleSiteSetting, setUseEleSiteSetting] = useState("false");
// モックAPI設定
const [mockApiEnabled, setMockApiEnabled] = useState(false);
const [mockTrainPositions, setMockTrainPositions] = useState<TrainEntry[] | null>(null);
// admin専用: モックAPI検証機能の有効化永続化
const [mockApiFeatureEnabled, setMockApiFeatureEnabledState] = useState(false);
@@ -142,17 +138,12 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
const setMockApiFeatureEnabled = (value: boolean) => {
setMockApiFeatureEnabledState(value);
AS.setItem(STORAGE_KEYS.MOCK_API_FEATURE_ENABLED, value.toString());
// 機能をオフにしたらアクティブスイッチもリセット
if (!value) setMockApiEnabled(false);
// 機能をオフにしたらデータをリセット
if (!value) setMockTrainPositions(MOCK_TRAIN_POSITIONS);
};
// モックがオフになったらサンプルデータで再初期化(次回オンに備える)
useEffect(() => {
if (!mockApiEnabled) setMockTrainPositions(MOCK_TRAIN_POSITIONS);
}, [mockApiEnabled]);
const mockApiConfig: MockApiConfig | null =
mockApiFeatureEnabled && mockApiEnabled && mockTrainPositions
mockApiFeatureEnabled && mockTrainPositions
? { trainPositions: mockTrainPositions }
: null;
@@ -256,8 +247,6 @@ export const TrainMenuProvider: FC<props> = ({ children }) => {
dataSourcePermission,
injectJavascript,
injectJavascriptBeforeContentLoaded,
mockApiEnabled,
setMockApiEnabled,
mockTrainPositions,
setMockTrainPositions,
mockApiFeatureEnabled,