Skip to content

Commit 5aa4330

Browse files
committed
Merge branch 'yearly-pricing' into dev
2 parents d06cb34 + 60cf365 commit 5aa4330

File tree

13 files changed

+216
-25
lines changed

13 files changed

+216
-25
lines changed

apps/app-server/src/env.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ declare namespace NodeJS {
5454

5555
STRIPE_PUBLISHABLE_KEY: string;
5656
STRIPE_SECRET_KEY: string;
57-
STRIPE_PRICE_ID: string;
5857
STRIPE_WEBHOOK_SECRET: string;
58+
STRIPE_MONTHLY_PRICE_ID: string;
59+
STRIPE_YEARLY_PRICE_ID: string;
5960
}
6061
}

apps/app-server/src/trpc/api/users/account/stripe/create-checkout-session.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@ import { once } from 'lodash';
55
import type { InferProcedureOpts } from 'src/trpc/helpers';
66
import { authProcedure } from 'src/trpc/helpers';
77
import type Stripe from 'stripe';
8+
import { z } from 'zod';
89

9-
const baseProcedure = authProcedure;
10+
const baseProcedure = authProcedure.input(
11+
z.object({
12+
billingFrequency: z.enum(['monthly', 'yearly']).optional(),
13+
}),
14+
);
1015

1116
export const createCheckoutSessionProcedure = once(() =>
1217
baseProcedure.mutation(createCheckoutSession),
1318
);
1419

