├── .github └── workflows │ └── autoblack.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── canceled.html ├── css │ ├── bootstrap-5.3.3.min.css │ └── global.css ├── darkmode.js ├── index.html ├── index.js └── success.html ├── babel.cfg ├── locales ├── en │ └── LC_MESSAGES │ │ └── messages.po └── fr │ └── LC_MESSAGES │ └── messages.po ├── requirements.txt └── server.py /.github/workflows/autoblack.yml: -------------------------------------------------------------------------------- 1 | name: Check / auto apply Black 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | black: 8 | name: Check / auto apply black 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Check files using the black formatter 13 | uses: psf/black@stable 14 | id: black 15 | with: 16 | options: "." 17 | continue-on-error: true 18 | - shell: pwsh 19 | id: check_files_changed 20 | run: | 21 | # Diff HEAD with the previous commit 22 | $diff = git diff 23 | $HasDiff = $diff.Length -gt 0 24 | Write-Host "::set-output name=files_changed::$HasDiff" 25 | - name: Create Pull Request 26 | if: steps.check_files_changed.outputs.files_changed == 'true' 27 | uses: peter-evans/create-pull-request@v6 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | title: "Format Python code with Black" 31 | commit-message: ":art: Format Python code with Black" 32 | body: | 33 | This pull request uses the [psf/black](https://github.com/psf/black) formatter. 34 | base: ${{ github.head_ref }} # Creates pull request onto pull request or commit branch 35 | branch: actions/black 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Config file 132 | settings.py 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019- Stripe, Inc. (https://stripe.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pepettes, a donation form 2 | 3 | A small form to donate money to a project. Support one-time or recuring payment. 4 | 5 | **Demo** 6 | 7 | The demo is running in test mode -- use `4242424242424242` as a test card number with any CVC + future expiration date. 8 | 9 | Use the `4000002500003155` test card number to trigger a 3D Secure challenge flow. 10 | 11 | Read more about testing on Stripe at . 12 | 13 | ## How to run locally 14 | 15 | Follow the steps below to run locally. 16 | 17 | ```bash 18 | git clone https://github.com/YunoHost/pepettes 19 | cd pepettes 20 | python3 -m venv venv 21 | source venv/bin/activate 22 | python3 -m pip install -r requirements.txt 23 | ``` 24 | 25 | Create a settings.py file with : 26 | 27 | ```python 28 | ENV = 'development' 29 | PORT = 8000 30 | DOMAIN = 'http://localhost:8000' 31 | SECRET_KEY = '712AZPOC87HXD5SQSb12rd' 32 | SECRET_CSRF_KEY = '712AZPOC87HXD5SQSb12' 33 | LANGUAGES = ['en', 'fr'] 34 | BABEL_TRANSLATION_DIRECTORIES = 'locales' 35 | 36 | # Customization 37 | CUSTOM = {} 38 | CUSTOM['name'] = 'YunoHost' 39 | # You can activate a donation management portal on https://dashboard.stripe.com/settings/billing/portal and display a link to it on the homepage. 40 | CUSTOM['portal_url'] = '' 41 | CUSTOM['contact_url'] = 'mailto:donate-6521@yunohost.org' 42 | CUSTOM['logo'] = 'https://yunohost.org/user/images/logo.png' 43 | CUSTOM['favicon'] = 'https://yunohost.org/user/themes/yunohost-docs/images/favicon.png' 44 | CUSTOM['currencies'] = [ 45 | ('EUR', '€'), 46 | ('USD', '$') 47 | ] 48 | 49 | # Stripe keys 50 | CUSTOM['stripe_publishable_key'] = 'pk_test_gOgGjacs9YfvDJY03BRZ576O' 51 | STRIPE_SECRET_KEY = 'sk_test_' 52 | 53 | # Stripe subscription data 54 | DONATION={'one_time':{}, 'recuring': {}} 55 | DONATION['one_time']['EUR'] = 'price_1IKuPVE7vOmTpJBiYMq7ztLH' 56 | DONATION['one_time']['USD'] = 'price_1IKuQfE7vOmTpJBi0A3nRGCJ' 57 | DONATION['recuring']['EUR'] = 'price_1IKumjE7vOmTpJBikyqS2NqD' 58 | DONATION['recuring']['USD'] = 'price_1IKumAE7vOmTpJBiO4CEfa3Q' 59 | ``` 60 | 61 | ```bash 62 | export FLASK_APP=server.py 63 | python3 -m flask run --port=8000 64 | ``` 65 | 66 | ```bash 67 | # extract new strings or update existing strings 68 | pybabel extract -F babel.cfg -o messages.pot *.py assets/*.html 69 | 70 | # then update the locale files, please translate into french any new string 71 | pybabel update -i messages.pot -d locales 72 | ``` 73 | -------------------------------------------------------------------------------- /assets/canceled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ _('Donate to %(name)s', name=name) }} 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | {{ _('%(name)s Logo', name=name) }} 21 | 22 |

