Skip to content

Premium

En este desarrollo se buscaba crear toda la funcionalidad relacionada a las suscripciones Premium, pero también teniendo en cuenta otras suscripciones que puedan existir a futuro.

A continuación se redactará el progreso actual del desarrollo para su posterior continuación.


Las cards que han sido modificadas, o sus componentes, son las siguientes:

✨CardsEpisode.vue Se ha añadido el prop “isPremium”, el cual indica si dicho episodio es Premium o no.

Además, se ha añadido “tag” el cual puede ser Premium o vacio, dependiendo del prop “isPremium”, y se le pasa al “headerCard” para pintar el tag Premium.

También se le han corregido los estilos para ser mas representativos y responsivos.

Imagen episodio premium

✨CardsSerie.vue Se ha añadido el prop “isPremium”, el cual indica si dicha serie es Premium o no.

Además, se ha añadido “tag” el cual puede ser Premium o vacio, dependiendo del prop “isPremium”, y se le pasa al “headerCard” para pintar el tag Premium.

Imagen serie premium

✨HeaderLayout.vue Se ha añadido un inject del “isPremium” para que sepa cuando pintar el icono correspondiente al tag Premium.

Además, se le han cambiado los estilos según diseño.

✨CardButton.vue Se ha añadido la lógica correspondiente a deshabilitar el botón de las cards cuando el usuario no sea Premium pero la card corresponda a un contenido que sí lo sea.

✨InfoCardHorizontal.vue Se han modificado los estilos para mejorar el apartado visual de las secciones horizontales de plan-site, a corde al contenido metido en los JSON.

✨Premium.vue Se ha implementado la nueva Store dedicada a las suscripciones para que sean reactivas a la suscripción Premium actual del usuario.

Además, se ha añadido un estilo deshabilitado que se activa cuando el usuario tiene la suscripción de dicha card, es decir, si el usuario tiene el Premium Mensual, desactivará la card de Miembro (que esta siempre desactivada por defecto) y la card correspondiente al Premium Mensual.

También se ha añadido una lógica temporal para que asigne al usuario, mediante la store, el rol Premium deseado para hacer pruebas.

Imagen cards premium

✨EpisodeCount.vue Se ha cambiado el icono a otro que se pedía en diseño y se ha cambiado su tamaño


Se ha añadido el middleware premium-products.ts para que, segun el parametro PREMIUM_PRODUCT (el cual puede ser episode, serieName o product), haga la verificación de si el contenido al que se quiere acceder es Premium, pero el usuario no, lo redirige a la página de “Mi Cuenta” a la sección de “Suscripciones”.

Ejemplo de uso en [episode].vue:

const layout = chooseLayoutPage({ page: "episode" });
const PREMIUM_PRODUCT = "episode";
definePageMeta({
layout: "small-footer",
middleware: ["premium-product"],
PREMIUM_PRODUCT,
});

También se tiene pensado añadir el middleware user-subscriptions.global.ts, el cual tiene como fin guardar, en la Store de suscripciones, todas las suscripciones del usuario que lleguen por Strapi cuando se termine dicho desarrollo.

Esto es un planteamiento inicial:

export default defineNuxtRouteMiddleware(async () => {
if (import.meta.server) return;
const { subscribe, unsubscribe } = useSubscriptionStore();
if (localStorage.getItem("userSubscriptions")) {
return;
}
// const userSubscriptions = funcion<Subscription[]>()... // Para sacar las suscripciones del usuario
// Esta función no es el useServices, seria una función a parte que devuelve un array de tipo
// Subscription que contendrá todas las suscripciones ya preparadas para ser cargadas
// if (!userSubscriptions) {
// unsubscribe() // Si no encuentra, lo manda a la mierda quitandole todos los roles, si no tuviese roles de por si, no hace nada
// return
// }
// userSubscriptions.forEach(ubscription => {
// subscribe(userSubscriptions.type, userSubscriptions.payment)
// });
});

