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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ app-example
google-services.json
GoogleService-Info.plist
ios/**/GoogleService-Info.plist

# agent tooling
.omc/
6 changes: 3 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"expo": {
"name": "모아동",
"slug": "moadong-app",
"version": "1.6.0",
"version": "1.7.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "moadongapp",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"ios": {
"supportsTablet": false,
"buildNumber": "16",
"buildNumber": "17",
"googleServicesFile": "./GoogleService-Info.plist",
"bundleIdentifier": "com.moadong.moadong",
"associatedDomains": [
Expand All @@ -26,7 +26,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 16,
"versionCode": 17,
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
Expand Down
8 changes: 4 additions & 4 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 = 16;
CURRENT_PROJECT_VERSION = 17;
DEVELOPMENT_TEAM = 2QMK9GBWN6;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
Expand All @@ -430,7 +430,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.6.0;
MARKETING_VERSION = 1.7.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand All @@ -454,15 +454,15 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = app/app.entitlements;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 17;
DEVELOPMENT_TEAM = 2QMK9GBWN6;
INFOPLIST_FILE = app/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.6.0;
MARKETING_VERSION = 1.7.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down
4 changes: 2 additions & 2 deletions ios/app/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.6.0</string>
<string>1.7.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand All @@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>16</string>
<string>17</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSRequiresIPhoneOS</key>
Expand Down
12 changes: 11 additions & 1 deletion services/auth-token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,21 @@ export async function issueAccessToken(): Promise<string> {
return token;
}

let issuePromise: Promise<string> | null = null;

export async function ensureAccessToken(): Promise<string> {
const storedToken = await getStoredAccessToken();
if (storedToken) {
return storedToken;
}

return issueAccessToken();
// 첫 실행 시 부트스트랩과 웹뷰가 동시에 호출하면 서로 다른 sub/토큰이 발급되어
// 앱 신원과 웹뷰 신원이 갈린다. 발급은 항상 한 번만 수행한다.
if (!issuePromise) {
issuePromise = issueAccessToken().finally(() => {
issuePromise = null;
});
}

return issuePromise;
}
42 changes: 41 additions & 1 deletion ui/home/home-webview-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useHomeWebViewPreloadContext } from '@/contexts/home-webview-preload-context';
import { useMixpanelContext } from '@/contexts/mixpanel-context';
import { useSubscribedClubsContext } from '@/contexts/subscribed-clubs-context';
import { ensureAccessToken } from '@/services/auth-token.service';
import { appendSessionId, getWebViewUserAgent } from '@/utils/webview';
import Constants from 'expo-constants';
import { useRouter } from 'expo-router';
Expand Down Expand Up @@ -30,12 +31,50 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) {
const canGoBackRef = useRef(false);
const loadFailedRef = useRef(false);
const [loaded, setLoaded] = useState(false);
const [studentToken, setStudentToken] = useState<string | null>(null);
const [tokenResolved, setTokenResolved] = useState(false);

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

const url = sessionLoading ? null : appendSessionId(BASE_URL, sessionId);
// 웹의 첫 API 호출 전에 토큰이 준비돼 있어야 하므로, 조회가 끝난 뒤에 웹뷰를 렌더한다.
// 발급에 실패하면 주입 없이 렌더하고 웹이 자체 토큰으로 폴백한다.
useEffect(() => {
let cancelled = false;
ensureAccessToken()
.then((token) => {
if (!cancelled) setStudentToken(token);
})
.catch(() => {
if (!cancelled) setStudentToken(null);
})
.finally(() => {
if (!cancelled) setTokenResolved(true);
});

return () => {
cancelled = true;
};
}, []);

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

// 주입 스크립트는 웹뷰가 로드하는 모든 문서에서 실행되므로,
// origin 가드 없이는 외부 사이트로 이동했을 때 베어러 토큰이 노출된다.
// origin 비교는 웹뷰 안에서 한다. RN 의 URL 폴리필은 호스트 대소문자와 기본 포트를
// 정규화하지 않아 window.location.origin 과 어긋날 수 있다. 파싱에 실패하면 주입하지 않는다.
const injectedToken = studentToken
? `(function(){
try {
if (new URL(${JSON.stringify(BASE_URL)}).origin !== window.location.origin) return;
} catch (e) {
return;
}
window.__MOADONG_STUDENT_TOKEN__ = ${JSON.stringify(studentToken)};
})(); true;`
: undefined;

useEffect(() => {
if (url) {
Expand Down Expand Up @@ -195,6 +234,7 @@ export function HomeWebViewScreen({ onError }: HomeWebViewScreenProps) {
style={{ flex: 1 }}
source={{ uri: url }}
userAgent={USER_AGENT}
injectedJavaScriptBeforeContentLoaded={injectedToken}
onMessage={handleMessage}
onLoadEnd={handleLoadEnd}
onNavigationStateChange={handleNavigationStateChange}
Expand Down
Loading