Merged in feat/webviews (pull request #198)

Feat/webviews

Approved-by: Michael Zetterberg
This commit is contained in:
Christel Westerberg
2024-05-30 13:51:35 +00:00
committed by Michael Zetterberg
32 changed files with 561 additions and 127 deletions

View File

@@ -1,4 +1,4 @@
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
import { firaMono, firaSans } from "@/app/fonts"
import styles from "./layout.module.css"

View File

@@ -7,7 +7,7 @@ import { findLang } from "@/constants/languages"
import { login } from "@/constants/routes/handleAuth"
import { SESSION_EXPIRED } from "@/server/errors/trpc"
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
import { firaMono, firaSans } from "@/app/fonts"
import styles from "./error.module.css"

View File

@@ -5,11 +5,10 @@ import Script from "next/script"
import TrpcProvider from "@/lib/trpc/Provider"
import { biroScriptPlus, firaMono, firaSans } from "@/app/fonts"
import AdobeScript from "@/components/Current/AdobeScript"
import VwoScript from "@/components/Current/VwoScript"
import { biroScriptPlus, firaMono, firaSans } from "./fonts"
import type { Metadata } from "next"
import type { LangParams, LayoutArgs } from "@/types/params"

View File

@@ -1,4 +1,4 @@
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
import { firaMono, firaSans } from "@/app/fonts"
import styles from "./page.module.css"

View File

@@ -0,0 +1,26 @@
import { notFound } from "next/navigation"
import AccountPage from "@/components/ContentType/Webviews/AccountPage"
import LoyaltyPage from "@/components/ContentType/Webviews/LoyaltyPage"
import {
ContentTypeWebviewParams,
LangParams,
PageArgs,
UIDParams,
} from "@/types/params"
export default async function ContentTypePage({
params,
}: PageArgs<LangParams & ContentTypeWebviewParams & UIDParams, {}>) {
switch (params.contentType) {
case "loyalty-page":
return <LoyaltyPage lang={params.lang} />
case "account-page":
return <AccountPage lang={params.lang} />
default:
const type: never = params.contentType
console.error(`Unsupported content type given: ${type}`)
notFound()
}
}

View File

@@ -0,0 +1,5 @@
.layout {
font-family: var(--ff-fira-sans);
background-color: var(--Scandic-Brand-Warm-White);
min-height: 100dvh;
}

View File

@@ -1,3 +1,12 @@
import "@/app/globals.css"
import "@scandic-hotels/design-system/style.css"
import TrpcProvider from "@/lib/trpc/Provider"
import { biroScriptPlus, firaMono, firaSans } from "@/app/fonts"
import styles from "./layout.module.css"
import type { Metadata } from "next"
import type { LangParams, LayoutArgs } from "@/types/params"
@@ -12,7 +21,11 @@ export default function RootLayout({
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
return (
<html lang={params.lang}>
<body>{children}</body>
<body
className={`${firaMono.variable} ${firaSans.variable} ${biroScriptPlus.variable} ${styles.layout}`}
>
<TrpcProvider lang={params.lang}>{children}</TrpcProvider>
</body>
</html>
)
}

View File

@@ -0,0 +1,6 @@
.container {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}

View File

@@ -0,0 +1,11 @@
import LoadingSpinner from "@/components/LoadingSpinner"
import styles from "./page.module.css"
export default function Refresh() {
return (
<div className={styles.container}>
<LoadingSpinner />
</div>
)
}

View File

@@ -1,15 +0,0 @@
import type { Metadata } from "next"
export const metadata: Metadata = {
title: "Hello World from Webview",
}
export default function WebViewTestPage() {
return (
<main>
<header>
<h1>Hello From WebView Test Page!</h1>
</header>
</main>
)
}

View File

@@ -16,7 +16,7 @@ export const firaSans = Fira_Sans({
export const biroScriptPlus = localFont({
src: [
{
path: "../../../public/_static/fonts/biro-script-plus/Biro-Script-Plus.ttf",
path: "../public/_static/fonts/biro-script-plus/Biro-Script-Plus.ttf",
style: "normal",
weight: "500",
},

View File

@@ -1,6 +1,6 @@
"use client"
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
import { firaMono, firaSans } from "@/app/fonts"
import styles from "./global-error.module.css"

View File

@@ -0,0 +1,26 @@
import "@/app/globals.css"
import "@scandic-hotels/design-system/style.css"
import { overview } from "@/constants/routes/webviews"
import { serverClient } from "@/lib/trpc/server"
import MaxWidth from "@/components/MaxWidth"
import Content from "@/components/MyPages/AccountPage/Webview/Content"
import LinkToOverview from "@/components/Webviews/LinkToOverview"
import styles from "./accountPage.module.css"
import { LangParams } from "@/types/params"
export default async function MyPages({ lang }: LangParams) {
const accountPage = await serverClient().contentstack.accountPage.get()
const linkToOverview = `/${lang}/webview${accountPage.url}` !== overview[lang]
return (
<MaxWidth className={styles.blocks} tag="main">
{linkToOverview ? <LinkToOverview lang={lang} /> : null}
<Content lang={lang} content={accountPage.content} />
</MaxWidth>
)
}

View File

@@ -0,0 +1,29 @@
import { serverClient } from "@/lib/trpc/server"
import { Blocks } from "@/components/Loyalty/Blocks/WebView"
import Sidebar from "@/components/Loyalty/Sidebar"
import MaxWidth from "@/components/MaxWidth"
import LinkToOverview from "@/components/Webviews/LinkToOverview"
import styles from "./loyaltyPage.module.css"
import { LangParams } from "@/types/params"
export default async function AboutScandicFriends({ lang }: LangParams) {
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get()
return (
<section className={styles.content}>
<LinkToOverview lang={lang} />
{loyaltyPage.sidebar ? (
<section className={styles.sidebar}>
<Sidebar blocks={loyaltyPage.sidebar} />
</section>
) : null}
<MaxWidth tag="main">
<Blocks blocks={loyaltyPage.blocks} lang={lang} />
</MaxWidth>
</section>
)
}

View File

@@ -0,0 +1,5 @@
.blocks {
display: grid;
gap: var(--Spacing-x5);
padding: var(--Spacing-x2);
}

View File

@@ -0,0 +1,10 @@
.content {
display: grid;
padding: var(--Spacing-x2);
gap: var(--Spacing-x5);
}
.sidebar {
margin-left: calc(var(--Spacing-x2) * -1);
margin-right: calc(var(--Spacing-x2) * -1);
}

View File

@@ -26,7 +26,7 @@
width: 3px;
height: 9px;
border-radius: 20%;
background: var(--Brand-Main-Strong);
background: var(--Scandic-Brand-Burgundy);
}
.spinner div:nth-child(1) {

View File

@@ -0,0 +1,54 @@
import JsonToHtml from "@/components/JsonToHtml"
import DynamicContentBlock from "@/components/Loyalty/Blocks/DynamicContent"
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
import { modWebviewLink } from "@/utils/webviews"
import CardsGrid from "../CardsGrid"
import type { BlocksProps } from "@/types/components/loyalty/blocks"
import { LoyaltyBlocksTypenameEnum } from "@/types/components/loyalty/enums"
import { LangParams } from "@/types/params"
export function Blocks({ lang, blocks }: BlocksProps & LangParams) {
return blocks.map((block) => {
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return <CardsGrid cards_grid={block.cards_grid} />
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
return (
<section>
<JsonToHtml
nodes={block.content.content.json.children}
embeds={block.content.content.embedded_itemsConnection.edges}
/>
</section>
)
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
const dynamicContent = {
...block.dynamic_content,
link: block.dynamic_content.link
? {
...block.dynamic_content.link,
href: modWebviewLink(block.dynamic_content.link.href, lang),
}
: undefined,
}
return <DynamicContentBlock dynamicContent={dynamicContent} />
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
const shortcuts = block.shortcuts.shortcuts.map((shortcut) => ({
...shortcut,
url: modWebviewLink(shortcut.url, lang),
}))
return (
<Shortcuts
shortcuts={shortcuts}
title={block.shortcuts.title}
subtitle={block.shortcuts.preamble}
/>
)
default:
return null
}
})
}

View File

@@ -0,0 +1,101 @@
import JsonToHtml from "@/components/JsonToHtml"
import Overview from "@/components/MyPages/Blocks/Overview"
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
import { modWebviewLink } from "@/utils/webviews"
import CurrentBenefitsBlock from "../../Blocks/Benefits/CurrentLevel"
import NextLevelBenefitsBlock from "../../Blocks/Benefits/NextLevel"
import CurrentPointsBalance from "../../Blocks/Points/CurrentPointsBalance"
import EarnAndBurn from "../../Blocks/Points/EarnAndBurn"
import {
AccountPageContentProps,
ContentProps,
} from "@/types/components/myPages/myPage/accountPage"
import {
ContentEntries,
DynamicContentComponents,
} from "@/types/components/myPages/myPage/enums"
function DynamicComponent({ component, props }: AccountPageContentProps) {
switch (component) {
case DynamicContentComponents.membership_overview:
return <Overview title={props.title} />
case DynamicContentComponents.current_benefits:
return <CurrentBenefitsBlock {...props} />
case DynamicContentComponents.next_benefits:
return <NextLevelBenefitsBlock {...props} />
case DynamicContentComponents.my_points:
return <CurrentPointsBalance {...props} />
case DynamicContentComponents.expiring_points:
// TODO: Add once available
// return <ExpiringPoints />
return null
case DynamicContentComponents.earn_and_burn:
return <EarnAndBurn {...props} />
default:
return null
}
}
export default function Content({ lang, content }: ContentProps) {
return (
<>
{content.map((item) => {
switch (item.__typename) {
case ContentEntries.AccountPageContentDynamicContent:
const link = item.dynamic_content.link.linkConnection.edges.length
? {
href:
item.dynamic_content.link.linkConnection.edges[0].node
.original_url ||
`/${lang}/webview${item.dynamic_content.link.linkConnection.edges[0].node.url}`,
text: item.dynamic_content.link.link_text,
}
: null
const componentProps = {
lang,
title: item.dynamic_content.title,
// TODO: rename preamble to subtitle in Contentstack?
subtitle: item.dynamic_content.preamble,
...(link && { link }),
}
return (
<DynamicComponent
component={item.dynamic_content.component}
props={componentProps}
/>
)
case ContentEntries.AccountPageContentShortcuts:
const shortcuts = item.shortcuts.shortcuts.map((shortcut) => {
return {
...shortcut,
url: modWebviewLink(shortcut.url, lang),
}
})
return (
<Shortcuts
shortcuts={shortcuts}
subtitle={item.shortcuts.preamble}
title={item.shortcuts.title}
/>
)
case ContentEntries.AccountPageContentTextContent:
return (
<section>
<JsonToHtml
embeds={
item.text_content.content.embedded_itemsConnection.edges
}
nodes={item.text_content.content.json.children}
/>
</section>
)
default:
return null
}
})}
</>
)
}

View File

@@ -1,15 +0,0 @@
import { serverClient } from "@/lib/trpc/server"
import styles from "./user.module.css"
export default async function User() {
const user = await serverClient().user.get()
return (
<div className={styles.user}>
{user.firstName[0].toUpperCase()}
{user.lastName[0].toUpperCase()}
<span className={styles.alert}>1</span>
</div>
)
}

View File

@@ -1,43 +0,0 @@
.user {
align-items: center;
background-color: var(--some-black-color, #000);
border-radius: 50%;
color: var(--some-white-color, #fff);
display: flex;
font-family: var(--ff-fira-sans);
font-size: 1.2rem;
font-weight: 600;
height: 3.5rem;
justify-content: center;
position: relative;
width: 3.5rem;
}
.alert {
align-items: center;
background-color: var(--some-red-color, #ed2027);
border-radius: 50%;
display: flex;
font-size: 1rem;
height: 2rem;
justify-content: center;
position: absolute;
right: -1rem;
top: -0.5rem;
width: 2rem;
}
@media screen and (min-width: 950px) {
.user {
height: 2.8rem;
width: 2.8rem;
}
.alert {
font-size: 0.6rem;
height: 1rem;
right: -0.2rem;
top: -0.1rem;
width: 1rem;
}
}

View File

@@ -0,0 +1,18 @@
import { ArrowLeft } from "react-feather"
import { overview } from "@/constants/routes/webviews"
import { _ } from "@/lib/translation"
import Link from "@/components/TempDesignSystem/Link"
import styles from "./linkToOverview.module.css"
import { LangParams } from "@/types/params"
export default function LinkToOverview({ lang }: LangParams) {
return (
<Link className={styles.overviewLink} href={overview[lang]}>
<ArrowLeft height={20} width={20} /> {_("Go back to overview")}
</Link>
)
}

View File

@@ -0,0 +1,7 @@
.overviewLink {
font-size: var(--Spacing-x2);
color: var(--Scandic-Brand-Burgundy, #4d001b);
display: flex;
align-items: center;
gap: 1rem;
}

View File

@@ -0,0 +1,71 @@
const myPages = {
da: "/da/webview/mine-sider",
de: "/de/webview/mein-profil",
en: "/en/webview/my-pages",
fi: "/fi/webview/minun-sivujani",
no: "/no/webview/mine-sider",
sv: "/sv/webview/mina-sidor",
}
export const overview = {
da: `${myPages.da}/oversigt`,
de: `${myPages.de}/uberblick`,
en: `${myPages.en}/overview`,
fi: `${myPages.fi}/yleiskatsaus`,
no: `${myPages.no}/oversikt`,
sv: `${myPages.sv}/oversikt`,
}
export const benefits = {
da: `${myPages.da}/fordele`,
de: `${myPages.de}/vorteile`,
en: `${myPages.en}/benefits`,
fi: `${myPages.fi}/etuja`,
no: `${myPages.no}/fordeler`,
sv: `${myPages.sv}/formaner`,
}
export const points = {
da: `${myPages.da}/points`,
de: `${myPages.de}/points`,
en: `${myPages.en}/points`,
fi: `${myPages.fi}/points`,
no: `${myPages.no}/points`,
sv: `${myPages.sv}/points`,
}
export const programOverview = {
da: `/da/webview/about-scandic-friends`,
de: `/de/webview/about-scandic-friends`,
en: `/en/webview/about-scandic-friends`,
fi: `/fi/webview/about-scandic-friends`,
no: `/no/webview/om-scandic-friends`,
sv: `/sv/webview/om-scandic-friends`,
}
const refreshUrl = {
da: `/da/webview/refresh`,
de: `/de/webview/refresh`,
en: `/en/webview/refresh`,
fi: `/fi/webview/refresh`,
no: `/no/webview/refresh`,
sv: `/sv/webview/refresh`,
}
export const webviews = [
...Object.values(benefits),
...Object.values(overview),
...Object.values(points),
...Object.values(programOverview),
...Object.values(refreshUrl),
]
export const myPagesWebviews = [
...Object.values(benefits),
...Object.values(overview),
...Object.values(points),
]
export const loyaltyPagesWebviews = [...Object.values(programOverview)]
export const refreshWebviews = [...Object.values(refreshUrl)]

View File

@@ -127,15 +127,6 @@ query GetLoyaltyPage($locale: String!, $uid: String!) {
}
}
}
web {
breadcrumbs {
title
parents {
href
title
}
}
}
system {
uid
created_at

View File

@@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server"
import { redirect } from "next/navigation"
import { Lang } from "@/constants/languages"
import { webviews } from "@/constants/routes/webviews"
import { appRouter } from "@/server"
import { createContext } from "@/server/context"
import { internalServerError } from "@/server/errors/next"
@@ -21,8 +22,28 @@ export function serverClient() {
if (error instanceof TRPCError) {
if (error.code === "UNAUTHORIZED") {
const lang = ctx?.lang || Lang.en
const pathname = ctx?.pathname || "/"
let lang = Lang.en
let pathname = "/"
let fullUrl = "/"
if (ctx) {
lang = ctx.lang
pathname = ctx.pathname
fullUrl = ctx.url
}
const fullPathname = new URL(fullUrl).pathname
if (webviews.includes(fullPathname)) {
const redirectUrl = `/${lang}/webview/refresh?returnurl=${encodeURIComponent(fullUrl)}`
console.error(
"Unautorized in webview, redirecting to: ",
redirectUrl
)
redirect(redirectUrl)
}
redirect(
`/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}`
)

View File

@@ -7,7 +7,10 @@ export function getDefaultRequestHeaders(request: NextRequest) {
const headers = new Headers(request.headers)
headers.set("x-lang", lang)
headers.set("x-pathname", request.nextUrl.pathname.replace(`/${lang}`, ""))
headers.set(
"x-pathname",
request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "")
)
headers.set("x-url", request.nextUrl.href)
return headers

View File

@@ -1,48 +1,129 @@
import { type NextMiddleware, NextResponse } from "next/server"
import { findLang } from "@/constants/languages"
import {
loyaltyPagesWebviews,
myPagesWebviews,
refreshWebviews,
webviews,
} from "@/constants/routes/webviews"
import { env } from "@/env/server"
import { badRequest, internalServerError } from "@/server/errors/next"
import { badRequest, notFound } from "@/server/errors/next"
import { decryptData } from "@/utils/aes"
import { resolve as resolveEntry } from "@/utils/entry"
import { getDefaultRequestHeaders } from "./utils"
import type { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = async (request) => {
const { nextUrl } = request
const lang = findLang(nextUrl.pathname)
// If user is redirected to /lang/webview/refresh/, the webview token is invalid and we remove the cookie
if (refreshWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(
`/${lang}/webview/refresh?${nextUrl.searchParams.toString()}`,
nextUrl
),
{
headers: {
"Set-Cookie": `webviewToken=0; Max-Age=0; Secure; HttpOnly; Path=/; SameSite=Strict;`,
},
}
)
}
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}/webview`, "")
const { uid } = await resolveEntry(pathNameWithoutLang, lang)
if (!uid) {
throw notFound(
`Unable to resolve CMS entry for locale "${lang}": ${pathNameWithoutLang}`
)
}
const headers = getDefaultRequestHeaders(request)
headers.set("x-uid", uid)
const webviewToken = request.cookies.get("webviewToken")
if (webviewToken) {
// since the token exists, this is a subsequent visit
// we're done, allow it
return NextResponse.next()
}
// Authorization header is required for webviews
// It should be base64 encoded
const authorization = request.headers.get("Authorization")!
if (!authorization) {
return badRequest()
}
// Initialization vector header is required for webviews
// It should be base64 encoded
const initializationVector = request.headers.get("X-AES-IV")!
if (!initializationVector) {
return badRequest()
if (myPagesWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/account-page/${uid}`, nextUrl),
{
request: {
headers,
},
}
)
} else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/loyalty-page/${uid}`, nextUrl),
{
request: {
headers,
},
}
)
} else {
return notFound()
}
}
try {
// Authorization header is required for webviews
// It should be base64 encoded
const authorization = request.headers.get("X-Authorization")!
if (!authorization) {
console.error("Authorization header is missing")
return badRequest("Authorization header is missing")
}
// Initialization vector header is required for webviews
// It should be base64 encoded
const initializationVector = request.headers.get("X-AES-IV")!
if (!initializationVector) {
console.error("initializationVector header is missing")
return badRequest("initializationVector header is missing")
}
const decryptedData = await decryptData(
env.WEBVIEW_ENCRYPTION_KEY,
initializationVector,
authorization
)
// Pass the webview token via cookie to the page
return NextResponse.next({
headers: {
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly;`,
},
})
headers.append("Cookie", `webviewToken=${decryptedData}`)
if (myPagesWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/account-page/${uid}`, nextUrl),
{
headers: {
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
},
request: {
headers,
},
}
)
} else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/loyalty-page/${uid}`, nextUrl),
{
headers: {
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
},
request: {
headers,
},
}
)
}
} catch (e) {
if (e instanceof Error) {
console.error(`${e.name}: ${e.message}`)
@@ -54,7 +135,6 @@ export const middleware: NextMiddleware = async (request) => {
export const matcher: MiddlewareMatcher = (request) => {
const { nextUrl } = request
const lang = findLang(nextUrl.pathname)
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")
return pathNameWithoutLang.startsWith("/webview/")
return webviews.includes(nextUrl.pathname)
}

View File

@@ -1,15 +1,21 @@
import { headers } from "next/headers"
import { cookies, headers } from "next/headers"
import { type Session } from "next-auth"
import { Lang } from "@/constants/languages"
import { auth } from "@/auth"
import { unauthorizedError } from "./errors/trpc"
typeof auth
type CreateContextOptions = {
auth: typeof auth
auth: () => Promise<Session>
lang: Lang
pathname: string
uid?: string | null
url: string
webToken?: string
}
/** Use this helper for:
@@ -23,6 +29,7 @@ export function createContextInner(opts: CreateContextOptions) {
pathname: opts.pathname,
uid: opts.uid,
url: opts.url,
webToken: opts.webToken,
}
}
@@ -33,12 +40,24 @@ export function createContextInner(opts: CreateContextOptions) {
export function createContext() {
const h = headers()
const cookie = cookies()
const webviewTokenCookie = cookie.get("webviewToken")
return createContextInner({
auth,
auth: async () => {
const session = await auth()
const webToken = webviewTokenCookie?.value
if (!session?.token && !webToken) {
throw unauthorizedError()
}
return session || ({ token: { access_token: webToken } } as Session)
},
lang: h.get("x-lang") as Lang,
pathname: h.get("x-pathname")!,
uid: h.get("x-uid"),
url: h.get("x-url")!,
webToken: webviewTokenCookie?.value,
})
}

View File

@@ -30,7 +30,6 @@ export const contentstackProcedure = t.procedure.use(async function (opts) {
export const protectedProcedure = t.procedure.use(async function (opts) {
const authRequired = opts.meta?.authRequired ?? true
const session = await opts.ctx.auth()
if (!authRequired && env.NODE_ENV === "development") {
console.info(
`❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌`
@@ -42,10 +41,6 @@ export const protectedProcedure = t.procedure.use(async function (opts) {
throw sessionExpiredError()
}
if (!session?.user) {
throw unauthorizedError()
}
return opts.next({
ctx: {
session,

View File

@@ -20,6 +20,10 @@ export type ContentTypeParams = {
contentType: "loyalty-page" | "content-page"
}
export type ContentTypeWebviewParams = {
contentType: "loyalty-page" | "account-page"
}
export type UIDParams = {
uid: string
}

13
utils/webviews.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Lang } from "@/constants/languages"
import { webviews } from "@/constants/routes/webviews"
export function modWebviewLink(url: string, lang: Lang) {
const urlWithoutLang = url.replace(`/${lang}`, "")
const webviewUrl = `/${lang}/webview${urlWithoutLang}`
if (webviews.includes(webviewUrl) || url.startsWith("/webview")) {
return webviewUrl
} else {
return url
}
}