304 lines
9.3 KiB
Python
304 lines
9.3 KiB
Python
import dash_bootstrap_components as dbc
|
|
from dash import Input, Output, State, callback, dcc, html, register_page
|
|
from flask_login import current_user
|
|
|
|
from src.pages._compte_shell import account_guard, account_shell
|
|
from src.subscriptions import db, plans
|
|
from src.utils.frontend import format_date_french
|
|
|
|
register_page(
|
|
__name__,
|
|
path="/compte/abonnement",
|
|
title="Abonnement | colibre",
|
|
name="Abonnement",
|
|
description="Gestion de votre abonnement colibre.",
|
|
)
|
|
|
|
|
|
def _csrf_input():
|
|
from flask_wtf.csrf import generate_csrf
|
|
|
|
return dcc.Input(type="hidden", name="csrf_token", value=generate_csrf())
|
|
|
|
|
|
def _price_text(meta: dict) -> str | None:
|
|
ht = meta.get("prix_ht")
|
|
if ht is None:
|
|
return None
|
|
return f"{ht} € HT / mois ({round(ht * 1.2, 2):g} € TTC)"
|
|
|
|
|
|
def _reabo_button():
|
|
return html.Div(
|
|
[
|
|
dcc.Markdown(
|
|
"[Abonnez-vous](/a-propos/abonnement) pour accéder aux fonctionnalités réservées "
|
|
"aux abonné·es.",
|
|
className="mb-3",
|
|
),
|
|
],
|
|
className="mb-4",
|
|
)
|
|
|
|
|
|
def _free_access_view():
|
|
from src.pages.a_propos.abonnement import abonnement_features
|
|
|
|
return html.Div(
|
|
[
|
|
html.H5(
|
|
"Vous avez temporairement accès à toutes les fonctionnalités",
|
|
className="mb-3",
|
|
),
|
|
html.P("Votre accès gratuit débloque :"),
|
|
abonnement_features,
|
|
dcc.Markdown(
|
|
"Pensez à copier les liens vers vos vues pour les conserver si vous "
|
|
"ne comptez pas souscrire à un abonnement payant.",
|
|
className="text-muted mt-3",
|
|
),
|
|
],
|
|
className="mb-4",
|
|
)
|
|
|
|
|
|
def _no_sub_view(tous_abonnes: bool, row):
|
|
if tous_abonnes:
|
|
return _free_access_view()
|
|
blocks = []
|
|
if row is not None and row["status"] == "expired":
|
|
blocks.append(
|
|
dbc.Alert("Votre abonnement a expiré.", color="warning", className="mb-4")
|
|
)
|
|
blocks.append(_reabo_button())
|
|
return html.Div(blocks)
|
|
|
|
|
|
def _active_view(row):
|
|
meta = plans.plan_meta(row["plan"]) or {"label": row["plan"]}
|
|
end = format_date_french(row["current_period_end"])
|
|
blocks = [html.H3(meta["label"], className="mb-1")]
|
|
price = _price_text(meta)
|
|
if price:
|
|
blocks.append(html.P(price, className="text-muted mb-3"))
|
|
|
|
if row["status"] == "pending":
|
|
blocks.extend(
|
|
[
|
|
dbc.Alert(
|
|
"Sans méthode de paiement enregistrée, votre abonnement sera "
|
|
"résilié à la fin de la période d'essai.",
|
|
color="warning",
|
|
className="mb-3",
|
|
),
|
|
html.Form(
|
|
method="POST",
|
|
action="/subscriptions/add-payment",
|
|
children=[
|
|
_csrf_input(),
|
|
html.Button(
|
|
"Ajouter une méthode de paiement",
|
|
type="submit",
|
|
className="btn btn-secondary mb-3",
|
|
),
|
|
],
|
|
),
|
|
]
|
|
)
|
|
elif row["status"] == "trial":
|
|
blocks.append(
|
|
dbc.Alert(
|
|
f"Essai gratuit jusqu'au {end}, puis facturation et débit automatique à chaque date anniversaire.",
|
|
color="info",
|
|
)
|
|
)
|
|
elif row["status"] == "cancelled":
|
|
blocks.append(
|
|
dbc.Alert(f"Abonnement résilié, actif jusqu'au {end}.", color="warning")
|
|
)
|
|
elif row["status"] == "active":
|
|
blocks.append(html.P(f"Prochaine facturation : {end}"))
|
|
|
|
if row["status"] in ("pending", "trial", "active"):
|
|
blocks.append(
|
|
html.A(
|
|
"Configurer mon abonnement",
|
|
href="/compte/abonnement/mes-infos",
|
|
className="btn btn-outline-primary mt-3 me-2",
|
|
)
|
|
)
|
|
|
|
if row["status"] in ("trial", "active"):
|
|
blocks.append(
|
|
html.Form(
|
|
method="POST",
|
|
action="/subscriptions/change-payment-method",
|
|
children=[
|
|
_csrf_input(),
|
|
html.Button(
|
|
"Changer de méthode de paiement",
|
|
type="submit",
|
|
className="btn btn-outline-secondary mt-3 me-2",
|
|
),
|
|
],
|
|
style={"display": "inline-block"},
|
|
)
|
|
)
|
|
|
|
if row["status"] in ("pending", "trial", "active"):
|
|
blocks.append(
|
|
html.Button(
|
|
"Me désabonner",
|
|
id="resiliation-trigger",
|
|
n_clicks=0,
|
|
className="btn btn-outline-danger mt-3",
|
|
)
|
|
)
|
|
|
|
return html.Div(blocks)
|
|
|
|
|
|
def _resiliation_modal(end_raw):
|
|
end = format_date_french(end_raw) if end_raw else None
|
|
if end:
|
|
body_text = (
|
|
f"Êtes-vous sûr de vouloir mettre fin à votre abonnement ? "
|
|
f"Vous resterez abonné jusqu'à la date anniversaire, le {end}."
|
|
)
|
|
else:
|
|
body_text = "Êtes-vous sûr de vouloir mettre fin à votre abonnement ?"
|
|
return dbc.Modal(
|
|
[
|
|
dbc.ModalHeader(dbc.ModalTitle("Résilier l'abonnement")),
|
|
dbc.ModalBody(body_text),
|
|
dbc.ModalFooter(
|
|
[
|
|
dbc.Button(
|
|
"Annuler",
|
|
id="resiliation-annuler",
|
|
color="secondary",
|
|
className="me-2",
|
|
n_clicks=0,
|
|
),
|
|
html.Form(
|
|
method="POST",
|
|
action="/subscriptions/cancel",
|
|
children=[
|
|
_csrf_input(),
|
|
html.Button(
|
|
"Je suis sûr",
|
|
type="submit",
|
|
className="btn btn-danger",
|
|
),
|
|
],
|
|
style={"display": "inline"},
|
|
),
|
|
]
|
|
),
|
|
],
|
|
id="resiliation-modal",
|
|
is_open=False,
|
|
)
|
|
|
|
|
|
def _feedback(query):
|
|
msgs = {
|
|
"succes": (
|
|
"Merci, votre abonnement est activé. Bonne exploration !",
|
|
"success",
|
|
),
|
|
"annule": ("Paiement annulé.", "secondary"),
|
|
}
|
|
out = []
|
|
if query.get("paiement") in msgs:
|
|
text, color = msgs[query["paiement"]]
|
|
out.append(dbc.Alert(text, color=color))
|
|
if query.get("resiliation") == "ok":
|
|
out.append(dbc.Alert("Votre abonnement a été résilié.", color="info"))
|
|
if query.get("carte") == "succes":
|
|
out.append(dbc.Alert("Méthode de paiement mise à jour.", color="success"))
|
|
if query.get("carte") == "annule":
|
|
out.append(dbc.Alert("Modification annulée.", color="secondary"))
|
|
if query.get("error") == "frisbii":
|
|
out.append(
|
|
dbc.Alert(
|
|
"Une erreur est survenue avec le service de paiement.", color="primary"
|
|
)
|
|
)
|
|
if query.get("maj") == "succes":
|
|
out.append(dbc.Alert("Votre abonnement a été mis à jour.", color="success"))
|
|
return out
|
|
|
|
|
|
_salaire_modal = dbc.Modal(
|
|
[
|
|
dbc.ModalHeader(dbc.ModalTitle("Salaire médian — coût employeur")),
|
|
dbc.ModalBody(html.Img(src="/assets/salaire.png", style={"width": "100%"})),
|
|
],
|
|
id="salaire-modal",
|
|
size="lg",
|
|
is_open=False,
|
|
)
|
|
|
|
|
|
@callback(
|
|
Output("salaire-modal", "is_open"),
|
|
Input("salaire-modal-trigger", "n_clicks"),
|
|
prevent_initial_call=True,
|
|
)
|
|
def _open_salaire_modal(_):
|
|
return True
|
|
|
|
|
|
@callback(
|
|
Output("resiliation-modal", "is_open"),
|
|
Input("resiliation-trigger", "n_clicks"),
|
|
Input("resiliation-annuler", "n_clicks"),
|
|
State("resiliation-modal", "is_open"),
|
|
prevent_initial_call=True,
|
|
)
|
|
def _toggle_resiliation(_, __, is_open):
|
|
return not is_open
|
|
|
|
|
|
def _tous_abonnes_banner():
|
|
from src.utils import TOUS_ABONNES
|
|
|
|
if not TOUS_ABONNES:
|
|
return None
|
|
return dbc.Alert(
|
|
"Les fonctionnalités normalement accessibles contre un abonnement de "
|
|
"20 € HT par mois sont accessibles à tous et toutes en attendant la "
|
|
"validation du dossier pour recevoir des paiements.",
|
|
color="info",
|
|
)
|
|
|
|
|
|
def _show_active_view(row) -> bool:
|
|
return row is not None and row["status"] not in ("failed", "expired")
|
|
|
|
|
|
def layout(**query):
|
|
guard = account_guard("/compte/abonnement", require_subscription=False)
|
|
if guard is not None:
|
|
return guard
|
|
|
|
row = db.get_current(current_user.id) if current_user.is_authenticated else None
|
|
|
|
body = [html.H2("Abonnement", className="mb-4")]
|
|
banner = _tous_abonnes_banner()
|
|
if banner is not None:
|
|
body.append(banner)
|
|
body.extend(_feedback(query))
|
|
|
|
if _show_active_view(row):
|
|
body.append(_active_view(row))
|
|
body.append(_resiliation_modal(row["current_period_end"]))
|
|
else:
|
|
from src.utils import TOUS_ABONNES
|
|
|
|
body.append(_no_sub_view(TOUS_ABONNES, row))
|
|
|
|
body.append(_salaire_modal)
|
|
return account_shell("abonnement", html.Div(body))
|