Skip to content

Commit 96663d5

Browse files
author
root
committed
feat: unified peak design subscribe page, stripe one-time checkout, and chat persistence across payment redirects
1 parent 861e1d6 commit 96663d5

9 files changed

Lines changed: 792 additions & 135 deletions

File tree

.eslintrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ globals:
3333
definePageMeta: readonly
3434
useSharedChat: readonly
3535
useRoute: readonly
36+
useRouter: readonly
3637
useNuxtApp: readonly
3738
useAnalytics: readonly
3839
defineEmits: readonly

components/ChatVisualization/PdfDownloadPopup.vue

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,22 @@
7171
</v-btn>
7272

7373
<!-- Payment section if not subscribed -->
74-
<div v-else>
75-
<ChatVisualizationPayment
76-
:amount="price"
77-
:currency="currency"
78-
@onApprove="onApprove"
79-
@onCreateOrder="onCreateOrder"
80-
@onError="onError"
81-
/>
74+
<div v-else class="text-center">
75+
<v-btn
76+
color="success"
77+
size="large"
78+
class="mb-3"
79+
:loading="isOneTimeLoading"
80+
@click="payOneTimeStripe"
81+
>
82+
<v-icon class="mr-1">mdi-credit-card</v-icon>
83+
<span
84+
>{{ $t("chooseOneTime") }} ({{ price }} {{ currency }})</span
85+
>
86+
</v-btn>
8287
<v-alert density="compact" type="info" prominent>
8388
<span v-html="$t('subscriptionHint')"></span>
84-
<v-btn to="/subscribe">
89+
<v-btn to="/subscribe" class="ml-2" variant="tonal">
8590
<span v-html="$t('openSubscriptionPage')"></span>
8691
</v-btn>
8792
</v-alert>
@@ -208,6 +213,7 @@
208213
import { saveAs } from "file-saver";
209214
import { markRaw } from "vue";
210215
import { GTAG_PAYMENT, GTAG_PDF, gtagEvent } from "~/utils/gtagValues";
216+
import { fetchOneTimeCheckoutUrl } from "~/utils/subscription";
211217
import PDFWorker from "~/assets/js/pdf.worker.js?worker";
212218
import { loadImage, objectToDictionary } from "~/utils/utils";
213219
@@ -224,6 +230,7 @@ export default {
224230
return {
225231
showDownloadPopup: false,
226232
isLoading: false,
233+
isOneTimeLoading: false,
227234
GTAG_PAYMENT,
228235
GTAG_PDF,
229236
progress: 0,
@@ -243,14 +250,23 @@ export default {
243250
this.download(false);
244251
this.showDownloadPopup = false;
245252
},
246-
onCreateOrder() {
253+
async payOneTimeStripe() {
254+
if (this.isOneTimeLoading) return;
247255
gtagEvent("created", GTAG_PAYMENT, 0);
256+
this.isOneTimeLoading = true;
257+
try {
258+
const url = await fetchOneTimeCheckoutUrl({
259+
successUrl: `${window.location.origin}/?session_id={CHECKOUT_SESSION_ID}&payment_success=true`,
260+
cancelUrl: window.location.href,
261+
});
262+
if (!url) throw new Error("No checkout URL returned");
263+
window.location.assign(url);
264+
} catch (err) {
265+
console.error("Error creating one-time Stripe checkout:", err);
266+
alert("Failed to start checkout. Please try again.");
267+
this.isOneTimeLoading = false;
268+
}
248269
},
249-
onApprove() {
250-
gtagEvent("approved", GTAG_PAYMENT, 10);
251-
this.downloadFull();
252-
},
253-
onError() {},
254270
async download(isSample = false) {
255271
if (!import.meta.client) return;
256272

functions-wrapped/src/stripe/createCheckoutSession.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,51 @@ import {
77
} from "./common";
88
import * as logger from "firebase-functions/logger";
99

10+
interface CreateCheckoutSessionRequest {
11+
priceId?: string;
12+
mode?: "subscription" | "payment";
13+
successUrl?: string;
14+
cancelUrl?: string;
15+
}
16+
1017
export const createCheckoutSession = onCall(
1118
{ secrets: [stripeSecretKey] },
1219
async (request) => {
1320
const stripe = getStripe();
1421

1522
// Validate origin from request headers
1623
const origin = validateOrigin(request.rawRequest.get("origin"));
24+
const data = (request.data || {}) as CreateCheckoutSessionRequest;
25+
const mode = data.mode === "payment" ? "payment" : "subscription";
26+
const price = data.priceId || proPriceId.value();
27+
28+
const defaultSuccessUrl =
29+
mode === "payment"
30+
? `${origin}/?session_id={CHECKOUT_SESSION_ID}&payment_success=true`
31+
: `${origin}/subscription/verify?session_id={CHECKOUT_SESSION_ID}`;
32+
const defaultCancelUrl = `${origin}/subscribe`;
33+
34+
const success_url = data.successUrl
35+
? data.successUrl.replace(
36+
"{CHECKOUT_SESSION_ID}",
37+
"{CHECKOUT_SESSION_ID}"
38+
)
39+
: defaultSuccessUrl;
40+
const cancel_url = data.cancelUrl || defaultCancelUrl;
1741

1842
try {
1943
const session = await stripe.checkout.sessions.create({
20-
mode: "subscription",
44+
mode,
2145
line_items: [
2246
{
23-
price: proPriceId.value(),
47+
price,
2448
quantity: 1,
2549
},
2650
],
27-
success_url: `${origin}/wrapped/subscription/success?session_id={CHECKOUT_SESSION_ID}`,
28-
cancel_url: `${origin}/wrapped`,
51+
success_url,
52+
cancel_url,
2953
});
54+
3055
// ❗ onCall cannot redirect → return the URL.
3156
return { url: session.url };
3257
} catch (error: any) {

nuxt.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export default defineNuxtConfig({
111111
stripePriceId: local
112112
? "price_1Sc6u074KJ57kF2wxb5cnIZL"
113113
: "price_1SgOxVL4rDqbYflowSbSteJQ",
114+
stripeOneTimePriceId: local
115+
? "price_1ShAqrL4rDqbYfloWx53VxpL"
116+
: "price_1ShAqrL4rDqbYfloWx53VxpL",
114117
wrappedFirebase: {
115118
apiKey: "AIzaSyBaVob5g3xHdzJnkOI2dtbdYND-__Tzutc",
116119
authDomain: "whatsanalyze-wrapped-prod.firebaseapp.com",

pages/index.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,11 @@ import {
178178
import { debounce } from "lodash-es";
179179
import { useSubscriptionStore } from "~/stores/subscription";
180180
import { storeToRefs } from "pinia";
181+
import {
182+
saveChatSession,
183+
loadChatSession,
184+
clearChatSession,
185+
} from "~/utils/chatSession";
181186
182187
export default {
183188
async setup() {
@@ -248,6 +253,15 @@ export default {
248253
this.$nextTick(() => {
249254
window.scrollTo({ top: 0, behavior: "instant" });
250255
});
256+
} else {
257+
const savedSession = loadChatSession();
258+
if (savedSession && savedSession.messages?.length > 0) {
259+
this.isShowingChats = true;
260+
this.newMessages({
261+
messages: savedSession.messages,
262+
attachments: savedSession.attachments || [],
263+
});
264+
}
251265
}
252266
},
253267
beforeUnmount() {
@@ -270,6 +284,7 @@ export default {
270284
GTAG_NUM_PERSONS,
271285
0
272286
);
287+
saveChatSession(chatObject);
273288
}
274289
},
275290
rando() {
@@ -286,6 +301,7 @@ export default {
286301
this.attachments = undefined;
287302
const sharedChat = useSharedChat();
288303
sharedChat.value = null;
304+
clearChatSession();
289305
},
290306
},
291307
};

0 commit comments

Comments
 (0)