Se ha creado una Store para las suscripciones, la cual gestionará tanto la suscripción del usuario en si cuando entra a cualquier página, como la comprobación de si está suscrito a X rol o si se quiere eliminar una suscripción de sus cuenta.

import CryptoJS from "crypto-js";
import type { Subscription, SubscriptionType, SubscriptionPayment } from "@/types/subscriptions";
const STORAGE_KEY = "userSubscriptions";
const decryptionKey = import.meta.env.VITE_SECRET_KEY;
export const useSubscriptionStore = defineStore("subscription", () => {
const userSubscription = ref<Subscription[]>([]);
const loadFromStorage = () => {
const encryptedStorage = localStorage.getItem(STORAGE_KEY);
if (encryptedStorage && decryptionKey) {
const bytes = CryptoJS.AES.decrypt(encryptedStorage, decryptionKey);
const decryptedStorage = bytes.toString(CryptoJS.enc.Utf8);
if (decryptedStorage) {
userSubscription.value = JSON.parse(decryptedStorage);
}
}
};
const saveToStorage = () => {
const stringifiedStorage = JSON.stringify(userSubscription.value);
if (decryptionKey) {
const encryptedStorage = CryptoJS.AES.encrypt(stringifiedStorage, decryptionKey).toString();
localStorage.setItem(STORAGE_KEY, encryptedStorage);
}
};
const subscribe = (type: SubscriptionType, payment: SubscriptionPayment) => {
userSubscription.value.push({
type,
payment,
});
saveToStorage();
};
const unsubscribe = (type?: SubscriptionType, payment?: SubscriptionPayment) => {
const index = userSubscription.value.findIndex(
(subscription) => subscription.type === type && subscription.payment === payment
);
if (index !== -1) {
userSubscription.value.splice(index, 1);
saveToStorage();
} else {
if (localStorage.getItem(STORAGE_KEY)) {
localStorage.removeItem(STORAGE_KEY);
window.location.reload();
}
}
};
const isSubscribed = computed(() => (type: SubscriptionType, payment?: SubscriptionPayment) => {
if (payment) {
return userSubscription.value.some(
(subscription) => subscription.type === type && subscription.payment === payment
);
}
return userSubscription.value.some((subscription) => subscription.type === type);
});
loadFromStorage();
return {
userSubscription,
isSubscribed,
subscribe,
unsubscribe,
};
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useSubscriptionStore, import.meta.hot));
}

La Store almacena los datos cifrados de suscripción del usuario para no estar haciendo llamadas a Strapi continuamente.

La encriptación se realiza con la librería Crypto-JS, la cual encripta los datos usando una llave de desencriptado para asegurar la seguridad de los datos.

Esta llave se debe guardar en el .env bajo la key VITE_SECRET_KEY.

La forma en la que se usa es la siguiente:

  1. Definir STORAGE_KEY, la cual será la clave del localStorage:

    const STORAGE_KEY = "claveLocalStorage";
  2. Definir la decryptionKey, su valor proviene de VITE_SECRET_KEY que está definido en el .env:

    const decryptionKey = import.meta.env.VITE_SECRET_KEY
  3. Encriptar los datos:

    const encryptedStorage = CryptoJS.AES.encrypt("contenido a cifrar", decryptionKey).toString();
  4. Guardar el contenido cifrado:

    localStorage.setItem(STORAGE_KEY, encryptedStorage)
  5. Recibir los datos encriptados:

    localStorage.getItem(STORAGE_KEY)
  6. Desencriptar el contenido

    const bytes = CryptoJS.AES.decrypt(encryptedStorage, decryptionKey)
  7. Transformar los bytes desencriptados a string:

    const decryptedStorage = bytes.toString(CryptoJS.enc.Utf8)

En este caso, CryptoJS es el objeto de la susodicha librería, AES es el método de encriptación y encrypt, y decrypt, son los métodos para encriptar y desencriptar el contenido

