Skip to content
Merged
6 changes: 3 additions & 3 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const googleServicesFile =
const config: ExpoConfig = {
name: appName,
slug: 'konect-react-native',
version: '1.0.7',
version: '1.0.8',
orientation: 'portrait',
icon: './assets/images/icon.png',
scheme: 'konect',
Expand All @@ -20,13 +20,13 @@ const config: ExpoConfig = {
supportsTablet: true,
usesAppleSignIn: true,
bundleIdentifier: packageName,
buildNumber: '1010700',
buildNumber: '1010800',
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
},
},
android: {
versionCode: 1010700,
versionCode: 1010800,
package: packageName,
googleServicesFile: googleServicesFile,
},
Expand Down
172 changes: 136 additions & 36 deletions app/webview/[path].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useEffect, useCallback } from 'react';
import { useRef, useEffect, useCallback, useState } from 'react';
import {
BackHandler,
Platform,
Expand All @@ -9,7 +9,7 @@ import {
AppState,
AppStateStatus,
} from 'react-native';
import { Slot, useLocalSearchParams } from 'expo-router';
import { Slot, Stack, useLocalSearchParams } from 'expo-router';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import { SafeAreaView } from 'react-native-safe-area-context';
import CookieManager from '@preeternal/react-native-cookie-manager';
Expand All @@ -21,12 +21,37 @@ import { webUrl } from '../../constants/constants';
import { getStoredToken } from '../../utils/pushTokenStore';
import { saveAccessToken, clearAccessToken } from '../../services/nativeAuthStore';
import { registerPushToken, unregisterPushToken } from '../../services/pushTokenApi';
import { disableTimerDisplayMode, enableTimerDisplayMode } from '../../services/timerDisplayMode';

const ALLOWED_URL_SCHEMES = ['kakaotalk', 'nidlogin'];
const ALLOWED_ORIGINS = [new URL(webUrl).origin];
const NATIVE_BACK_REQUEST_EVENT = 'KONECT_NATIVE_BACK_REQUEST';

const userAgent = generateUserAgent();

function getUrlOrigin(url: string): string | null {
try {
return new URL(url).origin;
} catch {
return null;
}
}

type NativeBridgeMessage =
| { type: 'LOGIN_COMPLETE'; accessToken?: string }
| { type: 'TOKEN_REFRESH'; accessToken?: string }
| { type: 'LOGOUT' }
| { type: 'TIMER_ACTIVE'; keepAwake?: boolean; dimScreen?: boolean; brightnessLevel?: number }
| { type: 'TIMER_INACTIVE' }
| { type: 'NAVIGATE_BACK' };

interface TimerDisplayModeState {
brightnessLevel?: number;
dimScreen: boolean;
isActive: boolean;
keepAwake: boolean;
}

const injectedJavaScript = `
(function () {
const allowedOrigins = ${JSON.stringify(ALLOWED_ORIGINS)};
Expand Down Expand Up @@ -58,16 +83,34 @@ const handleOnShouldStartLoadWithRequest = ({ url }: ShouldStartLoadRequest) =>
export default function Index() {
const webViewRef = useRef<WebView>(null);
const canGoBackRef = useRef(false);
const currentOriginRef = useRef<string | null>(ALLOWED_ORIGINS[0]);
const timerDisplayModeRef = useRef<TimerDisplayModeState>({
brightnessLevel: undefined,
isActive: false,
keepAwake: true,
dimScreen: true,
});
const local = useLocalSearchParams();
const [isTimerActive, setIsTimerActive] = useState(false);

const requestWebBackConfirmation = useCallback(() => {
webViewRef.current?.injectJavaScript(
`window.dispatchEvent(new Event(${JSON.stringify(NATIVE_BACK_REQUEST_EVENT)}));true;`
);
}, []);

const handleMessage = useCallback(async (event: WebViewMessageEvent) => {
const origin = event.nativeEvent.url;
if (!origin || !ALLOWED_ORIGINS.some((allowed) => origin.startsWith(allowed))) {
const messageOrigin = getUrlOrigin(event.nativeEvent.url);
if (
messageOrigin === null ||
messageOrigin === 'null' ||
!ALLOWED_ORIGINS.includes(messageOrigin)
) {
return;
}

try {
const data = JSON.parse(event.nativeEvent.data);
const data: NativeBridgeMessage = JSON.parse(event.nativeEvent.data);
const { type } = data;

if (type === 'LOGIN_COMPLETE') {
Expand Down Expand Up @@ -110,6 +153,37 @@ export default function Index() {
}
await clearAccessToken();
console.log('LOGOUT: accessToken 삭제 완료');
} else if (type === 'TIMER_ACTIVE') {
const keepAwake = data.keepAwake !== false;
const dimScreen = data.dimScreen !== false;
const brightnessLevel = data.brightnessLevel;

timerDisplayModeRef.current = {
brightnessLevel,
isActive: true,
keepAwake,
dimScreen,
};
setIsTimerActive(true);

await enableTimerDisplayMode({ keepAwake, dimScreen, brightnessLevel });
} else if (type === 'TIMER_INACTIVE') {
timerDisplayModeRef.current = {
...timerDisplayModeRef.current,
isActive: false,
};
setIsTimerActive(false);

await disableTimerDisplayMode();
} else if (type === 'NAVIGATE_BACK') {
if (webViewRef.current && canGoBackRef.current) {
webViewRef.current.goBack();
return;
}

if (Platform.OS === 'android') {
BackHandler.exitApp();
}
}
} catch {
// JSON 파싱 실패 등 무시
Expand All @@ -119,6 +193,15 @@ export default function Index() {
useEffect(() => {
if (Platform.OS === 'android') {
const onBackPress = () => {
const isAllowedOrigin = currentOriginRef.current
? ALLOWED_ORIGINS.includes(currentOriginRef.current)
: false;

if (timerDisplayModeRef.current.isActive && isAllowedOrigin) {
requestWebBackConfirmation();
return true;
}
Comment thread
ff1451 marked this conversation as resolved.

if (webViewRef.current && canGoBackRef.current) {
webViewRef.current.goBack();
return true;
Expand All @@ -128,53 +211,70 @@ export default function Index() {
const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => subscription.remove();
}
}, []);
}, [requestWebBackConfirmation]);

useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (nextAppState === 'background' || nextAppState === 'inactive') {
CookieManager.flush();
void disableTimerDisplayMode();
return;
}

if (nextAppState === 'active' && timerDisplayModeRef.current.isActive) {
void enableTimerDisplayMode({
brightnessLevel: timerDisplayModeRef.current.brightnessLevel,
keepAwake: timerDisplayModeRef.current.keepAwake,
dimScreen: timerDisplayModeRef.current.dimScreen,
});
}
};

const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => subscription.remove();
return () => {
subscription.remove();
void disableTimerDisplayMode();
};
}, []);

if (Platform.OS === 'web') {
return <Slot />;
}

return (
<SafeAreaView
style={styles.container}
edges={Platform.OS === 'ios' ? ['top', 'left', 'right'] : undefined}
>
<StatusBar barStyle={'dark-content'} />
<WebView
ref={webViewRef}
onNavigationStateChange={(navState) => {
canGoBackRef.current = navState.canGoBack;
}}
source={{ uri: `${webUrl}/${local.path ?? ''}` }}
style={styles.webview}
javaScriptEnabled
domStorageEnabled
thirdPartyCookiesEnabled={true}
sharedCookiesEnabled={true}
userAgent={userAgent}
hideKeyboardAccessoryView={Platform.OS === 'ios'}
injectedJavaScript={injectedJavaScript}
onShouldStartLoadWithRequest={handleOnShouldStartLoadWithRequest}
setSupportMultipleWindows
onOpenWindow={(event) => {
WebBrowser.openBrowserAsync(event.nativeEvent.targetUrl);
}}
originWhitelist={['*']}
startInLoadingState
onMessage={handleMessage}
/>
</SafeAreaView>
<>
<Stack.Screen options={{ gestureEnabled: Platform.OS === 'ios' ? !isTimerActive : true }} />
<SafeAreaView
style={styles.container}
edges={Platform.OS === 'ios' ? ['top', 'left', 'right'] : undefined}
>
<StatusBar barStyle={'dark-content'} />
<WebView
ref={webViewRef}
onNavigationStateChange={(navState) => {
canGoBackRef.current = navState.canGoBack;
currentOriginRef.current = getUrlOrigin(navState.url);
}}
source={{ uri: `${webUrl}/${local.path ?? ''}` }}
style={styles.webview}
javaScriptEnabled
domStorageEnabled
thirdPartyCookiesEnabled={true}
sharedCookiesEnabled={true}
userAgent={userAgent}
hideKeyboardAccessoryView={Platform.OS === 'ios'}
injectedJavaScript={injectedJavaScript}
onShouldStartLoadWithRequest={handleOnShouldStartLoadWithRequest}
setSupportMultipleWindows
onOpenWindow={(event) => {
WebBrowser.openBrowserAsync(event.nativeEvent.targetUrl);
}}
originWhitelist={['*']}
startInLoadingState
onMessage={handleMessage}
/>
</SafeAreaView>
</>
);
}

Expand Down
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,24 @@
"dependencies": {
"@babel/runtime": "^7.28.6",
"@preeternal/react-native-cookie-manager": "^6.3.1",
"expo": "^55.0.8",
"expo": "~55.0.9",
"expo-apple-authentication": "~55.0.9",
"expo-application": "~55.0.10",
"expo-brightness": "~55.0.8",
"expo-build-properties": "~55.0.10",
"expo-constants": "~55.0.9",
"expo-device": "~55.0.10",
"expo-font": "~55.0.4",
"expo-linking": "~55.0.8",
"expo-notifications": "~55.0.13",
"expo-router": "~55.0.7",
"expo-keep-awake": "~55.0.4",
"expo-linking": "~55.0.9",
"expo-notifications": "~55.0.14",
"expo-router": "~55.0.8",
"expo-secure-store": "~55.0.9",
"expo-splash-screen": "~55.0.12",
"expo-splash-screen": "~55.0.13",
"expo-web-browser": "~55.0.10",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "^0.83.2",
"react-native": "0.83.2",
"react-native-gesture-handler": "~2.30.1",
"react-native-reanimated": "~4.2.1",
"react-native-safe-area-context": "~5.6.0",
Expand Down
Loading