Created
August 21, 2025 20:17
-
-
Save TanjinAlam/8abc18991dfe3dffd8d56c5d6c8be083 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "_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