1520
export async function createCheckoutSession({
1621
ctx,
22+
input,
1723
}: InferProcedureOpts<typeof baseProcedure>) {
1824
return await ctx.usingLocks(
1925
[[`user-lock:${ctx.userId}`]],
@@ -102,7 +108,10 @@ export async function createCheckoutSession({
102108

103109
line_items: [
104110
{
105-
price: process.env.STRIPE_PRICE_ID,
111+
price:
112+
input.billingFrequency === 'yearly'
113+
? process.env.STRIPE_YEARLY_PRICE_ID
114+
: process.env.STRIPE_MONTHLY_PRICE_ID,
106115
quantity: 1,
107116
},
108117
],

apps/client/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {}
77

88
declare module '@vue/runtime-core' {
99
export interface GlobalComponents {
10+
BillingFrequencyToggle: typeof import('./src/components/BillingFrequencyToggle.vue')['default']
1011
Checkbox: typeof import('./src/components/Checkbox.vue')['default']
1112
ColorPalette: typeof import('./src/components/ColorPalette.vue')['default']
1213
ColorSquare: typeof import('./src/components/ColorSquare.vue')['default']
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<template>
2+
<button
3+
style="
4+
background-color: transparent;
5+
6+
border: 1px solid rgba(255, 255, 255, 0.2);
7+
border-radius: 8px;
8+
9+
padding: 2px;
10+
11+
display: flex;
12+
13+
cursor: pointer;
14+
15+
color: inherit;
16+
17+
position: relative;
18+
"
19+
@click="
20+
$emit(
21+
'update:modelValue',
22+
modelValue === 'monthly' ? 'yearly' : 'monthly',
23+
)
24+
"
25+
>
26+
<div
27+
style="
28+
position: absolute;
29+
30+
border-radius: 6px;
31+
32+
background-color: rgba(255, 255, 255, 0.2);
33+
34+
left: 2px;
35+
top: 2px;
36+
right: 50%;
37+
bottom: 2px;
38+
39+
transition: left 0.2s, right 0.2s;
40+
"
41+
:style="{
42+
left: modelValue === 'monthly' ? '2px' : '50%',
43+
right: modelValue === 'monthly' ? '50%' : '2px',
44+
}"
45+
></div>
46+
47+
<div class="button">Monthly</div>
48+
<div
49+
class="button"
50+
style="position: relative"
51+
>
52+
Yearly
53+
54+
<div
55+
style="
56+
position: absolute;
57+
58+
left: 50%;
59+
top: -8px;
60+
61+
transform: translate(-50%, -100%);
62+
63+
padding: 0 8px;
64+
65+
border-radius: 4px;
66+
background-color: #006dd2;
67+
68+
font-size: 12px;
69+
font-weight: bold;
70+
71+
white-space: nowrap;
72+
73+
white-space: nowrap;
74+
"
75+
>
76+
Save 20%
77+
78+
<div
79+
style="
80+
position: absolute;
81+
82+
width: 0;
83+
height: 0;
84+
85+
border-left: 7px solid transparent;
86+
border-right: 7px solid transparent;
87+
border-top: 8px solid #006dd2;
88+
89+
left: 50%;
90+
transform: translateX(-50%);
91+
"
92+
></div>
93+
</div>
94+
</div>
95+
</button>
96+
</template>
97+
98+
<script setup lang="ts">
99+
defineProps<{
100+
modelValue: 'monthly' | 'yearly';
101+
}>();
102+
103+
defineEmits(['update:modelValue']);
104+
</script>
105+
106+
<style scoped>
107+
.button {
108+
padding: 4px;
109+
width: 100px;
110+
border-radius: 6px;
111+
text-align: center;
112+
}
113+
</style>

apps/client/src/env.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ declare namespace NodeJS {
6666

6767
STRIPE_PUBLISHABLE_KEY: string;
6868
STRIPE_SECRET_KEY: string;
69-
STRIPE_PRICE_ID: string;
7069
STRIPE_WEBHOOK_SECRET: string;
70+
STRIPE_MONTHLY_PRICE_ID: string;
71+
STRIPE_YEARLY_PRICE_ID: string;
7172
}
7273
}

apps/client/src/layouts/HomeLayout/Footer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<q-footer
33
style="
44
background-color: #202020;
5-
border-top: 1px solid rgba(255, 255, 255, 0.18);
5+
border-top: 1px solid rgba(255, 255, 255, 0.15);
66
"
77
>
88
<ResponsiveContainer>

apps/client/src/pages/home/Index/Index.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,13 @@
153153
<template v-if="!($q.platform.is.capacitor && $q.platform.is.ios)">
154154
<Gap style="height: 170px" />
155155

156-
<PricingSection />
156+
<div style="display: flex; justify-content: center">
157+
<BillingFrequencyToggle v-model="billingFrequency" />
158+
</div>
159+
160+
<Gap style="height: 34px" />
161+
162+
<PricingSection :billing-frequency="billingFrequency" />
157163
</template>
158164

159165
<div>
@@ -302,6 +308,8 @@ onMounted(async () => {
302308
303309
loading.value = false;
304310
});
311+
312+
const billingFrequency = ref<'monthly' | 'yearly'>('monthly');
305313
</script>
306314

307315
<style lang="scss">

apps/client/src/pages/home/Index/PricingSection.vue

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@
1616
margin: 16px;
1717
padding: 28px;
1818
19-
border: 1px solid rgba(255, 255, 255, 0.3);
19+
border: 1px solid rgba(255, 255, 255, 0.13);
2020
border-radius: 12px;
21+
22+
background-color: #222222;
2123
"
2224
>
2325
<div style="font-size: 30px; font-weight: bold">Basic plan</div>
2426

2527
<Gap style="height: 32px" />
2628

27-
<div style="text-align: center; font-weight: bold">
29+
<div style="text-align: center">
2830
<span style="font-size: 46px">$0</span>
29-
<span style="font-size: 12px; color: #d0d0d0">/ month</span>
31+
<span style="font-size: 12px; color: #d0d0d0; font-weight: bold"
32+
>/ month</span
33+
>
3034
</div>
3135

3236
<Gap style="height: 32px" />
@@ -79,17 +83,35 @@
7983
margin: 16px;
8084
padding: 28px;
8185
82-
border: 1px solid rgba(255, 255, 255, 0.3);
86+
border: 1px solid rgba(255, 255, 255, 0.13);
8387
border-radius: 12px;
88+
89+
background-color: #222222;
8490
"
8591
>
8692
<div style="font-size: 30px; font-weight: bold">Pro plan</div>
8793

8894
<Gap style="height: 32px" />
8995

90-
<div style="text-align: center; font-weight: bold">
91-
<span style="font-size: 46px">$5</span>
96+
<div style="text-align: center; position: relative">
97+
<span style="font-size: 46px">
98+
${{ billingFrequency === 'monthly' ? 4.99 : 3.99 }}
99+
</span>
92100
<span style="font-size: 12px; color: #d0d0d0">/ month</span>
101+
102+
<div
103+
v-if="billingFrequency === 'yearly'"
104+
style="
105+
position: absolute;
106+
width: 100%;
107+
margin-top: -7px;
108+
text-align: center;
109+
font-size: 13px;
110+
color: #d0d0d0;
111+
"
112+
>
113+
(billed anually)
114+
</div>
93115
</div>
94116

95117
<Gap style="height: 32px" />
@@ -131,6 +153,12 @@
131153
</div>
132154
</template>
133155

156+
<script setup lang="ts">
157+
defineProps<{
158+
billingFrequency: 'monthly' | 'yearly';
159+
}>();
160+
</script>
161+
134162
<style scoped lang="scss">
135163
ul :deep() {
136164
list-style: none;

apps/client/src/pages/home/Index/Thumbnail.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ defineProps<{
4545
.highlight {
4646
flex: 1;
4747
margin: 8px;
48-
border: 1px solid rgba(255, 255, 255, 0.3);
48+
border: 1px solid rgba(255, 255, 255, 0.2);
4949
border-radius: 12px;
5050
5151
background-size: contain;

apps/client/src/pages/home/Pricing/PlanCard.vue

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
margin-bottom: 40px;
66
width: 300px;
77
8-
border: 1px solid rgba(255, 255, 255, 0.3);
8+
border: 1px solid rgba(255, 255, 255, 0.13);
99
border-radius: 12px;
1010
padding: 28px;
1111
12+
background-color: #222222;
13+
1214
display: flex;
1315
flex-direction: column;
1416
"
@@ -19,14 +21,28 @@
1921

2022
<Gap style="height: 40px" />
2123

22-
<div style="text-align: center; font-weight: bold">
23-
<span style="font-size: 46px">${{ price }}</span>
24+
<div style="text-align: center; position: relative">
25+
<span style="font-size: 46px">${{ monthlyPrice }}</span>
2426
<span style="font-size: 12px; color: #d0d0d0">/ month</span>
27+
28+
<div
29+
v-if="billingFrequency === 'yearly'"
30+
style="
31+
position: absolute;
32+
width: 100%;
33+
margin-top: -6px;
34+
text-align: center;
35+
font-size: 13px;
36+
color: #d0d0d0;
37+
"
38+
>
39+
(billed anually)
40+
</div>
2541
</div>
2642

27-
<Gap style="height: 48px" />
43+
<Gap style="height: 47px" />
2844

29-
<div style="font-weight: bold; color: #4e9cea; font-size: 14px">
45+
<div style="font-weight: bold; color: #2d9aff; font-size: 14px">
3046
<template v-if="previous == null">Features:</template>
3147
<template v-else>Everything in {{ previous }}, plus:</template>
3248
</div>
@@ -76,7 +92,8 @@
7692
<script setup lang="ts">
7793
defineProps<{
7894
title: string;
79-
price: number;
95+
monthlyPrice: string;
96+
billingFrequency?: 'monthly' | 'yearly';
8097
previous?: string;
8198
features: {
8299
icon: string;

0 commit comments

Comments
 (0)