Compare commits

..

29 Commits

Author SHA1 Message Date
pennyteenycat
e724f47ea0
Merge pull request #87 from pennyteenycat/develop
Develop
2025-12-06 03:24:53 +03:00
pennyteenycat
79e903d35a
Merge pull request #84 from pennyteenycat/develop
Develop
2025-12-01 02:26:32 +01:00
pennyteenycat
e5d7b23d99
Merge pull request #82 from pennyteenycat/develop
update images
2025-11-09 01:46:55 +01:00
pennyteenycat
a60bada93f
Merge pull request #81 from pennyteenycat/develop
update ap consultation
2025-11-09 01:41:06 +01:00
pennyteenycat
b863afe77c
Merge pull request #79 from pennyteenycat/develop
fix redirect
2025-11-01 01:22:03 +01:00
pennyteenycat
be0d0952fb
Merge pull request #78 from pennyteenycat/develop
Develop
2025-10-31 22:31:20 +01:00
pennyteenycat
2c2dc846d8
Merge pull request #77 from pennyteenycat/develop
Develop
2025-10-30 03:42:31 +01:00
pennyteenycat
4f78f8bd9d
Merge pull request #69 from pennyteenycat/develop
fix
2025-10-19 02:36:10 +02:00
pennyteenycat
ff5f022cf8
Merge pull request #68 from pennyteenycat/develop
fix text
2025-10-19 02:28:11 +02:00
pennyteenycat
94da453d02
Merge pull request #67 from pennyteenycat/develop
Develop
2025-10-19 01:56:06 +02:00
pennyteenycat
17e32430dc
Merge pull request #66 from pennyteenycat/develop
Develop
2025-10-18 23:08:34 +02:00
pennyteenycat
70e79962c9
Merge pull request #64 from pennyteenycat/develop
add text soulmate
2025-10-13 18:47:49 +02:00
pennyteenycat
9b5f82bc1e
Merge pull request #63 from pennyteenycat/develop
fix portrait click
2025-10-09 19:58:51 +02:00
pennyteenycat
550b3fc98c
Merge pull request #62 from pennyteenycat/develop
Develop
2025-10-09 02:16:39 +02:00
pennyteenycat
f371c7266e
Merge pull request #61 from pennyteenycat/develop
disable auto topup after fail
2025-10-06 19:46:20 +02:00
pennyteenycat
16b8d10859
Merge pull request #60 from pennyteenycat/develop
remove renewal when cancelled
2025-10-03 04:37:26 +02:00
pennyteenycat
0766dbcac8
Merge pull request #59 from pennyteenycat/develop
PAST_DUE status is active
2025-09-26 11:45:26 +02:00
pennyteenycat
3379eab873
Merge pull request #58 from pennyteenycat/develop
add blur
2025-09-22 18:48:43 +02:00
pennyteenycat
60edce97fe
Merge pull request #57 from pennyteenycat/develop
Develop
2025-09-19 01:18:17 +02:00
pennyteenycat
60cabbaa59
Merge pull request #56 from pennyteenycat/develop
Develop
2025-09-13 23:15:53 +02:00
pennyteenycat
f1071ccb0d
Merge pull request #49 from pennyteenycat/develop
Develop
2025-09-08 00:16:18 +02:00
pennyteenycat
fefb1b74f8
Merge pull request #48 from pennyteenycat/develop
Develop
2025-09-04 23:38:16 +02:00
pennyteenycat
5e51d2c2cf
Merge pull request #45 from pennyteenycat/develop
Develop
2025-08-25 00:40:14 +02:00
pennyteenycat
e992e01223
Merge pull request #41 from pennyteenycat/develop
Develop
2025-08-19 23:47:18 +02:00
pennyteenycat
7981159da1
Merge pull request #39 from pennyteenycat/develop
Develop
2025-08-19 23:22:58 +02:00
pennyteenycat
ed83a93afe
Merge pull request #35 from pennyteenycat/develop
Develop
2025-08-08 12:56:11 +03:00
pennyteenycat
51a610eae3
Merge pull request #30 from pennyteenycat/develop
yandex metrika
2025-07-28 01:49:29 +03:00
pennyteenycat
0fe7f4b454
Merge pull request #29 from pennyteenycat/develop
Develop
2025-07-27 22:15:41 +03:00
pennyteenycat
b4ddcc574f
Merge pull request #27 from pennyteenycat/develop
Develop
2025-07-27 03:16:04 +03:00
12 changed files with 35 additions and 253 deletions

View File

