Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"newArchEnabled": true,
"ios": {
"supportsTablet": false,
"buildNumber": "14",
"buildNumber": "15",
"googleServicesFile": "./GoogleService-Info.plist",
"bundleIdentifier": "com.moadong.moadong",
"associatedDomains": [
Expand All @@ -26,7 +26,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 14,
"versionCode": 15,
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
Expand Down
11 changes: 9 additions & 2 deletions app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { useHomeWebViewPreloadContext } from '@/contexts/home-webview-preload-context';
import { HomeWebViewScreen } from '@/ui/home/home-webview-screen';
import React, { Suspense, lazy, useState } from 'react';
import React, { Suspense, lazy, useCallback, useState } from 'react';

const LazyHomeScreen = lazy(() => import('@/ui/home/home-screen'));

export default function HomeTab() {
const [webViewFailed, setWebViewFailed] = useState(false);
const { markFailed } = useHomeWebViewPreloadContext();

const handleWebViewError = useCallback(() => {
markFailed();
setWebViewFailed(true);
}, [markFailed]);

if (webViewFailed) {
return (
Expand All @@ -14,5 +21,5 @@ export default function HomeTab() {
);
}

return <HomeWebViewScreen onError={() => setWebViewFailed(true)} />;
return <HomeWebViewScreen onError={handleWebViewError} />;
}
40 changes: 36 additions & 4 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { Stack, usePathname } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { getTrackingPermissionsAsync, requestTrackingPermissionsAsync } from 'expo-tracking-transparency';
Expand All @@ -12,6 +12,10 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
import { BootstrapErrorDialog } from '@/components/bootstrap-error-dialog';
import { CustomSplashScreen } from '@/components/custom-splash-screen';
import { ForceUpdateDialog } from '@/components/force-update-dialog';
import {
HomeWebViewPreloadProvider,
useHomeWebViewPreloadContext,
} from '@/contexts/home-webview-preload-context';
import { MixpanelProvider } from '@/contexts/mixpanel-context';
import { SubscribedClubsProvider } from '@/contexts/subscribed-clubs-context';
import { useFcm } from '@/hooks/use-fcm';
Expand All @@ -36,6 +40,15 @@ type BootstrapStatus = 'idle' | 'running' | 'success' | 'failed';
const EMPTY_SUBSCRIBED_CLUB_IDS: string[] = [];

export default function RootLayout() {
return (
<HomeWebViewPreloadProvider>
<RootLayoutContent />
</HomeWebViewPreloadProvider>
);
}

function RootLayoutContent() {
const pathname = usePathname();
const [showSplash, setShowSplash] = useState(true);
const [forceUpdateRequired, setForceUpdateRequired] = useState(false);
const [forceUpdateChecked, setForceUpdateChecked] = useState(false);
Expand All @@ -44,9 +57,15 @@ export default function RootLayout() {
const [bootstrapResult, setBootstrapResult] = useState<BootstrapResult | null>(null);
const bootstrapStatusRef = useRef<BootstrapStatus>('idle');
const nativeSplashHiddenRef = useRef(false);
const { isSettled: homeWebViewPreloadSettled, status: homeWebViewPreloadStatus } =
useHomeWebViewPreloadContext();

const bootstrapSucceeded = bootstrapStatus === 'success';
const shouldBlockSplash = forceUpdateRequired || !bootstrapSucceeded;
const shouldWaitForHomeWebView = pathname === '/';
const shouldBlockSplash =
forceUpdateRequired ||
!bootstrapSucceeded ||
(shouldWaitForHomeWebView && !homeWebViewPreloadSettled);

// 강제 업데이트가 필요한 경우(또는 체크 전)에는 FCM 권한 프롬프트/핸들러 설정이 뜨지 않도록 비활성화
useFcm(forceUpdateChecked && !forceUpdateRequired && bootstrapSucceeded);
Expand Down Expand Up @@ -181,11 +200,22 @@ export default function RootLayout() {
customSplashDismissed: Date.now(),
});
if (shouldBlockSplash) {
console.log('⛔️ 스플래시 유지:', { forceUpdateRequired, bootstrapStatus });
console.log('⛔️ 스플래시 유지:', {
forceUpdateRequired,
bootstrapStatus,
pathname,
homeWebViewPreloadStatus,
});
return;
}
setShowSplash(false);
}, [forceUpdateRequired, bootstrapStatus, shouldBlockSplash]);
}, [
forceUpdateRequired,
bootstrapStatus,
pathname,
homeWebViewPreloadStatus,
shouldBlockSplash,
]);

