diff --git a/messages/en.json b/messages/en.json
index a03f631..4325b53 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -181,8 +181,15 @@
"button": "Done"
},
"SubscriptionStopped": {
- "title": "Subscription stopped successfully!",
- "icon": "π"
+ "title": "Billing paused successfully!",
+ "icon": "βΈοΈ",
+ "description": "Your subscription remains active, but billing has been paused. You can still use all features."
+ },
+ "FreeChatActivated": {
+ "title": "Free credits added!",
+ "icon": "π",
+ "description": "You've received 1200 free credits for 30 minutes of chat!",
+ "button": "Start chatting"
},
"DatePicker": {
"year": "YYYY",
diff --git a/src/app/[locale]/(core)/retaining/free-chat-activated/page.module.scss b/src/app/[locale]/(core)/retaining/free-chat-activated/page.module.scss
new file mode 100644
index 0000000..28fe25a
--- /dev/null
+++ b/src/app/[locale]/(core)/retaining/free-chat-activated/page.module.scss
@@ -0,0 +1,27 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ gap: 24px;
+ padding: 40px 20px;
+ min-height: 60vh;
+}
+
+.title {
+ font-size: 24px;
+ line-height: 1.3;
+}
+
+.icon {
+ font-size: 60px;
+ margin: 20px 0;
+}
+
+.description {
+ font-size: 16px;
+ line-height: 1.4;
+ color: var(--text-secondary);
+ max-width: 300px;
+}
diff --git a/src/app/[locale]/(core)/retaining/free-chat-activated/page.tsx b/src/app/[locale]/(core)/retaining/free-chat-activated/page.tsx
new file mode 100644
index 0000000..a5eab30
--- /dev/null
+++ b/src/app/[locale]/(core)/retaining/free-chat-activated/page.tsx
@@ -0,0 +1,23 @@
+import { getTranslations } from "next-intl/server";
+
+import { FreeChatActivatedButton } from "@/components/domains/retaining/free-chat-activated";
+import { Typography } from "@/components/ui";
+
+import styles from "./page.module.scss";
+
+export default async function FreeChatActivated() {
+ const t = await getTranslations("FreeChatActivated");
+
+ return (
+
+
+ {t("title")}
+
+ {t("icon")}
+
+
+ {t("description")}
+
+
+ );
+}
diff --git a/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx b/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx
index c1df217..f4880ab 100644
--- a/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx
+++ b/src/components/domains/chat/GlobalNewMessagesBanner/GlobalNewMessagesBanner.tsx
@@ -15,13 +15,14 @@ import styles from "./GlobalNewMessagesBanner.module.scss";
export default function GlobalNewMessagesBanner() {
const { unreadChats } = useChats();
- // Exclude banner on chat-related and settings (profile) pages
+ // Exclude banner on chat-related, settings (profile), and retention funnel pages
const pathname = usePathname();
const locale = useLocale();
const pathnameWithoutLocale = stripLocale(pathname, locale);
const isExcluded =
pathnameWithoutLocale.startsWith(ROUTES.chat()) ||
- pathnameWithoutLocale.startsWith(ROUTES.profile());
+ pathnameWithoutLocale.startsWith(ROUTES.profile()) ||
+ pathnameWithoutLocale.startsWith("/retaining");
const hasHydrated = useAppUiStore(state => state._hasHydrated);
const { isVisibleAll } = useAppUiStore(state => state.home.newMessages);
diff --git a/src/components/domains/profile/subscriptions/CancelSubscriptionModalProvider/CancelSubscriptionModalProvider.tsx b/src/components/domains/profile/subscriptions/CancelSubscriptionModalProvider/CancelSubscriptionModalProvider.tsx
index d4446d8..b7bb934 100644
--- a/src/components/domains/profile/subscriptions/CancelSubscriptionModalProvider/CancelSubscriptionModalProvider.tsx
+++ b/src/components/domains/profile/subscriptions/CancelSubscriptionModalProvider/CancelSubscriptionModalProvider.tsx
@@ -7,14 +7,15 @@ import {
useContext,
useState,
} from "react";
+import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { Button, Typography } from "@/components/ui";
import Modal from "@/components/ui/Modal/Modal";
-import { performUserSubscriptionAction } from "@/entities/subscriptions/actions";
import { UserSubscription } from "@/entities/subscriptions/types";
import { useRetainingStore } from "@/providers/retaining-store-provider";
-import { useToast } from "@/providers/toast-provider";
+import { ROUTES } from "@/shared/constants/client-routes";
+import { ERetainingFunnel } from "@/stores/retaining-store";
import styles from "./CancelSubscriptionModalProvider.module.scss";
@@ -35,14 +36,16 @@ export default function CancelSubscriptionModalProvider({
children: ReactNode;
}) {
const t = useTranslations("Subscriptions");
- const { addToast } = useToast();
+ const router = useRouter();
const [isOpen, setIsOpen] = useState(false);
- const [isLoadingCancelButton, setIsLoadingCancelButton] =
- useState(false);
- const { setCancellingSubscription, cancellingSubscription } =
- useRetainingStore(state => state);
+ const {
+ setCancellingSubscription,
+ cancellingSubscription,
+ setRetainingData,
+ startJourney,
+ } = useRetainingStore(state => state);
const close = useCallback(() => setIsOpen(false), []);
const open = useCallback(
@@ -54,30 +57,21 @@ export default function CancelSubscriptionModalProvider({
);
const handleCancel = useCallback(async () => {
- // router.push(ROUTES.retainingFunnelCancelSubscription());
+ if (!cancellingSubscription) return;
- if (isLoadingCancelButton) return;
- setIsLoadingCancelButton(true);
-
- const response = await performUserSubscriptionAction({
- subscriptionId: cancellingSubscription?.id || "",
- action: "cancel",
+ // Set up retention funnel data with default Red funnel
+ setRetainingData({
+ funnel: ERetainingFunnel.Red,
+ cancellingSubscription,
});
- if (response?.data?.status === "success") {
- close();
- addToast({
- variant: "success",
- message: t("success_cancel_message"),
- duration: 3000,
- });
- // Data will be automatically refreshed due to cache invalidation in the action
- // No need to redirect, let the user see the updated subscription status
- }
- setIsLoadingCancelButton(false);
+ // Start journey tracking
+ startJourney(cancellingSubscription.id);
+ // Close modal and redirect to retention funnel
close();
- }, [isLoadingCancelButton, cancellingSubscription?.id, close, addToast, t]);
+ router.push(ROUTES.retainingFunnelCancelSubscription());
+ }, [cancellingSubscription, setRetainingData, startJourney, close, router]);
const handleStay = useCallback(() => {
close();
@@ -102,18 +96,10 @@ export default function CancelSubscriptionModalProvider({
-
diff --git a/src/components/domains/retaining/cancel-subscription/Buttons/Buttons.tsx b/src/components/domains/retaining/cancel-subscription/Buttons/Buttons.tsx
index 699b583..4faf356 100644
--- a/src/components/domains/retaining/cancel-subscription/Buttons/Buttons.tsx
+++ b/src/components/domains/retaining/cancel-subscription/Buttons/Buttons.tsx
@@ -25,7 +25,7 @@ export default function Buttons() {
});
const { addToast } = useToast();
- const { cancellingSubscription, setFunnel } = useRetainingStore(
+ const { cancellingSubscription, setFunnel, journey } = useRetainingStore(
state => state
);
@@ -51,6 +51,7 @@ export default function Buttons() {
const response = await performUserSubscriptionAction({
subscriptionId: cancellingSubscription?.id || "",
action: "discount_50",
+ retainingJourney: journey || undefined,
});
if (response?.data?.status === "success") {
diff --git a/src/components/domains/retaining/cancellation-of-subscription/Buttons/Buttons.tsx b/src/components/domains/retaining/cancellation-of-subscription/Buttons/Buttons.tsx
index a9d5762..3a40208 100644
--- a/src/components/domains/retaining/cancellation-of-subscription/Buttons/Buttons.tsx
+++ b/src/components/domains/retaining/cancellation-of-subscription/Buttons/Buttons.tsx
@@ -4,7 +4,7 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
-import { Spinner, Toast } from "@/components/ui";
+import { Spinner } from "@/components/ui";
import { performUserSubscriptionAction } from "@/entities/subscriptions/actions";
import { useRetainingStore } from "@/providers/retaining-store-provider";
import { useToast } from "@/providers/toast-provider";
@@ -19,9 +19,8 @@ export default function Buttons() {
const router = useRouter();
const { addToast } = useToast();
- const { cancellingSubscription } = useRetainingStore(state => state);
+ const { cancellingSubscription, journey } = useRetainingStore(state => state);
- const [isToastVisible, setIsToastVisible] = useState(false);
const [isLoadingOfferButton, setIsLoadingOfferButton] =
useState(false);
const [isLoadingCancelButton, setIsLoadingCancelButton] =
@@ -33,6 +32,7 @@ export default function Buttons() {
const response = await performUserSubscriptionAction({
subscriptionId: cancellingSubscription?.id || "",
action: "pause_60",
+ retainingJourney: journey || undefined,
});
if (response?.data?.status === "success") {
@@ -47,20 +47,17 @@ export default function Buttons() {
};
const handleCancelClick = async () => {
- if (isToastVisible || isLoadingOfferButton || isLoadingCancelButton) return;
+ if (isLoadingOfferButton || isLoadingCancelButton) return;
setIsLoadingCancelButton(true);
const response = await performUserSubscriptionAction({
subscriptionId: cancellingSubscription?.id || "",
action: "cancel",
+ retainingJourney: journey || undefined,
});
if (response?.data?.status === "success") {
- setIsToastVisible(true);
- const timer = setTimeout(() => {
- router.push(ROUTES.profile());
- }, 7000);
- return () => clearTimeout(timer);
+ return router.push(ROUTES.retainingFunnelPlanCancelled());
}
setIsLoadingCancelButton(false);
addToast({
@@ -95,11 +92,6 @@ export default function Buttons() {
)}
{t("cancel_button")}
- {isToastVisible && (
-
- {t("toast_message")}
-
- )}
>
);
}
diff --git a/src/components/domains/retaining/change-mind/Buttons/Buttons.tsx b/src/components/domains/retaining/change-mind/Buttons/Buttons.tsx
index 70373fd..34fe201 100644
--- a/src/components/domains/retaining/change-mind/Buttons/Buttons.tsx
+++ b/src/components/domains/retaining/change-mind/Buttons/Buttons.tsx
@@ -29,10 +29,18 @@ export default function Buttons({ answers }: ButtonsProps) {
const [activeAnswer, setActiveAnswer] = useState(
null
);
- const { funnel } = useRetainingStore(state => state);
+ const { funnel, addChoice } = useRetainingStore(state => state);
const handleNext = (answer: ChangeMindAnswer) => {
setActiveAnswer(answer);
+
+ // Track user choice
+ addChoice({
+ step: "change-mind",
+ choiceId: answer.id,
+ choiceTitle: answer.title,
+ });
+
const timer = setTimeout(() => {
if (funnel === ERetainingFunnel.Red) {
router.push(ROUTES.retainingFunnelStopFor30Days());
diff --git a/src/components/domains/retaining/free-chat-activated/Button/Button.module.scss b/src/components/domains/retaining/free-chat-activated/Button/Button.module.scss
new file mode 100644
index 0000000..2c1b7be
--- /dev/null
+++ b/src/components/domains/retaining/free-chat-activated/Button/Button.module.scss
@@ -0,0 +1,5 @@
+.button {
+ width: 100%;
+ max-width: 280px;
+ margin-top: 20px;
+}
diff --git a/src/components/domains/retaining/free-chat-activated/Button/Button.tsx b/src/components/domains/retaining/free-chat-activated/Button/Button.tsx
new file mode 100644
index 0000000..17f4a01
--- /dev/null
+++ b/src/components/domains/retaining/free-chat-activated/Button/Button.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+
+import { ROUTES } from "@/shared/constants/client-routes";
+
+import { RetainingButton } from "../..";
+
+import styles from "./Button.module.scss";
+
+export default function Button() {
+ const t = useTranslations("FreeChatActivated");
+ const router = useRouter();
+
+ const handleButtonClick = () => {
+ router.push(ROUTES.home());
+ };
+
+ return (
+
+ {t("button")}
+
+ );
+}
diff --git a/src/components/domains/retaining/free-chat-activated/index.ts b/src/components/domains/retaining/free-chat-activated/index.ts
new file mode 100644
index 0000000..c1f7622
--- /dev/null
+++ b/src/components/domains/retaining/free-chat-activated/index.ts
@@ -0,0 +1 @@
+export { default as FreeChatActivatedButton } from "./Button/Button";
diff --git a/src/components/domains/retaining/index.ts b/src/components/domains/retaining/index.ts
index a7002e1..77c48ed 100644
--- a/src/components/domains/retaining/index.ts
+++ b/src/components/domains/retaining/index.ts
@@ -1,4 +1,5 @@
export { default as RetainingButton } from "./Button/Button";
export { default as CheckMark } from "./CheckMark/CheckMark";
+export * from "./free-chat-activated";
export { default as Offer } from "./Offer/Offer";
export { default as RetainingStepper } from "./RetainingStepper/RetainingStepper";
diff --git a/src/components/domains/retaining/second-chance/SecondChancePage/SecondChancePage.tsx b/src/components/domains/retaining/second-chance/SecondChancePage/SecondChancePage.tsx
index 8cd4350..21dce73 100644
--- a/src/components/domains/retaining/second-chance/SecondChancePage/SecondChancePage.tsx
+++ b/src/components/domains/retaining/second-chance/SecondChancePage/SecondChancePage.tsx
@@ -35,7 +35,9 @@ export default function SecondChancePage() {
"pause_30"
);
const [isLoadingButton, setIsLoadingButton] = useState(false);
- const { funnel, cancellingSubscription } = useRetainingStore(state => state);
+ const { funnel, cancellingSubscription, journey } = useRetainingStore(
+ state => state
+ );
const handleOfferClick = (offer: "pause_30" | "free_chat_30") => {
if (isLoadingButton) return;
@@ -49,10 +51,15 @@ export default function SecondChancePage() {
const response = await performUserSubscriptionAction({
subscriptionId: cancellingSubscription?.id || "",
action: activeOffer,
+ retainingJourney: journey || undefined,
});
if (response?.data?.status === "success") {
- return router.push(ROUTES.retainingFunnelPlanCancelled());
+ if (activeOffer === "pause_30") {
+ return router.push(ROUTES.retainingFunnelSubscriptionStopped());
+ } else if (activeOffer === "free_chat_30") {
+ return router.push(ROUTES.retainingFunnelFreeChatActivated());
+ }
}
setIsLoadingButton(false);
addToast({
diff --git a/src/components/domains/retaining/stop-for-30-days/Buttons/Buttons.tsx b/src/components/domains/retaining/stop-for-30-days/Buttons/Buttons.tsx
index 579d772..9506c12 100644
--- a/src/components/domains/retaining/stop-for-30-days/Buttons/Buttons.tsx
+++ b/src/components/domains/retaining/stop-for-30-days/Buttons/Buttons.tsx
@@ -29,7 +29,9 @@ export default function Buttons() {
const [isLoadingButton, setIsLoadingButton] = useState(false);
- const { funnel, cancellingSubscription } = useRetainingStore(state => state);
+ const { funnel, cancellingSubscription, journey } = useRetainingStore(
+ state => state
+ );
const handleStopClick = async () => {
if (isLoadingButton) return;
@@ -38,6 +40,7 @@ export default function Buttons() {
const response = await performUserSubscriptionAction({
subscriptionId: cancellingSubscription?.id || "",
action: "pause_30",
+ retainingJourney: journey || undefined,
});
if (response?.data?.status === "success") {
diff --git a/src/components/domains/retaining/what-reason/Buttons/Buttons.tsx b/src/components/domains/retaining/what-reason/Buttons/Buttons.tsx
index 82b1da7..47b9582 100644
--- a/src/components/domains/retaining/what-reason/Buttons/Buttons.tsx
+++ b/src/components/domains/retaining/what-reason/Buttons/Buttons.tsx
@@ -24,7 +24,9 @@ interface ButtonsProps {
export default function Buttons({ answers }: ButtonsProps) {
const router = useRouter();
- const { setFunnel } = useRetainingStore(state => state);
+ const { setFunnel, addChoice, updateCurrentStep } = useRetainingStore(
+ state => state
+ );
const [activeAnswer, setActiveAnswer] = useState(
null
@@ -33,15 +35,34 @@ export default function Buttons({ answers }: ButtonsProps) {
const handleNext = (answer: WhatReasonAnswer) => {
setActiveAnswer(answer);
setFunnel(answer.funnel);
+
+ // Track user choice
+ addChoice({
+ step: "what-reason",
+ choiceId: answer.id,
+ choiceTitle: answer.title,
+ });
+
const timer = setTimeout(() => {
+ let nextStep = "";
+ let nextRoute = "";
+
if (answer.funnel === ERetainingFunnel.Red) {
- router.push(ROUTES.retainingFunnelSecondChance());
+ nextStep = "second-chance";
+ nextRoute = ROUTES.retainingFunnelSecondChance();
}
if (answer.funnel === ERetainingFunnel.Green) {
- router.push(ROUTES.retainingFunnelStopFor30Days());
+ nextStep = "stop-for-30-days";
+ nextRoute = ROUTES.retainingFunnelStopFor30Days();
}
if (answer.funnel === ERetainingFunnel.Purple) {
- router.push(ROUTES.retainingFunnelChangeMind());
+ nextStep = "change-mind";
+ nextRoute = ROUTES.retainingFunnelChangeMind();
+ }
+
+ if (nextStep) {
+ updateCurrentStep(nextStep);
+ router.push(nextRoute);
}
}, 1000);
return () => clearTimeout(timer);
diff --git a/src/components/layout/NavigationBar/NavigationBar.tsx b/src/components/layout/NavigationBar/NavigationBar.tsx
index 45a0aee..16e56ba 100644
--- a/src/components/layout/NavigationBar/NavigationBar.tsx
+++ b/src/components/layout/NavigationBar/NavigationBar.tsx
@@ -24,6 +24,11 @@ export default function NavigationBar() {
const pathnameWithoutLocale = stripLocale(pathname, locale);
const { totalUnreadCount } = useChats();
+ // Hide navigation bar on retaining funnel pages
+ const isRetainingFunnel = pathnameWithoutLocale.startsWith("/retaining");
+
+ if (isRetainingFunnel) return null;
+
return (