{{ _('Your payment was canceled') }}

23 | 24 | 27 | 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /assets/css/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | display: flex; 8 | align-items: center; 9 | padding-top: 40px; 10 | padding-bottom: 40px; 11 | } 12 | 13 | @media (prefers-color-scheme: dark) { 14 | img { 15 | filter: invert(100%); 16 | } 17 | } 18 | 19 | img { 20 | max-width: 13em 21 | } 22 | 23 | .form-donate { 24 | width: 100%; 25 | max-width: 330px; 26 | padding: 15px; 27 | margin: auto; 28 | } 29 | 30 | .form-donate .checkbox { 31 | font-weight: 400; 32 | } 33 | 34 | .form-donate .form-control { 35 | position: relative; 36 | box-sizing: border-box; 37 | height: auto; 38 | padding: 10px; 39 | font-size: 16px; 40 | } 41 | 42 | .form-donate .form-control:focus { 43 | z-index: 2; 44 | } 45 | 46 | .form-donate input[type="email"] { 47 | margin-bottom: -1px; 48 | border-bottom-right-radius: 0; 49 | border-bottom-left-radius: 0; 50 | } 51 | 52 | .form-donate input[type="password"] { 53 | margin-bottom: 10px; 54 | border-top-left-radius: 0; 55 | border-top-right-radius: 0; 56 | } -------------------------------------------------------------------------------- /assets/darkmode.js: -------------------------------------------------------------------------------- 1 | // Set theme to the user's preferred color scheme 2 | function updateTheme() { 3 | const colorMode = window.matchMedia("(prefers-color-scheme: dark)").matches ? 4 | "dark" : 5 | "light"; 6 | document.querySelector("html").setAttribute("data-bs-theme", colorMode); 7 | } 8 | 9 | // Set theme on load 10 | updateTheme() 11 | 12 | // Update theme when the preferred scheme changes 13 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme) 14 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ _('Donate to %(name)s', name=name) }} 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 | {{ _('%(name)s Logo', name=name) }} 23 |

{{ _('I want to give to %(name)s', name=name) }}

24 | 25 |
26 | 27 | 28 | 29 | 31 | 32 | 37 | 38 | 42 |
43 | 44 | 47 | 48 |
49 | {% if portal_url %} 50 |

51 | {{ _('Manage your monthly donation') }} 52 |

53 | {% endif %} 54 | 55 |

56 | {{ _('Contact us') }} 57 |

58 |
59 | 60 |
61 |
62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /assets/index.js: -------------------------------------------------------------------------------- 1 | window.stripe = Stripe(document.getElementById('public_key').value); 2 | 3 | // When the form is submitted... 4 | var submitBtn = document.querySelector('#submit'); 5 | submitBtn.addEventListener('click', function (evt) { 6 | var quantity = parseInt(document.getElementById('quantity').value); 7 | var currency = document.getElementById('currency').value; 8 | var frequency = document.getElementById('frequency').value; 9 | var csrf = document.getElementById('csrf').value; 10 | 11 | // Create the checkout session. 12 | fetch('/create-checkout-session', { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | body: JSON.stringify({ 18 | user_csrf: csrf, 19 | quantity: quantity, 20 | currency: currency, 21 | frequency: frequency 22 | }), 23 | }).then(function (result) { 24 | return result.json(); 25 | }).then(function (data) { 26 | // Redirect to Checkout. with the ID of the 27 | // CheckoutSession created on the server. 28 | stripe.redirectToCheckout({ 29 | sessionId: data.sessionId, 30 | }) 31 | .then(function(result) { 32 | // If redirection fails, display an error to the customer. 33 | if (result.error) { 34 | var displayError = document.getElementById('error-message'); 35 | displayError.textContent = result.error.message; 36 | } 37 | }); 38 | }); 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /assets/success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ _('Donate to %(name)s', name=name) }} 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | {{ _('%(name)s Logo', name=name) }} 21 | 22 |