const handleRetryBootstrap = useCallback(async () => {
if (bootstrapStatus === 'running') {
Expand All @@ -205,6 +235,8 @@ export default function RootLayout() {
showSplash,
bootstrapStatus,
forceUpdateRequired,
pathname,
homeWebViewPreloadStatus,
});
}

Expand Down
27 changes: 9 additions & 18 deletions components/custom-splash-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ interface CustomSplashScreenProps {
type IntroAnimationStep = 'logoOpacity' | 'logoScale' | 'textOpacity' | 'textTranslateY';

const INTRO_ANIMATION_STEP_COUNT = 4;
const MIN_SPLASH_VISIBLE_DURATION_MS = 700;
const SPLASH_FADE_OUT_DURATION_MS = 300;

export function CustomSplashScreen({ onFinish, isReady, blockFinish = false }: CustomSplashScreenProps) {
Expand All @@ -41,7 +40,6 @@ export function CustomSplashScreen({ onFinish, isReady, blockFinish = false }: C
const [introCompleted, setIntroCompleted] = useState(false);
const hasAnimatedOnce = useRef(false);
const hasStartedFadeOut = useRef(false);
const animationStartedAt = useRef(Date.now());
const completedIntroAnimationSteps = useRef(new Set<IntroAnimationStep>());

const markIntroAnimationStepComplete = useCallback((step: IntroAnimationStep) => {
Expand All @@ -56,24 +54,19 @@ export function CustomSplashScreen({ onFinish, isReady, blockFinish = false }: C
}
}, []);

const triggerFadeOut = useCallback((delayMs: number) => {
fadeOutOpacity.value = withDelay(
delayMs,
withTiming(
0,
{ duration: SPLASH_FADE_OUT_DURATION_MS, easing: Easing.in(Easing.ease) },
(finished) => {
if (finished) {
runOnJS(onFinish)();
}
const triggerFadeOut = useCallback(() => {
fadeOutOpacity.value = withTiming(
0,
{ duration: SPLASH_FADE_OUT_DURATION_MS, easing: Easing.in(Easing.ease) },
(finished) => {
if (finished) {
runOnJS(onFinish)();
}
)
}
);
}, [fadeOutOpacity, onFinish]);

const startAnimation = useCallback(() => {
animationStartedAt.current = Date.now();

logoOpacity.value = withTiming(
1,
{
Expand Down Expand Up @@ -154,9 +147,7 @@ export function CustomSplashScreen({ onFinish, isReady, blockFinish = false }: C
}

hasStartedFadeOut.current = true;
const elapsedMs = Date.now() - animationStartedAt.current;
const remainingMinimumMs = Math.max(0, MIN_SPLASH_VISIBLE_DURATION_MS - elapsedMs);
triggerFadeOut(remainingMinimumMs);
triggerFadeOut();
}, [isReady, introCompleted, blockFinish, fadeOutOpacity, triggerFadeOut]);

const logoAnimatedStyle = useAnimatedStyle(() => ({
Expand Down
71 changes: 71 additions & 0 deletions contexts/home-webview-preload-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';

export type HomeWebViewPreloadStatus = 'idle' | 'loading' | 'ready' | 'failed';

interface HomeWebViewPreloadContextType {
status: HomeWebViewPreloadStatus;
isSettled: boolean;
markLoading: () => void;
markReady: () => void;
markFailed: () => void;
reset: () => void;
}

const HomeWebViewPreloadContext = createContext<HomeWebViewPreloadContextType | undefined>(
undefined,
);

interface HomeWebViewPreloadProviderProps {
children: React.ReactNode;
}

export function HomeWebViewPreloadProvider({
children,
}: HomeWebViewPreloadProviderProps) {
const [status, setStatus] = useState<HomeWebViewPreloadStatus>('idle');

const markLoading = useCallback(() => {
setStatus((currentStatus) => (currentStatus === 'ready' ? currentStatus : 'loading'));
}, []);

const markReady = useCallback(() => {
setStatus('ready');
}, []);

const markFailed = useCallback(() => {
setStatus('failed');
}, []);

const reset = useCallback(() => {
setStatus('idle');
}, []);

const value = useMemo(
() => ({
status,
isSettled: status === 'ready' || status === 'failed',
markLoading,
markReady,
markFailed,
reset,
}),
[markFailed, markLoading, markReady, reset, status],
);

return (
<HomeWebViewPreloadContext.Provider value={value}>
{children}
</HomeWebViewPreloadContext.Provider>
);
}

export function useHomeWebViewPreloadContext(): HomeWebViewPreloadContextType {
const context = useContext(HomeWebViewPreloadContext);
if (!context) {
throw new Error(
'useHomeWebViewPreloadContext must be used within HomeWebViewPreloadProvider',
);
}

return context;
}
2 changes: 1 addition & 1 deletion contexts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
*/

export * from './mixpanel-context';
export * from './home-webview-preload-context';
export * from './subscribed-clubs-context';

4 changes: 2 additions & 2 deletions ios/app.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = app/app.entitlements;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = 2QMK9GBWN6;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
Expand Down Expand Up @@ -454,7 +454,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = app/app.entitlements;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = 2QMK9GBWN6;
INFOPLIST_FILE = app/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
Expand Down
2 changes: 1 addition & 1 deletion ios/app/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>14</string>
<string>15</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
Expand Down
24 changes: 20 additions & 4 deletions ui/home/home-webview-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useHomeWebViewPreloadContext } from '@/contexts/home-webview-preload-context';
import { useMixpanelContext } from '@/contexts/mixpanel-context';
import { useSubscribedClubsContext } from '@/contexts/subscribed-clubs-context';
import { appendSessionId, getWebViewUserAgent } from '@/utils/webview';
Expand All @@ -20,18 +21,22 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) {
const insets = useSafeAreaInsets();
const router = useRouter();
const webViewRef = useRef<WebView>(null);
const loadFailedRef = useRef(false);
const [loaded, setLoaded] = useState(false);

const { markLoading, markReady, markFailed } = useHomeWebViewPreloadContext();
const { sessionId, isLoading: sessionLoading } = useMixpanelContext();
const { subscribedClubIds, toggleSubscribe } = useSubscribedClubsContext();

const url = sessionLoading ? null : appendSessionId(BASE_URL, sessionId);

useEffect(() => {
if (url) {
loadFailedRef.current = false;
markLoading();
console.log('[StartupTiming] homeWebViewSourceReady', Date.now());
}
}, [url]);
}, [markLoading, url]);

const sendMessage = useCallback((data: object) => {
webViewRef.current?.injectJavaScript(
Expand Down Expand Up @@ -97,8 +102,19 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) {

const handleLoadEnd = useCallback(() => {
console.log('[StartupTiming] homeWebViewLoadEnd', Date.now());
if (loadFailedRef.current) {
return;
}

markReady();
setLoaded(true);
}, []);
}, [markReady]);

const handleError = useCallback(() => {
loadFailedRef.current = true;
markFailed();
onError();
}, [markFailed, onError]);

return (
<Container style={{ paddingTop: insets.top }}>
Expand All @@ -115,8 +131,8 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) {
userAgent={USER_AGENT}
onMessage={handleMessage}
onLoadEnd={handleLoadEnd}
onError={onError}
onHttpError={onError}
onError={handleError}
onHttpError={handleError}
javaScriptEnabled
domStorageEnabled
pullToRefreshEnabled
Expand Down
Loading