@ -3,38 +3,6 @@ import { NextRequest, NextResponse } from "next/server";
import { createPaymentCheckout } from "@/entities/payment/api";
import { ROUTES } from "@/shared/constants/client-routes";
/**
* Convert base64 string to bytes (handles UTF-8 properly)
* MDN recommended approach: https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa#unicode_strings
*/
function base64ToBytes(base64: string): Uint8Array {
const binString = atob(base64);
return Uint8Array.from(binString, (m) => m.codePointAt(0) ?? 0);
}
/**
* Decode URL-safe base64 state parameter to UTM object
* Supports UTF-8 encoded content (e.g., utm_campaign=)
*/
function decodeStateParam(state: string): Record<string, string> | undefined {
if (!state) return undefined;
try {
// Restore URL-safe base64 to standard base64
let base64 = state.replace(/-/g, "+").replace(/_/g, "/");
// Add padding if needed
while (base64.length % 4) {
base64 += "=";
}
// Decode base64 to bytes, then decode UTF-8
const bytes = base64ToBytes(base64);
const json = new TextDecoder().decode(bytes);
return JSON.parse(json);
} catch {
return undefined;
}
}
export async function GET(req: NextRequest) {
const productId = req.nextUrl.searchParams.get("productId");
const placementId = req.nextUrl.searchParams.get("placementId");
@ -42,18 +10,11 @@ export async function GET(req: NextRequest) {
const fbPixels = req.nextUrl.searchParams.get("fb_pixels");
const productPrice = req.nextUrl.searchParams.get("price");
const currency = req.nextUrl.searchParams.get("currency");
const sessionId = req.nextUrl.searchParams.get("sessionId");
const state = req.nextUrl.searchParams.get("state");
// Decode state param to get UTM
const utm = state ? decodeStateParam(state) : undefined;
const data = await createPaymentCheckout({
productId: productId || "",
placementId: placementId || "",
paywallId: paywallId || "",
sessionId: sessionId || undefined,
utm,
});
let redirectUrl: URL = new URL(data?.paymentUrl || "", req.nextUrl.origin);

View File

@ -39,8 +39,6 @@ export async function GET(req: NextRequest) {
const productPrice = searchParams.get("price");
const currency = searchParams.get("currency");
const nextUrl = searchParams.get("nextUrl");
const sessionId = searchParams.get("sessionId");
const state = searchParams.get("state");
const redirectUrl = new URL(
`${nextUrl || ROUTES.payment()}`,
@ -53,8 +51,6 @@ export async function GET(req: NextRequest) {
if (fbPixels) redirectUrl.searchParams.set("fb_pixels", fbPixels);
if (productPrice) redirectUrl.searchParams.set("price", productPrice);
if (currency) redirectUrl.searchParams.set("currency", currency);
if (sessionId) redirectUrl.searchParams.set("sessionId", sessionId);
if (state) redirectUrl.searchParams.set("state", state);
const trackingCookies = extractTrackingCookiesFromUrl(req.nextUrl);

View File

@ -7,7 +7,6 @@ import { Button, Typography } from "@/components/ui";
import { BlurComponent } from "@/components/widgets";
import { ROUTES } from "@/shared/constants/client-routes";
import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate";
import { getCurrentUtmParams } from "@/shared/utils/url";
import styles from "./LandingButtonWrapper.module.scss";
@ -18,8 +17,7 @@ export default function LandingButtonWrapper() {
);
const handleContinue = () => {
const utmParams = getCurrentUtmParams();
router.push(ROUTES.emailMarketingCompatibilityV1SpecialOffer(utmParams));
router.push(ROUTES.emailMarketingCompatibilityV1SpecialOffer());
};
return (

View File

@ -7,10 +7,6 @@ import { Button, Typography } from "@/components/ui";
import { BlurComponent } from "@/components/widgets";
import { ROUTES } from "@/shared/constants/client-routes";
import { translatePathEmailMarketingCompatibilityV1 } from "@/shared/constants/translate";
import {
getSessionIdFromUrl,
getStateParamForRedirect,
} from "@/shared/utils/url";
import styles from "./SpecialOfferButtonWrapper.module.scss";
@ -31,20 +27,13 @@ export default function SpecialOfferButtonWrapper({
);
const openPaymentModal = () => {
const state = getStateParamForRedirect();
const sessionId = getSessionIdFromUrl();
const params: Record<string, string> = {
router.push(
ROUTES.payment({
productId,
placementId,
paywallId,
};
if (state) {
params.state = state;
}
if (sessionId) {
params.sessionId = sessionId;
}
router.push(ROUTES.payment(params));
})
);
};
return (

View File

@ -7,7 +7,6 @@ import { Button, Typography } from "@/components/ui";
import { BlurComponent } from "@/components/widgets";
import { ROUTES } from "@/shared/constants/client-routes";
import { translatePathEmailMarketingSoulmateV1 } from "@/shared/constants/translate";
import { getCurrentUtmParams } from "@/shared/utils/url";
import styles from "./LandingButtonWrapper.module.scss";
@ -16,8 +15,7 @@ export default function LandingButtonWrapper() {
const t = useTranslations(translatePathEmailMarketingSoulmateV1("Landing"));
const handleContinue = () => {
const utmParams = getCurrentUtmParams();
router.push(ROUTES.emailMarketingSoulmateV1SpecialOffer(utmParams));
router.push(ROUTES.emailMarketingSoulmateV1SpecialOffer());
};
return (

View File

@ -7,10 +7,6 @@ import { Button, Typography } from "@/components/ui";
import { BlurComponent } from "@/components/widgets";
import { ROUTES } from "@/shared/constants/client-routes";
import { translatePathEmailMarketingSoulmateV1 } from "@/shared/constants/translate";
import {
getSessionIdFromUrl,
getStateParamForRedirect,
} from "@/shared/utils/url";
import styles from "./SpecialOfferButtonWrapper.module.scss";
@ -31,20 +27,13 @@ export default function SpecialOfferButtonWrapper({
);
const openPaymentModal = () => {
const state = getStateParamForRedirect();
const sessionId = getSessionIdFromUrl();
const params: Record<string, string> = {
router.push(
ROUTES.payment({
productId,
placementId,
paywallId,
};
if (state) {
params.state = state;
}
if (sessionId) {
params.sessionId = sessionId;
}
router.push(ROUTES.payment(params));
})
);
};
return (

View File

@ -6,7 +6,6 @@ import { useTranslations } from "next-intl";
import { Button, Typography } from "@/components/ui";
import { usePeriod } from "@/hooks/translations/usePeriod";
import { ROUTES } from "@/shared/constants/client-routes";
import { getCurrentUtmParams } from "@/shared/utils/url";
import { PeriodType } from "@/types/period";
import styles from "./Button.module.scss";
@ -25,8 +24,7 @@ export default function SaveOffButton({
const router = useRouter();
const handleNext = () => {
const utmParams = getCurrentUtmParams();
router.push(ROUTES.secretDiscount(utmParams));
router.push(ROUTES.secretDiscount());
};
return (

View File

@ -6,10 +6,6 @@ import { useTranslations } from "next-intl";
import { Button, Typography } from "@/components/ui";
import { usePeriod } from "@/hooks/translations/usePeriod";
import { ROUTES } from "@/shared/constants/client-routes";
import {
getSessionIdFromUrl,
getStateParamForRedirect,
} from "@/shared/utils/url";
import { PeriodType } from "@/types/period";
import styles from "./Button.module.scss";
@ -34,20 +30,13 @@ export default function SecretDiscountButton({
const router = useRouter();
const handleNext = () => {
const state = getStateParamForRedirect();
const sessionId = getSessionIdFromUrl();
const params: Record<string, string> = {
router.push(
ROUTES.payment({
productId,
placementId,
paywallId,
};
if (state) {
params.state = state;
}
if (sessionId) {
params.sessionId = sessionId;
}
router.push(ROUTES.payment(params));
})
);
};
return (

View File

@ -4,8 +4,6 @@ export const CheckoutRequestSchema = z.object({
productId: z.string(),
placementId: z.string(),
paywallId: z.string(),
sessionId: z.string().optional(),
utm: z.record(z.string()).optional(),
});
export type CheckoutRequest = z.infer<typeof CheckoutRequestSchema>;

View File

@ -52,7 +52,7 @@ export const UserSchema = z.object({
signDate: z.string().nullable().optional(),
password: z.string(),
externalId: z.string().optional(),
klaviyoId: z.string().nullable().optional(),
klaviyoId: z.string().nullable(),
assistants: z.array(z.string()),
createdAt: z.string().optional(),
updatedAt: z.string(),

View File

@ -74,10 +74,8 @@ export const ROUTES = {
paymentFailed: () => createRoute(["payment", "failed"]),
// Secret Discount
saveOff: (queryParams?: Record<string, string>) =>
createRoute(["save-off"], queryParams),
secretDiscount: (queryParams?: Record<string, string>) =>
createRoute(["secret-discount"], queryParams),
saveOff: () => createRoute(["save-off"]),
secretDiscount: () => createRoute(["secret-discount"]),
// Chat
chat: (id?: string) => createRoute(["chat", id]),
@ -88,16 +86,16 @@ export const ROUTES = {
additionalPurchases: (type?: string) => createRoute(["ap", type]),
// Email Marketing Compatibility V1
emailMarketingCompatibilityV1Landing: (queryParams?: Record<string, string>) =>
createRoute([emailMarketingCompatibilityV1Prefix, "landing"], queryParams),
emailMarketingCompatibilityV1SpecialOffer: (queryParams?: Record<string, string>) =>
createRoute([emailMarketingCompatibilityV1Prefix, "special-offer"], queryParams),
emailMarketingCompatibilityV1Landing: () =>
createRoute([emailMarketingCompatibilityV1Prefix, "landing"]),
emailMarketingCompatibilityV1SpecialOffer: () =>
createRoute([emailMarketingCompatibilityV1Prefix, "special-offer"]),
// Email Marketing Soulmate V1
emailMarketingSoulmateV1Landing: (queryParams?: Record<string, string>) =>
createRoute([emailMarketingSoulmateV1Prefix, "landing"], queryParams),
emailMarketingSoulmateV1SpecialOffer: (queryParams?: Record<string, string>) =>
createRoute([emailMarketingSoulmateV1Prefix, "special-offer"], queryParams),
emailMarketingSoulmateV1Landing: () =>
createRoute([emailMarketingSoulmateV1Prefix, "landing"]),
emailMarketingSoulmateV1SpecialOffer: () =>
createRoute([emailMarketingSoulmateV1Prefix, "special-offer"]),
// // Compatibility
// compatibilities: () => createRoute(["compatibilities"]),

View File

@ -1,132 +0,0 @@
// Params that should NOT be included in state (they are passed separately)
const EXCLUDED_STATE_PARAMS = [
"paywallId",
"placementId",
"productId",
"jwtToken",
"price",
"currency",
"fb_pixels",
"sessionId",
"state",
// Tracking cookies (passed separately)
"_fbc",
"_fbp",
"_ym_uid",
"_ym_d",
"_ym_isad",
"_ym_visorc",
"yandexuid",
"ymex",
];
/**
* Parse current query params from URL
*/
export const parseQueryParams = (): Record<string, string> => {
if (typeof window === "undefined") return {};
const params = new URLSearchParams(window.location.search);
const result: Record<string, string> = {};
for (const [key, value] of params.entries()) {
result[key] = value;
}
return result;
};
/**
* Get current query params that should be passed between screens and to payment
* Includes ALL params except internal ones (productId, placementId, etc.)
* Works with utm_*, fbclid, gclid, and any other marketing params
*/
export const getCurrentQueryParams = (): Record<string, string> => {
if (typeof window === "undefined") return {};
const params = parseQueryParams();
const utmParams: Record<string, string> = {};
for (const [key, value] of Object.entries(params)) {
const isExcluded =
EXCLUDED_STATE_PARAMS.includes(key) ||
key.startsWith("_ga") ||
key.startsWith("_gid");
if (!isExcluded && value) {
utmParams[key] = value;
}
}
return utmParams;
};
/**
* Convert bytes to base64 string (handles UTF-8 properly)
* MDN recommended approach: https://developer.mozilla.org/en-US/docs/Web/API/Window/btoa#unicode_strings
*/
const bytesToBase64 = (bytes: Uint8Array): string => {
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte)
).join("");
return btoa(binString);
};
/**
* Encode params as base64 JSON for state parameter
* Uses URL-safe base64 encoding with UTF-8 support
* Handles Unicode characters (e.g., utm_campaign=)
*/
export const encodeStateParam = (params: Record<string, string>): string => {
if (Object.keys(params).length === 0) return "";
try {
const json = JSON.stringify(params);
// Encode string as UTF-8 bytes, then convert to base64
const bytes = new TextEncoder().encode(json);
const base64 = bytesToBase64(bytes)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
return base64;
} catch {
return "";
}
};
/**
* Get base64-encoded state parameter with current query params
*/
export const getStateParamForRedirect = (): string => {
const params = getCurrentQueryParams();
return encodeStateParam(params);
};
/**
* Get sessionId from current URL if present
*/
export const getSessionIdFromUrl = (): string | undefined => {
if (typeof window === "undefined") return undefined;
const params = new URLSearchParams(window.location.search);
return params.get("sessionId") || undefined;
};
/**
* Build URL with current query params preserved
*/
export const buildUrlWithQueryParams = (baseUrl: string): string => {
const params = getCurrentQueryParams();
if (Object.keys(params).length === 0) return baseUrl;
const url = new URL(baseUrl, window.location.origin);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
return url.pathname + url.search;
};
// Backward compatibility aliases
export const getCurrentUtmParams = getCurrentQueryParams;
export const buildUrlWithUtmParams = buildUrlWithQueryParams;