{{ _('Thanks for your donation 🙂') }}

23 | 24 | 27 | 28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: server.py] 2 | [jinja2: assets/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /locales/en/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # English translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2025-03-31 17:03+0200\n" 11 | "PO-Revision-Date: 2021-02-18 23:33+0100\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: en\n" 14 | "Language-Team: en \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: assets/canceled.html:8 assets/index.html:8 assets/success.html:8 22 | #, python-format 23 | msgid "Donate to %(name)s" 24 | msgstr "" 25 | 26 | #: assets/canceled.html:20 assets/index.html:22 assets/success.html:20 27 | #, python-format 28 | msgid "%(name)s Logo" 29 | msgstr "" 30 | 31 | #: assets/canceled.html:22 32 | msgid "Your payment was canceled" 33 | msgstr "" 34 | 35 | #: assets/canceled.html:25 assets/success.html:25 36 | msgid "Go back to the donate form" 37 | msgstr "" 38 | 39 | #: assets/index.html:23 40 | #, python-format 41 | msgid "I want to give to %(name)s" 42 | msgstr "" 43 | 44 | #: assets/index.html:30 45 | msgid "Amount" 46 | msgstr "" 47 | 48 | #: assets/index.html:32 49 | msgid "Currency" 50 | msgstr "" 51 | 52 | #: assets/index.html:38 53 | msgid "Frequency" 54 | msgstr "" 55 | 56 | #: assets/index.html:39 57 | msgid "/ month" 58 | msgstr "" 59 | 60 | #: assets/index.html:40 61 | msgid "one time" 62 | msgstr "" 63 | 64 | #: assets/index.html:45 65 | msgid "Donate" 66 | msgstr "" 67 | 68 | #: assets/index.html:51 69 | msgid "Manage your monthly donation" 70 | msgstr "" 71 | 72 | #: assets/index.html:56 73 | msgid "Contact us" 74 | msgstr "" 75 | 76 | #: assets/success.html:22 77 | msgid "Thanks for your donation 🙂" 78 | msgstr "" 79 | 80 | -------------------------------------------------------------------------------- /locales/fr/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # French translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2025-03-31 17:03+0200\n" 11 | "PO-Revision-Date: 2021-02-18 23:33+0100\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: fr\n" 14 | "Language-Team: fr \n" 15 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.17.0\n" 20 | 21 | #: assets/canceled.html:8 assets/index.html:8 assets/success.html:8 22 | #, python-format 23 | msgid "Donate to %(name)s" 24 | msgstr "Donner à %(name)s" 25 | 26 | #: assets/canceled.html:20 assets/index.html:22 assets/success.html:20 27 | #, python-format 28 | msgid "%(name)s Logo" 29 | msgstr "Logo de %(name)s" 30 | 31 | #: assets/canceled.html:22 32 | msgid "Your payment was canceled" 33 | msgstr "Votre paiement a été annulé" 34 | 35 | #: assets/canceled.html:25 assets/success.html:25 36 | msgid "Go back to the donate form" 37 | msgstr "Retourner au formulaire de don" 38 | 39 | #: assets/index.html:23 40 | #, python-format 41 | msgid "I want to give to %(name)s" 42 | msgstr "Je veux donner à %(name)s" 43 | 44 | #: assets/index.html:30 45 | #, fuzzy 46 | msgid "Amount" 47 | msgstr "Montant" 48 | 49 | #: assets/index.html:32 50 | msgid "Currency" 51 | msgstr "Devise" 52 | 53 | #: assets/index.html:38 54 | msgid "Frequency" 55 | msgstr "Fréquence" 56 | 57 | #: assets/index.html:39 58 | msgid "/ month" 59 | msgstr "/ mois" 60 | 61 | #: assets/index.html:40 62 | msgid "one time" 63 | msgstr "une fois" 64 | 65 | #: assets/index.html:45 66 | msgid "Donate" 67 | msgstr "Donner" 68 | 69 | #: assets/index.html:51 70 | msgid "Manage your monthly donation" 71 | msgstr "Gérer votre don mensuel" 72 | 73 | #: assets/index.html:56 74 | msgid "Contact us" 75 | msgstr "Contactez-nous" 76 | 77 | #: assets/success.html:22 78 | msgid "Thanks for your donation 🙂" 79 | msgstr "Merci pour votre don 🙂" 80 | 81 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gunicorn 2 | jinja2==3.0 3 | stripe 4 | itsdangerous==2.0.1 5 | flask 6 | flask-simple-csrf 7 | flask-babel 8 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3.6 2 | 3 | """ 4 | server.py 5 | Stripe Sample. 6 | Python 3.6 or newer required. 7 | """ 8 | 9 | import stripe 10 | import json 11 | import os 12 | import random 13 | import string 14 | 15 | from flask import Flask, render_template, jsonify, request, session 16 | from flask_babel import Babel 17 | from flask_simple_csrf import CSRF 18 | 19 | 20 | static_dir = str(os.path.abspath(os.path.join(__file__, "..", "assets"))) 21 | app = Flask( 22 | __name__, static_folder=static_dir, static_url_path="", template_folder=static_dir 23 | ) 24 | app.config.from_pyfile("settings.py") 25 | stripe.api_key = app.config["STRIPE_SECRET_KEY"] 26 | CSRF = CSRF(config={"SECRET_CSRF_KEY": app.config["SECRET_CSRF_KEY"]}) 27 | app = CSRF.init_app(app) 28 | babel = Babel(app) 29 | 30 | 31 | @app.before_request 32 | def before_request(): 33 | if "CSRF_TOKEN" not in session or "USER_CSRF" not in session: 34 | session["USER_CSRF"] = "".join( 35 | random.SystemRandom().choice(string.ascii_uppercase + string.digits) 36 | for _ in range(64) 37 | ) 38 | session["CSRF_TOKEN"] = CSRF.create(session["USER_CSRF"]) 39 | 40 | 41 | @babel.localeselector 42 | def get_locale(): 43 | return request.accept_languages.best_match(app.config["LANGUAGES"]) 44 | 45 | 46 | @app.context_processor 47 | def utility_processor(): 48 | return dict(lang=babel.locale_selector_func()) 49 | 50 | 51 | @app.route("/", methods=["GET"]) 52 | def get_index(): 53 | return render_template( 54 | "index.html", **app.config["CUSTOM"], csrf=session["USER_CSRF"] 55 | ) 56 | 57 | 58 | @app.route("/success", methods=["GET"]) 59 | def get_success(): 60 | return render_template("success.html", **app.config["CUSTOM"]) 61 | 62 | 63 | @app.route("/canceled", methods=["GET"]) 64 | def get_canceled(): 65 | return render_template("canceled.html", **app.config["CUSTOM"]) 66 | 67 | 68 | @app.route("/create-checkout-session", methods=["POST"]) 69 | def create_checkout_session(): 70 | data = json.loads(request.data) 71 | domain_url = app.config["DOMAIN"] 72 | try: 73 | donation = app.config["DONATION"] 74 | currencies = [iso for iso, symbol in app.config["CUSTOM"]["currencies"]] 75 | if ( 76 | CSRF.verify(data["user_csrf"], session["CSRF_TOKEN"]) is False 77 | or data["frequency"] not in ["recuring", "one_time"] 78 | or data["currency"] not in currencies 79 | or int(data["quantity"]) <= 0 80 | ): 81 | return jsonify(error="Bad value"), 400 82 | 83 | # Create new Checkout Session for the order 84 | price = donation[data["frequency"]][data["currency"]] 85 | mode = "payment" if data["frequency"] == "one_time" else "subscription" 86 | 87 | checkout_session = stripe.checkout.Session.create( 88 | success_url=domain_url + "/success?session_id={CHECKOUT_SESSION_ID}", 89 | cancel_url=domain_url + "/canceled", 90 | payment_method_types=["card"], 91 | mode=mode, 92 | line_items=[{"price": price, "quantity": data["quantity"]}], 93 | ) 94 | return jsonify({"sessionId": checkout_session["id"]}) 95 | except Exception as e: 96 | return jsonify(error=str(e)), 403 97 | 98 | 99 | if __name__ == "__main__": 100 | app.run(port=app.config["PORT"], debug=app.debug) 101 | --------------------------------------------------------------------------------