Se han añadido los siguientes tipos para la Store de suscripciones:

import { SUBSCRIPTIONS } from "@/constants/subscriptions";
export type SubscriptionType = (typeof SUBSCRIPTIONS)[number];
export type SubscriptionPayment = "Monthly" | "Yearly";
export type Subscription = {
type: SubscriptionType;
payment: SubscriptionPayment;
};

Código actual de las constantes:

export const SUBSCRIPTIONS = ['Premium'] as const

Se han modificado las siguientes páginas:

Se ha añadido un nuevo componente llamado ManageSubscriptions.vue, el cual sirve para darle al usuario la capacidad de gestionar su suscripción desde el apartado de suscripciones de Mi Cuenta.

Actualmente el botón, que está contenido en el componente, solo hace un llamado a una función para quitar las suscripciones Premium que pueda tener el usuario guardadas en la Store con el fin de probar funcionalidades tanto como usuario Premium como no Premium.

Imagen de gestion de suscripciones

También se ha eliminado el componente correspondiente a las cards de Premium que iban por medio de JSONs en vez de usar las que ya habían creadas en plan-site

Además, se ha creado SubscriptionCards.vue, que es un wraper para las cards de Premium dentro del apartado de suscripciones.

Por último, se ha añadido la sección de preguntas frecuentes sobre Premium, que ya estaba presente en plan-site.

Código correspondiente a AllSeries.vue y nameCategorySerie.vue:

<script setup lang="ts">
...
const subscriptionProducts = await getSubscriptionProducts('Premium Mensual')
const slugs = subscriptionProducts.series.map((serie) => serie.slug)
...
</script>
<template>
<UiCardsSerie ... :slugs />
</template>

Se ha añadido la lógica del servicio temporal para sacar los datos de los productos asociados a la suscripción Premium.

De esta forma, se sacan todas las series que están asociadas a Premium y se pasan slugs de los mismos por props a las cards, donde se hace la comprobación de si la serie es exclusiva para usuarios Premium o no, y, si lo es, aplica toda la funcionalidad necesaria.

Se ha añadido la misma lógica que en Series para aplicar toda la lógica de Premium, tanto middlewares como apartado visual.

Código correspondiente a DetailSerie.vue, donde se pintan las cards de los episodios:

<script setup lang="ts">
...
const subscriptionProducts = await getSubscriptionProducts('Premium Mensual')
const slugs = subscriptionProducts.episodes.map((episode) => episode.slug)
...
</script>
<template>
<UiCardsEpisode ... :slugs />
</template>

Se ha metido todo el contenido que venia por Strapi en locales, tanto las cards, como el summary, y se ha comprobado que el contenido de las otras verticales también aparezca.

Este contenido se ha guardado en un JSON dedicado a cada vertical que lo tenga llamado premium.json


Se ha creado un servicio básico temporal, en el archivo subscriptionProducts.ts para sacar los productos correspondientes a Premium.

Se espera que este servicio sea reemplazado, en un futuro, por el servicio definitivo cuando el back termine su desarrollo.

import type { Episode } from '@/interfaces/api/shared/episodes'
import type { Serie } from '@/interfaces/api/shared/series'
import type { Product } from '@/interfaces/previewProduct'
type SubscriptionProducts = {
name: string
subscriptionType: string
trialPeriodDays: number
vertical: string[]
includedProducts: Product[]
episodes: Episode[]
series: Serie[]
}
export const getSubscriptionProducts = async (name: string) => {
const { find } = useStrapi()
const { data } = await find<SubscriptionProducts>('product-subscriptions', {
filters: { name }, // Filtra por nombre de la suscripción
fields: ['name', 'subscriptionType'], // Devuelve el nombre y tipo de suscripción
populate: {
episodes: {
fields: ['slug'], // Devuelve el slug de los episodios
},
series: {
fields: ['slug'], // Devuelve el slug de las series
},
},
})
const response = data[0]
return response
}