feat(abonnement): page publique avec cards et bouton Je m'abonne conditionnel
La page publique /a-propos/abonnement affiche désormais les cards de formule (informatives, sans bouton par card), un explainer des fonctionnalités, et un unique CTA "Je m'abonne" dont la cible dépend de l'état d'authentification/abonnement. Retrait de la sous-section "Fonctionnalités incluses" des CGU (doublon avec l'explainer). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
|
import dash_bootstrap_components as dbc
|
||||||
from dash import dcc, html, register_page
|
from dash import dcc, html, register_page
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
from src.pages._apropos_shell import apropos_shell
|
from src.pages._apropos_shell import apropos_shell
|
||||||
|
from src.subscriptions import db as sub_db
|
||||||
|
from src.subscriptions import plans
|
||||||
|
from src.utils import TOUS_ABONNES
|
||||||
from src.utils.seo import META_CONTENT
|
from src.utils.seo import META_CONTENT
|
||||||
|
|
||||||
register_page(
|
register_page(
|
||||||
@@ -17,6 +22,106 @@ abonnement_features = dcc.Markdown("""
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_card(meta: dict, trial: int | None):
|
||||||
|
badge = (
|
||||||
|
html.Div(f"{trial} jours d'essai gratuit", className="mb-3") if trial else None
|
||||||
|
)
|
||||||
|
return dbc.Card(
|
||||||
|
dbc.CardBody(
|
||||||
|
[
|
||||||
|
html.H4(meta["label"], className="mb-1"),
|
||||||
|
html.P(
|
||||||
|
f"{meta['prix_ht']} € HT / mois "
|
||||||
|
f"({str(int(meta['prix_ht']) * 1.2).replace('.0', '')} € TTC)",
|
||||||
|
className="text-muted mb-3",
|
||||||
|
),
|
||||||
|
html.P(meta["description"], className="mb-3"),
|
||||||
|
badge,
|
||||||
|
],
|
||||||
|
className="p-4",
|
||||||
|
),
|
||||||
|
className="h-100",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_cards(trial_for=plans.trial_days):
|
||||||
|
cards = []
|
||||||
|
for key in ("simple", "soutien"):
|
||||||
|
meta = plans.plan_meta(key)
|
||||||
|
if meta:
|
||||||
|
cards.append(_plan_card(meta, trial_for(key)))
|
||||||
|
return dbc.Row([dbc.Col(c, md=6) for c in cards], className="g-4 mb-4")
|
||||||
|
|
||||||
|
|
||||||
|
def _explainer():
|
||||||
|
col_left = dbc.Col(
|
||||||
|
[html.H4("Fonctionnalités réservées aux abonné·es :"), abonnement_features],
|
||||||
|
md=6,
|
||||||
|
style={
|
||||||
|
"borderRight": "1px solid var(--bs-border-color)",
|
||||||
|
"paddingRight": "2rem",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
col_right = dbc.Col(
|
||||||
|
[
|
||||||
|
html.H4("Ce que les abonnements permettent"),
|
||||||
|
html.Ul(
|
||||||
|
[
|
||||||
|
html.Li(
|
||||||
|
"passer plus de temps à développer colibre et moins de temps "
|
||||||
|
"à chercher des missions"
|
||||||
|
),
|
||||||
|
html.Li(
|
||||||
|
"rédaction d'études à partir des données, par exemple sur les "
|
||||||
|
"acheteurs dont les données sont introuvables et les raisons "
|
||||||
|
"de cette non-publication."
|
||||||
|
),
|
||||||
|
html.Li(
|
||||||
|
"fédération des bonnes volontés souhaitant militer pour une "
|
||||||
|
"législation plus ambitieuse sur la transparence de la "
|
||||||
|
"commande publique."
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
md=6,
|
||||||
|
style={"paddingLeft": "2rem"},
|
||||||
|
)
|
||||||
|
return dbc.Row([col_left, col_right], className="align-items-start pt-2 mb-4")
|
||||||
|
|
||||||
|
|
||||||
|
def _subscribe_button(
|
||||||
|
authenticated: bool, has_active_subscription: bool, tous_abonnes: bool
|
||||||
|
):
|
||||||
|
if tous_abonnes:
|
||||||
|
return html.Div(
|
||||||
|
[
|
||||||
|
dbc.Alert(
|
||||||
|
"Les fonctionnalités normalement accessibles contre un abonnement "
|
||||||
|
"de 20 € HT par mois sont accessibles à tous et toutes en attendant "
|
||||||
|
"la validation de mon dossier pour recevoir des paiements.",
|
||||||
|
color="info",
|
||||||
|
),
|
||||||
|
html.A(
|
||||||
|
"Je m'abonne",
|
||||||
|
href="#",
|
||||||
|
className="btn btn-secondary disabled",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
className="text-center my-4",
|
||||||
|
)
|
||||||
|
if authenticated and has_active_subscription:
|
||||||
|
label, href = "Gérer mon abonnement", "/compte/abonnement"
|
||||||
|
elif authenticated:
|
||||||
|
label, href = "Je m'abonne", "/compte/abonnement/mes-infos"
|
||||||
|
else:
|
||||||
|
label, href = "Je m'abonne", "/inscription"
|
||||||
|
return html.Div(
|
||||||
|
html.A(label, href=href, className="btn btn-primary btn-lg"),
|
||||||
|
className="text-center my-4",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
subscription_terms = html.Div(
|
subscription_terms = html.Div(
|
||||||
[
|
[
|
||||||
html.H2("Abonnement"),
|
html.H2("Abonnement"),
|
||||||
@@ -26,17 +131,6 @@ L'accès aux fonctionnalités de base de colibre est gratuit et sans inscription
|
|||||||
il est possible de souscrire à un abonnement mensuel qui donne accès à des fonctionnalités supplémentaires. Cet abonnement s'adresse tant aux professionnel·les qu'aux particuliers.
|
il est possible de souscrire à un abonnement mensuel qui donne accès à des fonctionnalités supplémentaires. Cet abonnement s'adresse tant aux professionnel·les qu'aux particuliers.
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
html.H4("Fonctionnalités incluses"),
|
|
||||||
dcc.Markdown(
|
|
||||||
"""
|
|
||||||
Les fonctionnalités accessibles aux abonné·es évoluent au fil du développement du service.
|
|
||||||
|
|
||||||
Fonctionnalités réservées aux abonné·es :
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
abonnement_features,
|
|
||||||
dcc.Markdown("""
|
|
||||||
Les fonctionnalités en cours de développement et soumises au vote sont visibles dans la section [Roadmap](/a-propos/roadmap)."""),
|
|
||||||
html.H4("Tarifs"),
|
html.H4("Tarifs"),
|
||||||
dcc.Markdown(
|
dcc.Markdown(
|
||||||
"""
|
"""
|
||||||
@@ -112,4 +206,14 @@ Conformément au RGPD, vous pouvez demander l'accès, la rectification ou la sup
|
|||||||
|
|
||||||
|
|
||||||
def layout(**_):
|
def layout(**_):
|
||||||
return apropos_shell("abonnement", subscription_terms)
|
authenticated = current_user.is_authenticated
|
||||||
|
has_active = authenticated and sub_db.has_active_subscription(current_user.id)
|
||||||
|
body = html.Div(
|
||||||
|
[
|
||||||
|
_plan_cards(),
|
||||||
|
_explainer(),
|
||||||
|
_subscribe_button(authenticated, has_active, TOUS_ABONNES),
|
||||||
|
subscription_terms,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return apropos_shell("abonnement", body)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _plan_env(monkeypatch):
|
||||||
|
monkeypatch.setenv("FRISBII_PLAN_SIMPLE", "plan_simple")
|
||||||
|
monkeypatch.setenv("FRISBII_PLAN_SOUTIEN", "plan_soutien")
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_cards_informative_no_button():
|
||||||
|
from src.app import app # noqa: F401
|
||||||
|
from src.pages.a_propos import abonnement as page
|
||||||
|
|
||||||
|
text = str(page._plan_cards(trial_for=lambda key: 2))
|
||||||
|
assert "Abonnement" in text
|
||||||
|
assert "Abonnement de soutien" in text
|
||||||
|
assert "2 jours d'essai gratuit" in text
|
||||||
|
assert "S'abonner" not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_subscribe_button_visitor_goes_to_inscription():
|
||||||
|
from src.app import app # noqa: F401
|
||||||
|
from src.pages.a_propos import abonnement as page
|
||||||
|
|
||||||
|
text = str(page._subscribe_button(False, False, False))
|
||||||
|
assert "Je m'abonne" in text
|
||||||
|
assert "href='/inscription'" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_subscribe_button_authenticated_no_sub_goes_to_mes_infos():
|
||||||
|
from src.app import app # noqa: F401
|
||||||
|
from src.pages.a_propos import abonnement as page
|
||||||
|
|
||||||
|
text = str(page._subscribe_button(True, False, False))
|
||||||
|
assert "href='/compte/abonnement/mes-infos'" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_subscribe_button_active_sub_manages():
|
||||||
|
from src.app import app # noqa: F401
|
||||||
|
from src.pages.a_propos import abonnement as page
|
||||||
|
|
||||||
|
text = str(page._subscribe_button(True, True, False))
|
||||||
|
assert "Gérer mon abonnement" in text
|
||||||
|
assert "href='/compte/abonnement'" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_subscribe_button_disabled_when_tous_abonnes():
|
||||||
|
from src.app import app # noqa: F401
|
||||||
|
from src.pages.a_propos import abonnement as page
|
||||||
|
|
||||||
|
text = str(page._subscribe_button(False, False, True))
|
||||||
|
assert "disabled" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_cgu_terms_trimmed_of_features_section():
|
||||||
|
from src.app import app # noqa: F401
|
||||||
|
from src.pages.a_propos import abonnement as page
|
||||||
|
|
||||||
|
text = str(page.subscription_terms)
|
||||||
|
assert "Fonctionnalités incluses" not in text
|
||||||
|
assert "Résiliation" in text
|
||||||
|
assert "Tarifs" in text
|
||||||
Reference in New Issue
Block a user