Skip to content

Instantly share code, notes, and snippets.

@TanjinAlam
Created August 21, 2025 20:17
Show Gist options
  • Select an option

  • Save TanjinAlam/8abc18991dfe3dffd8d56c5d6c8be083 to your computer and use it in GitHub Desktop.

Select an option

Save TanjinAlam/8abc18991dfe3dffd8d56c5d6c8be083 to your computer and use it in GitHub Desktop.
{
"_id" : ObjectId("68a6ae2d6e2b9ed1415eb72a"),
"filePath" : "resources/js/components/Onboarding/OnboardingPlans.vue",
"lineStart" : NumberInt(118),
"lineEnd" : NumberInt(147),
"content" : "**Issue**: Function getButtonLabel() has complex conditional logic but doesn't return a default value, which could result in undefined button text.\n\n**Suggestion**: Add a default return value to ensure the button always has text:\n\n```\nfunction getButtonLabel() {\n if (\n props.hasActiveSubscription &&\n props.currentPlan &&\n !props.currentPlan?.is_plan_for_advertising\n ) {\n if (\n props.currentPlan?.interval === 'monthly' &&\n props.stripeMeta.plan.interval === 'yearly'\n ) {\n return 'Upgrade'\n }\n if (\n props.currentPlan?.interval === 'yearly' &&\n props.stripeMeta.plan.interval === 'monthly'\n ) {\n return 'Downgrade'\n }\n if (\n Number(props.currentPlan?.price || 0) <\n Number(props.stripeMeta.plan.price)\n ) {\n return 'Upgrade'\n }\n if (\n Number(props.currentPlan?.price || 0) >\n Number(props.stripeMeta.plan.price)\n ) {\n return 'Downgrade'\n }\n }\n return 'Continue'\n}\n```",
"codeSnippet" : "function getButtonLabel() {\n if (\n props.hasActiveSubscription &&\n props.currentPlan &&\n !props.currentPlan?.is_plan_for_advertising\n ) {\n if (\n props.currentPlan?.interval === 'monthly' &&\n props.stripeMeta.plan.interval === 'yearly'\n ) {\n return 'Upgrade'\n }\n if (\n props.currentPlan?.interval === 'yearly' &&\n props.stripeMeta.plan.interval === 'monthly'\n ) {\n return 'Downgrade'\n }\n if (\n Number(props.currentPlan?.price || 0) <\n Number(props.stripeMeta.plan.price)\n ) {\n return 'Upgrade'\n }\n if (\n Number(props.currentPlan?.price || 0) >\n Number(props.stripeMeta.plan.price)\n ) {\n return 'Downgrade'\n }\n }\n}",
"codeSnippetLineStart" : NumberInt(118),
"severity" : "Critical",
"category" : "Logic",
"pullRequestAnalysisId" : ObjectId("68a6adfb6e2b9ed1415eb71e"),
"createdAt" : ISODate("2025-08-21T05:27:09.029+0000"),
"updatedAt" : ISODate("2025-08-21T05:27:09.029+0000")
}
//event
{
"_id" : ObjectId("68a6ae486e2b9ed1415eb748"),
"provider" : "bitbucket",
"prId" : "1750",
"prUser" : "Wasiul Islam",
"prUserAvatar" : "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/6119f6119fd388006a489097/526ead1f-3385-4c7e-bb76-2721dde22ebe/128",
"owner" : "melioraweb",
"repo" : "GetHookdAI",
"prNumber" : "1750",
"prUrl" : "https://bitbucket.org/melioraweb/gethookdai/pull-requests/1750",
"installationId" : "bitbucket_integration",
"prRepoName" : "melioraweb/gethookdai",
"prTitle" : "Feature/GAI-2379 update plan page fe",
"prBody" : "* GAI-2379 change button text, show banner only when loading\n* updated onboarding based on reagan's feedback\n\n‌\n\n[ ](https://coderabbit.ai/<!-- This is an auto-generated comment: release notes by coderabbit.ai -->)\n\n## Summary by CodeRabbit\n\n- New Features\n - Checkout button now dynamically shows Upgrade or Downgrade based on your current subscription and selected plan/interval.\n - Checkout flow adapts to your existing subscription to tailor actions.\n- Bug Fixes\n - “Please wait” notice now only appears while the checkout is submitting.\n- UI/Copy Updates\n - Recap step retitled to “Book Your Free Onboarding Call (Optional)” with simplified, clearer copy.\n - Minor layout/spacing improvements with no functional changes.\n\n[ ](https://coderabbit.ai/<!-- end of auto-generated comment: release notes by coderabbit.ai -->)",
"prState" : "OPEN",
"prCreatedAt" : "2025-08-21T05:26:02.853913+00:00",
"prUpdatedAt" : "2025-08-21T05:27:22.262572+00:00",
"prClosedAt" : "",
"prMergedAt" : "",
"prHeadBranch" : "feature/GAI-2379-update-plan-page-fe",
"prBaseBranch" : "staging",
"prHeadSha" : "4a370802c82e",
"prBaseSha" : "e0b6ad8f2b3d",
"prFilesChanged" : NumberInt(4),
"prFiles" : [
{
"prFileName" : "resources/js/components/Checkout/CheckoutForm.vue",
"prFileStatus" : "modified",
"prFileAdditions" : NumberInt(41),
"prFileDeletions" : NumberInt(1),
"prFileChanges" : NumberInt(42),
"prFileContentBefore" : "<script setup>\nimport { Api } from '@/api'\nimport CheckoutMessages from '@/components/Checkout/CheckoutMessages.vue'\nimport mixpanel from '@/vendors/mixpanel'\nimport { loadStripe } from '@stripe/stripe-js'\nimport { useColorMode } from '@vueuse/core'\nimport { computed, onMounted, ref } from 'vue'\n\nconst props = defineProps({\n stripeMeta: {\n type: Object,\n default: null,\n },\n})\n\nconst emit = defineEmits(['close'])\n\nconst colorMode = useColorMode()\n\nlet stripe\nlet elements\n\nconst intervalCode = {\n monthly: 'month',\n yearly: 'year',\n}\n\n// refs\nconst isSubmitting = ref(false)\nconst isInitialized = ref(false)\nconst messages = ref([])\n\nasync function handleSubmit() {\n mixpanel.track('Clicked Continue button on plans checkout form')\n\n if (props.stripeMeta.proration_datetime) {\n isSubmitting.value = true\n try {\n const res = await Api.post(`/api/subscription/update`, {\n plan_id: props.stripeMeta.plan.id,\n proration_datetime: props.stripeMeta.proration_datetime,\n proration_amount: props.stripeMeta.proration_amount,\n })\n } catch (err) {\n } finally {\n setTimeout(async () => {\n window.location = window.location.origin + '/plans?status=success'\n }, 5000)\n }\n return\n }\n\n if (!isInitialized.value || isSubmitting.value) return\n\n isSubmitting.value = true\n\n const { error } = await stripe.confirmSetup({\n elements,\n confirmParams: {\n return_url: `${window.location.origin}/plans?publishable_key=${props.stripeMeta.stripePublicKey}&planId=${props.stripeMeta.plan.id}`,\n },\n })\n\n if (error.type === 'card_error' || error.type === 'validation_error') {\n messages.value = [error.message]\n } else {\n messages.value = ['An unexpected error occured.']\n }\n\n isSubmitting.value = false\n}\n\nonMounted(async () => {\n if (props.stripeMeta.proration_datetime) return\n\n stripe = await loadStripe(props.stripeMeta.stripePublicKey)\n const appearance = {\n rules: {\n '.Label': {\n color: colorMode.value === 'light' ? '#051524' : '#E7E9EB',\n },\n '.Error': {\n color: '#F55',\n },\n },\n\n variables: {\n borderRadius: '8px',\n },\n }\n\n elements = stripe.elements({\n clientSecret: props.stripeMeta.clientSecret,\n appearance,\n })\n const paymentElement = elements.create('payment')\n paymentElement.mount('#payment-element')\n\n const linkAuthenticationElement = elements.create('linkAuthentication')\n linkAuthenticationElement.mount('#link-authentication-element')\n\n isInitialized.value = true\n})\n\nconst newPlanInterval = computed(\n () => intervalCode[props.stripeMeta.new_plan.interval]\n)\n\nconst newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n</script>\n\n<template>\n <section\n class=\"mt-8 bg-white p-[32px] rounded-[8px] flex flex-col gap-4 max-w-lg mx-auto items-center\"\n >\n <button\n class=\"cursor-pointer underline underline-offset-4 text-gray-500\"\n @click=\"emit('close')\"\n >\n Go back\n </button>\n\n <div\n class=\"flex flex-col flex-wrap self-start justify-between w-full my-3 gap-2 text-[#3F444D]\"\n >\n <div>\n <span class=\"font-bold\">Plan: </span>{{ stripeMeta.plan.title }}\n </div>\n\n <div v-if=\"stripeMeta.total && stripeMeta.total > 0\">\n <span class=\"font-bold\">Subtotal: </span> ${{ stripeMeta.total }} will\n be paid now and then ${{ newPlanPrice }} will be charged\n {{ newPlanInterval === 'month' ? 'monthly' : 'yearly' }}\n </div>\n\n <div v-else-if=\"stripeMeta.total && stripeMeta.total < 0\">\n <span class=\"font-bold\">Subtotal: </span> ${{\n Math.abs(stripeMeta.total)\n }}\n will be refunded now and then ${{ newPlanPrice }} will be charged\n {{ newPlanInterval === 'month' ? 'monthly' : 'yearly' }}\n </div>\n\n <div v-else>Subtotal: ${{ newPlanPrice }}/{{ newPlanInterval }}</div>\n </div>\n\n <form\n id=\"payment-form\"\n @submit.prevent=\"handleSubmit\"\n class=\"w-full flex flex-col\"\n >\n <div id=\"link-authentication-element\" class=\"\" />\n <div id=\"payment-element\" class=\"mt-4\" />\n\n <div\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n Please wait and do not close this page...\n </div>\n </div>\n\n <button\n id=\"submit\"\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n </button>\n\n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\n </form>\n </section>\n</template>\n\n<style scoped></style>\n",
"prFileContentAfter" : "<script setup>\nimport { Api } from '@/api'\nimport CheckoutMessages from '@/components/Checkout/CheckoutMessages.vue'\nimport mixpanel from '@/vendors/mixpanel'\nimport { loadStripe } from '@stripe/stripe-js'\nimport { useColorMode } from '@vueuse/core'\nimport { computed, onMounted, ref } from 'vue'\n\nconst props = defineProps({\n stripeMeta: {\n type: Object,\n default: null,\n },\n hasActiveSubscription: {\n type: Boolean,\n },\n currentPlan: {\n type: Object,\n },\n})\n\nconst emit = defineEmits(['close'])\n\nconst colorMode = useColorMode()\n\nlet stripe\nlet elements\n\nconst intervalCode = {\n monthly: 'month',\n yearly: 'year',\n}\n\n// refs\nconst isSubmitting = ref(false)\nconst isInitialized = ref(false)\nconst messages = ref([])\n\nasync function handleSubmit() {\n mixpanel.track('Clicked Continue button on plans checkout form')\n\n if (props.stripeMeta.proration_datetime) {\n isSubmitting.value = true\n try {\n const res = await Api.post(`/api/subscription/update`, {\n plan_id: props.stripeMeta.plan.id,\n proration_datetime: props.stripeMeta.proration_datetime,\n proration_amount: props.stripeMeta.proration_amount,\n })\n } catch (err) {\n } finally {\n setTimeout(async () => {\n window.location = window.location.origin + '/plans?status=success'\n }, 5000)\n }\n return\n }\n\n if (!isInitialized.value || isSubmitting.value) return\n\n isSubmitting.value = true\n\n const { error } = await stripe.confirmSetup({\n elements,\n confirmParams: {\n return_url: `${window.location.origin}/plans?publishable_key=${props.stripeMeta.stripePublicKey}&planId=${props.stripeMeta.plan.id}`,\n },\n })\n\n if (error.type === 'card_error' || error.type === 'validation_error') {\n messages.value = [error.message]\n } else {\n messages.value = ['An unexpected error occured.']\n }\n\n isSubmitting.value = false\n}\n\nonMounted(async () => {\n if (props.stripeMeta.proration_datetime) return\n\n stripe = await loadStripe(props.stripeMeta.stripePublicKey)\n const appearance = {\n rules: {\n '.Label': {\n color: colorMode.value === 'light' ? '#051524' : '#E7E9EB',\n },\n '.Error': {\n color: '#F55',\n },\n },\n\n variables: {\n borderRadius: '8px',\n },\n }\n\n elements = stripe.elements({\n clientSecret: props.stripeMeta.clientSecret,\n appearance,\n })\n const paymentElement = elements.create('payment')\n paymentElement.mount('#payment-element')\n\n const linkAuthenticationElement = elements.create('linkAuthentication')\n linkAuthenticationElement.mount('#link-authentication-element')\n\n isInitialized.value = true\n})\n\nconst newPlanInterval = computed(\n () => intervalCode[props.stripeMeta.new_plan.interval]\n)\n\nconst newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n\nfunction getButtonLabel() {\n if (\n props.hasActiveSubscription &&\n props.currentPlan &&\n !props.currentPlan?.is_plan_for_advertising\n ) {\n if (\n props.currentPlan?.interval === 'monthly' &&\n props.stripeMeta.plan.interval === 'yearly'\n ) {\n return 'Upgrade'\n }\n if (\n props.currentPlan?.interval === 'yearly' &&\n props.stripeMeta.plan.interval === 'monthly'\n ) {\n return 'Downgrade'\n }\n if (\n Number(props.currentPlan?.price || 0) <\n Number(props.stripeMeta.plan.price)\n ) {\n return 'Upgrade'\n }\n if (\n Number(props.currentPlan?.price || 0) >\n Number(props.stripeMeta.plan.price)\n ) {\n return 'Downgrade'\n }\n }\n}\n</script>\n\n<template>\n <section\n class=\"mt-8 bg-white p-[32px] rounded-[8px] flex flex-col gap-4 max-w-lg mx-auto items-center\"\n >\n <button\n class=\"cursor-pointer underline underline-offset-4 text-gray-500\"\n @click=\"emit('close')\"\n >\n Go back\n </button>\n\n <div\n class=\"flex flex-col flex-wrap self-start justify-between w-full my-3 gap-2 text-[#3F444D]\"\n >\n <div>\n <span class=\"font-bold\">Plan: </span>{{ stripeMeta.plan.title }}\n </div>\n\n <div v-if=\"stripeMeta.total && stripeMeta.total > 0\">\n <span class=\"font-bold\">Subtotal: </span> ${{ stripeMeta.total }} will\n be paid now and then ${{ newPlanPrice }} will be charged\n {{ newPlanInterval === 'month' ? 'monthly' : 'yearly' }}\n </div>\n\n <div v-else-if=\"stripeMeta.total && stripeMeta.total < 0\">\n <span class=\"font-bold\">Subtotal: </span> ${{\n Math.abs(stripeMeta.total)\n }}\n will be refunded now and then ${{ newPlanPrice }} will be charged\n {{ newPlanInterval === 'month' ? 'monthly' : 'yearly' }}\n </div>\n\n <div v-else>Subtotal: ${{ newPlanPrice }}/{{ newPlanInterval }}</div>\n </div>\n\n <form\n id=\"payment-form\"\n @submit.prevent=\"handleSubmit\"\n class=\"w-full flex flex-col\"\n >\n <div id=\"link-authentication-element\" class=\"\" />\n <div id=\"payment-element\" class=\"mt-4\" />\n\n <div\n v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n Please wait and do not close this page...\n </div>\n </div>\n\n <button\n id=\"submit\"\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n\n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\n </form>\n </section>\n</template>\n\n<style scoped></style>\n",
"prFileDiff" : "diff --git a/resources/js/components/Checkout/CheckoutForm.vue b/resources/js/components/Checkout/CheckoutForm.vue\nindex 41566ee43e..645bc2a1b5 100644\n--- a/resources/js/components/Checkout/CheckoutForm.vue\n+++ b/resources/js/components/Checkout/CheckoutForm.vue\n@@ -11,6 +11,12 @@ const props = defineProps({\n type: Object,\n default: null,\n },\n+ hasActiveSubscription: {\n+ type: Boolean,\n+ },\n+ currentPlan: {\n+ type: Object,\n+ },\n })\n \n const emit = defineEmits(['close'])\n@@ -107,6 +113,39 @@ const newPlanInterval = computed(\n )\n \n const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n+\n+function getButtonLabel() {\n+ if (\n+ props.hasActiveSubscription &&\n+ props.currentPlan &&\n+ !props.currentPlan?.is_plan_for_advertising\n+ ) {\n+ if (\n+ props.currentPlan?.interval === 'monthly' &&\n+ props.stripeMeta.plan.interval === 'yearly'\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ props.currentPlan?.interval === 'yearly' &&\n+ props.stripeMeta.plan.interval === 'monthly'\n+ ) {\n+ return 'Downgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) <\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) >\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Downgrade'\n+ }\n+ }\n+}\n </script>\n \n <template>\n@@ -153,6 +192,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n <div id=\"payment-element\" class=\"mt-4\" />\n \n <div\n+ v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n@@ -165,7 +205,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n- {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n+ {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n \n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\ndiff --git a/resources/js/components/Onboarding/OnboardingPlans.vue b/resources/js/components/Onboarding/OnboardingPlans.vue\nindex f0c21c8d43..fc2879227f 100644\n--- a/resources/js/components/Onboarding/OnboardingPlans.vue\n+++ b/resources/js/components/Onboarding/OnboardingPlans.vue\n@@ -272,18 +272,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n@@ -692,6 +692,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\ndiff --git a/resources/js/components/Onboarding/RecapStep.vue b/resources/js/components/Onboarding/RecapStep.vue\nindex 02b12652ae..eaf25500e2 100644\n--- a/resources/js/components/Onboarding/RecapStep.vue\n+++ b/resources/js/components/Onboarding/RecapStep.vue\n@@ -73,7 +73,6 @@ async function handleAccess() {\n console.error(err)\n }\n }\n-\n </script>\n \n <template>\n@@ -87,7 +86,9 @@ async function handleAccess() {\n </div>\n \n <div class=\"flex flex-col gap-2\">\n- <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n+ <h1 class=\"text-2xl font-bold\">\n+ Book Your Free Onboarding Call (Optional)\n+ </h1>\n \n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n@@ -103,9 +104,12 @@ async function handleAccess() {\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n-\n >\n- <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n+ <span\n+ v-if=\"feature.is_new\"\n+ class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n+ >New</span\n+ >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n@@ -119,7 +123,9 @@ async function handleAccess() {\n )}`\"\n >\n {{ feature.name }}\n- <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n+ <span v-if=\"feature.count\" class=\"text-[#979797]\"\n+ >({{ feature.count }})</span\n+ >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n@@ -138,8 +144,8 @@ async function handleAccess() {\n </div>\n \n <p class=\"text-lg font-semibold mb-4\">\n- It’s time to register for the Gethookd Webinar hosted by my 7-figure\n- e-commerce success coach, where we’ll give you:\n+ It’s time to register for your free onboarding call where one of my\n+ advisors will make sure you got access to the software, AND give you:\n </p>\n \n <ul class=\"flex flex-col gap-3\">\n@@ -170,7 +176,10 @@ async function handleAccess() {\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n- <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n+ <i\n+ >Over 57% of our clients tell us this one onboarding webinar saved\n+ them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n+ >\n </p>\n </div>\n \ndiff --git a/resources/js/pages/PlansPage.vue b/resources/js/pages/PlansPage.vue\nindex 5f319c99f0..b26ff8e3b4 100644\n--- a/resources/js/pages/PlansPage.vue\n+++ b/resources/js/pages/PlansPage.vue\n@@ -255,18 +255,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n@@ -657,6 +657,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\n",
"prFileDiffHunks" : [
"@@ -11,6 +11,12 @@ const props = defineProps({\n type: Object,\n default: null,\n },\n+ hasActiveSubscription: {\n+ type: Boolean,\n+ },\n+ currentPlan: {\n+ type: Object,\n+ },\n })\n \n const emit = defineEmits(['close'])\n",
"@@ -107,6 +113,39 @@ const newPlanInterval = computed(\n )\n \n const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n+\n+function getButtonLabel() {\n+ if (\n+ props.hasActiveSubscription &&\n+ props.currentPlan &&\n+ !props.currentPlan?.is_plan_for_advertising\n+ ) {\n+ if (\n+ props.currentPlan?.interval === 'monthly' &&\n+ props.stripeMeta.plan.interval === 'yearly'\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ props.currentPlan?.interval === 'yearly' &&\n+ props.stripeMeta.plan.interval === 'monthly'\n+ ) {\n+ return 'Downgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) <\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) >\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Downgrade'\n+ }\n+ }\n+}\n </script>\n \n <template>\n",
"@@ -153,6 +192,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n <div id=\"payment-element\" class=\"mt-4\" />\n \n <div\n+ v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n",
"@@ -165,7 +205,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n- {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n+ {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n \n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\ndiff --git a/resources/js/components/Onboarding/OnboardingPlans.vue b/resources/js/components/Onboarding/OnboardingPlans.vue\nindex f0c21c8d43..fc2879227f 100644\n--- a/resources/js/components/Onboarding/OnboardingPlans.vue\n+++ b/resources/js/components/Onboarding/OnboardingPlans.vue\n",
"@@ -272,18 +272,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n",
"@@ -692,6 +692,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\ndiff --git a/resources/js/components/Onboarding/RecapStep.vue b/resources/js/components/Onboarding/RecapStep.vue\nindex 02b12652ae..eaf25500e2 100644\n--- a/resources/js/components/Onboarding/RecapStep.vue\n+++ b/resources/js/components/Onboarding/RecapStep.vue\n",
"@@ -73,7 +73,6 @@ async function handleAccess() {\n console.error(err)\n }\n }\n-\n </script>\n \n <template>\n",
"@@ -87,7 +86,9 @@ async function handleAccess() {\n </div>\n \n <div class=\"flex flex-col gap-2\">\n- <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n+ <h1 class=\"text-2xl font-bold\">\n+ Book Your Free Onboarding Call (Optional)\n+ </h1>\n \n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n",
"@@ -103,9 +104,12 @@ async function handleAccess() {\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n-\n >\n- <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n+ <span\n+ v-if=\"feature.is_new\"\n+ class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n+ >New</span\n+ >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n",
"@@ -119,7 +123,9 @@ async function handleAccess() {\n )}`\"\n >\n {{ feature.name }}\n- <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n+ <span v-if=\"feature.count\" class=\"text-[#979797]\"\n+ >({{ feature.count }})</span\n+ >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n",
"@@ -138,8 +144,8 @@ async function handleAccess() {\n </div>\n \n <p class=\"text-lg font-semibold mb-4\">\n- It’s time to register for the Gethookd Webinar hosted by my 7-figure\n- e-commerce success coach, where we’ll give you:\n+ It’s time to register for your free onboarding call where one of my\n+ advisors will make sure you got access to the software, AND give you:\n </p>\n \n <ul class=\"flex flex-col gap-3\">\n",
"@@ -170,7 +176,10 @@ async function handleAccess() {\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n- <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n+ <i\n+ >Over 57% of our clients tell us this one onboarding webinar saved\n+ them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n+ >\n </p>\n </div>\n \ndiff --git a/resources/js/pages/PlansPage.vue b/resources/js/pages/PlansPage.vue\nindex 5f319c99f0..b26ff8e3b4 100644\n--- a/resources/js/pages/PlansPage.vue\n+++ b/resources/js/pages/PlansPage.vue\n",
"@@ -255,18 +255,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n",
"@@ -657,6 +657,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\n"
],
"prFileBlobUrl" : "https://api.bitbucket.org/2.0/repositories/melioraweb/gethookdai/src/4a370802c82e135a9a44572822be9e863cf64991/resources/js/components/Checkout/CheckoutForm.vue",
"_id" : ObjectId("68a6ae486e2b9ed1415eb749")
},
{
"prFileName" : "resources/js/components/Onboarding/OnboardingPlans.vue",
"prFileStatus" : "modified",
"prFileAdditions" : NumberInt(6),
"prFileDeletions" : NumberInt(4),
"prFileChanges" : NumberInt(10),
"prFileContentBefore" : "<script setup>\nimport { Api } from '@/api'\nimport Button from '@/components/Button/Button.vue'\nimport CheckoutForm from '@/components/Checkout/CheckoutForm.vue'\nimport Loading from '@/components/Loading/Loading.vue'\nimport Modal from '@/components/Modal/Modal.vue'\nimport Tab from '@/components/Tab/Tab.vue'\nimport AlertColorIcon from '@/icons/AlertColorIcon.vue'\nimport CheckColorIcon from '@/icons/CheckColorIcon.vue'\nimport { useAuthStore } from '@/stores/AuthStore'\nimport { handleSessionError } from '@/utils/utils'\nimport mixpanel from '@/vendors/mixpanel'\nimport { loadStripe } from '@stripe/stripe-js'\nimport { useQuery } from '@tanstack/vue-query'\nimport { useDebounceFn } from '@vueuse/core'\nimport { useToast } from 'primevue/usetoast'\nimport { computed, onBeforeMount, onUnmounted, ref, watchEffect } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport CardIcon from '@/icons/CardIcon.vue'\nimport PromoCode from '@/components/Plans/PromoCode.vue'\nimport usePromocode from '@/composables/usePromocode'\nimport PlanItemOnboarding from '../Plans/PlanItemOnboarding.vue'\nimport PaperPlaneWhite from '@/icons/PaperPlaneWhite.vue'\nimport RocketIconWhite from '@/icons/RocketIconWhite.vue'\nimport UFOWhite from '@/icons/UFOWhite.vue'\nimport CurrentPlanIndicatorV2 from '../Plans/CurrentPlanIndicatorV2.vue'\nimport MenuLayout from '@/layouts/MenuLayout.vue'\nimport ArrowRight from '@/icons/ArrowRight.vue'\nimport Reviews from '../Plans/Reviews.vue'\nimport PlansFAQ from '../Plans/PlansFAQ.vue'\nimport ComparePlans from '../Plans/ComparePlans.vue'\nimport KeyFeatures from '../Plans/KeyFeatures.vue'\nimport PlanPageIntro from '../Plans/PlanPageIntro.vue'\nimport AeroplanBlack from '@/icons/AeroplanBlack.vue'\nimport BrandLogos from '../Plans/BrandLogos.vue'\nimport ToastV2 from '../Toast/ToastV2.vue'\nimport ManageSubscription from '../Plans/ManageSubscription.vue'\nimport OnboardingFooter from './OnboardingFooter.vue'\n\nconst emit = defineEmits(['skip', 'continue'])\n\nconst props = defineProps({\n updateStep: Function,\n})\n\n// COUPON ENV\nconst couponFromEnv = document.querySelector('#coupon').value\n\nconst authStore = useAuthStore()\nconst toast = useToast()\nconst { removePromoCode, promoLoading } = usePromocode()\n\nconst SPECIAL_PLANS = ['elite_budapest', 'q4_special', 'offer97', 'UNLIMITED']\n\n// refs\nconst stripeMeta = ref({})\nconst isChoosingPlan = ref(false)\nconst isDoingAction = ref(false)\nconst stripeStatus = ref({\n text: '',\n status: '',\n})\nconst flag = ref(false)\nconst route = useRoute()\nconst router = useRouter()\nconst isRedirecting = ref(false)\nconst waitingBanner = ref(false)\nconst successBanner = ref(true)\nconst intervalId = ref()\n\nconst couponModal = ref(false)\nconst selectedPlan = ref()\nconst showModalCouponField = ref(false)\nconst couponCode = ref('')\nconst couponCodeLoading = ref(false)\nconst couponCodeData = ref()\nconst couponCodeError = ref(false)\n\nconst selectedTab = ref(0)\nconst showTab = ref(false)\nconst hasYearlyPlan = ref(false)\nconst showPromoPlan = ref(false)\n\nconst isOwnWorkspace = computed(() => {\n return authStore.authData?.user?.current_workspace_is_owned || false\n})\n\n// fetch plans\nconst { isLoading, data, refetch } = useQuery(['plans-data'], getPlans, {\n refetchOnWindowFocus: false,\n enabled: isOwnWorkspace.value ? flag : false,\n cacheTime: 0,\n})\n\nconst tabs = ref()\n\nconst plansData = computed(() => {\n if (!data.value) return []\n\n if (showPromoPlan.value) {\n // if there is a special promo code applied then only show those plans\n if (selectedTab.value === 0 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'monthly' && showPromoPlan.value.includes(item.slug)\n )\n }\n\n if (selectedTab.value === 1 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'yearly' && showPromoPlan.value.includes(item.slug)\n )\n }\n\n return data.value.data.filter((item) =>\n showPromoPlan.value.includes(item.slug)\n )\n } else {\n // otherwise show regular plans\n if (selectedTab.value === 0 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'monthly' && !SPECIAL_PLANS.includes(item.slug)\n )\n }\n\n if (selectedTab.value === 1 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'yearly' && !SPECIAL_PLANS.includes(item.slug)\n )\n }\n\n return data.value.data.filter((item) => !SPECIAL_PLANS.includes(item.slug))\n }\n})\n\nasync function getPlans() {\n try {\n let res = await Api.get('/api/get-plans')\n\n if (res.data.user_plan?.is_high_ticket) {\n props.updateStep(3.1)\n }\n\n authStore.authData.user_plan = res.data.user_plan\n authStore.authData.free_trial_days_remaining =\n res.data.free_trial_days_remaining\n authStore.authData.has_active_subscription =\n res.data.has_active_subscription\n\n // set tabs\n tabs.value = [\n {\n id: 0,\n title: 'Monthly',\n // badgeText:\n // !authStore.authData.has_active_subscription &&\n // res.data.next_endpoint !== 'payment_method_update' &&\n // !res.data.promo_code\n // ? '50% Off'\n // : null,\n },\n ]\n\n if (stripeStatus.value.text) {\n router.replace({ name: 'route.swipe-file' })\n }\n\n const isSpecialPlanApplied = SPECIAL_PLANS.includes(\n res.data.promo_code?.promo_code\n )\n\n if (res.data.data.find((item) => item.interval === 'yearly')) {\n hasYearlyPlan.value = true\n // if there are no tabs for yearly plans then add it\n if (!tabs.value.find((item) => item.title === 'Yearly')) {\n tabs.value = [\n ...tabs.value,\n {\n id: 1,\n title: 'Yearly',\n badgeText: !isSpecialPlanApplied\n ? '3 months free'\n : res.data.promo_code?.promo_code === 'UNLIMITED'\n ? 'Save ~25%'\n : `Save 17%`,\n },\n ]\n }\n }\n\n let specialPlanCode = null\n if (isSpecialPlanApplied) {\n showPromoPlan.value = res.data.promo_code?.promo_code\n specialPlanCode = res.data.promo_code?.promo_code\n }\n\n const hasMonthlyPromoPlan = res.data.data.find(\n (item) =>\n item.interval === 'monthly' && specialPlanCode?.includes(item.slug)\n )\n const hasYearlyPromoPlan = res.data.data.find(\n (item) =>\n item.interval === 'yearly' && specialPlanCode?.includes(item.slug)\n )\n const hasMonthlyRegularPlan = res.data.data.find(\n (item) =>\n item.interval === 'monthly' && !specialPlanCode?.includes(item.slug)\n )\n const hasYearlyRegularPlan = res.data.data.find(\n (item) =>\n item.interval === 'yearly' && !specialPlanCode?.includes(item.slug)\n )\n\n // select tab initially\n if (showPromoPlan.value) {\n if (hasMonthlyPromoPlan && hasYearlyPromoPlan) {\n showTab.value = true\n selectedTab.value = 0\n }\n } else {\n if (hasMonthlyRegularPlan && hasYearlyRegularPlan) {\n showTab.value = true\n selectedTab.value = 0\n }\n }\n\n waitingBanner.value = false\n\n return res.data\n } catch (err) {\n handleSessionError(err)\n }\n}\n\nasync function redirectToStripeBillingPortal() {\n try {\n isRedirecting.value = true\n const res = await Api.get('/api/subscription/billing-portal')\n window.location.href = res.data.data.redirect_url\n } catch (err) {\n handleSessionError(err)\n toast.add({\n severity: 'error',\n summary: 'Failed',\n detail: 'Something went wrong!',\n life: 3000,\n })\n }\n}\n\nasync function choosePlan(payload) {\n try {\n mixpanel.track('clicked_pricingPlan_onboarding', {\n pricingPlan_type: payload.plan.title,\n })\n } catch (err) {}\n\n try {\n if (payload.plan.next_endpoint === 'update') {\n isChoosingPlan.value = String(payload.plan.id)\n const res = await Api.post(\n `/api/subscription/${payload.plan.next_endpoint}/${payload.plan.id}`\n )\n // mixpanel.track(`Switching plan`, {\n // plan_id: payload.plan.id,\n // })\n stripeMeta.value = res.data.data\n stripeMeta.value.plan = payload.plan\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n \n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n })\n \n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n \n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n } else {\n throw new Error('No redirect URL received from checkout session')\n }\n } else if (payload.plan.next_endpoint === 'resubscribe') {\n isChoosingPlan.value = String(payload.plan.id)\n\n const res = await Api.post(`/api/subscription/create`, {\n payment_method: data.value.data[0].default_payment_id,\n planId: Number(payload.plan.id),\n })\n\n intervalId.value = setInterval(async () => {\n let res = await Api.get('/api/get-plans')\n\n if (res.data.user_plan && !res.data.is_new_user) {\n clearInterval(intervalId.value)\n window.location = window.location.origin + '/plans?status=success'\n return\n }\n\n if (res.data.payment_method_invalid) {\n clearInterval(intervalId.value)\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: res.data.payment_method_invalid,\n life: 30000,\n })\n await Api.post('/api/remove-payment-method-cache')\n\n // Clear all query parameters from URL\n router.replace({\n path: router.currentRoute.value.path,\n query: {},\n })\n\n isChoosingPlan.value = false\n return\n }\n }, 1000)\n } else if (\n payload.plan.next_endpoint === 'resume' ||\n payload.plan.next_endpoint === 'resume_required'\n ) {\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: 'Please renew your plan before proceeding!',\n life: 6000,\n })\n return\n }\n } catch (err) {\n handleSessionError(err)\n\n if (\n err.response.data.message ===\n 'Please subscribe to a plan first. After that, you can choose a different plan.'\n ) {\n // update payment method\n mixpanel.track('Redirecting to Stripe | cause: update payment method')\n await redirectToStripeBillingPortal()\n return\n }\n\n if (err.response.status === 422) {\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: err.response.data.message,\n life: 6000,\n })\n isChoosingPlan.value = false\n return\n }\n }\n}\n\nfunction handleCloseCheckout() {\n stripeMeta.value = {}\n mixpanel.track(`Closed plan upgrade/downgrade page`)\n}\n\nasync function handleStripeStatus(publishableKey, clientSecret) {\n try {\n const stripe = await loadStripe(publishableKey)\n\n let { setupIntent } = await stripe.retrieveSetupIntent(clientSecret)\n\n switch (setupIntent.status) {\n case 'succeeded': {\n stripeStatus.value.text = 'Success! Payment received.'\n stripeStatus.value.type = 'success'\n\n // only required for the first time when no plan is choosen\n if (route.query.planId) {\n await Api.post(`/api/subscription/create`, {\n payment_method: setupIntent.payment_method,\n planId: Number(route.query.planId),\n })\n }\n\n break\n }\n\n case 'processing': {\n stripeStatus.value.text =\n \"Payment processing! We'll update you when payment is received.\"\n stripeStatus.value.type = 'success'\n break\n }\n\n case 'requires_payment_method': {\n stripeStatus.value.text =\n 'Payment failed! Please try another payment method.'\n stripeStatus.value.type = 'error'\n break\n }\n\n default: {\n stripeStatus.value.text = 'Something went wrong! Please try again.'\n stripeStatus.value.type = 'error'\n break\n }\n }\n } catch (err) {\n } finally {\n setTimeout(() => {\n // refetch plans after 10 sec\n flag.value = true\n }, 10000)\n }\n}\n\nconst handleCouponInput = useDebounceFn(async () => {\n couponCodeLoading.value = true\n couponCodeError.value = false\n couponCodeData.value = null\n\n try {\n let res = await Api.get(\n `/api/coupon-applied-preview?plan_id=${selectedPlan.value.id}&coupon_code=${couponCode.value}`\n )\n couponCodeData.value = res.data\n couponCodeLoading.value = false\n } catch (err) {\n couponCodeLoading.value = false\n couponCodeError.value = true\n\n handleSessionError(err)\n }\n}, 1000)\n\nconst choosePlanWithCoupon = async () => {\n isChoosingPlan.value = String(selectedPlan.value.id)\n\n // if coupon from ENV is available then automatically apply that coupon\n let coupon = null\n if (\n couponFromEnv &&\n selectedPlan.value.interval === 'monthly' &&\n !authStore.authData.has_active_subscription &&\n selectedPlan.value?.next_endpoint !== 'payment_method_update' &&\n !data?.value.promo_code\n ) {\n coupon = couponFromEnv\n } else {\n coupon = couponCode.value\n }\n\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${selectedPlan.value.id} ${\n coupon ? `?coupon_code=${coupon}` : ''\n }`\n )\n // mixpanel.track(`Clicked on Choose Plan with Coupon`)\n window.location.href = res.data.data.redirect_url\n}\n\nonBeforeMount(async () => {\n const publishableKey = new URLSearchParams(window.location.search).get(\n 'publishable_key'\n )\n const clientSecret = new URLSearchParams(window.location.search).get(\n 'setup_intent_client_secret'\n )\n const customerId = new URLSearchParams(window.location.search).get('customer')\n\n // if there is no publishable key and there is no customer id, then dont wait before fetching plans data, by making the flag true\n if (!publishableKey && !customerId) {\n flag.value = true\n return\n }\n\n // if publishable key is available then fetch plans data\n\n // if customer id is available then keep cheking if the backend is updated\n if (customerId) {\n waitingBanner.value = true\n\n intervalId.value = setInterval(async () => {\n let res = await Api.get('/api/get-plans')\n\n if (res.data.payment_method_invalid) {\n clearInterval(intervalId.value)\n toast.add({\n severity: 'error',\n summary: 'Card verification failed.',\n detail: res.data.payment_method_invalid,\n life: 60000,\n })\n\n await Api.post('/api/remove-payment-method-cache')\n\n // Clear all query parameters from URL\n router.replace({\n path: router.currentRoute.value.path,\n query: {},\n })\n\n waitingBanner.value = false\n flag.value = true\n return\n }\n\n if (res.data.user_plan && !res.data.is_new_user) {\n const email = authStore.authData?.user?.email\n\n // Push to GTM Data Layer\n window.dataLayer = window.dataLayer || []\n\n window.dataLayer.push({\n event: 'free_trial_signup',\n user_id: email,\n event_category: 'sign_up',\n event_label: 'Free Trial Started',\n })\n\n clearInterval(intervalId.value)\n\n mixpanel.track('onboarding_plan_selected')\n props.updateStep(3.1)\n }\n }, 1000)\n\n return\n }\n\n // handleStripeStatus(publishableKey, clientSecret)\n})\n\nfunction handleTabChange(val) {\n selectedTab.value = val\n}\n\nonUnmounted(() => {\n if (intervalId.value) {\n clearInterval(intervalId.value)\n }\n})\n\nconst indexIconMap = {\n 0: PaperPlaneWhite,\n 1: AeroplanBlack,\n 2: RocketIconWhite,\n 3: UFOWhite,\n}\n</script>\n\n<template>\n <div class=\"bg-transparent h-full min-h-screen relative w-full\">\n <ToastV2 />\n\n <div class=\"mx-auto flex flex-col text-white max-w-[1440px]\">\n <div\n class=\"bg-[#FFF0F0] rounded-[16px] py-[16px] px-[32px] max-w-[453px] mx-auto border border-[#BC1414] flex gap-2 items-center mb-4 flex-wrap justify-center\"\n v-if=\"\n isOwnWorkspace &&\n data &&\n data.data[0]?.next_endpoint === 'payment_method_update'\n \"\n >\n <div\n class=\"size-[48px] flex items-center justify-center rounded-full\"\n style=\"background: rgba(188, 20, 20, 0.08)\"\n >\n <CardIcon class=\"size-5 fill-[#BC1414]\" />\n </div>\n\n <div class=\"flex flex-col text-center text-[#BC1414]\">\n <p class=\"text-base\">There seems to be an issue with your card.</p>\n <p class=\"text-xs\">Please double-check your details and try again.</p>\n </div>\n </div>\n\n <PlanPageIntro v-if=\"!stripeMeta.plan\" class=\"pb-3\">\n <CurrentPlanIndicatorV2\n v-if=\"data && !isLoading && data.user_plan && !data.is_new_user\"\n :planTitle=\"data.user_plan?.title\"\n :trialDaysRemaining=\"data.free_trial_days_remaining\"\n :price=\"String(data.user_plan?.price)\"\n :interval=\"data.user_plan?.interval\"\n />\n </PlanPageIntro>\n\n <div\n class=\"flex justify-center sm:justify-between items-center gap-4 flex-wrap mt-8 lg:mt-4\"\n >\n <!-- promo code -->\n <PromoCode\n v-if=\"!isLoading && !stripeMeta.plan && data\"\n :promocodeData=\"data?.promo_code\"\n :couponcodeData=\"data?.coupon\"\n class=\"self-center\"\n variant=\"large\"\n />\n\n <Tab\n class=\"w-full sm:w-max justify-self-center sm:justify-self-end\"\n v-if=\"data && showTab && !stripeMeta.plan\"\n :selected=\"selectedTab\"\n :options=\"tabs\"\n @change=\"(val) => handleTabChange(val)\"\n />\n\n <ManageSubscription\n v-if=\"\n isOwnWorkspace &&\n data &&\n data.data[0]?.next_endpoint === 'payment_method_update'\n \"\n :isLoading=\"isRedirecting\"\n @click=\"redirectToStripeBillingPortal\"\n />\n </div>\n\n <!-- if not user's workspace then show this warning -->\n <template v-if=\"!isOwnWorkspace\">\n <div class=\"flex flex-col gap-4\">\n <p class=\"bg-warning p-2 text-center text-black rounded-md mt-6\">\n You don't have access to manage plans in this workspace\n </p>\n <button\n class=\"rounded-[16px] bg-[--brand-secondary] px-[16px] py-[10px] text-base w-full sm:w-max self-end mt-6 hover:brightness-75\"\n @click=\"emit('skip')\"\n >\n Skip for now\n </button>\n </div>\n </template>\n\n <!-- if user's workspace, then show plans -->\n <template v-else>\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"waitingBanner\"\n >\n <component :is=\"CheckColorIcon\" class=\"min-w-max\"> </component>\n\n <div :class=\"`font-semibold w-full`\">\n Please wait and do not close this page...\n </div>\n </div>\n\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"stripeStatus.text && !stripeMeta.plan && !successBanner\"\n >\n <component\n :is=\"\n stripeStatus.type === 'success' ? CheckColorIcon : AlertColorIcon\n \"\n class=\"min-w-max\"\n >\n </component>\n\n <div :class=\"`font-semibold w-full`\">\n {{ stripeStatus.text }}\n {{ isLoading ? ' Please wait and do not close this page...' : '' }}\n </div>\n\n <button @click=\"stripeStatus.text = ''\" class=\"p-1 mr-0\">✕</button>\n </div>\n\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"\n route.query.status === 'success' &&\n successBanner &&\n !stripeMeta.plan\n \"\n >\n <component :is=\"CheckColorIcon\" class=\"min-w-max\"> </component>\n\n <div :class=\"`font-semibold w-full`\">\n You changed your plan successfully!\n </div>\n\n <button @click=\"successBanner = false\" class=\"p-1 mr-0\">✕</button>\n </div>\n\n <CheckoutForm\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n />\n\n <template v-else>\n <div\n v-if=\"isLoading || !data\"\n class=\"mt-8 w-full flex items-center justify-center\"\n >\n <Loading />\n </div>\n\n <div v-else class=\"flex flex-col justify-center\">\n <!-- <button\n v-if=\"\n showPromoPlan && !SPECIAL_PLANS.includes(data.user_plan?.slug)\n \"\n class=\"max-w-max self-center text-[#6c7a88] underline underline-offset-4 decoration-[#6c7a88] flex items-center gap-3\"\n @click=\"removePromoCode\"\n :disabled=\"promoLoading\"\n >\n Skip this offer\n <Loading v-if=\"promoLoading\" size=\"xs\" />\n </button> -->\n\n <div\n :class=\"\n showPromoPlan\n ? 'flex flex-wrap mt-4 gap-4 justify-center'\n : `mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4`\n \"\n >\n <template v-for=\"(plan, index) in plansData\" :key=\"index\">\n <template v-if=\"plan.active\">\n <PlanItemOnboarding\n :icon=\"indexIconMap[index]\"\n :planData=\"plan\"\n :isPopular=\"index === 1\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n :currentPlan=\"data.user_plan\"\n :isCustomPlan=\"Boolean(plan.url)\"\n :isLoading=\"isChoosingPlan || isDoingAction || promoLoading\"\n @choose=\"() => choosePlan({ plan: plan })\"\n :showOffer=\"\n false && // remove this if we want to show offer\n !data.has_active_subscription &&\n plan.next_endpoint !== 'payment_method_update' &&\n !data.promo_code\n \"\n :trialRemaining=\"data.free_trial_days_remaining\"\n :hasCoupon=\"Boolean(data?.coupon)\"\n :showPromoPlan=\"Boolean(showPromoPlan)\"\n :planUsageType=\"authStore.authData?.user?.plan_usage_type\"\n :isNewUser=\"data.is_new_user\"\n />\n </template>\n </template>\n </div>\n </div>\n </template>\n\n <!-- <PlanTrustedIcons v-if=\"!isLoading\" class=\"self-end mt-4\" /> -->\n\n <KeyFeatures\n v-if=\"!isLoading\"\n class=\"mt-[90px]\"\n :isUnlimitedPlan=\"showPromoPlan === 'UNLIMITED'\"\n />\n\n <BrandLogos\n v-if=\"!isLoading && showPromoPlan !== 'UNLIMITED'\"\n class=\"my-[150px] self-center\"\n />\n\n <Reviews\n v-if=\"!isLoading && showPromoPlan !== 'UNLIMITED'\"\n class=\"mt-10\"\n />\n\n <ComparePlans\n v-if=\"!isLoading && !showPromoPlan\"\n :isLoading=\"isChoosingPlan || isDoingAction || promoLoading\"\n :currentPlan=\"data.user_plan\"\n :plansData=\"data\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n class=\"my-[150px] self-center\"\n @choose=\"\n (val) => {\n choosePlan(val)\n }\n \"\n :trialRemaining=\"data.free_trial_days_remaining\"\n :isNewUser=\"data.is_new_user\"\n />\n\n <PlansFAQ v-if=\"!isLoading && !showPromoPlan\" />\n </template>\n\n <OnboardingFooter\n delayed\n v-if=\"\n !isLoading &&\n isOwnWorkspace &&\n (data.has_active_subscription ||\n (data.user_plan && !data.is_new_user))\n \"\n >\n <div>\n <button\n class=\"flex items-center gap-2 rounded-2xl font-medium bg-[--brand-secondary] px-8 py-[8px] text-sm text-white w-full sm:w-max self-end hover:brightness-75\"\n @click=\"emit('continue')\"\n >\n Continue\n <ArrowRight class=\"fill-white\" />\n </button>\n </div>\n </OnboardingFooter>\n\n <Modal\n v-if=\"couponModal\"\n @close=\"\n () => {\n couponModal = false\n showModalCouponField = false\n couponCodeData = null\n couponCodeError = false\n }\n \"\n heading=\"Choose plan\"\n size=\"content\"\n >\n <div class=\"flex flex-col gap-3\">\n <p>\n You have selected\n <span class=\"font-bold\">{{ selectedPlan.title }}</span> plan\n </p>\n\n <p v-if=\"!couponCodeData\">\n Total price:\n <span class=\"font-bold\"\n >${{ selectedPlan.price.toLocaleString() }}</span\n >\n </p>\n\n <p v-else class=\"flex gap-1\">\n <span> Total price: </span>\n <span class=\"line-through text-[var(--text-secondary)]\"\n >${{\n couponCodeData.plan_price.toFixed(2).toLocaleString()\n }}</span\n >\n <span class=\"font-bold\">${{ couponCodeData.total_price }}</span>\n </p>\n\n <button\n v-if=\"!showModalCouponField\"\n class=\"text-xs text-center self-center w-max text-gray-400 underline underline-offset-2 decoration-gray-400\"\n @click=\"showModalCouponField = true\"\n >\n I have a coupon code\n </button>\n\n <div class=\"flex gap-3 items-center flex-wrap\">\n <input\n v-if=\"showModalCouponField\"\n type=\"text\"\n placeholder=\"Coupon Code\"\n class=\"input-text max-w-[300px]\"\n v-model=\"couponCode\"\n @input=\"\n (e) => {\n if (e.target.value) {\n couponCodeLoading = true\n handleCouponInput()\n } else {\n couponCodeLoading = false\n couponCodeError = false\n couponCodeData = false\n }\n }\n \"\n @change=\"mixpanel.track(`Typing coupon code`)\"\n />\n\n <Loading v-if=\"couponCodeLoading\" />\n\n <p\n v-if=\"couponCodeError && !couponCodeLoading\"\n class=\"text-sm text-[#E51E43]\"\n >\n Invalid code!\n </p>\n\n <p\n v-if=\"couponCodeData && !couponCodeLoading\"\n class=\"text-[#0AC25D] text-sm\"\n >\n Coupon Applied!\n </p>\n </div>\n\n <Button\n label=\"Continue\"\n class=\"w-full\"\n @click=\"choosePlanWithCoupon\"\n :loading=\"Boolean(isChoosingPlan)\"\n :disabled=\"couponCodeError || couponCodeLoading || isChoosingPlan\"\n />\n </div>\n </Modal>\n </div>\n </div>\n</template>\n",
"prFileContentAfter" : "<script setup>\nimport { Api } from '@/api'\nimport Button from '@/components/Button/Button.vue'\nimport CheckoutForm from '@/components/Checkout/CheckoutForm.vue'\nimport Loading from '@/components/Loading/Loading.vue'\nimport Modal from '@/components/Modal/Modal.vue'\nimport Tab from '@/components/Tab/Tab.vue'\nimport AlertColorIcon from '@/icons/AlertColorIcon.vue'\nimport CheckColorIcon from '@/icons/CheckColorIcon.vue'\nimport { useAuthStore } from '@/stores/AuthStore'\nimport { handleSessionError } from '@/utils/utils'\nimport mixpanel from '@/vendors/mixpanel'\nimport { loadStripe } from '@stripe/stripe-js'\nimport { useQuery } from '@tanstack/vue-query'\nimport { useDebounceFn } from '@vueuse/core'\nimport { useToast } from 'primevue/usetoast'\nimport { computed, onBeforeMount, onUnmounted, ref, watchEffect } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport CardIcon from '@/icons/CardIcon.vue'\nimport PromoCode from '@/components/Plans/PromoCode.vue'\nimport usePromocode from '@/composables/usePromocode'\nimport PlanItemOnboarding from '../Plans/PlanItemOnboarding.vue'\nimport PaperPlaneWhite from '@/icons/PaperPlaneWhite.vue'\nimport RocketIconWhite from '@/icons/RocketIconWhite.vue'\nimport UFOWhite from '@/icons/UFOWhite.vue'\nimport CurrentPlanIndicatorV2 from '../Plans/CurrentPlanIndicatorV2.vue'\nimport MenuLayout from '@/layouts/MenuLayout.vue'\nimport ArrowRight from '@/icons/ArrowRight.vue'\nimport Reviews from '../Plans/Reviews.vue'\nimport PlansFAQ from '../Plans/PlansFAQ.vue'\nimport ComparePlans from '../Plans/ComparePlans.vue'\nimport KeyFeatures from '../Plans/KeyFeatures.vue'\nimport PlanPageIntro from '../Plans/PlanPageIntro.vue'\nimport AeroplanBlack from '@/icons/AeroplanBlack.vue'\nimport BrandLogos from '../Plans/BrandLogos.vue'\nimport ToastV2 from '../Toast/ToastV2.vue'\nimport ManageSubscription from '../Plans/ManageSubscription.vue'\nimport OnboardingFooter from './OnboardingFooter.vue'\n\nconst emit = defineEmits(['skip', 'continue'])\n\nconst props = defineProps({\n updateStep: Function,\n})\n\n// COUPON ENV\nconst couponFromEnv = document.querySelector('#coupon').value\n\nconst authStore = useAuthStore()\nconst toast = useToast()\nconst { removePromoCode, promoLoading } = usePromocode()\n\nconst SPECIAL_PLANS = ['elite_budapest', 'q4_special', 'offer97', 'UNLIMITED']\n\n// refs\nconst stripeMeta = ref({})\nconst isChoosingPlan = ref(false)\nconst isDoingAction = ref(false)\nconst stripeStatus = ref({\n text: '',\n status: '',\n})\nconst flag = ref(false)\nconst route = useRoute()\nconst router = useRouter()\nconst isRedirecting = ref(false)\nconst waitingBanner = ref(false)\nconst successBanner = ref(true)\nconst intervalId = ref()\n\nconst couponModal = ref(false)\nconst selectedPlan = ref()\nconst showModalCouponField = ref(false)\nconst couponCode = ref('')\nconst couponCodeLoading = ref(false)\nconst couponCodeData = ref()\nconst couponCodeError = ref(false)\n\nconst selectedTab = ref(0)\nconst showTab = ref(false)\nconst hasYearlyPlan = ref(false)\nconst showPromoPlan = ref(false)\n\nconst isOwnWorkspace = computed(() => {\n return authStore.authData?.user?.current_workspace_is_owned || false\n})\n\n// fetch plans\nconst { isLoading, data, refetch } = useQuery(['plans-data'], getPlans, {\n refetchOnWindowFocus: false,\n enabled: isOwnWorkspace.value ? flag : false,\n cacheTime: 0,\n})\n\nconst tabs = ref()\n\nconst plansData = computed(() => {\n if (!data.value) return []\n\n if (showPromoPlan.value) {\n // if there is a special promo code applied then only show those plans\n if (selectedTab.value === 0 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'monthly' && showPromoPlan.value.includes(item.slug)\n )\n }\n\n if (selectedTab.value === 1 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'yearly' && showPromoPlan.value.includes(item.slug)\n )\n }\n\n return data.value.data.filter((item) =>\n showPromoPlan.value.includes(item.slug)\n )\n } else {\n // otherwise show regular plans\n if (selectedTab.value === 0 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'monthly' && !SPECIAL_PLANS.includes(item.slug)\n )\n }\n\n if (selectedTab.value === 1 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'yearly' && !SPECIAL_PLANS.includes(item.slug)\n )\n }\n\n return data.value.data.filter((item) => !SPECIAL_PLANS.includes(item.slug))\n }\n})\n\nasync function getPlans() {\n try {\n let res = await Api.get('/api/get-plans')\n\n if (res.data.user_plan?.is_high_ticket) {\n props.updateStep(3.1)\n }\n\n authStore.authData.user_plan = res.data.user_plan\n authStore.authData.free_trial_days_remaining =\n res.data.free_trial_days_remaining\n authStore.authData.has_active_subscription =\n res.data.has_active_subscription\n\n // set tabs\n tabs.value = [\n {\n id: 0,\n title: 'Monthly',\n // badgeText:\n // !authStore.authData.has_active_subscription &&\n // res.data.next_endpoint !== 'payment_method_update' &&\n // !res.data.promo_code\n // ? '50% Off'\n // : null,\n },\n ]\n\n if (stripeStatus.value.text) {\n router.replace({ name: 'route.swipe-file' })\n }\n\n const isSpecialPlanApplied = SPECIAL_PLANS.includes(\n res.data.promo_code?.promo_code\n )\n\n if (res.data.data.find((item) => item.interval === 'yearly')) {\n hasYearlyPlan.value = true\n // if there are no tabs for yearly plans then add it\n if (!tabs.value.find((item) => item.title === 'Yearly')) {\n tabs.value = [\n ...tabs.value,\n {\n id: 1,\n title: 'Yearly',\n badgeText: !isSpecialPlanApplied\n ? '3 months free'\n : res.data.promo_code?.promo_code === 'UNLIMITED'\n ? 'Save ~25%'\n : `Save 17%`,\n },\n ]\n }\n }\n\n let specialPlanCode = null\n if (isSpecialPlanApplied) {\n showPromoPlan.value = res.data.promo_code?.promo_code\n specialPlanCode = res.data.promo_code?.promo_code\n }\n\n const hasMonthlyPromoPlan = res.data.data.find(\n (item) =>\n item.interval === 'monthly' && specialPlanCode?.includes(item.slug)\n )\n const hasYearlyPromoPlan = res.data.data.find(\n (item) =>\n item.interval === 'yearly' && specialPlanCode?.includes(item.slug)\n )\n const hasMonthlyRegularPlan = res.data.data.find(\n (item) =>\n item.interval === 'monthly' && !specialPlanCode?.includes(item.slug)\n )\n const hasYearlyRegularPlan = res.data.data.find(\n (item) =>\n item.interval === 'yearly' && !specialPlanCode?.includes(item.slug)\n )\n\n // select tab initially\n if (showPromoPlan.value) {\n if (hasMonthlyPromoPlan && hasYearlyPromoPlan) {\n showTab.value = true\n selectedTab.value = 0\n }\n } else {\n if (hasMonthlyRegularPlan && hasYearlyRegularPlan) {\n showTab.value = true\n selectedTab.value = 0\n }\n }\n\n waitingBanner.value = false\n\n return res.data\n } catch (err) {\n handleSessionError(err)\n }\n}\n\nasync function redirectToStripeBillingPortal() {\n try {\n isRedirecting.value = true\n const res = await Api.get('/api/subscription/billing-portal')\n window.location.href = res.data.data.redirect_url\n } catch (err) {\n handleSessionError(err)\n toast.add({\n severity: 'error',\n summary: 'Failed',\n detail: 'Something went wrong!',\n life: 3000,\n })\n }\n}\n\nasync function choosePlan(payload) {\n try {\n mixpanel.track('clicked_pricingPlan_onboarding', {\n pricingPlan_type: payload.plan.title,\n })\n } catch (err) {}\n\n try {\n if (payload.plan.next_endpoint === 'update') {\n isChoosingPlan.value = String(payload.plan.id)\n const res = await Api.post(\n `/api/subscription/${payload.plan.next_endpoint}/${payload.plan.id}`\n )\n // mixpanel.track(`Switching plan`, {\n // plan_id: payload.plan.id,\n // })\n stripeMeta.value = res.data.data\n stripeMeta.value.plan = payload.plan\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n } else {\n throw new Error('No redirect URL received from checkout session')\n }\n } else if (payload.plan.next_endpoint === 'resubscribe') {\n isChoosingPlan.value = String(payload.plan.id)\n\n const res = await Api.post(`/api/subscription/create`, {\n payment_method: data.value.data[0].default_payment_id,\n planId: Number(payload.plan.id),\n })\n\n intervalId.value = setInterval(async () => {\n let res = await Api.get('/api/get-plans')\n\n if (res.data.user_plan && !res.data.is_new_user) {\n clearInterval(intervalId.value)\n window.location = window.location.origin + '/plans?status=success'\n return\n }\n\n if (res.data.payment_method_invalid) {\n clearInterval(intervalId.value)\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: res.data.payment_method_invalid,\n life: 30000,\n })\n await Api.post('/api/remove-payment-method-cache')\n\n // Clear all query parameters from URL\n router.replace({\n path: router.currentRoute.value.path,\n query: {},\n })\n\n isChoosingPlan.value = false\n return\n }\n }, 1000)\n } else if (\n payload.plan.next_endpoint === 'resume' ||\n payload.plan.next_endpoint === 'resume_required'\n ) {\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: 'Please renew your plan before proceeding!',\n life: 6000,\n })\n return\n }\n } catch (err) {\n handleSessionError(err)\n\n if (\n err.response.data.message ===\n 'Please subscribe to a plan first. After that, you can choose a different plan.'\n ) {\n // update payment method\n mixpanel.track('Redirecting to Stripe | cause: update payment method')\n await redirectToStripeBillingPortal()\n return\n }\n\n if (err.response.status === 422) {\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: err.response.data.message,\n life: 6000,\n })\n isChoosingPlan.value = false\n return\n }\n }\n}\n\nfunction handleCloseCheckout() {\n stripeMeta.value = {}\n mixpanel.track(`Closed plan upgrade/downgrade page`)\n}\n\nasync function handleStripeStatus(publishableKey, clientSecret) {\n try {\n const stripe = await loadStripe(publishableKey)\n\n let { setupIntent } = await stripe.retrieveSetupIntent(clientSecret)\n\n switch (setupIntent.status) {\n case 'succeeded': {\n stripeStatus.value.text = 'Success! Payment received.'\n stripeStatus.value.type = 'success'\n\n // only required for the first time when no plan is choosen\n if (route.query.planId) {\n await Api.post(`/api/subscription/create`, {\n payment_method: setupIntent.payment_method,\n planId: Number(route.query.planId),\n })\n }\n\n break\n }\n\n case 'processing': {\n stripeStatus.value.text =\n \"Payment processing! We'll update you when payment is received.\"\n stripeStatus.value.type = 'success'\n break\n }\n\n case 'requires_payment_method': {\n stripeStatus.value.text =\n 'Payment failed! Please try another payment method.'\n stripeStatus.value.type = 'error'\n break\n }\n\n default: {\n stripeStatus.value.text = 'Something went wrong! Please try again.'\n stripeStatus.value.type = 'error'\n break\n }\n }\n } catch (err) {\n } finally {\n setTimeout(() => {\n // refetch plans after 10 sec\n flag.value = true\n }, 10000)\n }\n}\n\nconst handleCouponInput = useDebounceFn(async () => {\n couponCodeLoading.value = true\n couponCodeError.value = false\n couponCodeData.value = null\n\n try {\n let res = await Api.get(\n `/api/coupon-applied-preview?plan_id=${selectedPlan.value.id}&coupon_code=${couponCode.value}`\n )\n couponCodeData.value = res.data\n couponCodeLoading.value = false\n } catch (err) {\n couponCodeLoading.value = false\n couponCodeError.value = true\n\n handleSessionError(err)\n }\n}, 1000)\n\nconst choosePlanWithCoupon = async () => {\n isChoosingPlan.value = String(selectedPlan.value.id)\n\n // if coupon from ENV is available then automatically apply that coupon\n let coupon = null\n if (\n couponFromEnv &&\n selectedPlan.value.interval === 'monthly' &&\n !authStore.authData.has_active_subscription &&\n selectedPlan.value?.next_endpoint !== 'payment_method_update' &&\n !data?.value.promo_code\n ) {\n coupon = couponFromEnv\n } else {\n coupon = couponCode.value\n }\n\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${selectedPlan.value.id} ${\n coupon ? `?coupon_code=${coupon}` : ''\n }`\n )\n // mixpanel.track(`Clicked on Choose Plan with Coupon`)\n window.location.href = res.data.data.redirect_url\n}\n\nonBeforeMount(async () => {\n const publishableKey = new URLSearchParams(window.location.search).get(\n 'publishable_key'\n )\n const clientSecret = new URLSearchParams(window.location.search).get(\n 'setup_intent_client_secret'\n )\n const customerId = new URLSearchParams(window.location.search).get('customer')\n\n // if there is no publishable key and there is no customer id, then dont wait before fetching plans data, by making the flag true\n if (!publishableKey && !customerId) {\n flag.value = true\n return\n }\n\n // if publishable key is available then fetch plans data\n\n // if customer id is available then keep cheking if the backend is updated\n if (customerId) {\n waitingBanner.value = true\n\n intervalId.value = setInterval(async () => {\n let res = await Api.get('/api/get-plans')\n\n if (res.data.payment_method_invalid) {\n clearInterval(intervalId.value)\n toast.add({\n severity: 'error',\n summary: 'Card verification failed.',\n detail: res.data.payment_method_invalid,\n life: 60000,\n })\n\n await Api.post('/api/remove-payment-method-cache')\n\n // Clear all query parameters from URL\n router.replace({\n path: router.currentRoute.value.path,\n query: {},\n })\n\n waitingBanner.value = false\n flag.value = true\n return\n }\n\n if (res.data.user_plan && !res.data.is_new_user) {\n const email = authStore.authData?.user?.email\n\n // Push to GTM Data Layer\n window.dataLayer = window.dataLayer || []\n\n window.dataLayer.push({\n event: 'free_trial_signup',\n user_id: email,\n event_category: 'sign_up',\n event_label: 'Free Trial Started',\n })\n\n clearInterval(intervalId.value)\n\n mixpanel.track('onboarding_plan_selected')\n props.updateStep(3.1)\n }\n }, 1000)\n\n return\n }\n\n // handleStripeStatus(publishableKey, clientSecret)\n})\n\nfunction handleTabChange(val) {\n selectedTab.value = val\n}\n\nonUnmounted(() => {\n if (intervalId.value) {\n clearInterval(intervalId.value)\n }\n})\n\nconst indexIconMap = {\n 0: PaperPlaneWhite,\n 1: AeroplanBlack,\n 2: RocketIconWhite,\n 3: UFOWhite,\n}\n</script>\n\n<template>\n <div class=\"bg-transparent h-full min-h-screen relative w-full\">\n <ToastV2 />\n\n <div class=\"mx-auto flex flex-col text-white max-w-[1440px]\">\n <div\n class=\"bg-[#FFF0F0] rounded-[16px] py-[16px] px-[32px] max-w-[453px] mx-auto border border-[#BC1414] flex gap-2 items-center mb-4 flex-wrap justify-center\"\n v-if=\"\n isOwnWorkspace &&\n data &&\n data.data[0]?.next_endpoint === 'payment_method_update'\n \"\n >\n <div\n class=\"size-[48px] flex items-center justify-center rounded-full\"\n style=\"background: rgba(188, 20, 20, 0.08)\"\n >\n <CardIcon class=\"size-5 fill-[#BC1414]\" />\n </div>\n\n <div class=\"flex flex-col text-center text-[#BC1414]\">\n <p class=\"text-base\">There seems to be an issue with your card.</p>\n <p class=\"text-xs\">Please double-check your details and try again.</p>\n </div>\n </div>\n\n <PlanPageIntro v-if=\"!stripeMeta.plan\" class=\"pb-3\">\n <CurrentPlanIndicatorV2\n v-if=\"data && !isLoading && data.user_plan && !data.is_new_user\"\n :planTitle=\"data.user_plan?.title\"\n :trialDaysRemaining=\"data.free_trial_days_remaining\"\n :price=\"String(data.user_plan?.price)\"\n :interval=\"data.user_plan?.interval\"\n />\n </PlanPageIntro>\n\n <div\n class=\"flex justify-center sm:justify-between items-center gap-4 flex-wrap mt-8 lg:mt-4\"\n >\n <!-- promo code -->\n <PromoCode\n v-if=\"!isLoading && !stripeMeta.plan && data\"\n :promocodeData=\"data?.promo_code\"\n :couponcodeData=\"data?.coupon\"\n class=\"self-center\"\n variant=\"large\"\n />\n\n <Tab\n class=\"w-full sm:w-max justify-self-center sm:justify-self-end\"\n v-if=\"data && showTab && !stripeMeta.plan\"\n :selected=\"selectedTab\"\n :options=\"tabs\"\n @change=\"(val) => handleTabChange(val)\"\n />\n\n <ManageSubscription\n v-if=\"\n isOwnWorkspace &&\n data &&\n data.data[0]?.next_endpoint === 'payment_method_update'\n \"\n :isLoading=\"isRedirecting\"\n @click=\"redirectToStripeBillingPortal\"\n />\n </div>\n\n <!-- if not user's workspace then show this warning -->\n <template v-if=\"!isOwnWorkspace\">\n <div class=\"flex flex-col gap-4\">\n <p class=\"bg-warning p-2 text-center text-black rounded-md mt-6\">\n You don't have access to manage plans in this workspace\n </p>\n <button\n class=\"rounded-[16px] bg-[--brand-secondary] px-[16px] py-[10px] text-base w-full sm:w-max self-end mt-6 hover:brightness-75\"\n @click=\"emit('skip')\"\n >\n Skip for now\n </button>\n </div>\n </template>\n\n <!-- if user's workspace, then show plans -->\n <template v-else>\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"waitingBanner\"\n >\n <component :is=\"CheckColorIcon\" class=\"min-w-max\"> </component>\n\n <div :class=\"`font-semibold w-full`\">\n Please wait and do not close this page...\n </div>\n </div>\n\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"stripeStatus.text && !stripeMeta.plan && !successBanner\"\n >\n <component\n :is=\"\n stripeStatus.type === 'success' ? CheckColorIcon : AlertColorIcon\n \"\n class=\"min-w-max\"\n >\n </component>\n\n <div :class=\"`font-semibold w-full`\">\n {{ stripeStatus.text }}\n {{ isLoading ? ' Please wait and do not close this page...' : '' }}\n </div>\n\n <button @click=\"stripeStatus.text = ''\" class=\"p-1 mr-0\">✕</button>\n </div>\n\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"\n route.query.status === 'success' &&\n successBanner &&\n !stripeMeta.plan\n \"\n >\n <component :is=\"CheckColorIcon\" class=\"min-w-max\"> </component>\n\n <div :class=\"`font-semibold w-full`\">\n You changed your plan successfully!\n </div>\n\n <button @click=\"successBanner = false\" class=\"p-1 mr-0\">✕</button>\n </div>\n\n <CheckoutForm\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n :currentPlan=\"data.user_plan\"\n />\n\n <template v-else>\n <div\n v-if=\"isLoading || !data\"\n class=\"mt-8 w-full flex items-center justify-center\"\n >\n <Loading />\n </div>\n\n <div v-else class=\"flex flex-col justify-center\">\n <!-- <button\n v-if=\"\n showPromoPlan && !SPECIAL_PLANS.includes(data.user_plan?.slug)\n \"\n class=\"max-w-max self-center text-[#6c7a88] underline underline-offset-4 decoration-[#6c7a88] flex items-center gap-3\"\n @click=\"removePromoCode\"\n :disabled=\"promoLoading\"\n >\n Skip this offer\n <Loading v-if=\"promoLoading\" size=\"xs\" />\n </button> -->\n\n <div\n :class=\"\n showPromoPlan\n ? 'flex flex-wrap mt-4 gap-4 justify-center'\n : `mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4`\n \"\n >\n <template v-for=\"(plan, index) in plansData\" :key=\"index\">\n <template v-if=\"plan.active\">\n <PlanItemOnboarding\n :icon=\"indexIconMap[index]\"\n :planData=\"plan\"\n :isPopular=\"index === 1\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n :currentPlan=\"data.user_plan\"\n :isCustomPlan=\"Boolean(plan.url)\"\n :isLoading=\"isChoosingPlan || isDoingAction || promoLoading\"\n @choose=\"() => choosePlan({ plan: plan })\"\n :showOffer=\"\n false && // remove this if we want to show offer\n !data.has_active_subscription &&\n plan.next_endpoint !== 'payment_method_update' &&\n !data.promo_code\n \"\n :trialRemaining=\"data.free_trial_days_remaining\"\n :hasCoupon=\"Boolean(data?.coupon)\"\n :showPromoPlan=\"Boolean(showPromoPlan)\"\n :planUsageType=\"authStore.authData?.user?.plan_usage_type\"\n :isNewUser=\"data.is_new_user\"\n />\n </template>\n </template>\n </div>\n </div>\n </template>\n\n <!-- <PlanTrustedIcons v-if=\"!isLoading\" class=\"self-end mt-4\" /> -->\n\n <KeyFeatures\n v-if=\"!isLoading\"\n class=\"mt-[90px]\"\n :isUnlimitedPlan=\"showPromoPlan === 'UNLIMITED'\"\n />\n\n <BrandLogos\n v-if=\"!isLoading && showPromoPlan !== 'UNLIMITED'\"\n class=\"my-[150px] self-center\"\n />\n\n <Reviews\n v-if=\"!isLoading && showPromoPlan !== 'UNLIMITED'\"\n class=\"mt-10\"\n />\n\n <ComparePlans\n v-if=\"!isLoading && !showPromoPlan\"\n :isLoading=\"isChoosingPlan || isDoingAction || promoLoading\"\n :currentPlan=\"data.user_plan\"\n :plansData=\"data\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n class=\"my-[150px] self-center\"\n @choose=\"\n (val) => {\n choosePlan(val)\n }\n \"\n :trialRemaining=\"data.free_trial_days_remaining\"\n :isNewUser=\"data.is_new_user\"\n />\n\n <PlansFAQ v-if=\"!isLoading && !showPromoPlan\" />\n </template>\n\n <OnboardingFooter\n delayed\n v-if=\"\n !isLoading &&\n isOwnWorkspace &&\n (data.has_active_subscription ||\n (data.user_plan && !data.is_new_user))\n \"\n >\n <div>\n <button\n class=\"flex items-center gap-2 rounded-2xl font-medium bg-[--brand-secondary] px-8 py-[8px] text-sm text-white w-full sm:w-max self-end hover:brightness-75\"\n @click=\"emit('continue')\"\n >\n Continue\n <ArrowRight class=\"fill-white\" />\n </button>\n </div>\n </OnboardingFooter>\n\n <Modal\n v-if=\"couponModal\"\n @close=\"\n () => {\n couponModal = false\n showModalCouponField = false\n couponCodeData = null\n couponCodeError = false\n }\n \"\n heading=\"Choose plan\"\n size=\"content\"\n >\n <div class=\"flex flex-col gap-3\">\n <p>\n You have selected\n <span class=\"font-bold\">{{ selectedPlan.title }}</span> plan\n </p>\n\n <p v-if=\"!couponCodeData\">\n Total price:\n <span class=\"font-bold\"\n >${{ selectedPlan.price.toLocaleString() }}</span\n >\n </p>\n\n <p v-else class=\"flex gap-1\">\n <span> Total price: </span>\n <span class=\"line-through text-[var(--text-secondary)]\"\n >${{\n couponCodeData.plan_price.toFixed(2).toLocaleString()\n }}</span\n >\n <span class=\"font-bold\">${{ couponCodeData.total_price }}</span>\n </p>\n\n <button\n v-if=\"!showModalCouponField\"\n class=\"text-xs text-center self-center w-max text-gray-400 underline underline-offset-2 decoration-gray-400\"\n @click=\"showModalCouponField = true\"\n >\n I have a coupon code\n </button>\n\n <div class=\"flex gap-3 items-center flex-wrap\">\n <input\n v-if=\"showModalCouponField\"\n type=\"text\"\n placeholder=\"Coupon Code\"\n class=\"input-text max-w-[300px]\"\n v-model=\"couponCode\"\n @input=\"\n (e) => {\n if (e.target.value) {\n couponCodeLoading = true\n handleCouponInput()\n } else {\n couponCodeLoading = false\n couponCodeError = false\n couponCodeData = false\n }\n }\n \"\n @change=\"mixpanel.track(`Typing coupon code`)\"\n />\n\n <Loading v-if=\"couponCodeLoading\" />\n\n <p\n v-if=\"couponCodeError && !couponCodeLoading\"\n class=\"text-sm text-[#E51E43]\"\n >\n Invalid code!\n </p>\n\n <p\n v-if=\"couponCodeData && !couponCodeLoading\"\n class=\"text-[#0AC25D] text-sm\"\n >\n Coupon Applied!\n </p>\n </div>\n\n <Button\n label=\"Continue\"\n class=\"w-full\"\n @click=\"choosePlanWithCoupon\"\n :loading=\"Boolean(isChoosingPlan)\"\n :disabled=\"couponCodeError || couponCodeLoading || isChoosingPlan\"\n />\n </div>\n </Modal>\n </div>\n </div>\n</template>\n",
"prFileDiff" : "diff --git a/resources/js/components/Checkout/CheckoutForm.vue b/resources/js/components/Checkout/CheckoutForm.vue\nindex 41566ee43e..645bc2a1b5 100644\n--- a/resources/js/components/Checkout/CheckoutForm.vue\n+++ b/resources/js/components/Checkout/CheckoutForm.vue\n@@ -11,6 +11,12 @@ const props = defineProps({\n type: Object,\n default: null,\n },\n+ hasActiveSubscription: {\n+ type: Boolean,\n+ },\n+ currentPlan: {\n+ type: Object,\n+ },\n })\n \n const emit = defineEmits(['close'])\n@@ -107,6 +113,39 @@ const newPlanInterval = computed(\n )\n \n const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n+\n+function getButtonLabel() {\n+ if (\n+ props.hasActiveSubscription &&\n+ props.currentPlan &&\n+ !props.currentPlan?.is_plan_for_advertising\n+ ) {\n+ if (\n+ props.currentPlan?.interval === 'monthly' &&\n+ props.stripeMeta.plan.interval === 'yearly'\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ props.currentPlan?.interval === 'yearly' &&\n+ props.stripeMeta.plan.interval === 'monthly'\n+ ) {\n+ return 'Downgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) <\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) >\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Downgrade'\n+ }\n+ }\n+}\n </script>\n \n <template>\n@@ -153,6 +192,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n <div id=\"payment-element\" class=\"mt-4\" />\n \n <div\n+ v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n@@ -165,7 +205,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n- {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n+ {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n \n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\ndiff --git a/resources/js/components/Onboarding/OnboardingPlans.vue b/resources/js/components/Onboarding/OnboardingPlans.vue\nindex f0c21c8d43..fc2879227f 100644\n--- a/resources/js/components/Onboarding/OnboardingPlans.vue\n+++ b/resources/js/components/Onboarding/OnboardingPlans.vue\n@@ -272,18 +272,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n@@ -692,6 +692,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\ndiff --git a/resources/js/components/Onboarding/RecapStep.vue b/resources/js/components/Onboarding/RecapStep.vue\nindex 02b12652ae..eaf25500e2 100644\n--- a/resources/js/components/Onboarding/RecapStep.vue\n+++ b/resources/js/components/Onboarding/RecapStep.vue\n@@ -73,7 +73,6 @@ async function handleAccess() {\n console.error(err)\n }\n }\n-\n </script>\n \n <template>\n@@ -87,7 +86,9 @@ async function handleAccess() {\n </div>\n \n <div class=\"flex flex-col gap-2\">\n- <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n+ <h1 class=\"text-2xl font-bold\">\n+ Book Your Free Onboarding Call (Optional)\n+ </h1>\n \n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n@@ -103,9 +104,12 @@ async function handleAccess() {\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n-\n >\n- <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n+ <span\n+ v-if=\"feature.is_new\"\n+ class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n+ >New</span\n+ >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n@@ -119,7 +123,9 @@ async function handleAccess() {\n )}`\"\n >\n {{ feature.name }}\n- <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n+ <span v-if=\"feature.count\" class=\"text-[#979797]\"\n+ >({{ feature.count }})</span\n+ >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n@@ -138,8 +144,8 @@ async function handleAccess() {\n </div>\n \n <p class=\"text-lg font-semibold mb-4\">\n- It’s time to register for the Gethookd Webinar hosted by my 7-figure\n- e-commerce success coach, where we’ll give you:\n+ It’s time to register for your free onboarding call where one of my\n+ advisors will make sure you got access to the software, AND give you:\n </p>\n \n <ul class=\"flex flex-col gap-3\">\n@@ -170,7 +176,10 @@ async function handleAccess() {\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n- <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n+ <i\n+ >Over 57% of our clients tell us this one onboarding webinar saved\n+ them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n+ >\n </p>\n </div>\n \ndiff --git a/resources/js/pages/PlansPage.vue b/resources/js/pages/PlansPage.vue\nindex 5f319c99f0..b26ff8e3b4 100644\n--- a/resources/js/pages/PlansPage.vue\n+++ b/resources/js/pages/PlansPage.vue\n@@ -255,18 +255,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n@@ -657,6 +657,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\n",
"prFileDiffHunks" : [
"@@ -11,6 +11,12 @@ const props = defineProps({\n type: Object,\n default: null,\n },\n+ hasActiveSubscription: {\n+ type: Boolean,\n+ },\n+ currentPlan: {\n+ type: Object,\n+ },\n })\n \n const emit = defineEmits(['close'])\n",
"@@ -107,6 +113,39 @@ const newPlanInterval = computed(\n )\n \n const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n+\n+function getButtonLabel() {\n+ if (\n+ props.hasActiveSubscription &&\n+ props.currentPlan &&\n+ !props.currentPlan?.is_plan_for_advertising\n+ ) {\n+ if (\n+ props.currentPlan?.interval === 'monthly' &&\n+ props.stripeMeta.plan.interval === 'yearly'\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ props.currentPlan?.interval === 'yearly' &&\n+ props.stripeMeta.plan.interval === 'monthly'\n+ ) {\n+ return 'Downgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) <\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) >\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Downgrade'\n+ }\n+ }\n+}\n </script>\n \n <template>\n",
"@@ -153,6 +192,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n <div id=\"payment-element\" class=\"mt-4\" />\n \n <div\n+ v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n",
"@@ -165,7 +205,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n- {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n+ {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n \n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\ndiff --git a/resources/js/components/Onboarding/OnboardingPlans.vue b/resources/js/components/Onboarding/OnboardingPlans.vue\nindex f0c21c8d43..fc2879227f 100644\n--- a/resources/js/components/Onboarding/OnboardingPlans.vue\n+++ b/resources/js/components/Onboarding/OnboardingPlans.vue\n",
"@@ -272,18 +272,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n",
"@@ -692,6 +692,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\ndiff --git a/resources/js/components/Onboarding/RecapStep.vue b/resources/js/components/Onboarding/RecapStep.vue\nindex 02b12652ae..eaf25500e2 100644\n--- a/resources/js/components/Onboarding/RecapStep.vue\n+++ b/resources/js/components/Onboarding/RecapStep.vue\n",
"@@ -73,7 +73,6 @@ async function handleAccess() {\n console.error(err)\n }\n }\n-\n </script>\n \n <template>\n",
"@@ -87,7 +86,9 @@ async function handleAccess() {\n </div>\n \n <div class=\"flex flex-col gap-2\">\n- <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n+ <h1 class=\"text-2xl font-bold\">\n+ Book Your Free Onboarding Call (Optional)\n+ </h1>\n \n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n",
"@@ -103,9 +104,12 @@ async function handleAccess() {\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n-\n >\n- <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n+ <span\n+ v-if=\"feature.is_new\"\n+ class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n+ >New</span\n+ >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n",
"@@ -119,7 +123,9 @@ async function handleAccess() {\n )}`\"\n >\n {{ feature.name }}\n- <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n+ <span v-if=\"feature.count\" class=\"text-[#979797]\"\n+ >({{ feature.count }})</span\n+ >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n",
"@@ -138,8 +144,8 @@ async function handleAccess() {\n </div>\n \n <p class=\"text-lg font-semibold mb-4\">\n- It’s time to register for the Gethookd Webinar hosted by my 7-figure\n- e-commerce success coach, where we’ll give you:\n+ It’s time to register for your free onboarding call where one of my\n+ advisors will make sure you got access to the software, AND give you:\n </p>\n \n <ul class=\"flex flex-col gap-3\">\n",
"@@ -170,7 +176,10 @@ async function handleAccess() {\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n- <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n+ <i\n+ >Over 57% of our clients tell us this one onboarding webinar saved\n+ them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n+ >\n </p>\n </div>\n \ndiff --git a/resources/js/pages/PlansPage.vue b/resources/js/pages/PlansPage.vue\nindex 5f319c99f0..b26ff8e3b4 100644\n--- a/resources/js/pages/PlansPage.vue\n+++ b/resources/js/pages/PlansPage.vue\n",
"@@ -255,18 +255,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n",
"@@ -657,6 +657,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\n"
],
"prFileBlobUrl" : "https://api.bitbucket.org/2.0/repositories/melioraweb/gethookdai/src/4a370802c82e135a9a44572822be9e863cf64991/resources/js/components/Onboarding/OnboardingPlans.vue",
"_id" : ObjectId("68a6ae486e2b9ed1415eb74a")
},
{
"prFileName" : "resources/js/components/Onboarding/RecapStep.vue",
"prFileStatus" : "modified",
"prFileAdditions" : NumberInt(17),
"prFileDeletions" : NumberInt(8),
"prFileChanges" : NumberInt(25),
"prFileContentBefore" : "<script setup>\nimport { ref } from 'vue'\nimport ArrowRightIcon from '@/icons/ArrowRightV3.vue'\nimport RocketIcon from '@/icons/RocketIcon.vue'\nimport mixpanel from '@/vendors/mixpanel'\nimport OnboardingFooter from './OnboardingFooter.vue'\nimport Modal from '@/components/Modal/Modal.vue'\nimport Button from '../Button/ButtonV2.vue'\nimport CancelXIcon from '@/icons/CancelXIcon.vue'\nimport DiamondIcon from '@/icons/DiamondIcon.vue'\nimport BrandSpyIcon from '@/icons/BrandSpyIcon.vue'\nimport BulbIcon from '@/icons/BulbIcon.vue'\nimport BriefIcon from '@/icons/BriefIcon.vue'\nimport AiVideosIcon from '@/icons/AiVideosIcon.vue'\nimport TemplatesIcon from '@/icons/TemplatesIcon.vue'\nimport HeadphoneIcon from '@/icons/HeadphoneIcon.vue'\nimport CreativeVariationsIcon from '@/icons/CreativeVariationsIcon.vue'\nimport DashboardIcon from '@/icons/DashboardIcon.vue'\nimport WorkspaceIcon from '@/icons/WorkspaceIcon.vue'\nimport ProductsIcon from '@/icons/ProductsIcon.vue'\nimport FunnelTemplatesIcon from '@/icons/FunnelTemplatesIcon.vue'\nimport { Api } from '@/api'\nimport { featureList } from '@/components/Onboarding/constants'\nimport { useColorMode } from '@vueuse/core'\n\nconst colorMode = useColorMode()\n\nconst props = defineProps({\n currentStep: Number,\n updateStep: Function,\n})\n\nconst featureListIcons = {\n 0: BrandSpyIcon,\n 1: DiamondIcon,\n 2: BulbIcon,\n 3: BriefIcon,\n 4: ProductsIcon,\n 5: WorkspaceIcon,\n 6: CreativeVariationsIcon,\n 7: DashboardIcon,\n 8: TemplatesIcon,\n 9: FunnelTemplatesIcon,\n 10: AiVideosIcon,\n 11: HeadphoneIcon,\n}\n\nconst modalOpen = ref(false)\n\nfunction getFeatureTextColor(missingFeature, property = 'text') {\n if (missingFeature === '1') {\n return `${property}-[#979797]`\n }\n\n return '#3f444d'\n}\n\nfunction getFeatureIconColor(missingFeature) {\n if (missingFeature === '1') {\n return `fill-[#979797]`\n }\n\n return `fill-[#3f444d] dark:fill-[#fcfcfc]`\n}\n\nasync function handleAccess() {\n try {\n await Api.post('/api/onboarding/hide-onboarding')\n window.location.href = '/get-started'\n mixpanel.track('onboarding_completed')\n colorMode.value = 'light'\n } catch (err) {\n console.error(err)\n }\n}\n\n</script>\n\n<template>\n <div\n class=\"flex flex-col gap-6 text-white mt-[10px] lg:mt-[70px] pb-[50px] sm:pb-[100px]\"\n >\n <div class=\"flex flex-col gap-4 lg:max-w-[550px] xl:max-w-[680px]\">\n <div class=\"flex items-center gap-2\">\n <RocketIcon class=\"size-6\" />\n <h2 class=\"text-2xl font-bold\">Final step:</h2>\n </div>\n\n <div class=\"flex flex-col gap-2\">\n <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n\n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n a killer ad script in minutes - which is a tiny fraction of how\n Gethookd can help you scale profitably!\n </p>\n </div>\n\n <div\n class=\"grid grid-rows-12 sm:grid-rows-6 xl:grid-rows-4 grid-flow-col gap-2 my-6\"\n >\n <div\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n\n >\n <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n feature.missing_feature\n )} flex-shrink-0`\"\n ></component>\n\n <div\n :class=\"`text-base relative ${getFeatureTextColor(\n feature.missing_feature\n )}`\"\n >\n {{ feature.name }}\n <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n feature.missing_feature,\n 'bg'\n )} top-[11px] left-[-22px]`\"\n style=\"width: calc(100% + 22px)\"\n ></div>\n </div>\n </div>\n <div\n class=\"ml-auto mt-[20px] hidden lg:block lg:absolute lg:left-[650px] xl:left-[750px] lg:top-[420px] z-20 lg:w-[35vw] max-w-[500px]\"\n >\n <img src=\"/images/you_only_did_that.png\" alt=\"\" class=\"w-[200px]\" />\n </div>\n </div>\n\n <p class=\"text-lg font-semibold mb-4\">\n It’s time to register for the Gethookd Webinar hosted by my 7-figure\n e-commerce success coach, where we’ll give you:\n </p>\n\n <ul class=\"flex flex-col gap-3\">\n <li class=\"flex items-start gap-4\">\n <ArrowRightIcon class=\"size-3.5 flex-shrink-0 mt-1 fill-[#2075FF]\" />\n <span class=\"text-base font-semibold\"\n >A FREE course on how to build a high-converting Shopify store\n <br /><span class=\"text-[#979797]\"\n >(we used to sell this course for $1,000)</span\n ></span\n >\n </li>\n\n <li class=\"flex items-start gap-4\">\n <ArrowRightIcon class=\"size-3.5 flex-shrink-0 mt-1 fill-[#2075FF]\" />\n <span class=\"text-base font-semibold\"\n >A full walkthrough of how to use GetHookd to maximize your creative\n potential as soon as this week</span\n >\n </li>\n\n <li class=\"flex items-start gap-4\">\n <ArrowRightIcon class=\"size-3.5 flex-shrink-0 mt-1 fill-[#2075FF]\" />\n <span class=\"text-base font-semibold\"\n >A guide on how our most successful clients use Gethookd to create\n winning ads faster than anyone else</span\n >\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n </p>\n </div>\n\n <OnboardingFooter delayed>\n <div class=\"flex flex-col w-full md:flex-row items-end gap-4 justify-end\">\n <button\n @click=\"\n () => {\n handleAccess()\n mixpanel.track('clicked_Continue_to_Platform_onboarding')\n }\n \"\n class=\"border-none rounded-2xl text-base min-h-[60px] sm:min-h-[74px] bg-[#141414)] font-semibold flex flex-col justify-center items-center hover:bg-[#141414)]/40 px-8 w-full sm:w-[297px]\"\n >\n <span class=\"text-sm sm:text-lg flex items-center gap-2 sm:gap-4\"\n >Continue to Gethookd\n </span>\n <span class=\"text-white text-xs text-opacity-60\"\n >Explore the platform yourself</span\n >\n </button>\n <button\n @click=\"\n () => {\n modalOpen = true\n mixpanel.track('clicked_Book_a_Free_Expert_Call_onboarding')\n }\n \"\n class=\"border-none rounded-2xl text-base min-h-[60px] sm:min-h-[74px] bg-[#9C4CBD] font-semibold flex flex-col justify-center items-center hover:bg-[#9C4CBD]/80 px-8 w-full sm:w-[297px]\"\n >\n <span class=\"text-sm sm:text-lg flex items-center gap-2 sm:gap-4\"\n >Book a Free Expert Call\n <ArrowRightIcon class=\"size-3.5 shrink-0 fill-white\"\n /></span>\n <span class=\"text-white text-xs text-opacity-60\"\n >Get FREE $1,000 Course Now</span\n >\n </button>\n </div>\n </OnboardingFooter>\n </div>\n <Modal\n v-if=\"modalOpen\"\n @close=\"modalOpen = false\"\n size=\"mediumLarge\"\n bottom\n :closeButton=\"false\"\n :closeOnTop=\"true\"\n :classes=\"{\n root: '!bg-white !px-2 sm:!px-8 absolute !rounded-lg top-[30px] left-[50%] transform -translate-x-1/2',\n }\"\n >\n <div>\n <h2 class=\"text-[22px] font-bold text-[#292929]\">\n Book a Free Expert Call\n </h2>\n <iframe\n class=\"mt-5 w-full h-[60vh]\"\n src=\"https://go.gethookd.ai/gh-client-booking\"\n frameborder=\"0\"\n ></iframe>\n </div>\n <Button\n class=\"w-full mt-5 md:w-fit !bg-[--white-grey] px-6 !rounded-lg text-[--blue-grey-40] font-semibold dark:bg-[#0f1e2d] dark:!text-[var(--pale-sky)] dark:fill-[var(--pale-sky)]\"\n variant=\"outline\"\n label=\"Cancel\"\n size=\"smallMedium\"\n :iconLeft=\"CancelXIcon\"\n iconLeftClass=\"fill-[--blue-grey-40] dark:fill-[var(--pale-sky)]\"\n @click=\"\n () => {\n modalOpen = false\n mixpanel.track('clicked_cancelBooking_onboarding')\n }\n \"\n >\n </Button>\n </Modal>\n</template>\n",
"prFileContentAfter" : "<script setup>\nimport { ref } from 'vue'\nimport ArrowRightIcon from '@/icons/ArrowRightV3.vue'\nimport RocketIcon from '@/icons/RocketIcon.vue'\nimport mixpanel from '@/vendors/mixpanel'\nimport OnboardingFooter from './OnboardingFooter.vue'\nimport Modal from '@/components/Modal/Modal.vue'\nimport Button from '../Button/ButtonV2.vue'\nimport CancelXIcon from '@/icons/CancelXIcon.vue'\nimport DiamondIcon from '@/icons/DiamondIcon.vue'\nimport BrandSpyIcon from '@/icons/BrandSpyIcon.vue'\nimport BulbIcon from '@/icons/BulbIcon.vue'\nimport BriefIcon from '@/icons/BriefIcon.vue'\nimport AiVideosIcon from '@/icons/AiVideosIcon.vue'\nimport TemplatesIcon from '@/icons/TemplatesIcon.vue'\nimport HeadphoneIcon from '@/icons/HeadphoneIcon.vue'\nimport CreativeVariationsIcon from '@/icons/CreativeVariationsIcon.vue'\nimport DashboardIcon from '@/icons/DashboardIcon.vue'\nimport WorkspaceIcon from '@/icons/WorkspaceIcon.vue'\nimport ProductsIcon from '@/icons/ProductsIcon.vue'\nimport FunnelTemplatesIcon from '@/icons/FunnelTemplatesIcon.vue'\nimport { Api } from '@/api'\nimport { featureList } from '@/components/Onboarding/constants'\nimport { useColorMode } from '@vueuse/core'\n\nconst colorMode = useColorMode()\n\nconst props = defineProps({\n currentStep: Number,\n updateStep: Function,\n})\n\nconst featureListIcons = {\n 0: BrandSpyIcon,\n 1: DiamondIcon,\n 2: BulbIcon,\n 3: BriefIcon,\n 4: ProductsIcon,\n 5: WorkspaceIcon,\n 6: CreativeVariationsIcon,\n 7: DashboardIcon,\n 8: TemplatesIcon,\n 9: FunnelTemplatesIcon,\n 10: AiVideosIcon,\n 11: HeadphoneIcon,\n}\n\nconst modalOpen = ref(false)\n\nfunction getFeatureTextColor(missingFeature, property = 'text') {\n if (missingFeature === '1') {\n return `${property}-[#979797]`\n }\n\n return '#3f444d'\n}\n\nfunction getFeatureIconColor(missingFeature) {\n if (missingFeature === '1') {\n return `fill-[#979797]`\n }\n\n return `fill-[#3f444d] dark:fill-[#fcfcfc]`\n}\n\nasync function handleAccess() {\n try {\n await Api.post('/api/onboarding/hide-onboarding')\n window.location.href = '/get-started'\n mixpanel.track('onboarding_completed')\n colorMode.value = 'light'\n } catch (err) {\n console.error(err)\n }\n}\n</script>\n\n<template>\n <div\n class=\"flex flex-col gap-6 text-white mt-[10px] lg:mt-[70px] pb-[50px] sm:pb-[100px]\"\n >\n <div class=\"flex flex-col gap-4 lg:max-w-[550px] xl:max-w-[680px]\">\n <div class=\"flex items-center gap-2\">\n <RocketIcon class=\"size-6\" />\n <h2 class=\"text-2xl font-bold\">Final step:</h2>\n </div>\n\n <div class=\"flex flex-col gap-2\">\n <h1 class=\"text-2xl font-bold\">\n Book Your Free Onboarding Call (Optional)\n </h1>\n\n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n a killer ad script in minutes - which is a tiny fraction of how\n Gethookd can help you scale profitably!\n </p>\n </div>\n\n <div\n class=\"grid grid-rows-12 sm:grid-rows-6 xl:grid-rows-4 grid-flow-col gap-2 my-6\"\n >\n <div\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n >\n <span\n v-if=\"feature.is_new\"\n class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n >New</span\n >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n feature.missing_feature\n )} flex-shrink-0`\"\n ></component>\n\n <div\n :class=\"`text-base relative ${getFeatureTextColor(\n feature.missing_feature\n )}`\"\n >\n {{ feature.name }}\n <span v-if=\"feature.count\" class=\"text-[#979797]\"\n >({{ feature.count }})</span\n >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n feature.missing_feature,\n 'bg'\n )} top-[11px] left-[-22px]`\"\n style=\"width: calc(100% + 22px)\"\n ></div>\n </div>\n </div>\n <div\n class=\"ml-auto mt-[20px] hidden lg:block lg:absolute lg:left-[650px] xl:left-[750px] lg:top-[420px] z-20 lg:w-[35vw] max-w-[500px]\"\n >\n <img src=\"/images/you_only_did_that.png\" alt=\"\" class=\"w-[200px]\" />\n </div>\n </div>\n\n <p class=\"text-lg font-semibold mb-4\">\n It’s time to register for your free onboarding call where one of my\n advisors will make sure you got access to the software, AND give you:\n </p>\n\n <ul class=\"flex flex-col gap-3\">\n <li class=\"flex items-start gap-4\">\n <ArrowRightIcon class=\"size-3.5 flex-shrink-0 mt-1 fill-[#2075FF]\" />\n <span class=\"text-base font-semibold\"\n >A FREE course on how to build a high-converting Shopify store\n <br /><span class=\"text-[#979797]\"\n >(we used to sell this course for $1,000)</span\n ></span\n >\n </li>\n\n <li class=\"flex items-start gap-4\">\n <ArrowRightIcon class=\"size-3.5 flex-shrink-0 mt-1 fill-[#2075FF]\" />\n <span class=\"text-base font-semibold\"\n >A full walkthrough of how to use GetHookd to maximize your creative\n potential as soon as this week</span\n >\n </li>\n\n <li class=\"flex items-start gap-4\">\n <ArrowRightIcon class=\"size-3.5 flex-shrink-0 mt-1 fill-[#2075FF]\" />\n <span class=\"text-base font-semibold\"\n >A guide on how our most successful clients use Gethookd to create\n winning ads faster than anyone else</span\n >\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n <i\n >Over 57% of our clients tell us this one onboarding webinar saved\n them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n >\n </p>\n </div>\n\n <OnboardingFooter delayed>\n <div class=\"flex flex-col w-full md:flex-row items-end gap-4 justify-end\">\n <button\n @click=\"\n () => {\n handleAccess()\n mixpanel.track('clicked_Continue_to_Platform_onboarding')\n }\n \"\n class=\"border-none rounded-2xl text-base min-h-[60px] sm:min-h-[74px] bg-[#141414)] font-semibold flex flex-col justify-center items-center hover:bg-[#141414)]/40 px-8 w-full sm:w-[297px]\"\n >\n <span class=\"text-sm sm:text-lg flex items-center gap-2 sm:gap-4\"\n >Continue to Gethookd\n </span>\n <span class=\"text-white text-xs text-opacity-60\"\n >Explore the platform yourself</span\n >\n </button>\n <button\n @click=\"\n () => {\n modalOpen = true\n mixpanel.track('clicked_Book_a_Free_Expert_Call_onboarding')\n }\n \"\n class=\"border-none rounded-2xl text-base min-h-[60px] sm:min-h-[74px] bg-[#9C4CBD] font-semibold flex flex-col justify-center items-center hover:bg-[#9C4CBD]/80 px-8 w-full sm:w-[297px]\"\n >\n <span class=\"text-sm sm:text-lg flex items-center gap-2 sm:gap-4\"\n >Book a Free Expert Call\n <ArrowRightIcon class=\"size-3.5 shrink-0 fill-white\"\n /></span>\n <span class=\"text-white text-xs text-opacity-60\"\n >Get FREE $1,000 Course Now</span\n >\n </button>\n </div>\n </OnboardingFooter>\n </div>\n <Modal\n v-if=\"modalOpen\"\n @close=\"modalOpen = false\"\n size=\"mediumLarge\"\n bottom\n :closeButton=\"false\"\n :closeOnTop=\"true\"\n :classes=\"{\n root: '!bg-white !px-2 sm:!px-8 absolute !rounded-lg top-[30px] left-[50%] transform -translate-x-1/2',\n }\"\n >\n <div>\n <h2 class=\"text-[22px] font-bold text-[#292929]\">\n Book a Free Expert Call\n </h2>\n <iframe\n class=\"mt-5 w-full h-[60vh]\"\n src=\"https://go.gethookd.ai/gh-client-booking\"\n frameborder=\"0\"\n ></iframe>\n </div>\n <Button\n class=\"w-full mt-5 md:w-fit !bg-[--white-grey] px-6 !rounded-lg text-[--blue-grey-40] font-semibold dark:bg-[#0f1e2d] dark:!text-[var(--pale-sky)] dark:fill-[var(--pale-sky)]\"\n variant=\"outline\"\n label=\"Cancel\"\n size=\"smallMedium\"\n :iconLeft=\"CancelXIcon\"\n iconLeftClass=\"fill-[--blue-grey-40] dark:fill-[var(--pale-sky)]\"\n @click=\"\n () => {\n modalOpen = false\n mixpanel.track('clicked_cancelBooking_onboarding')\n }\n \"\n >\n </Button>\n </Modal>\n</template>\n",
"prFileDiff" : "diff --git a/resources/js/components/Checkout/CheckoutForm.vue b/resources/js/components/Checkout/CheckoutForm.vue\nindex 41566ee43e..645bc2a1b5 100644\n--- a/resources/js/components/Checkout/CheckoutForm.vue\n+++ b/resources/js/components/Checkout/CheckoutForm.vue\n@@ -11,6 +11,12 @@ const props = defineProps({\n type: Object,\n default: null,\n },\n+ hasActiveSubscription: {\n+ type: Boolean,\n+ },\n+ currentPlan: {\n+ type: Object,\n+ },\n })\n \n const emit = defineEmits(['close'])\n@@ -107,6 +113,39 @@ const newPlanInterval = computed(\n )\n \n const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n+\n+function getButtonLabel() {\n+ if (\n+ props.hasActiveSubscription &&\n+ props.currentPlan &&\n+ !props.currentPlan?.is_plan_for_advertising\n+ ) {\n+ if (\n+ props.currentPlan?.interval === 'monthly' &&\n+ props.stripeMeta.plan.interval === 'yearly'\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ props.currentPlan?.interval === 'yearly' &&\n+ props.stripeMeta.plan.interval === 'monthly'\n+ ) {\n+ return 'Downgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) <\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) >\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Downgrade'\n+ }\n+ }\n+}\n </script>\n \n <template>\n@@ -153,6 +192,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n <div id=\"payment-element\" class=\"mt-4\" />\n \n <div\n+ v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n@@ -165,7 +205,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n- {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n+ {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n \n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\ndiff --git a/resources/js/components/Onboarding/OnboardingPlans.vue b/resources/js/components/Onboarding/OnboardingPlans.vue\nindex f0c21c8d43..fc2879227f 100644\n--- a/resources/js/components/Onboarding/OnboardingPlans.vue\n+++ b/resources/js/components/Onboarding/OnboardingPlans.vue\n@@ -272,18 +272,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n@@ -692,6 +692,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\ndiff --git a/resources/js/components/Onboarding/RecapStep.vue b/resources/js/components/Onboarding/RecapStep.vue\nindex 02b12652ae..eaf25500e2 100644\n--- a/resources/js/components/Onboarding/RecapStep.vue\n+++ b/resources/js/components/Onboarding/RecapStep.vue\n@@ -73,7 +73,6 @@ async function handleAccess() {\n console.error(err)\n }\n }\n-\n </script>\n \n <template>\n@@ -87,7 +86,9 @@ async function handleAccess() {\n </div>\n \n <div class=\"flex flex-col gap-2\">\n- <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n+ <h1 class=\"text-2xl font-bold\">\n+ Book Your Free Onboarding Call (Optional)\n+ </h1>\n \n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n@@ -103,9 +104,12 @@ async function handleAccess() {\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n-\n >\n- <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n+ <span\n+ v-if=\"feature.is_new\"\n+ class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n+ >New</span\n+ >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n@@ -119,7 +123,9 @@ async function handleAccess() {\n )}`\"\n >\n {{ feature.name }}\n- <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n+ <span v-if=\"feature.count\" class=\"text-[#979797]\"\n+ >({{ feature.count }})</span\n+ >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n@@ -138,8 +144,8 @@ async function handleAccess() {\n </div>\n \n <p class=\"text-lg font-semibold mb-4\">\n- It’s time to register for the Gethookd Webinar hosted by my 7-figure\n- e-commerce success coach, where we’ll give you:\n+ It’s time to register for your free onboarding call where one of my\n+ advisors will make sure you got access to the software, AND give you:\n </p>\n \n <ul class=\"flex flex-col gap-3\">\n@@ -170,7 +176,10 @@ async function handleAccess() {\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n- <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n+ <i\n+ >Over 57% of our clients tell us this one onboarding webinar saved\n+ them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n+ >\n </p>\n </div>\n \ndiff --git a/resources/js/pages/PlansPage.vue b/resources/js/pages/PlansPage.vue\nindex 5f319c99f0..b26ff8e3b4 100644\n--- a/resources/js/pages/PlansPage.vue\n+++ b/resources/js/pages/PlansPage.vue\n@@ -255,18 +255,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n@@ -657,6 +657,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\n",
"prFileDiffHunks" : [
"@@ -11,6 +11,12 @@ const props = defineProps({\n type: Object,\n default: null,\n },\n+ hasActiveSubscription: {\n+ type: Boolean,\n+ },\n+ currentPlan: {\n+ type: Object,\n+ },\n })\n \n const emit = defineEmits(['close'])\n",
"@@ -107,6 +113,39 @@ const newPlanInterval = computed(\n )\n \n const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n+\n+function getButtonLabel() {\n+ if (\n+ props.hasActiveSubscription &&\n+ props.currentPlan &&\n+ !props.currentPlan?.is_plan_for_advertising\n+ ) {\n+ if (\n+ props.currentPlan?.interval === 'monthly' &&\n+ props.stripeMeta.plan.interval === 'yearly'\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ props.currentPlan?.interval === 'yearly' &&\n+ props.stripeMeta.plan.interval === 'monthly'\n+ ) {\n+ return 'Downgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) <\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) >\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Downgrade'\n+ }\n+ }\n+}\n </script>\n \n <template>\n",
"@@ -153,6 +192,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n <div id=\"payment-element\" class=\"mt-4\" />\n \n <div\n+ v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n",
"@@ -165,7 +205,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n- {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n+ {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n \n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\ndiff --git a/resources/js/components/Onboarding/OnboardingPlans.vue b/resources/js/components/Onboarding/OnboardingPlans.vue\nindex f0c21c8d43..fc2879227f 100644\n--- a/resources/js/components/Onboarding/OnboardingPlans.vue\n+++ b/resources/js/components/Onboarding/OnboardingPlans.vue\n",
"@@ -272,18 +272,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n",
"@@ -692,6 +692,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\ndiff --git a/resources/js/components/Onboarding/RecapStep.vue b/resources/js/components/Onboarding/RecapStep.vue\nindex 02b12652ae..eaf25500e2 100644\n--- a/resources/js/components/Onboarding/RecapStep.vue\n+++ b/resources/js/components/Onboarding/RecapStep.vue\n",
"@@ -73,7 +73,6 @@ async function handleAccess() {\n console.error(err)\n }\n }\n-\n </script>\n \n <template>\n",
"@@ -87,7 +86,9 @@ async function handleAccess() {\n </div>\n \n <div class=\"flex flex-col gap-2\">\n- <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n+ <h1 class=\"text-2xl font-bold\">\n+ Book Your Free Onboarding Call (Optional)\n+ </h1>\n \n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n",
"@@ -103,9 +104,12 @@ async function handleAccess() {\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n-\n >\n- <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n+ <span\n+ v-if=\"feature.is_new\"\n+ class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n+ >New</span\n+ >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n",
"@@ -119,7 +123,9 @@ async function handleAccess() {\n )}`\"\n >\n {{ feature.name }}\n- <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n+ <span v-if=\"feature.count\" class=\"text-[#979797]\"\n+ >({{ feature.count }})</span\n+ >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n",
"@@ -138,8 +144,8 @@ async function handleAccess() {\n </div>\n \n <p class=\"text-lg font-semibold mb-4\">\n- It’s time to register for the Gethookd Webinar hosted by my 7-figure\n- e-commerce success coach, where we’ll give you:\n+ It’s time to register for your free onboarding call where one of my\n+ advisors will make sure you got access to the software, AND give you:\n </p>\n \n <ul class=\"flex flex-col gap-3\">\n",
"@@ -170,7 +176,10 @@ async function handleAccess() {\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n- <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n+ <i\n+ >Over 57% of our clients tell us this one onboarding webinar saved\n+ them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n+ >\n </p>\n </div>\n \ndiff --git a/resources/js/pages/PlansPage.vue b/resources/js/pages/PlansPage.vue\nindex 5f319c99f0..b26ff8e3b4 100644\n--- a/resources/js/pages/PlansPage.vue\n+++ b/resources/js/pages/PlansPage.vue\n",
"@@ -255,18 +255,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n",
"@@ -657,6 +657,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\n"
],
"prFileBlobUrl" : "https://api.bitbucket.org/2.0/repositories/melioraweb/gethookdai/src/4a370802c82e135a9a44572822be9e863cf64991/resources/js/components/Onboarding/RecapStep.vue",
"_id" : ObjectId("68a6ae486e2b9ed1415eb74b")
},
{
"prFileName" : "resources/js/pages/PlansPage.vue",
"prFileStatus" : "modified",
"prFileAdditions" : NumberInt(6),
"prFileDeletions" : NumberInt(4),
"prFileChanges" : NumberInt(10),
"prFileContentBefore" : "<script setup>\nimport { Api } from '@/api'\nimport Button from '@/components/Button/Button.vue'\nimport CheckoutForm from '@/components/Checkout/CheckoutForm.vue'\nimport Loading from '@/components/Loading/Loading.vue'\nimport Modal from '@/components/Modal/Modal.vue'\nimport Tab from '@/components/Tab/Tab.vue'\nimport AlertColorIcon from '@/icons/AlertColorIcon.vue'\nimport CheckColorIcon from '@/icons/CheckColorIcon.vue'\nimport { useAuthStore } from '@/stores/AuthStore'\nimport { handleSessionError } from '@/utils/utils'\nimport mixpanel from '@/vendors/mixpanel'\nimport { loadStripe } from '@stripe/stripe-js'\nimport { useQuery } from '@tanstack/vue-query'\nimport { useDebounceFn } from '@vueuse/core'\nimport { useToast } from 'primevue/usetoast'\nimport { computed, onBeforeMount, onUnmounted, ref, watchEffect } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport PlanPageIntro from '@/components/Plans/PlanPageIntro.vue'\nimport PlanPageHeader from '@/components/Plans/PlanPageHeader.vue'\nimport CardIcon from '@/icons/CardIcon.vue'\nimport PromoCode from '@/components/Plans/PromoCode.vue'\nimport usePromocode from '@/composables/usePromocode'\nimport Toast from '@/components/Toast/Toast.vue'\nimport PlanItemOnboarding from '@/components/Plans/PlanItemOnboarding.vue'\nimport KeyFeatures from '@/components/Plans/KeyFeatures.vue'\nimport BrandLogos from '@/components/Plans/BrandLogos.vue'\nimport PlansFAQ from '@/components/Plans/PlansFAQ.vue'\nimport Reviews from '@/components/Plans/Reviews.vue'\nimport ComparePlans from '@/components/Plans/ComparePlans.vue'\nimport { useCookies } from 'vue3-cookies'\nimport ManageSubscription from '@/components/Plans/ManageSubscription.vue'\nimport CurrentPlanIndicatorV2 from '@/components/Plans/CurrentPlanIndicatorV2.vue'\nimport { useGetOnboardingData } from '@/api/onboarding'\nimport AeroPlaneWhite from '@/icons/AeroPlaneWhite.vue'\nimport PaperPlaneBlack from '@/icons/PaperPlaneBlack.vue'\nimport RocketIconBlack from '@/icons/RocketIconBlack.vue'\nimport UFOBlack from '@/icons/UFOBlack.vue'\n\n// COUPON ENV\nconst couponFromEnv = document.querySelector('#coupon').value\nconst { data: onboardingData } = useGetOnboardingData()\nconst authStore = useAuthStore()\nconst toast = useToast()\nconst { removePromoCode, promoLoading } = usePromocode()\n\nconst SPECIAL_PLANS = ['elite_budapest', 'q4_special', 'offer97', 'UNLIMITED']\n\n// refs\nconst stripeMeta = ref({})\nconst isChoosingPlan = ref(false)\nconst isDoingAction = ref(false)\nconst stripeStatus = ref({\n text: '',\n status: '',\n})\nconst flag = ref(false)\nconst route = useRoute()\nconst router = useRouter()\nconst isRedirecting = ref(false)\nconst waitingBanner = ref(false)\nconst successBanner = ref(true)\nconst intervalId = ref()\n\nconst couponModal = ref(false)\nconst selectedPlan = ref()\nconst showModalCouponField = ref(false)\nconst couponCode = ref('')\nconst couponCodeLoading = ref(false)\nconst couponCodeData = ref()\nconst couponCodeError = ref(false)\n\nconst selectedTab = ref(0)\nconst showTab = ref(false)\nconst hasYearlyPlan = ref(false)\nconst showPromoPlan = ref(false)\n\nconst isOwnWorkspace = computed(() => {\n return authStore.authData?.user?.current_workspace_is_owned || false\n})\n\n// fetch plans\nconst { isLoading, data, refetch } = useQuery(['plans-data'], getPlans, {\n refetchOnWindowFocus: false,\n enabled: true,\n cacheTime: 0,\n})\n\nconst tabs = ref()\n\nconst plansData = computed(() => {\n if (!data.value) return []\n\n if (showPromoPlan.value) {\n // if there is a special promo code applied then only show those plans\n if (selectedTab.value === 0 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'monthly' && showPromoPlan.value.includes(item.slug)\n )\n }\n\n if (selectedTab.value === 1 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'yearly' && showPromoPlan.value.includes(item.slug)\n )\n }\n\n return data.value.data.filter((item) =>\n showPromoPlan.value.includes(item.slug)\n )\n } else {\n // otherwise show regular plans\n if (selectedTab.value === 0 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'monthly' && !SPECIAL_PLANS.includes(item.slug)\n )\n }\n\n if (selectedTab.value === 1 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'yearly' && !SPECIAL_PLANS.includes(item.slug)\n )\n }\n\n return data.value.data.filter((item) => !SPECIAL_PLANS.includes(item.slug))\n }\n})\n\nasync function getPlans() {\n try {\n let res = await Api.get('/api/get-plans')\n authStore.authData.user_plan = res.data.user_plan\n authStore.authData.free_trial_days_remaining =\n res.data.free_trial_days_remaining\n authStore.authData.has_active_subscription =\n res.data.has_active_subscription\n\n // set tabs\n tabs.value = [\n {\n id: 0,\n title: 'Monthly',\n // badgeText:\n // !authStore.authData.has_active_subscription &&\n // res.data.next_endpoint !== 'payment_method_update' &&\n // !res.data.promo_code\n // ? '50% Off'\n // : null,\n },\n ]\n\n if (stripeStatus.value.text) {\n router.replace({ name: 'route.swipe-file' })\n }\n\n const isSpecialPlanApplied = SPECIAL_PLANS.includes(\n res.data.promo_code?.promo_code\n )\n\n if (res.data.data.find((item) => item.interval === 'yearly')) {\n hasYearlyPlan.value = true\n // if there are no tabs for yearly plans then add it\n if (!tabs.value.find((item) => item.title === 'Yearly')) {\n tabs.value = [\n ...tabs.value,\n {\n id: 1,\n title: 'Yearly',\n badgeText: !isSpecialPlanApplied\n ? '3 months free'\n : res.data.promo_code?.promo_code === 'UNLIMITED'\n ? 'Save ~25%'\n : `Save 17%`,\n },\n ]\n }\n }\n\n let specialPlanCode = null\n if (isSpecialPlanApplied) {\n showPromoPlan.value = res.data.promo_code?.promo_code\n specialPlanCode = res.data.promo_code?.promo_code\n }\n\n const hasMonthlyPromoPlan = res.data.data.find(\n (item) =>\n item.interval === 'monthly' && specialPlanCode?.includes(item.slug)\n )\n const hasYearlyPromoPlan = res.data.data.find(\n (item) =>\n item.interval === 'yearly' && specialPlanCode?.includes(item.slug)\n )\n const hasMonthlyRegularPlan = res.data.data.find(\n (item) =>\n item.interval === 'monthly' && !specialPlanCode?.includes(item.slug)\n )\n const hasYearlyRegularPlan = res.data.data.find(\n (item) =>\n item.interval === 'yearly' && !specialPlanCode?.includes(item.slug)\n )\n\n // select tab initially\n if (showPromoPlan.value) {\n if (hasMonthlyPromoPlan && hasYearlyPromoPlan) {\n showTab.value = true\n selectedTab.value = 0\n }\n } else {\n if (hasMonthlyRegularPlan && hasYearlyRegularPlan) {\n showTab.value = true\n selectedTab.value = 0\n }\n }\n\n waitingBanner.value = false\n\n return res.data\n } catch (err) {\n handleSessionError(err)\n }\n}\n\nasync function redirectToStripeBillingPortal() {\n try {\n isRedirecting.value = true\n const res = await Api.get('/api/subscription/billing-portal')\n window.location.href = res.data.data.redirect_url\n } catch (err) {\n handleSessionError(err)\n toast.add({\n severity: 'error',\n summary: 'Failed',\n detail: 'Something went wrong!',\n life: 3000,\n })\n }\n}\n\nasync function choosePlan(payload) {\n try {\n if (payload.plan.next_endpoint === 'update') {\n isChoosingPlan.value = String(payload.plan.id)\n const res = await Api.post(\n `/api/subscription/${payload.plan.next_endpoint}/${payload.plan.id}`\n )\n mixpanel.track(`Switching plan`, {\n plan_id: payload.plan.id,\n })\n stripeMeta.value = res.data.data\n stripeMeta.value.plan = payload.plan\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n \n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n })\n \n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n \n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n } else {\n throw new Error('No redirect URL received from checkout session')\n }\n } else if (payload.plan.next_endpoint === 'resubscribe') {\n isChoosingPlan.value = String(payload.plan.id)\n\n const res = await Api.post(`/api/subscription/create`, {\n payment_method: data.value.data[0].default_payment_id,\n planId: Number(payload.plan.id),\n })\n\n intervalId.value = setInterval(async () => {\n let res = await Api.get('/api/get-plans')\n\n if (res.data.user_plan) {\n clearInterval(intervalId.value)\n window.location = window.location.origin + '/plans?status=success'\n return\n }\n\n if (res.data.payment_method_invalid) {\n clearInterval(intervalId.value)\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: res.data.payment_method_invalid,\n life: 30000,\n })\n await Api.post('/api/remove-payment-method-cache')\n\n // Clear all query parameters from URL\n router.replace({\n path: router.currentRoute.value.path,\n query: {},\n })\n\n isChoosingPlan.value = false\n return\n }\n }, 5000)\n } else if (\n payload.plan.next_endpoint === 'resume' ||\n payload.plan.next_endpoint === 'resume_required'\n ) {\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: 'Please renew your plan before proceeding!',\n life: 6000,\n })\n return\n } else if (payload.plan.next_endpoint === 'pending_cancellation') {\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail:\n \"Can't proceed! Your cancellation request is currently in pending.\",\n life: 6000,\n })\n }\n } catch (err) {\n handleSessionError(err)\n\n if (\n err.response.data.message ===\n 'Please subscribe to a plan first. After that, you can choose a different plan.'\n ) {\n // update payment method\n mixpanel.track('Redirecting to Stripe | cause: update payment method')\n await redirectToStripeBillingPortal()\n return\n }\n\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: err?.response?.data?.message,\n life: 6000,\n })\n isChoosingPlan.value = false\n return\n }\n}\n\nfunction handleCloseCheckout() {\n stripeMeta.value = {}\n mixpanel.track(`Closed plan upgrade/downgrade page`)\n}\n\nasync function handleStripeStatus(publishableKey, clientSecret) {\n try {\n const stripe = await loadStripe(publishableKey)\n\n let { setupIntent } = await stripe.retrieveSetupIntent(clientSecret)\n\n switch (setupIntent.status) {\n case 'succeeded': {\n stripeStatus.value.text = 'Success! Payment received.'\n stripeStatus.value.type = 'success'\n\n // only required for the first time when no plan is choosen\n if (route.query.planId) {\n await Api.post(`/api/subscription/create`, {\n payment_method: setupIntent.payment_method,\n planId: Number(route.query.planId),\n })\n }\n\n break\n }\n\n case 'processing': {\n stripeStatus.value.text =\n \"Payment processing! We'll update you when payment is received.\"\n stripeStatus.value.type = 'success'\n break\n }\n\n case 'requires_payment_method': {\n stripeStatus.value.text =\n 'Payment failed! Please try another payment method.'\n stripeStatus.value.type = 'error'\n break\n }\n\n default: {\n stripeStatus.value.text = 'Something went wrong! Please try again.'\n stripeStatus.value.type = 'error'\n break\n }\n }\n } catch (err) {\n } finally {\n setTimeout(() => {\n // refetch plans after 10 sec\n flag.value = true\n }, 10000)\n }\n}\n\nconst handleCouponInput = useDebounceFn(async () => {\n couponCodeLoading.value = true\n couponCodeError.value = false\n couponCodeData.value = null\n\n try {\n let res = await Api.get(\n `/api/coupon-applied-preview?plan_id=${selectedPlan.value.id}&coupon_code=${couponCode.value}`\n )\n couponCodeData.value = res.data\n couponCodeLoading.value = false\n } catch (err) {\n couponCodeLoading.value = false\n couponCodeError.value = true\n\n handleSessionError(err)\n }\n}, 1000)\n\nconst choosePlanWithCoupon = async () => {\n isChoosingPlan.value = String(selectedPlan.value.id)\n\n // if coupon from ENV is available then automatically apply that coupon\n let coupon = null\n if (\n couponFromEnv &&\n selectedPlan.value.interval === 'monthly' &&\n !authStore.authData?.has_active_subscription &&\n selectedPlan.value?.next_endpoint !== 'payment_method_update' &&\n !data?.value.promo_code\n ) {\n coupon = couponFromEnv\n } else {\n coupon = couponCode.value\n }\n\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${selectedPlan.value.id} ${\n coupon ? `?coupon_code=${coupon}` : ''\n }`\n )\n mixpanel.track(`Clicked on Choose Plan with Coupon`)\n window.location.href = res.data.data.redirect_url\n}\n\nonBeforeMount(async () => {\n const publishableKey = new URLSearchParams(window.location.search).get(\n 'publishable_key'\n )\n const clientSecret = new URLSearchParams(window.location.search).get(\n 'setup_intent_client_secret'\n )\n const customerId = new URLSearchParams(window.location.search).get('customer')\n\n if (\n onboardingData.value.is_completed_onboarding === false &&\n !authStore.authData?.is_allow_ad_template\n ) {\n // redirect to onboarding\n const url = router.resolve({\n name: 'route.onboarding',\n query: {\n publishable_key: publishableKey,\n setup_intent_client_secret: clientSecret,\n customer: customerId,\n },\n }).href\n\n window.location.href = url\n return\n }\n\n // if there is no publishable key and there is no customer id, then dont wait before fetching plans data, by making the flag true\n if (!publishableKey && !customerId) {\n flag.value = true\n return\n }\n\n // if customer id is available then keep cheking if the backend is updated\n if (customerId) {\n waitingBanner.value = true\n\n intervalId.value = setInterval(async () => {\n let res = await Api.get('/api/get-plans')\n if (res.data.user_plan) {\n clearInterval(intervalId.value)\n window.location = window.location.origin + '/plans?status=success'\n }\n }, 5000)\n\n return\n }\n\n // handleStripeStatus(publishableKey, clientSecret)\n})\n\nfunction handleTabChange(val) {\n selectedTab.value = val\n}\n\nonUnmounted(() => {\n if (intervalId.value) {\n clearInterval(intervalId.value)\n }\n})\n\nconst indexIconMap = {\n 0: PaperPlaneBlack,\n 1: AeroPlaneWhite,\n 2: RocketIconBlack,\n 3: UFOBlack,\n}\n</script>\n\n<template>\n <div class=\"bg-[#141414] p-[24px] h-full min-h-screen\">\n <Toast />\n\n <PlanPageHeader />\n\n <div class=\"max-w-[1500px] mx-auto flex flex-col text-white\">\n <div\n class=\"bg-[#FFF0F0] rounded-[16px] py-[16px] px-[32px] max-w-[453px] mx-auto border border-[#BC1414] flex gap-2 items-center mb-4 flex-wrap justify-center\"\n v-if=\"\n isOwnWorkspace &&\n data &&\n data.data[0]?.next_endpoint === 'payment_method_update'\n \"\n >\n <div\n class=\"size-[48px] flex items-center justify-center rounded-full\"\n style=\"background: rgba(188, 20, 20, 0.08)\"\n >\n <CardIcon class=\"size-5 fill-[#BC1414]\" />\n </div>\n\n <div class=\"flex flex-col text-center text-[#BC1414]\">\n <p class=\"text-base\">There seems to be an issue with your card.</p>\n <p class=\"text-xs\">Please double-check your details and try again.</p>\n </div>\n </div>\n\n <PlanPageIntro v-if=\"!stripeMeta.plan\" class=\"pb-3\">\n <CurrentPlanIndicatorV2\n v-if=\"data && !isLoading && data.user_plan && !data.is_new_user\"\n :planTitle=\"data.user_plan?.title\"\n :trialDaysRemaining=\"data.free_trial_days_remaining\"\n :price=\"String(data.user_plan?.discounted_price)\"\n :interval=\"data.user_plan?.interval\"\n :isHighTicket=\"Boolean(data.user_plan.is_high_ticket)\"\n />\n </PlanPageIntro>\n\n <div\n class=\"flex justify-center sm:justify-between items-center gap-4 flex-wrap mt-4\"\n >\n <!-- promo code -->\n <div class=\"flex gap-4 flex-wrap\">\n <PromoCode\n v-if=\"!isLoading && !stripeMeta.plan && data\"\n :promocodeData=\"data?.promo_code\"\n :couponcodeData=\"data?.coupon\"\n class=\"self-center\"\n variant=\"large\"\n />\n\n <ManageSubscription\n v-if=\"\n isOwnWorkspace &&\n data &&\n data.data[0]?.next_endpoint === 'payment_method_update'\n \"\n :isLoading=\"isRedirecting\"\n @click=\"redirectToStripeBillingPortal\"\n />\n </div>\n\n <Tab\n class=\"w-full sm:w-max justify-self-center sm:justify-self-end\"\n v-if=\"data && showTab && !stripeMeta.plan\"\n :selected=\"selectedTab\"\n :options=\"tabs\"\n @change=\"(val) => handleTabChange(val)\"\n />\n </div>\n\n <!-- if not user's workspace then show this warning -->\n <template v-if=\"!isOwnWorkspace\">\n <p class=\"bg-warning p-2 text-center text-black rounded-md mt-6\">\n You don't have access to manage plans in this workspace\n </p>\n </template>\n\n <!-- if user's workspace, then show plans -->\n <template v-else>\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"waitingBanner\"\n >\n <component :is=\"CheckColorIcon\" class=\"min-w-max\"> </component>\n\n <div :class=\"`font-semibold w-full`\">\n Please wait and do not close this page...\n </div>\n </div>\n\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"stripeStatus.text && !stripeMeta.plan && !successBanner\"\n >\n <component\n :is=\"\n stripeStatus.type === 'success' ? CheckColorIcon : AlertColorIcon\n \"\n class=\"min-w-max\"\n >\n </component>\n\n <div :class=\"`font-semibold w-full`\">\n {{ stripeStatus.text }}\n {{ isLoading ? ' Please wait and do not close this page...' : '' }}\n </div>\n\n <button @click=\"stripeStatus.text = ''\" class=\"p-1 mr-0\">✕</button>\n </div>\n\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"\n route.query.status === 'success' &&\n successBanner &&\n !stripeMeta.plan\n \"\n >\n <component :is=\"CheckColorIcon\" class=\"min-w-max\"> </component>\n\n <div :class=\"`font-semibold w-full`\">\n You changed your plan successfully!\n </div>\n\n <button @click=\"successBanner = false\" class=\"p-1 mr-0\">✕</button>\n </div>\n\n <CheckoutForm\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n />\n\n <template v-else>\n <div\n v-if=\"isLoading || !data\"\n class=\"mt-8 w-full flex items-center justify-center\"\n >\n <Loading />\n </div>\n\n <div v-else class=\"flex flex-col justify-center\">\n <!-- <button\n v-if=\"\n showPromoPlan && !SPECIAL_PLANS.includes(data.user_plan?.slug)\n \"\n class=\"max-w-max self-center text-[#6c7a88] underline underline-offset-4 decoration-[#6c7a88] flex items-center gap-3\"\n @click=\"removePromoCode\"\n :disabled=\"promoLoading\"\n >\n Skip this offer\n <Loading v-if=\"promoLoading\" size=\"xs\" />\n </button> -->\n\n <div\n :class=\"\n showPromoPlan\n ? 'flex flex-wrap mt-4 gap-4 justify-center'\n : `mt-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4`\n \"\n >\n <template v-for=\"(plan, index) in plansData\" :key=\"index\">\n <template v-if=\"plan.active\">\n <PlanItemOnboarding\n :icon=\"indexIconMap[index]\"\n :planData=\"plan\"\n :isPopular=\"index === 1\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n :currentPlan=\"data.user_plan\"\n :isCustomPlan=\"Boolean(plan.url)\"\n :isLoading=\"isChoosingPlan || isDoingAction || promoLoading\"\n @choose=\"() => choosePlan({ plan: plan })\"\n :showOffer=\"\n false && // remove this if we want to show offer\n !data.has_active_subscription &&\n plan.next_endpoint !== 'payment_method_update' &&\n !data.promo_code\n \"\n :trialRemaining=\"data.free_trial_days_remaining\"\n :hasCoupon=\"Boolean(data?.coupon)\"\n :showPromoPlan=\"Boolean(showPromoPlan)\"\n :planUsageType=\"authStore.authData?.user?.plan_usage_type\"\n :isNewUser=\"data.is_new_user\"\n />\n\n <!-- <PlanItem\n :planData=\"plan\"\n :currentPlan=\"data.user_plan\"\n @choose=\"choosePlan\"\n :isLoading=\"\n Boolean(isChoosingPlan) || isDoingAction || promoLoading\n \"\n :hasActiveSubscription=\"data.has_active_subscription\"\n :promoCode=\"data.promo_code\"\n @action=\"isDoingAction = true\"\n :isPopular=\"index === 1 || Boolean(showPromoPlan)\"\n :bookDemoPlan=\"Boolean(plan.url)\"\n /> -->\n </template>\n </template>\n </div>\n </div>\n </template>\n\n <KeyFeatures\n v-if=\"!isLoading\"\n class=\"mt-[90px]\"\n :isUnlimitedPlan=\"showPromoPlan === 'UNLIMITED'\"\n />\n\n <BrandLogos\n v-if=\"!isLoading && showPromoPlan !== 'UNLIMITED'\"\n class=\"my-[150px] self-center\"\n />\n\n <Reviews v-if=\"!isLoading && showPromoPlan !== 'UNLIMITED'\" />\n\n <ComparePlans\n v-if=\"!isLoading && !showPromoPlan\"\n :isLoading=\"isChoosingPlan || isDoingAction || promoLoading\"\n :currentPlan=\"data.user_plan\"\n :plansData=\"data\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n class=\"my-[150px] self-center\"\n @choose=\"\n (val) => {\n choosePlan(val)\n }\n \"\n :trialRemaining=\"data.free_trial_days_remaining\"\n :isNewUser=\"data.is_new_user\"\n />\n\n <PlansFAQ v-if=\"!isLoading && !showPromoPlan\" />\n </template>\n\n <Modal\n v-if=\"couponModal\"\n @close=\"\n () => {\n couponModal = false\n showModalCouponField = false\n couponCodeData = null\n couponCodeError = false\n }\n \"\n heading=\"Choose plan\"\n size=\"content\"\n >\n <div class=\"flex flex-col gap-3\">\n <p>\n You have selected\n <span class=\"font-bold\">{{ selectedPlan.title }}</span> plan\n </p>\n\n <p v-if=\"!couponCodeData\">\n Total price:\n <span class=\"font-bold\"\n >${{ selectedPlan.price.toLocaleString() }}</span\n >\n </p>\n\n <p v-else class=\"flex gap-1\">\n <span> Total price: </span>\n <span class=\"line-through text-[var(--text-secondary)]\"\n >${{\n couponCodeData.plan_price.toFixed(2).toLocaleString()\n }}</span\n >\n <span class=\"font-bold\">${{ couponCodeData.total_price }}</span>\n </p>\n\n <button\n v-if=\"!showModalCouponField\"\n class=\"text-xs text-center self-center w-max text-gray-400 underline underline-offset-2 decoration-gray-400\"\n @click=\"showModalCouponField = true\"\n >\n I have a coupon code\n </button>\n\n <div class=\"flex gap-3 items-center flex-wrap\">\n <input\n v-if=\"showModalCouponField\"\n type=\"text\"\n placeholder=\"Coupon Code\"\n class=\"input-text max-w-[300px]\"\n v-model=\"couponCode\"\n @input=\"\n (e) => {\n if (e.target.value) {\n couponCodeLoading = true\n handleCouponInput()\n } else {\n couponCodeLoading = false\n couponCodeError = false\n couponCodeData = false\n }\n }\n \"\n @change=\"mixpanel.track(`Typing coupon code`)\"\n />\n\n <Loading v-if=\"couponCodeLoading\" />\n\n <p\n v-if=\"couponCodeError && !couponCodeLoading\"\n class=\"text-sm text-[#E51E43]\"\n >\n Invalid code!\n </p>\n\n <p\n v-if=\"couponCodeData && !couponCodeLoading\"\n class=\"text-[#0AC25D] text-sm\"\n >\n Coupon Applied!\n </p>\n </div>\n\n <Button\n label=\"Continue\"\n class=\"w-full\"\n @click=\"choosePlanWithCoupon\"\n :loading=\"Boolean(isChoosingPlan)\"\n :disabled=\"couponCodeError || couponCodeLoading || isChoosingPlan\"\n />\n </div>\n </Modal>\n </div>\n </div>\n</template>\n",
"prFileContentAfter" : "<script setup>\nimport { Api } from '@/api'\nimport Button from '@/components/Button/Button.vue'\nimport CheckoutForm from '@/components/Checkout/CheckoutForm.vue'\nimport Loading from '@/components/Loading/Loading.vue'\nimport Modal from '@/components/Modal/Modal.vue'\nimport Tab from '@/components/Tab/Tab.vue'\nimport AlertColorIcon from '@/icons/AlertColorIcon.vue'\nimport CheckColorIcon from '@/icons/CheckColorIcon.vue'\nimport { useAuthStore } from '@/stores/AuthStore'\nimport { handleSessionError } from '@/utils/utils'\nimport mixpanel from '@/vendors/mixpanel'\nimport { loadStripe } from '@stripe/stripe-js'\nimport { useQuery } from '@tanstack/vue-query'\nimport { useDebounceFn } from '@vueuse/core'\nimport { useToast } from 'primevue/usetoast'\nimport { computed, onBeforeMount, onUnmounted, ref, watchEffect } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport PlanPageIntro from '@/components/Plans/PlanPageIntro.vue'\nimport PlanPageHeader from '@/components/Plans/PlanPageHeader.vue'\nimport CardIcon from '@/icons/CardIcon.vue'\nimport PromoCode from '@/components/Plans/PromoCode.vue'\nimport usePromocode from '@/composables/usePromocode'\nimport Toast from '@/components/Toast/Toast.vue'\nimport PlanItemOnboarding from '@/components/Plans/PlanItemOnboarding.vue'\nimport KeyFeatures from '@/components/Plans/KeyFeatures.vue'\nimport BrandLogos from '@/components/Plans/BrandLogos.vue'\nimport PlansFAQ from '@/components/Plans/PlansFAQ.vue'\nimport Reviews from '@/components/Plans/Reviews.vue'\nimport ComparePlans from '@/components/Plans/ComparePlans.vue'\nimport { useCookies } from 'vue3-cookies'\nimport ManageSubscription from '@/components/Plans/ManageSubscription.vue'\nimport CurrentPlanIndicatorV2 from '@/components/Plans/CurrentPlanIndicatorV2.vue'\nimport { useGetOnboardingData } from '@/api/onboarding'\nimport AeroPlaneWhite from '@/icons/AeroPlaneWhite.vue'\nimport PaperPlaneBlack from '@/icons/PaperPlaneBlack.vue'\nimport RocketIconBlack from '@/icons/RocketIconBlack.vue'\nimport UFOBlack from '@/icons/UFOBlack.vue'\n\n// COUPON ENV\nconst couponFromEnv = document.querySelector('#coupon').value\nconst { data: onboardingData } = useGetOnboardingData()\nconst authStore = useAuthStore()\nconst toast = useToast()\nconst { removePromoCode, promoLoading } = usePromocode()\n\nconst SPECIAL_PLANS = ['elite_budapest', 'q4_special', 'offer97', 'UNLIMITED']\n\n// refs\nconst stripeMeta = ref({})\nconst isChoosingPlan = ref(false)\nconst isDoingAction = ref(false)\nconst stripeStatus = ref({\n text: '',\n status: '',\n})\nconst flag = ref(false)\nconst route = useRoute()\nconst router = useRouter()\nconst isRedirecting = ref(false)\nconst waitingBanner = ref(false)\nconst successBanner = ref(true)\nconst intervalId = ref()\n\nconst couponModal = ref(false)\nconst selectedPlan = ref()\nconst showModalCouponField = ref(false)\nconst couponCode = ref('')\nconst couponCodeLoading = ref(false)\nconst couponCodeData = ref()\nconst couponCodeError = ref(false)\n\nconst selectedTab = ref(0)\nconst showTab = ref(false)\nconst hasYearlyPlan = ref(false)\nconst showPromoPlan = ref(false)\n\nconst isOwnWorkspace = computed(() => {\n return authStore.authData?.user?.current_workspace_is_owned || false\n})\n\n// fetch plans\nconst { isLoading, data, refetch } = useQuery(['plans-data'], getPlans, {\n refetchOnWindowFocus: false,\n enabled: true,\n cacheTime: 0,\n})\n\nconst tabs = ref()\n\nconst plansData = computed(() => {\n if (!data.value) return []\n\n if (showPromoPlan.value) {\n // if there is a special promo code applied then only show those plans\n if (selectedTab.value === 0 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'monthly' && showPromoPlan.value.includes(item.slug)\n )\n }\n\n if (selectedTab.value === 1 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'yearly' && showPromoPlan.value.includes(item.slug)\n )\n }\n\n return data.value.data.filter((item) =>\n showPromoPlan.value.includes(item.slug)\n )\n } else {\n // otherwise show regular plans\n if (selectedTab.value === 0 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'monthly' && !SPECIAL_PLANS.includes(item.slug)\n )\n }\n\n if (selectedTab.value === 1 && showTab.value) {\n return data.value.data.filter(\n (item) =>\n item.interval === 'yearly' && !SPECIAL_PLANS.includes(item.slug)\n )\n }\n\n return data.value.data.filter((item) => !SPECIAL_PLANS.includes(item.slug))\n }\n})\n\nasync function getPlans() {\n try {\n let res = await Api.get('/api/get-plans')\n authStore.authData.user_plan = res.data.user_plan\n authStore.authData.free_trial_days_remaining =\n res.data.free_trial_days_remaining\n authStore.authData.has_active_subscription =\n res.data.has_active_subscription\n\n // set tabs\n tabs.value = [\n {\n id: 0,\n title: 'Monthly',\n // badgeText:\n // !authStore.authData.has_active_subscription &&\n // res.data.next_endpoint !== 'payment_method_update' &&\n // !res.data.promo_code\n // ? '50% Off'\n // : null,\n },\n ]\n\n if (stripeStatus.value.text) {\n router.replace({ name: 'route.swipe-file' })\n }\n\n const isSpecialPlanApplied = SPECIAL_PLANS.includes(\n res.data.promo_code?.promo_code\n )\n\n if (res.data.data.find((item) => item.interval === 'yearly')) {\n hasYearlyPlan.value = true\n // if there are no tabs for yearly plans then add it\n if (!tabs.value.find((item) => item.title === 'Yearly')) {\n tabs.value = [\n ...tabs.value,\n {\n id: 1,\n title: 'Yearly',\n badgeText: !isSpecialPlanApplied\n ? '3 months free'\n : res.data.promo_code?.promo_code === 'UNLIMITED'\n ? 'Save ~25%'\n : `Save 17%`,\n },\n ]\n }\n }\n\n let specialPlanCode = null\n if (isSpecialPlanApplied) {\n showPromoPlan.value = res.data.promo_code?.promo_code\n specialPlanCode = res.data.promo_code?.promo_code\n }\n\n const hasMonthlyPromoPlan = res.data.data.find(\n (item) =>\n item.interval === 'monthly' && specialPlanCode?.includes(item.slug)\n )\n const hasYearlyPromoPlan = res.data.data.find(\n (item) =>\n item.interval === 'yearly' && specialPlanCode?.includes(item.slug)\n )\n const hasMonthlyRegularPlan = res.data.data.find(\n (item) =>\n item.interval === 'monthly' && !specialPlanCode?.includes(item.slug)\n )\n const hasYearlyRegularPlan = res.data.data.find(\n (item) =>\n item.interval === 'yearly' && !specialPlanCode?.includes(item.slug)\n )\n\n // select tab initially\n if (showPromoPlan.value) {\n if (hasMonthlyPromoPlan && hasYearlyPromoPlan) {\n showTab.value = true\n selectedTab.value = 0\n }\n } else {\n if (hasMonthlyRegularPlan && hasYearlyRegularPlan) {\n showTab.value = true\n selectedTab.value = 0\n }\n }\n\n waitingBanner.value = false\n\n return res.data\n } catch (err) {\n handleSessionError(err)\n }\n}\n\nasync function redirectToStripeBillingPortal() {\n try {\n isRedirecting.value = true\n const res = await Api.get('/api/subscription/billing-portal')\n window.location.href = res.data.data.redirect_url\n } catch (err) {\n handleSessionError(err)\n toast.add({\n severity: 'error',\n summary: 'Failed',\n detail: 'Something went wrong!',\n life: 3000,\n })\n }\n}\n\nasync function choosePlan(payload) {\n try {\n if (payload.plan.next_endpoint === 'update') {\n isChoosingPlan.value = String(payload.plan.id)\n const res = await Api.post(\n `/api/subscription/${payload.plan.next_endpoint}/${payload.plan.id}`\n )\n mixpanel.track(`Switching plan`, {\n plan_id: payload.plan.id,\n })\n stripeMeta.value = res.data.data\n stripeMeta.value.plan = payload.plan\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n } else {\n throw new Error('No redirect URL received from checkout session')\n }\n } else if (payload.plan.next_endpoint === 'resubscribe') {\n isChoosingPlan.value = String(payload.plan.id)\n\n const res = await Api.post(`/api/subscription/create`, {\n payment_method: data.value.data[0].default_payment_id,\n planId: Number(payload.plan.id),\n })\n\n intervalId.value = setInterval(async () => {\n let res = await Api.get('/api/get-plans')\n\n if (res.data.user_plan) {\n clearInterval(intervalId.value)\n window.location = window.location.origin + '/plans?status=success'\n return\n }\n\n if (res.data.payment_method_invalid) {\n clearInterval(intervalId.value)\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: res.data.payment_method_invalid,\n life: 30000,\n })\n await Api.post('/api/remove-payment-method-cache')\n\n // Clear all query parameters from URL\n router.replace({\n path: router.currentRoute.value.path,\n query: {},\n })\n\n isChoosingPlan.value = false\n return\n }\n }, 5000)\n } else if (\n payload.plan.next_endpoint === 'resume' ||\n payload.plan.next_endpoint === 'resume_required'\n ) {\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: 'Please renew your plan before proceeding!',\n life: 6000,\n })\n return\n } else if (payload.plan.next_endpoint === 'pending_cancellation') {\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail:\n \"Can't proceed! Your cancellation request is currently in pending.\",\n life: 6000,\n })\n }\n } catch (err) {\n handleSessionError(err)\n\n if (\n err.response.data.message ===\n 'Please subscribe to a plan first. After that, you can choose a different plan.'\n ) {\n // update payment method\n mixpanel.track('Redirecting to Stripe | cause: update payment method')\n await redirectToStripeBillingPortal()\n return\n }\n\n toast.add({\n severity: 'error',\n summary: 'Error',\n detail: err?.response?.data?.message,\n life: 6000,\n })\n isChoosingPlan.value = false\n return\n }\n}\n\nfunction handleCloseCheckout() {\n stripeMeta.value = {}\n mixpanel.track(`Closed plan upgrade/downgrade page`)\n}\n\nasync function handleStripeStatus(publishableKey, clientSecret) {\n try {\n const stripe = await loadStripe(publishableKey)\n\n let { setupIntent } = await stripe.retrieveSetupIntent(clientSecret)\n\n switch (setupIntent.status) {\n case 'succeeded': {\n stripeStatus.value.text = 'Success! Payment received.'\n stripeStatus.value.type = 'success'\n\n // only required for the first time when no plan is choosen\n if (route.query.planId) {\n await Api.post(`/api/subscription/create`, {\n payment_method: setupIntent.payment_method,\n planId: Number(route.query.planId),\n })\n }\n\n break\n }\n\n case 'processing': {\n stripeStatus.value.text =\n \"Payment processing! We'll update you when payment is received.\"\n stripeStatus.value.type = 'success'\n break\n }\n\n case 'requires_payment_method': {\n stripeStatus.value.text =\n 'Payment failed! Please try another payment method.'\n stripeStatus.value.type = 'error'\n break\n }\n\n default: {\n stripeStatus.value.text = 'Something went wrong! Please try again.'\n stripeStatus.value.type = 'error'\n break\n }\n }\n } catch (err) {\n } finally {\n setTimeout(() => {\n // refetch plans after 10 sec\n flag.value = true\n }, 10000)\n }\n}\n\nconst handleCouponInput = useDebounceFn(async () => {\n couponCodeLoading.value = true\n couponCodeError.value = false\n couponCodeData.value = null\n\n try {\n let res = await Api.get(\n `/api/coupon-applied-preview?plan_id=${selectedPlan.value.id}&coupon_code=${couponCode.value}`\n )\n couponCodeData.value = res.data\n couponCodeLoading.value = false\n } catch (err) {\n couponCodeLoading.value = false\n couponCodeError.value = true\n\n handleSessionError(err)\n }\n}, 1000)\n\nconst choosePlanWithCoupon = async () => {\n isChoosingPlan.value = String(selectedPlan.value.id)\n\n // if coupon from ENV is available then automatically apply that coupon\n let coupon = null\n if (\n couponFromEnv &&\n selectedPlan.value.interval === 'monthly' &&\n !authStore.authData?.has_active_subscription &&\n selectedPlan.value?.next_endpoint !== 'payment_method_update' &&\n !data?.value.promo_code\n ) {\n coupon = couponFromEnv\n } else {\n coupon = couponCode.value\n }\n\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${selectedPlan.value.id} ${\n coupon ? `?coupon_code=${coupon}` : ''\n }`\n )\n mixpanel.track(`Clicked on Choose Plan with Coupon`)\n window.location.href = res.data.data.redirect_url\n}\n\nonBeforeMount(async () => {\n const publishableKey = new URLSearchParams(window.location.search).get(\n 'publishable_key'\n )\n const clientSecret = new URLSearchParams(window.location.search).get(\n 'setup_intent_client_secret'\n )\n const customerId = new URLSearchParams(window.location.search).get('customer')\n\n if (\n onboardingData.value.is_completed_onboarding === false &&\n !authStore.authData?.is_allow_ad_template\n ) {\n // redirect to onboarding\n const url = router.resolve({\n name: 'route.onboarding',\n query: {\n publishable_key: publishableKey,\n setup_intent_client_secret: clientSecret,\n customer: customerId,\n },\n }).href\n\n window.location.href = url\n return\n }\n\n // if there is no publishable key and there is no customer id, then dont wait before fetching plans data, by making the flag true\n if (!publishableKey && !customerId) {\n flag.value = true\n return\n }\n\n // if customer id is available then keep cheking if the backend is updated\n if (customerId) {\n waitingBanner.value = true\n\n intervalId.value = setInterval(async () => {\n let res = await Api.get('/api/get-plans')\n if (res.data.user_plan) {\n clearInterval(intervalId.value)\n window.location = window.location.origin + '/plans?status=success'\n }\n }, 5000)\n\n return\n }\n\n // handleStripeStatus(publishableKey, clientSecret)\n})\n\nfunction handleTabChange(val) {\n selectedTab.value = val\n}\n\nonUnmounted(() => {\n if (intervalId.value) {\n clearInterval(intervalId.value)\n }\n})\n\nconst indexIconMap = {\n 0: PaperPlaneBlack,\n 1: AeroPlaneWhite,\n 2: RocketIconBlack,\n 3: UFOBlack,\n}\n</script>\n\n<template>\n <div class=\"bg-[#141414] p-[24px] h-full min-h-screen\">\n <Toast />\n\n <PlanPageHeader />\n\n <div class=\"max-w-[1500px] mx-auto flex flex-col text-white\">\n <div\n class=\"bg-[#FFF0F0] rounded-[16px] py-[16px] px-[32px] max-w-[453px] mx-auto border border-[#BC1414] flex gap-2 items-center mb-4 flex-wrap justify-center\"\n v-if=\"\n isOwnWorkspace &&\n data &&\n data.data[0]?.next_endpoint === 'payment_method_update'\n \"\n >\n <div\n class=\"size-[48px] flex items-center justify-center rounded-full\"\n style=\"background: rgba(188, 20, 20, 0.08)\"\n >\n <CardIcon class=\"size-5 fill-[#BC1414]\" />\n </div>\n\n <div class=\"flex flex-col text-center text-[#BC1414]\">\n <p class=\"text-base\">There seems to be an issue with your card.</p>\n <p class=\"text-xs\">Please double-check your details and try again.</p>\n </div>\n </div>\n\n <PlanPageIntro v-if=\"!stripeMeta.plan\" class=\"pb-3\">\n <CurrentPlanIndicatorV2\n v-if=\"data && !isLoading && data.user_plan && !data.is_new_user\"\n :planTitle=\"data.user_plan?.title\"\n :trialDaysRemaining=\"data.free_trial_days_remaining\"\n :price=\"String(data.user_plan?.discounted_price)\"\n :interval=\"data.user_plan?.interval\"\n :isHighTicket=\"Boolean(data.user_plan.is_high_ticket)\"\n />\n </PlanPageIntro>\n\n <div\n class=\"flex justify-center sm:justify-between items-center gap-4 flex-wrap mt-4\"\n >\n <!-- promo code -->\n <div class=\"flex gap-4 flex-wrap\">\n <PromoCode\n v-if=\"!isLoading && !stripeMeta.plan && data\"\n :promocodeData=\"data?.promo_code\"\n :couponcodeData=\"data?.coupon\"\n class=\"self-center\"\n variant=\"large\"\n />\n\n <ManageSubscription\n v-if=\"\n isOwnWorkspace &&\n data &&\n data.data[0]?.next_endpoint === 'payment_method_update'\n \"\n :isLoading=\"isRedirecting\"\n @click=\"redirectToStripeBillingPortal\"\n />\n </div>\n\n <Tab\n class=\"w-full sm:w-max justify-self-center sm:justify-self-end\"\n v-if=\"data && showTab && !stripeMeta.plan\"\n :selected=\"selectedTab\"\n :options=\"tabs\"\n @change=\"(val) => handleTabChange(val)\"\n />\n </div>\n\n <!-- if not user's workspace then show this warning -->\n <template v-if=\"!isOwnWorkspace\">\n <p class=\"bg-warning p-2 text-center text-black rounded-md mt-6\">\n You don't have access to manage plans in this workspace\n </p>\n </template>\n\n <!-- if user's workspace, then show plans -->\n <template v-else>\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"waitingBanner\"\n >\n <component :is=\"CheckColorIcon\" class=\"min-w-max\"> </component>\n\n <div :class=\"`font-semibold w-full`\">\n Please wait and do not close this page...\n </div>\n </div>\n\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"stripeStatus.text && !stripeMeta.plan && !successBanner\"\n >\n <component\n :is=\"\n stripeStatus.type === 'success' ? CheckColorIcon : AlertColorIcon\n \"\n class=\"min-w-max\"\n >\n </component>\n\n <div :class=\"`font-semibold w-full`\">\n {{ stripeStatus.text }}\n {{ isLoading ? ' Please wait and do not close this page...' : '' }}\n </div>\n\n <button @click=\"stripeStatus.text = ''\" class=\"p-1 mr-0\">✕</button>\n </div>\n\n <div\n class=\"flex items-center gap-4 bg-[#292929] p-4 rounded-lg mt-8 text-white\"\n v-if=\"\n route.query.status === 'success' &&\n successBanner &&\n !stripeMeta.plan\n \"\n >\n <component :is=\"CheckColorIcon\" class=\"min-w-max\"> </component>\n\n <div :class=\"`font-semibold w-full`\">\n You changed your plan successfully!\n </div>\n\n <button @click=\"successBanner = false\" class=\"p-1 mr-0\">✕</button>\n </div>\n\n <CheckoutForm\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n :currentPlan=\"data.user_plan\"\n />\n\n <template v-else>\n <div\n v-if=\"isLoading || !data\"\n class=\"mt-8 w-full flex items-center justify-center\"\n >\n <Loading />\n </div>\n\n <div v-else class=\"flex flex-col justify-center\">\n <!-- <button\n v-if=\"\n showPromoPlan && !SPECIAL_PLANS.includes(data.user_plan?.slug)\n \"\n class=\"max-w-max self-center text-[#6c7a88] underline underline-offset-4 decoration-[#6c7a88] flex items-center gap-3\"\n @click=\"removePromoCode\"\n :disabled=\"promoLoading\"\n >\n Skip this offer\n <Loading v-if=\"promoLoading\" size=\"xs\" />\n </button> -->\n\n <div\n :class=\"\n showPromoPlan\n ? 'flex flex-wrap mt-4 gap-4 justify-center'\n : `mt-8 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4`\n \"\n >\n <template v-for=\"(plan, index) in plansData\" :key=\"index\">\n <template v-if=\"plan.active\">\n <PlanItemOnboarding\n :icon=\"indexIconMap[index]\"\n :planData=\"plan\"\n :isPopular=\"index === 1\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n :currentPlan=\"data.user_plan\"\n :isCustomPlan=\"Boolean(plan.url)\"\n :isLoading=\"isChoosingPlan || isDoingAction || promoLoading\"\n @choose=\"() => choosePlan({ plan: plan })\"\n :showOffer=\"\n false && // remove this if we want to show offer\n !data.has_active_subscription &&\n plan.next_endpoint !== 'payment_method_update' &&\n !data.promo_code\n \"\n :trialRemaining=\"data.free_trial_days_remaining\"\n :hasCoupon=\"Boolean(data?.coupon)\"\n :showPromoPlan=\"Boolean(showPromoPlan)\"\n :planUsageType=\"authStore.authData?.user?.plan_usage_type\"\n :isNewUser=\"data.is_new_user\"\n />\n\n <!-- <PlanItem\n :planData=\"plan\"\n :currentPlan=\"data.user_plan\"\n @choose=\"choosePlan\"\n :isLoading=\"\n Boolean(isChoosingPlan) || isDoingAction || promoLoading\n \"\n :hasActiveSubscription=\"data.has_active_subscription\"\n :promoCode=\"data.promo_code\"\n @action=\"isDoingAction = true\"\n :isPopular=\"index === 1 || Boolean(showPromoPlan)\"\n :bookDemoPlan=\"Boolean(plan.url)\"\n /> -->\n </template>\n </template>\n </div>\n </div>\n </template>\n\n <KeyFeatures\n v-if=\"!isLoading\"\n class=\"mt-[90px]\"\n :isUnlimitedPlan=\"showPromoPlan === 'UNLIMITED'\"\n />\n\n <BrandLogos\n v-if=\"!isLoading && showPromoPlan !== 'UNLIMITED'\"\n class=\"my-[150px] self-center\"\n />\n\n <Reviews v-if=\"!isLoading && showPromoPlan !== 'UNLIMITED'\" />\n\n <ComparePlans\n v-if=\"!isLoading && !showPromoPlan\"\n :isLoading=\"isChoosingPlan || isDoingAction || promoLoading\"\n :currentPlan=\"data.user_plan\"\n :plansData=\"data\"\n :hasActiveSubscription=\"data.has_active_subscription\"\n class=\"my-[150px] self-center\"\n @choose=\"\n (val) => {\n choosePlan(val)\n }\n \"\n :trialRemaining=\"data.free_trial_days_remaining\"\n :isNewUser=\"data.is_new_user\"\n />\n\n <PlansFAQ v-if=\"!isLoading && !showPromoPlan\" />\n </template>\n\n <Modal\n v-if=\"couponModal\"\n @close=\"\n () => {\n couponModal = false\n showModalCouponField = false\n couponCodeData = null\n couponCodeError = false\n }\n \"\n heading=\"Choose plan\"\n size=\"content\"\n >\n <div class=\"flex flex-col gap-3\">\n <p>\n You have selected\n <span class=\"font-bold\">{{ selectedPlan.title }}</span> plan\n </p>\n\n <p v-if=\"!couponCodeData\">\n Total price:\n <span class=\"font-bold\"\n >${{ selectedPlan.price.toLocaleString() }}</span\n >\n </p>\n\n <p v-else class=\"flex gap-1\">\n <span> Total price: </span>\n <span class=\"line-through text-[var(--text-secondary)]\"\n >${{\n couponCodeData.plan_price.toFixed(2).toLocaleString()\n }}</span\n >\n <span class=\"font-bold\">${{ couponCodeData.total_price }}</span>\n </p>\n\n <button\n v-if=\"!showModalCouponField\"\n class=\"text-xs text-center self-center w-max text-gray-400 underline underline-offset-2 decoration-gray-400\"\n @click=\"showModalCouponField = true\"\n >\n I have a coupon code\n </button>\n\n <div class=\"flex gap-3 items-center flex-wrap\">\n <input\n v-if=\"showModalCouponField\"\n type=\"text\"\n placeholder=\"Coupon Code\"\n class=\"input-text max-w-[300px]\"\n v-model=\"couponCode\"\n @input=\"\n (e) => {\n if (e.target.value) {\n couponCodeLoading = true\n handleCouponInput()\n } else {\n couponCodeLoading = false\n couponCodeError = false\n couponCodeData = false\n }\n }\n \"\n @change=\"mixpanel.track(`Typing coupon code`)\"\n />\n\n <Loading v-if=\"couponCodeLoading\" />\n\n <p\n v-if=\"couponCodeError && !couponCodeLoading\"\n class=\"text-sm text-[#E51E43]\"\n >\n Invalid code!\n </p>\n\n <p\n v-if=\"couponCodeData && !couponCodeLoading\"\n class=\"text-[#0AC25D] text-sm\"\n >\n Coupon Applied!\n </p>\n </div>\n\n <Button\n label=\"Continue\"\n class=\"w-full\"\n @click=\"choosePlanWithCoupon\"\n :loading=\"Boolean(isChoosingPlan)\"\n :disabled=\"couponCodeError || couponCodeLoading || isChoosingPlan\"\n />\n </div>\n </Modal>\n </div>\n </div>\n</template>\n",
"prFileDiff" : "diff --git a/resources/js/components/Checkout/CheckoutForm.vue b/resources/js/components/Checkout/CheckoutForm.vue\nindex 41566ee43e..645bc2a1b5 100644\n--- a/resources/js/components/Checkout/CheckoutForm.vue\n+++ b/resources/js/components/Checkout/CheckoutForm.vue\n@@ -11,6 +11,12 @@ const props = defineProps({\n type: Object,\n default: null,\n },\n+ hasActiveSubscription: {\n+ type: Boolean,\n+ },\n+ currentPlan: {\n+ type: Object,\n+ },\n })\n \n const emit = defineEmits(['close'])\n@@ -107,6 +113,39 @@ const newPlanInterval = computed(\n )\n \n const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n+\n+function getButtonLabel() {\n+ if (\n+ props.hasActiveSubscription &&\n+ props.currentPlan &&\n+ !props.currentPlan?.is_plan_for_advertising\n+ ) {\n+ if (\n+ props.currentPlan?.interval === 'monthly' &&\n+ props.stripeMeta.plan.interval === 'yearly'\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ props.currentPlan?.interval === 'yearly' &&\n+ props.stripeMeta.plan.interval === 'monthly'\n+ ) {\n+ return 'Downgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) <\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) >\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Downgrade'\n+ }\n+ }\n+}\n </script>\n \n <template>\n@@ -153,6 +192,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n <div id=\"payment-element\" class=\"mt-4\" />\n \n <div\n+ v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n@@ -165,7 +205,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n- {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n+ {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n \n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\ndiff --git a/resources/js/components/Onboarding/OnboardingPlans.vue b/resources/js/components/Onboarding/OnboardingPlans.vue\nindex f0c21c8d43..fc2879227f 100644\n--- a/resources/js/components/Onboarding/OnboardingPlans.vue\n+++ b/resources/js/components/Onboarding/OnboardingPlans.vue\n@@ -272,18 +272,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n@@ -692,6 +692,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\ndiff --git a/resources/js/components/Onboarding/RecapStep.vue b/resources/js/components/Onboarding/RecapStep.vue\nindex 02b12652ae..eaf25500e2 100644\n--- a/resources/js/components/Onboarding/RecapStep.vue\n+++ b/resources/js/components/Onboarding/RecapStep.vue\n@@ -73,7 +73,6 @@ async function handleAccess() {\n console.error(err)\n }\n }\n-\n </script>\n \n <template>\n@@ -87,7 +86,9 @@ async function handleAccess() {\n </div>\n \n <div class=\"flex flex-col gap-2\">\n- <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n+ <h1 class=\"text-2xl font-bold\">\n+ Book Your Free Onboarding Call (Optional)\n+ </h1>\n \n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n@@ -103,9 +104,12 @@ async function handleAccess() {\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n-\n >\n- <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n+ <span\n+ v-if=\"feature.is_new\"\n+ class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n+ >New</span\n+ >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n@@ -119,7 +123,9 @@ async function handleAccess() {\n )}`\"\n >\n {{ feature.name }}\n- <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n+ <span v-if=\"feature.count\" class=\"text-[#979797]\"\n+ >({{ feature.count }})</span\n+ >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n@@ -138,8 +144,8 @@ async function handleAccess() {\n </div>\n \n <p class=\"text-lg font-semibold mb-4\">\n- It’s time to register for the Gethookd Webinar hosted by my 7-figure\n- e-commerce success coach, where we’ll give you:\n+ It’s time to register for your free onboarding call where one of my\n+ advisors will make sure you got access to the software, AND give you:\n </p>\n \n <ul class=\"flex flex-col gap-3\">\n@@ -170,7 +176,10 @@ async function handleAccess() {\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n- <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n+ <i\n+ >Over 57% of our clients tell us this one onboarding webinar saved\n+ them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n+ >\n </p>\n </div>\n \ndiff --git a/resources/js/pages/PlansPage.vue b/resources/js/pages/PlansPage.vue\nindex 5f319c99f0..b26ff8e3b4 100644\n--- a/resources/js/pages/PlansPage.vue\n+++ b/resources/js/pages/PlansPage.vue\n@@ -255,18 +255,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n@@ -657,6 +657,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\n",
"prFileDiffHunks" : [
"@@ -11,6 +11,12 @@ const props = defineProps({\n type: Object,\n default: null,\n },\n+ hasActiveSubscription: {\n+ type: Boolean,\n+ },\n+ currentPlan: {\n+ type: Object,\n+ },\n })\n \n const emit = defineEmits(['close'])\n",
"@@ -107,6 +113,39 @@ const newPlanInterval = computed(\n )\n \n const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n+\n+function getButtonLabel() {\n+ if (\n+ props.hasActiveSubscription &&\n+ props.currentPlan &&\n+ !props.currentPlan?.is_plan_for_advertising\n+ ) {\n+ if (\n+ props.currentPlan?.interval === 'monthly' &&\n+ props.stripeMeta.plan.interval === 'yearly'\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ props.currentPlan?.interval === 'yearly' &&\n+ props.stripeMeta.plan.interval === 'monthly'\n+ ) {\n+ return 'Downgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) <\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Upgrade'\n+ }\n+ if (\n+ Number(props.currentPlan?.price || 0) >\n+ Number(props.stripeMeta.plan.price)\n+ ) {\n+ return 'Downgrade'\n+ }\n+ }\n+}\n </script>\n \n <template>\n",
"@@ -153,6 +192,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n <div id=\"payment-element\" class=\"mt-4\" />\n \n <div\n+ v-if=\"isSubmitting\"\n class=\"flex justify-between items-center gap-4 bg-[#292929] text-white p-4 rounded-lg mt-8\"\n >\n <div :class=\"`font-semibold`\">\n",
"@@ -165,7 +205,7 @@ const newPlanPrice = computed(() => props.stripeMeta.new_plan.discounted_price)\n class=\"bg-[var(--btn-bg-primary)] text-[var(--btn-text-primary)] px-[20px] py-[12px] font-semibold flex items-center gap-[5px] rounded-[12px] normal-case btn-border justify-center transition-all active:scale-95 enabled:hover:brightness-[.85] text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100 mt-4\"\n :disabled=\"isSubmitting\"\n >\n- {{ isSubmitting ? 'Please wait...' : 'Continue' }}\n+ {{ isSubmitting ? 'Please wait...' : getButtonLabel() }}\n </button>\n \n <CheckoutMessages :messages=\"messages\" class=\"mt-4\" />\ndiff --git a/resources/js/components/Onboarding/OnboardingPlans.vue b/resources/js/components/Onboarding/OnboardingPlans.vue\nindex f0c21c8d43..fc2879227f 100644\n--- a/resources/js/components/Onboarding/OnboardingPlans.vue\n+++ b/resources/js/components/Onboarding/OnboardingPlans.vue\n",
"@@ -272,18 +272,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan (onboarding):', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n",
"@@ -692,6 +692,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\ndiff --git a/resources/js/components/Onboarding/RecapStep.vue b/resources/js/components/Onboarding/RecapStep.vue\nindex 02b12652ae..eaf25500e2 100644\n--- a/resources/js/components/Onboarding/RecapStep.vue\n+++ b/resources/js/components/Onboarding/RecapStep.vue\n",
"@@ -73,7 +73,6 @@ async function handleAccess() {\n console.error(err)\n }\n }\n-\n </script>\n \n <template>\n",
"@@ -87,7 +86,9 @@ async function handleAccess() {\n </div>\n \n <div class=\"flex flex-col gap-2\">\n- <h1 class=\"text-2xl font-bold\">Register For The Onboarding Webinar</h1>\n+ <h1 class=\"text-2xl font-bold\">\n+ Book Your Free Onboarding Call (Optional)\n+ </h1>\n \n <p class=\"text-[#979797] text-base\">\n You’ve successfully spied on another brand, saved their ad, and wrote\n",
"@@ -103,9 +104,12 @@ async function handleAccess() {\n v-for=\"(feature, index) in featureList\"\n :key=\"index\"\n :class=\"`flex sm:gap-2 items-start`\"\n-\n >\n- <span v-if=\"feature.is_new\" class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\">New</span>\n+ <span\n+ v-if=\"feature.is_new\"\n+ class=\"font-semibold !text-[10px] z-[1] mt-[-5px] sm:mt-[0] block mr-[-5px] sm:mr-[0] ml-[-28px] sm:ml-[-38px] text-xs text-[#141414] bg-[#1CC94E] px-1 rounded-full\"\n+ >New</span\n+ >\n <component\n :is=\"featureListIcons[index]\"\n :class=\"`size-5 ${getFeatureIconColor(\n",
"@@ -119,7 +123,9 @@ async function handleAccess() {\n )}`\"\n >\n {{ feature.name }}\n- <span v-if=\"feature.count\" class=\"text-[#979797]\">({{ feature.count }})</span>\n+ <span v-if=\"feature.count\" class=\"text-[#979797]\"\n+ >({{ feature.count }})</span\n+ >\n <div\n v-if=\"feature.missing_feature === '1'\"\n :class=\"`absolute h-[1px] ${getFeatureTextColor(\n",
"@@ -138,8 +144,8 @@ async function handleAccess() {\n </div>\n \n <p class=\"text-lg font-semibold mb-4\">\n- It’s time to register for the Gethookd Webinar hosted by my 7-figure\n- e-commerce success coach, where we’ll give you:\n+ It’s time to register for your free onboarding call where one of my\n+ advisors will make sure you got access to the software, AND give you:\n </p>\n \n <ul class=\"flex flex-col gap-3\">\n",
"@@ -170,7 +176,10 @@ async function handleAccess() {\n </li>\n </ul>\n <p class=\"text-lg font-semibold mb-4 bg-[#3A3A3A] p-4 rounded-lg\">\n- <i>Over 57% of our clients tell us this one onboarding webinar saved them $1,000s in wasted ad spend and they bought 100s of hours back!</i>\n+ <i\n+ >Over 57% of our clients tell us this one onboarding webinar saved\n+ them $1,000s in wasted ad spend and they bought 100s of hours back!</i\n+ >\n </p>\n </div>\n \ndiff --git a/resources/js/pages/PlansPage.vue b/resources/js/pages/PlansPage.vue\nindex 5f319c99f0..b26ff8e3b4 100644\n--- a/resources/js/pages/PlansPage.vue\n+++ b/resources/js/pages/PlansPage.vue\n",
"@@ -255,18 +255,18 @@ async function choosePlan(payload) {\n isChoosingPlan.value = false\n } else if (payload.plan.next_endpoint === 'create') {\n isChoosingPlan.value = String(payload.plan.id)\n- \n+\n // Debug logging for Scale plan issue\n console.log('Creating checkout session for plan:', {\n planId: payload.plan.id,\n planTitle: payload.plan.title,\n- endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`\n+ endpoint: `/api/subscription/checkout-session/create/${payload.plan.id}`,\n })\n- \n+\n const res = await Api.get(\n `/api/subscription/checkout-session/create/${payload.plan.id}`\n )\n- \n+\n if (res.data.data && res.data.data.redirect_url) {\n window.location.href = res.data.data.redirect_url\n return\n",
"@@ -657,6 +657,8 @@ const indexIconMap = {\n v-if=\"stripeMeta.plan\"\n @close=\"handleCloseCheckout\"\n :stripeMeta=\"stripeMeta\"\n+ :hasActiveSubscription=\"data.has_active_subscription\"\n+ :currentPlan=\"data.user_plan\"\n />\n \n <template v-else>\n"
],
"prFileBlobUrl" : "https://api.bitbucket.org/2.0/repositories/melioraweb/gethookdai/src/4a370802c82e135a9a44572822be9e863cf64991/resources/js/pages/PlansPage.vue",
"_id" : ObjectId("68a6ae486e2b9ed1415eb74c")
}
],
"createdAt" : ISODate("2025-08-21T05:27:36.140+0000"),
"updatedAt" : ISODate("2025-08-21T05:27:36.140+0000")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment