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
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import InviteGatheringContent from '@/app/invite/gathering/[gatheringId]/src/com
import InviteGatheringErrorFallback from '@/app/invite/gathering/[gatheringId]/src/components/InviteGatheringErrorFallback';
import InviteGatheringSkeleton from '@/app/invite/gathering/[gatheringId]/src/components/InviteGatheringSkeleton';
import { BackgroundTemplate, Button } from '@/shared/components';
import { useUserAgent } from '@/shared/components/provider/UserAgentProvider';
import SkeletonGuard from '@/shared/components/SkeletonGuard';
import { useParam } from '@/shared/hooks/useParam';
import { joinGatheringInAPP } from '@/shared/lib/deeplink';

const InviteGathering = () => {
const gatheringId = Number(useParam('gatheringId'));

const userAgent = useUserAgent();

return (
<BackgroundTemplate>
<BackgroundTemplate.Main className="px-5">
Expand All @@ -32,12 +36,7 @@ const InviteGathering = () => {
</QueryErrorResetBoundary>

<BackgroundTemplate.CTA>
<Button
className="w-full"
onClick={() =>
(window.location.href = `tuk-app://tuk/join-gathering?gatheringId=${gatheringId}`)
}
>
<Button className="w-full" onClick={() => joinGatheringInAPP(gatheringId, userAgent)}>
입장하기
</Button>
</BackgroundTemplate.CTA>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import InviteProposalContent from '@/app/invite/meet/[meetId]/src/components/Inv
import InviteProposalErrorFallback from '@/app/invite/meet/[meetId]/src/components/InviteProposalErrorFallback';
import InviteProposalSkeleton from '@/app/invite/meet/[meetId]/src/components/InviteProposalSkeleton';
import { BackgroundTemplate, Button } from '@/shared/components';
import { useUserAgent } from '@/shared/components/provider/UserAgentProvider';
import SkeletonGuard from '@/shared/components/SkeletonGuard';
import { useParam } from '@/shared/hooks/useParam';
import { joinProposalInAPP } from '@/shared/lib/deeplink';

const InviteProposal = () => {
const proposalId = Number(useParam('meetId'));

const userAgent = useUserAgent();

return (
<BackgroundTemplate>
<BackgroundTemplate.Main className="px-5">
Expand All @@ -32,10 +36,7 @@ const InviteProposal = () => {
</QueryErrorResetBoundary>

<BackgroundTemplate.CTA>
<Button
className="w-full"
onClick={() => (window.location.href = `tuk-app://tuk/proposal-detail/${proposalId}`)}
>
<Button className="w-full" onClick={() => joinProposalInAPP(proposalId, userAgent)}>
초대장 확인하기
</Button>
</BackgroundTemplate.CTA>
Expand Down
6 changes: 5 additions & 1 deletion apps/tuk-web/src/shared/components/AppInstallBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
'use client';

import { CloseIcon24 } from '@/shared/components/icon';
import { useUserAgent } from '@/shared/components/provider/UserAgentProvider';
import { joinHomeInAPP } from '@/shared/lib/deeplink';

const AppInstallBanner = ({ onClose }: { onClose: () => void }) => {
const userAgent = useUserAgent();

return (
<div className="fixed left-1/2 top-0 z-20 flex h-[calc(56px+env(safe-area-inset-top))] w-full max-w-[600px] -translate-x-1/2 items-center justify-between bg-gray-50 px-5 pt-[max(0px,env(safe-area-inset-top))] text-black-default shadow-[0_1px_0_0_rgba(0,0,0,0.04)] backdrop-blur-[2px]">
<div className="flex items-center gap-2">
Expand All @@ -17,7 +21,7 @@ const AppInstallBanner = ({ onClose }: { onClose: () => void }) => {

<button
className="pretendard-body-12-B rounded-[1.25rem] bg-gray-900 px-2.5 py-2 text-white-default"
onClick={() => (window.location.href = 'tuk-app://tuk')}
onClick={() => joinHomeInAPP(userAgent)}
>
앱 열기
</button>
Expand Down
116 changes: 116 additions & 0 deletions apps/tuk-web/src/shared/lib/deeplink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { UserAgent } from '@/shared/components/provider/UserAgentProvider';

const FALL_BACK_DELAY_MS = 1500;

const ANDROID_STORE_URL = 'https://play.google.com/store/apps/details?id=com.plottwist.tuk';
const IOS_STORE_URL_WEB =
'https://apps.apple.com/kr/app/%ED%88%AD-%EB%A7%8C%EB%82%8C%EC%9D%84-%EB%84%8C%EC%A7%80%EC%8B%9C/id6749781762';
const IOS_STORE_URL_APPS = 'itms-apps://itunes.apple.com/app/id6749781762';

const GATHERING_DEEP_LINK = (gatheringId: number) =>
`tuk-app://tuk/join-gathering?gatheringId=${gatheringId}`;

const PROPOSAL_DEEP_LINK = (proposalId: number) => `tuk-app://tuk/proposal-detail/${proposalId}`;

export const joinProposalInAPP = (proposalId: number, userAgent: UserAgent) => {
if (userAgent.isAndroid) {
window.location.href = generateAndroidIntentURL(
`tuk/proposal-detail/${proposalId}`,
ANDROID_STORE_URL
);

return;
}

if (userAgent.isIOS) {
appendIOSIframe(PROPOSAL_DEEP_LINK(proposalId));

return;
}

window.location.href = IOS_STORE_URL_WEB;
};

export const joinGatheringInAPP = (gatheringId: number, userAgent: UserAgent) => {
if (userAgent.isAndroid) {
window.location.href = generateAndroidIntentURL(
`tuk/join-gathering?gatheringId=${gatheringId}`,
ANDROID_STORE_URL
);
return;
}

if (userAgent.isIOS) {
appendIOSIframe(GATHERING_DEEP_LINK(gatheringId));

return;
}

window.location.href = IOS_STORE_URL_WEB;
};

export const joinHomeInAPP = (userAgent: UserAgent) => {
if (userAgent.isAndroid) {
window.location.href = generateAndroidIntentURL('tuk', ANDROID_STORE_URL);

return;
}

if (userAgent.isIOS) {
appendIOSIframe('tuk-app://tuk');

return;
}
};

const generateAndroidIntentURL = (deepLinkPath: string, playStoreURL: string) => {
return (
`intent://${deepLinkPath}` +
'#Intent;' +
'scheme=tuk-app;' +
'package=com.plottwist.tuk;' +
`S.browser_fallback_url=${encodeURIComponent(playStoreURL)};` +
'end'
);
};

const appendIOSIframe = (targetDeepLink: string) => {
let timer: number | null = null;

const cleanup = () => {
if (timer) {
window.clearTimeout(timer);
timer = null;
}

document.removeEventListener('visibilitychange', onHide, true);
window.removeEventListener('pagehide', onHide, true);
window.removeEventListener('blur', onHide, true);
const iframe = document.getElementById('__dl_iframe__');
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
};

const onHide = () => cleanup();

document.addEventListener('visibilitychange', onHide, true);
window.addEventListener('pagehide', onHide, true);
window.addEventListener('blur', onHide, true);

const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.id = '__dl_iframe__';
iframe.src = targetDeepLink;
document.body.appendChild(iframe);

timer = window.setTimeout(() => {
try {
window.location.replace(IOS_STORE_URL_APPS);
} catch {
window.location.replace(IOS_STORE_URL_WEB);
} finally {
cleanup();
}
}, FALL_BACK_DELAY_MS);

return;
};
Loading