Skip to content

Commit

Permalink
Billing / plus frontend (#2130)
Browse files Browse the repository at this point in the history
* [wip] initial

* [wip] subscriptions/plus frontend

* [wip] finish payment flow

* Charges page

* finish most subscriptions work

* Finish

* update eslint

* Fix issues

* fix intl extract

* fix omorphia locale extract

* fix responsiveness

* fix lint
  • Loading branch information
Geometrically authored Aug 16, 2024
1 parent 1b3744b commit 3a4843f
Show file tree
Hide file tree
Showing 44 changed files with 2,353 additions and 201 deletions.
3 changes: 0 additions & 3 deletions apps/app-frontend/.env

This file was deleted.

2 changes: 1 addition & 1 deletion apps/app-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"tauri-plugin-window-state-api": "github:tauri-apps/tauri-plugin-window-state#v1",
"vite-svg-loader": "^5.1.0",
"vue": "^3.4.21",
"vue-multiselect": "3.0.0-beta.3",
"vue-multiselect": "3.0.0",
"vue-router": "4.3.0",
"vue-typed-virtual-list": "^1.0.10"
},
Expand Down
14 changes: 13 additions & 1 deletion apps/frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export default defineNuxtConfig({
title: "Modrinth mods",
},
],
script: [
{
src: "https://js.stripe.com/v3/",
defer: true,
async: true,
},
],
},
},
vite: {
Expand Down Expand Up @@ -125,6 +132,7 @@ export default defineNuxtConfig({
homePageProjects?: any[];
homePageSearch?: any[];
homePageNotifs?: any[];
products?: any[];
} = {};

try {
Expand Down Expand Up @@ -165,6 +173,7 @@ export default defineNuxtConfig({
homePageProjects,
homePageSearch,
homePageNotifs,
products,
] = await Promise.all([
$fetch(`${API_URL}tag/category`, headers),
$fetch(`${API_URL}tag/loader`, headers),
Expand All @@ -174,6 +183,8 @@ export default defineNuxtConfig({
$fetch(`${API_URL}projects_random?count=60`, headers),
$fetch(`${API_URL}search?limit=3&query=leave&index=relevance`, headers),
$fetch(`${API_URL}search?limit=3&query=&index=updated`, headers),
// TODO: dehardcode
$fetch(`${API_URL.replace("/v2/", "/_internal/")}billing/products`, headers),
]);

state.categories = categories;
Expand All @@ -184,6 +195,7 @@ export default defineNuxtConfig({
state.homePageProjects = homePageProjects;
state.homePageSearch = homePageSearch;
state.homePageNotifs = homePageNotifs;
state.products = products;

await fs.writeFile("./src/generated/state.json", JSON.stringify(state));

Expand Down Expand Up @@ -236,7 +248,7 @@ export default defineNuxtConfig({
const omorphiaLocales: string[] = [];
const omorphiaLocaleSets = new Map<string, { files: { from: string }[] }>();

for await (const localeDir of globIterate("node_modules/omorphia/locales/*", {
for await (const localeDir of globIterate("node_modules/@modrinth/ui/src/locales/*", {
posix: true,
})) {
const tag = basename(localeDir);
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
"postinstall": "nuxi prepare",
"lint": "eslint . && prettier --check .",
"fix": "eslint . --fix && prettier --write .",
"intl:extract": "formatjs extract \"{,components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file locales/en-US/index.json --format crowdin --preserve-whitespace"
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace"
},
"devDependencies": {
"@formatjs/cli": "^6.2.12",
"@nuxt/devtools": "^1.3.3",
"@nuxtjs/turnstile": "^0.8.0",
"@types/node": "^20.1.0",
Expand Down Expand Up @@ -49,7 +50,7 @@
"pathe": "^1.1.2",
"qrcode.vue": "^3.4.0",
"semver": "^7.5.4",
"vue-multiselect": "3.0.0-alpha.2",
"vue-multiselect": "3.0.0",
"vue3-apexcharts": "^1.5.2",
"xss": "^1.0.14"
}
Expand Down
1 change: 0 additions & 1 deletion apps/frontend/src/assets/styles/layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,5 @@

.normal-page__content {
max-width: calc(60rem - 0.75rem);
overflow-x: hidden;
}
}
4 changes: 0 additions & 4 deletions apps/frontend/src/assets/styles/utils.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
display: none !important;
}

.w-100 {
width: 100%;
}

body {
overflow-y: scroll;
overflow-x: hidden;
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/src/composables/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export const initAuth = async (oldToken = null) => {
authCookie.value = route.query.code;
}

if (route.fullPath.includes("new_account=true") && route.path !== "/auth/welcome") {
const redirect = route.path.startsWith("/auth/") ? null : route.fullPath;

await navigateTo(
`/auth/welcome?authToken=${route.query.code}${
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ""
}`,
);
}

if (authCookie.value) {
auth.token = authCookie.value;

Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/src/composables/country.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const useUserCountry = () =>
useState("userCountry", () => {
const headers = useRequestHeaders(["cf-ipcountry"]);

return headers["cf-ipcountry"] ?? "US";
});
16 changes: 11 additions & 5 deletions apps/frontend/src/composables/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,27 @@ export const initUser = async () => {
const auth = (await useAuth()).value;

const user = {
notifications: [],
collections: [],
follows: [],
subscriptions: [],
lastUpdated: 0,
};

if (auth.user && auth.user.id) {
try {
const [follows, collections] = await Promise.all([
useBaseFetch(`user/${auth.user.id}/follows`),
useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 }),
const headers = {
Authorization: auth.token,
};

const [follows, collections, subscriptions] = await Promise.all([
useBaseFetch(`user/${auth.user.id}/follows`, { headers }, true),
useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3, headers }, true),
useBaseFetch(`billing/subscriptions`, { internal: true, headers }, true),
]);

user.collections = collections;
user.follows = follows;
user.subscriptions = subscriptions;
user.lastUpdated = Date.now();
} catch (err) {
console.error(err);
Expand Down Expand Up @@ -170,6 +177,5 @@ export const logout = async () => {

await useAuth("none");
useCookie("auth-token").value = null;
await navigateTo("/");
stopLoading();
};
33 changes: 33 additions & 0 deletions apps/frontend/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@
</nuxt-link>
</template>
</div>
<div
v-if="
user &&
user.subscriptions &&
user.subscriptions.some((x) => x.status === 'payment-failed') &&
route.path !== '/settings/billing'
"
class="email-nag"
>
<span>{{ formatMessage(subscriptionPaymentFailedBannerMessages.title) }}</span>
<nuxt-link class="btn" to="/settings/billing">
<SettingsIcon />
{{ formatMessage(subscriptionPaymentFailedBannerMessages.action) }}
</nuxt-link>
</div>
<div
v-if="
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') &&
Expand Down Expand Up @@ -454,6 +469,12 @@ const { formatMessage } = useVIntl();
const app = useNuxtApp();
const auth = await useAuth();
const user = ref();
if (import.meta.client) {
user.value = await useUser();
}
const cosmetics = useCosmetics();
const flags = useFeatureFlags();
const tags = useTags();
Expand Down Expand Up @@ -484,6 +505,18 @@ const addEmailBannerMessages = defineMessages({
},
});
const subscriptionPaymentFailedBannerMessages = defineMessages({
title: {
id: "layout.banner.subscription-payment-failed.title",
defaultMessage:
"Your subscription failed to renew. Please update your payment method to prevent losing access.",
},
action: {
id: "layout.banner.subscription-payment-failed.button",
defaultMessage: "Update billing info",
},
});
const stagingBannerMessages = defineMessages({
title: {
id: "layout.banner.staging.title",
Expand Down
96 changes: 96 additions & 0 deletions apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,12 @@
"layout.banner.staging.title": {
"message": "You’re viewing Modrinth’s staging environment."
},
"layout.banner.subscription-payment-failed.button": {
"message": "Update billing info"
},
"layout.banner.subscription-payment-failed.title": {
"message": "Your subscription failed to renew. Please update your payment method to prevent losing access."
},
"layout.banner.verify-email.action": {
"message": "Re-send verification email"
},
Expand Down Expand Up @@ -788,6 +794,93 @@
"settings.authorized-apps.title": {
"message": "Authorized apps"
},
"settings.billing.modal.cancel.action": {
"message": "Cancel subscription"
},
"settings.billing.modal.cancel.description": {
"message": "This will cancel your subscription. You will retain your perks until the end of the current billing cycle."
},
"settings.billing.modal.cancel.title": {
"message": "Are you sure you want to cancel your subscription?"
},
"settings.billing.modal.delete.action": {
"message": "Remove this payment method"
},
"settings.billing.modal.delete.description": {
"message": "This will remove this payment method forever (like really forever)."
},
"settings.billing.modal.delete.title": {
"message": "Are you sure you want to remove this payment method?"
},
"settings.billing.payment_method.action.add": {
"message": "Add payment method"
},
"settings.billing.payment_method.action.history": {
"message": "View past charges"
},
"settings.billing.payment_method.action.primary": {
"message": "Make primary"
},
"settings.billing.payment_method.card_display": {
"message": "{card_brand} ending in {last_four}"
},
"settings.billing.payment_method.card_expiry": {
"message": "Expires {month}/{year}"
},
"settings.billing.payment_method.none": {
"message": "You have not added any payment methods."
},
"settings.billing.payment_method.primary": {
"message": "Primary"
},
"settings.billing.payment_method.title": {
"message": "Payment methods"
},
"settings.billing.payment_method_type.amazon_pay": {
"message": "Amazon Pay"
},
"settings.billing.payment_method_type.amex": {
"message": "American Express"
},
"settings.billing.payment_method_type.cashapp": {
"message": "Cash App"
},
"settings.billing.payment_method_type.diners": {
"message": "Diners Club"
},
"settings.billing.payment_method_type.discover": {
"message": "Discover"
},
"settings.billing.payment_method_type.eftpos": {
"message": "EFTPOS"
},
"settings.billing.payment_method_type.jcb": {
"message": "JCB"
},
"settings.billing.payment_method_type.mastercard": {
"message": "MasterCard"
},
"settings.billing.payment_method_type.paypal": {
"message": "PayPal"
},
"settings.billing.payment_method_type.unionpay": {
"message": "UnionPay"
},
"settings.billing.payment_method_type.unknown": {
"message": "Unknown payment method"
},
"settings.billing.payment_method_type.visa": {
"message": "Visa"
},
"settings.billing.subscription.description": {
"message": "Manage your Modrinth subscriptions."
},
"settings.billing.subscription.title": {
"message": "Subscriptions"
},
"settings.billing.title": {
"message": "Billing and subscriptions"
},
"settings.display.banner.developer-mode.button": {
"message": "Deactivate developer mode"
},
Expand Down Expand Up @@ -827,6 +920,9 @@
"settings.display.project-list-layouts.user": {
"message": "User profile pages"
},
"settings.display.project-list.layouts.collection": {
"message": "Collection"
},
"settings.display.sidebar.advanced-rendering.description": {
"message": "Enables advanced rendering such as blur effects that may cause performance issues without hardware-accelerated rendering."
},
Expand Down
15 changes: 12 additions & 3 deletions apps/frontend/src/pages/[type]/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@
<hr class="card-divider" />
</div>
<div class="input-group">
<template v-if="auth.user">
<template v-if="auth.user && user && user.follows">
<button
v-if="!user.follows.find((x) => x.id === project.id)"
class="btn"
Expand Down Expand Up @@ -486,7 +486,12 @@
>our documentation</a
>.
</MessageBanner>
<Promotion v-if="tags.approvedStatuses.includes(project.status)" />
<Promotion
v-if="
tags.approvedStatuses.includes(project.status) &&
(!auth.user || !isPermission(auth.user.badges, 1 << 0))
"
/>
<div class="navigation-card">
<NavRow
:links="[
Expand Down Expand Up @@ -1128,7 +1133,11 @@ const route = useNativeRoute();
const config = useRuntimeConfig();
const auth = await useAuth();
const user = await useUser();
const user = ref();
if (import.meta.client) {
user.value = await useUser();
}
const cosmetics = useCosmetics();
const tags = useTags();
const flags = useFeatureFlags();
Expand Down
8 changes: 1 addition & 7 deletions apps/frontend/src/pages/auth/sign-in.vue
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,7 @@ const route = useNativeRoute();
const redirectTarget = route.query.redirect || "";
if (route.fullPath.includes("new_account=true")) {
await navigateTo(
`/auth/welcome?authToken=${route.query.code}${
route.query.redirect ? `&redirect=${encodeURIComponent(route.query.redirect)}` : ""
}`,
);
} else if (route.query.code) {
if (route.query.code && !route.fullPath.includes("new_account=true")) {
await finishSignIn();
}
Expand Down
Loading

0 comments on commit 3a4843f

Please sign in to comment.