-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathbilling.upgradePrompt.component.vue
More file actions
235 lines (231 loc) · 7.43 KB
/
Copy pathbilling.upgradePrompt.component.vue
File metadata and controls
235 lines (231 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
<template>
<!-- Post-grant variant: signup grant is depleted in meter mode (Free plan, balance = 0, ledger has signup_grant entry) -->
<div v-if="exhaustedAfterGrant && mode === 'meter'" class="my-4">
<v-alert type="info" variant="tonal" prominent>
<template #text>
<p class="font-weight-bold mb-1">Your signup grant is depleted</p>
<p>{{ grantDepletedMessage }}</p>
</template>
<template #append>
<div class="d-flex flex-column ga-2">
<v-btn
data-test="cta-pack"
color="primary"
variant="flat"
size="small"
class="text-none"
to="/pricing#units"
>
{{ packCtaLabel }}
</v-btn>
<v-btn
data-test="cta-upgrade"
variant="text"
size="small"
class="text-none"
to="/pricing"
>
{{ upgradeCtaLabel }}
</v-btn>
</div>
</template>
</v-alert>
</div>
<!-- Default / meter variant -->
<v-alert v-else type="info" variant="tonal" prominent class="my-4">
<template #text>
<span v-if="hasUsageInfo">{{ `You've used ${current} of ${limit} ${displayLabel}.` }}</span>
<span v-else-if="mode === 'meter'">{{ `You're out of compute. Buy more units or upgrade for ${meterPeriodWord} compute.` }}</span>
<span v-else>{{ `This feature requires the ${requiredPlan} plan.` }}</span>
</template>
<template #append>
<v-btn
v-if="mode === 'meter'"
color="primary"
variant="flat"
size="small"
class="text-none"
to="/pricing#units"
>
Buy units
</v-btn>
<v-btn
v-else
color="primary"
variant="flat"
size="small"
class="text-none"
to="/pricing"
>
Upgrade
</v-btn>
</template>
</v-alert>
</template>
<script>
/**
* Module dependencies.
*/
import { useQuota } from '../composables/billing.useQuota';
import { useBillingStore } from '../stores/billing.store.js';
import { resolveStaticContent } from '../lib/billing.resolveStaticContent.js';
// Static-content resolver call — same module-scope pattern as billing.subscriptions.component.vue.
// config.billing.staticContent (project override) wins per key, devkit default otherwise —
// see billing.resolveStaticContent.js for the resolution contract.
const { packs: packsConfig, plans: plansConfig, signupGrant: signupGrantConfig } = resolveStaticContent();
/**
* Component definition.
*/
export default {
name: 'BillingUpgradePrompt',
props: {
/**
* @desc The plan name required to access the gated feature.
*/
requiredPlan: {
type: String,
required: true,
},
/**
* @desc Optional resource name for specific usage info.
*/
resource: {
type: String,
default: '',
},
/**
* @desc Optional action name for specific usage info.
*/
action: {
type: String,
default: '',
},
/**
* @desc Optional display label for usage info, defaults to "${resource} ${action}".
*/
label: {
type: String,
default: '',
},
/**
* @desc Billing mode. Meter mode surfaces a "buy units" CTA that routes to the
* units section of the pricing page; subscription mode surfaces a plan-upgrade CTA.
*/
mode: {
type: String,
default: 'subscription',
/**
* @desc Validate that mode is a known billing mode.
* @param {string} value - The mode value to validate.
* @returns {boolean} True if value is a supported mode.
*/
validator: (value) => ['subscription', 'meter'].includes(value),
},
},
/**
* @desc Wires useQuota composable and billing store for exhaustedAfterGrant detection.
* @returns {{ usage: Object, limits: Object, billingStore: Object }}
*/
setup() {
const { usage, limits } = useQuota();
const billingStore = useBillingStore();
return { usage, limits, billingStore };
},
computed: {
/**
* @desc Whether the current user has exhausted their one-shot Free-tier signup grant.
* Delegates to the billing store getter for reactivity. Only rendered in meter mode
* (subscription mode is for feature-gating by plan, not meter exhaustion).
* @returns {boolean}
*/
exhaustedAfterGrant() {
return this.billingStore.exhaustedAfterGrant;
},
/**
* @desc Quota key derived from resource and action.
* @returns {string}
*/
key() {
return `${this.resource}.${this.action}`;
},
/**
* @desc Whether resource and action props are provided.
* @returns {boolean}
*/
hasUsageInfo() {
return !!(this.resource && this.action && this.limits[this.key] !== undefined);
},
/**
* @desc Current usage count for the resource action.
* @returns {number}
*/
current() {
return this.usage[this.key] || 0;
},
/**
* @desc Limit value for the resource action.
* @returns {number}
*/
limit() {
return this.limits[this.key] || 0;
},
/**
* @desc Display label for the usage message.
* @returns {string}
*/
displayLabel() {
return this.label || `${this.resource} ${this.action}`;
},
/**
* @desc First pack from the resolved static content — the pack featured by the
* post-grant prompt's primary CTA. `null` when a project configures no packs.
* @returns {Object|null}
*/
primaryPack() {
return packsConfig[0] || null;
},
/**
* @desc Post-grant depletion message, config-sourced (grant label + featured pack name).
* Devkit default: generic, numberless placeholder copy — no downstream product vocabulary.
* @returns {string}
*/
grantDepletedMessage() {
const packPhrase = this.primaryPack ? `a ${this.primaryPack.title}` : 'a compute pack';
return `You used your ${signupGrantConfig.label}. Buy ${packPhrase} to keep going, or upgrade for ${this.meterPeriodWord} compute.`;
},
/**
* @desc Billing period word used in the meter-mode upgrade copy (e.g. "monthly").
* Config-overridable so a downstream on a different billing period can rebrand this
* without editing the view.
* @returns {string}
*/
meterPeriodWord() {
return this.config?.billing?.meterPeriodWord ?? 'monthly';
},
/**
* @desc Primary pack CTA label, config-sourced from the resolved pack's own `cta` +
* price (e.g. "{pack.cta} — {pack.price.amount}"). Falls back to a generic label
* when a project configures no packs, or when a configured pack omits `cta`.
* @returns {string}
*/
packCtaLabel() {
if (!this.primaryPack) return 'Buy a compute pack';
const cta = this.primaryPack.cta || 'Buy a compute pack';
const amount = this.primaryPack.price?.amount;
return amount ? `${cta} — ${amount}` : cta;
},
/**
* @desc Secondary upgrade CTA label, config-sourced from the non-free plan titles
* in the resolved static content (e.g. "Upgrade Starter / Pro"). Falls back to a
* bare "Upgrade" when no paid plans are configured.
* @returns {string}
*/
upgradeCtaLabel() {
const paidPlanTitles = plansConfig
.filter((plan) => plan.id !== 'free' && plan.title)
.map((plan) => plan.title);
return paidPlanTitles.length > 0 ? `Upgrade ${paidPlanTitles.join(' / ')}` : 'Upgrade';
},
},
};
</script>