Шаги по созданию веб-сайта и платного доступа с использованием чистого Python

Абонентские услуги БУДУТ!

Абонентский бизнес - это растущая часть экономики, и нет никаких признаков ее замедления. Исследование от Zuora, компании, которая помогает своим пользователям создавать подписки, показало, что 71% взрослых в 12 странах имеют услуги подписки. Как человек, которому нравится создавать вещи с нуля вместо использования платных сервисов, я начал изучать PayPal API, чтобы увидеть, насколько легко настроить платный доступ по подписке для моих веб-сайтов, построенных с использованием Dash framework.

В этом руководстве я объясняю, как начать работу с API PayPal и создать базовый веб-сайт с помощью Dash, который позволяет людям создавать учетную запись пользователя, подписываться через PayPal и получать доступ к контенту на веб-сайте. Весь код прилагается!

Если вы новичок в Dash и хотите изучить основы, ознакомьтесь с моими учебниками:



Начало работы с PayPal

Честно говоря, когда я впервые начал работать с PayPal API, он показался мне немного запутанным и запутанным. Вдобавок ко всему, их последний PayPal Python SDK, похоже, не поддерживает API подписки изначально, поэтому мне пришлось добавить свой собственный код, чтобы воспользоваться преимуществами их SDK framework. В целом, использование библиотеки Python Requests и необработанных вызовов API, вероятно, было бы более простым, чем использование их SDK.

Создание учетной записи PayPal: бизнес, личный или разработчик?

Для завершения обучения вам потребуется доступ к API PayPal. Если вы уже знакомы с учетными записями PayPal и ключами API, пропустите этот раздел и перейдите к коду Dash. Внедрение услуг подписки с помощью PayPal от начала до конца можно свести к 5 шагам:

  1. Создайте учетную запись PayPal Business.
  2. Создайте подписку.
  3. Создайте приложение, чтобы получить ключи API.
  4. Разработка тестов с использованием учетных записей песочницы.
  5. Go Live (В этом руководстве не рассматривается запуск).

Важно отметить, что если вы хотите запустить подписку (начать работу), вам необходимо будет зарегистрировать учетную запись PayPal Business. Если вы просто хотите изучить API, не отправляя фактические платежи, вы можете использовать учетную запись Разработчик или Личный. . PayPal определяет свои счета следующим образом:

  • Учетная запись разработчика - самый простой способ начать работу с API. Требуется только ваша страна проживания и адрес электронной почты. Разрешает только вызовы API тестовой среды (тестовые).
  • Аккаунт продавца (бизнес) - еще несколько шагов, но гораздо больше функций. Вам понадобится учетная запись продавца, чтобы получить учетные данные реального API.

Чтобы зарегистрировать бизнес-аккаунт, начните с следующих инструкций:



После того, как ваша учетная запись будет настроена, вам будет предложено войти в панель управления разработчика, чтобы настроить приложение среды песочницы.

Важно использовать для тестирования учетную запись песочницы вместо реальной, потому что вы не хотите тестировать свое приложение с использованием реальных денежных переводов, пока не убедитесь, что код работает и сайт в безопасности.

После входа в личный кабинет разработчика ваш экран будет выглядеть так:

API PayPal требует использования идентификатора клиента и секретного ключа. Чтобы получить эти значения, нажмите Приложение по умолчанию. Появится информация о приложении:

Обратите внимание, что приложение по умолчанию зарегистрировано в бизнес-аккаунте песочницы, а не в вашем реальном бизнес-аккаунте. Идентификатор клиента и Секрет клиента - это ключи API, которые используются для создания токена доступа для отправки вызовов API.

Скопируйте Client ID и Secret в файл config.py. При запуске не забудьте обновить ключи своей реальной бизнес-учетной записи, а не использовать ключи песочницы.

Управляйте своими ключами API

Если вы новичок в управлении своими ключами, обязательно сохраните их в файле config.py вместо того, чтобы жестко кодировать их в своем приложении. Ключи API могут быть очень ценными и должны быть защищены. Добавьте файл конфигурации в файл gitignore, чтобы предотвратить его отправку и в репозиторий!

Теперь, когда у вас есть учетные данные Sandbox API, пора настроить подписку!

Настройка подписки в песочнице

Подписку можно создать с помощью API PayPal, но процесс ее создания на веб-сайте гораздо проще. Чтобы все функции были привязаны к песочнице, создайте подписку, используя бизнес-аккаунт песочницы.

Войдите на сайт песочницы, используя учетные данные учетной записи песочницы.



Выполните следующие действия, чтобы получить учетные данные бизнес-аккаунта Sandbox:

1. На панели инструментов разработчика PayPal выберите «Песочница» ›Учетные записи.
2. Обратите внимание на тип бизнеса.
3. Нажмите Управление учетными записями. Отобразится редактор учетной записи песочницы.

Войдите в sandbox.paypal.com, используя пароль учетной записи, связанной с бизнес-учетной записью песочницы PayPal.

Создайте службу подписки, которую вы хотите использовать для веб-сайта.

  • Нажмите "Оплатить и получить оплату".
  • Нажмите «Подписки» в разделе «Принять платежи».
  • Нажмите "Создать план".

Я собираюсь сделать этот пример простым и использовать опцию подписки с фиксированной ценой. Есть несколько вариантов на выбор, если требуется более сложная модель подписки.

После завершения настройки скопируйте идентификатор плана как plan_id в файл config.py, содержащий ключи API. Сохраняя его в config.py, его не нужно жестко запрограммировать в вызове API, и он защищен от загрузки на GitHub.

Настройка среды песочницы PayPal и подписка завершена!

Зачем использовать SDK PayPal Python?

Чтобы упростить интеграцию, PayPal предоставляет комплекты разработки программного обеспечения сервера REST (SDK) для нескольких языков через GitHub. Проблема в том, что их последний Python SDK не включает вызовы API подписок. Так зачем использовать его вместо простых запросов?

Я использую его, потому что он упрощает две вещи:

  • Генерация заголовков запроса и токена доступа.
  • Отправка полезной нагрузки данных JSON в вызове API.

Написать пару классов для создания объектов, связанных с подпиской, легко с помощью шаблона из SDK. Новый класс SubscriptionRequest используется с логикой кнопки «Подписаться» на веб-сайте. Новый класс SubscriptionActive проверяет, активна ли подписка для пользователя, входящего на сайт.

Начало работы с PayPal Python SDK

Установите следующие пакеты с помощью pip, чтобы получить возможности Python SDK, используемые в примерах в этом руководстве.

pip install paypalhttp
pip install Checkout-Python-SDK

Чтобы использовать Python SDK, импортируйте зависимости, необходимые для использования среды песочницы. Передайте client_id и client_secret SandboxEnvironment, чтобы создать соединение. Передайте среду классу PayPalHttpClient.

from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from config import client_id, client_secret
# Creating an environment
environment = SandboxEnvironment(client_id=client_id, client_secret=client_secret)
client = PayPalHttpClient(environment)

Улучшение SDK

Чтобы улучшить работу SDK с API подписки PayPal, я создал два класса:

  1. SubscriptionRequest
  2. ПодпискаАктивировать

Запрос на подписку

Создайте класс SubscriptionRequest, который позволит конечному пользователю подписаться на услугу подписки.

class SubscriptionRequest:
    """
    Creates an subscriber.
    """
    def __init__(self):
        self.verb = "POST"
        self.path = "/v1/billing/subscriptions"
        self.headers = {}
        self.headers["Content-Type"] = "application/json"
        self.body = None
def prefer(self, prefer):
        self.headers["Prefer"] = str(prefer)
def request_body(self, order):
        self.body = order
        return self

SubscritionRequest выполнит вызов API POST для / v1 / billing / subscriptions с правильным заголовком и телом запроса. Этот вызов API создает для пользователя подписку на основе данных, отправленных через request_body. Я подробнее расскажу о теле запроса при написании логики кнопки Подписаться.

Активировать подписку

Создайте класс SubscriptionActivate, который используется для проверки статуса подписки конечного пользователя.

class SubscriptionActivate:
    """
    Checks to see if the subscription is active.
    """
    def __init__(self, orderID):
        self.verb = "GET"
        self.path = f"/v1/billing/subscriptions/{orderID}"
        self.headers = {}
        self.headers["Content-Type"] = "application/json"
def prefer(self, prefer):
        self.headers["Prefer"] = str(prefer)

SubscritionActivate вызовет GET API к / v1 / billing / subscriptions /, передав идентификатор заказа в URI. Он вернет полезную нагрузку JSON, в которой будет указан статус подписки пользователя.

Сохраните оба класса в файл с именем SubscriptionRequests.py. Это будет выглядеть так:

from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from config import client_id, client_secret
# Creating an environment
environment = SandboxEnvironment(client_id=client_id, client_secret=client_secret)
client = PayPalHttpClient(environment)
class SubscriptionRequest:
    """
    Creates an subscriber.
    """
    def __init__(self):
        self.verb = "POST"
        self.path = "/v1/billing/subscriptions"
        self.headers = {}
        self.headers["Content-Type"] = "application/json"
        self.body = None
def prefer(self, prefer):
        self.headers["Prefer"] = str(prefer)
def request_body(self, order):
        self.body = order
        return self
class SubscriptionActivate:
    """
    Checks subscription status.
    """
    def __init__(self, orderID):
        self.verb = "GET"
        self.path = f"/v1/billing/subscriptions/{orderID}"
        self.headers = {}
        self.headers["Content-Type"] = "application/json"
def prefer(self, prefer):
        self.headers["Prefer"] = str(prefer)
def request_body(self, order):
        self.body = order
        return self

Создание приложения Dash

Dash - это фреймворк для Python, написанный поверх Flask, Plotly.js и React.js. Приложения Dash состоят из макетов и обратных вызовов:

Макет

Макет состоит из дерева компонентов, которые описывают, как выглядит приложение и как пользователи воспринимают контент.

Обратные вызовы

Обратные вызовы делают приложения Dash интерактивными. Обратные вызовы - это функции Python, которые автоматически вызываются при изменении свойства input.

Весь код включен, но эта статья является копией моего предыдущего руководства, в котором объясняется как настроить аутентификацию пользователей для веб-сайтов, созданных с использованием инфраструктуры Dash. Если вам нужны более подробные объяснения некоторых концепций аутентификации пользователей, используемых в приведенных ниже примерах, ознакомьтесь с этим руководством:



В Руководстве по аутентификации пользователя мы создаем веб-приложение, которое позволяет пользователям создавать имя пользователя и пароль, входить в систему и просматривать информационную панель:

В этой версии приложения Dash пользователь будет видеть панель данных только в том случае, если он подписан. Вместо этого неподписанный пользователь увидит экран с кнопкой подписаться. При нажатии Подписаться пользователю будет предложено войти в PayPal и будет перенаправлено для завершения транзакции:

Как только пользователь подписывается, можно выбрать набор данных, и кнопка подписки будет скрыта.

Файловая структура

В этом уроке я разбиваю приложение Dash на пять файлов. Файлы app.py, layouts.py и SubscriptionRequests.py содержат код, обеспечивающий работу приложения. Информация о пользователе будет храниться в файле базы данных SubscriptionDatabase.sqlite. Ключ API и Plan_ID хранятся в файле config.py.

Не забудьте добавить config.py в gitignore.

Файл SubscriptionDatabase.sqlite

Файл SubscriptionDatabase.sqlite является базой данных и хранит имя пользователя, пароль и адрес электронной почты, флаг подписки, статус подписки и идентификатор заказа в таблице Пользователи. Для защиты паролей пользователей пароль будет хеширован с помощью библиотеки werkzeug. Werkzeug - это расширенная библиотека утилит интерфейса шлюза веб-сервера (WSGI).

Настройка базы данных и таблицы пользователей

Если база данных еще не существует, она будет создана автоматически при подключении.

conn = sqlite3.connect('SubscriptionDatabase.sqlite')

Как видно на изображении рабочего процесса аутентификации выше, пользователи должны создавать свои собственные учетные записи. Соединение с базой данных и оператор вставки, используемые для записи учетной записи пользователя в базу данных, будут управляться библиотекой SQLAlchemy. Кроме того, поскольку Dash построен на основе Flask, я буду использовать Flask-SQLAlchemy, чтобы настроить сервер для взаимодействия с базой данных.

Используйте следующий шаблон для создания таблицы Пользователи:

engine = create_engine('sqlite:///SubscriptionDatabase.sqlite')
db = SQLAlchemy()
config = configparser.ConfigParser()
class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(15), unique=True, nullable = False)
    email = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(80))
    subscribed = db.Column(db.Integer)
    status = db.Column(db.String(80))
    orderID = db.Column(db.String(100))
Users_tbl = Table('users', Users.metadata)
def create_users_table():
    Users.metadata.create_all(engine)
#create the table
create_users_table()

Обратите внимание, что таблица создается с помощью функции create_users_table (). После создания таблицы вы можете закомментировать функцию, чтобы она не пыталась создать таблицу, которая уже существует, при перезапуске приложения.

Файл layout.py

Каждый шаг рабочего процесса аутентификации соответствует макету в файле app.py. Между аутентификацией и содержимым приложения Dash используется всего семь макетов:

Макеты - это дерево компонентов Dash в функциях, которые вызываются обратным вызовом page-content в файле app.py.

Создать макет

Этот макет используется, чтобы позволить пользователю создать свою учетную запись.

Схема входа в систему

Макет входа используется, чтобы позволить пользователю войти в систему после создания своей учетной записи.

Success_Sub Layout

Макет Success_sub отображает доступные наборы данных, к которым пользователь может получить доступ после аутентификации, если пользователь подписан.

Макет Success2

Макет Success2 отображает кнопку подписки после аутентификации, если пользователь не подписан.

Неудачный макет

Макет Failed отображается, когда пользователю не удается войти в систему.

Макет выхода

Макет выхода отображается, когда пользователь вышел из системы.

Схема данных

Этот макет отображает график, который был настроен в первом примере.

import dash_core_components as dcc
import dash_html_components as html
import dash
import dash_bootstrap_components as dbc
button = html.Div([dbc.Button("subscribe", id="sub-button", className="mr-2")])
def create_user():
    create = html.Div([ html.H1('Create User Account')
            , dcc.Location(id='create_user', refresh=True)
            , dcc.Input(id="username"
                , type="text"
                , placeholder="user name"
                , maxLength =15)
            , dcc.Input(id="password"
                , type="password"
                , placeholder="password")
            , dcc.Input(id="email"
                , type="email"
                , placeholder="email"
                , maxLength = 50)
            , html.Button('Create User', id='submit-val', n_clicks=0)
            , html.Div(id='container-button-basic')
        ])#end div
    return create
def user_login():
    # if current_user.is_authenticated:
    #     logout_user()
    
    login =  html.Div([dcc.Location(id='url_login', refresh=True)
                , html.H2('''Please log in to continue:''', id='h1')
                , dcc.Input(placeholder='Enter your username',
                        type='text',
                        id='uname-box')
                , dcc.Input(placeholder='Enter your password',
                        type='password',
                        id='pwd-box')
                , html.Button(children='Login',
                        n_clicks=0,
                        type='submit',
                        id='login-button')
                , html.Div(children='', id='output-state')
            ]) #end div
    return login
def login_success_sub():
    success = html.Div([dcc.Location(id='url_login_success', refresh=True)
                , html.Div([html.H2('Login successful.')
                        , html.Br()
                       # , button
                        , html.Br()
                       # , html.Div(id = 'iframe-div')
                        , html.P('Select a Dataset')
                        , dcc.Link('Data', href = '/data')
                    ]) #end div
                , html.Div([html.Br()
                        , html.Button(id='back-button', children='Go back', n_clicks=0)
                    ]) #end div
            ]) #end div
    return success
def login_success2():
    success = html.Div([dcc.Location(id='url_login_success2', refresh=True)
                , html.Div([html.H2('Login successful.')
                        , html.Br()
                        , button
                        , html.Br()
                        , html.Div(id = 'iframe-div')
                        , html.P('Select a Dataset')
                        #, dcc.Link('Data', href = '/data')
                    ]) #end div
                , html.Div([html.Br()
                        , html.Button(id='back-button', children='Go back', n_clicks=0)
                    ]) #end div
            ]) #end div
    return success
def data_page():
    data = html.Div([dcc.Dropdown(
                        id='dropdown',
                        options=[{'label': i, 'value': i} for i in ['Day 1', 'Day 2']],
                        value='Day 1')
                    , html.Br()
                    , html.Div([dcc.Graph(id='graph')])
                ]) #end div
    return data
def failed_login():
    failed = html.Div([ dcc.Location(id='url_login_df', refresh=True)
                , html.Div([html.H2('Log in Failed. Please try again.')
                        , html.Br()
                        , html.Div([user_login()])
                        , html.Br()
                        , html.Button(id='back-button', children='Go back', n_clicks=0)
                    ]) #end div
            ]) #end div
    return failed
def logout_page():
logout = html.Div([dcc.Location(id='logout', refresh=True)
            , html.Br()
            , html.Div(html.H2('You have been logged out - Please login'))
            , html.Br()
            , html.Div([user_login()])
            , html.Button(id='back-button', children='Go back', n_clicks=0)
        ])#end div
    return logout

Макеты станут интерактивными с обратными вызовами в файле app.py.

Создание app.py

Теперь, когда макеты созданы, пришло время настроить приложение и создать обратные вызовы, чтобы сделать приложение интерактивным. Здесь также применяется логика подключения к API PayPal. Я разобью этот файл на несколько частей, так как логика обратного вызова довольно длинная.

Настройка приложения Dash

Настройка приложения немного сложнее, чем простое приложение Dash, поскольку оно использует Flask и SQLAlchemy. Используйте этот бит шаблонного кода, чтобы настроить сервер Flask для приложения Dash и заставить его взаимодействовать с базой данных через SQLAlchemy. Большая часть этого кода должна быть вам знакома, если вы использовали SQLAlchemy для построения таблицы Users.

app = dash.Dash(__name__)
server = app.server
app.config.suppress_callback_exceptions = True
# server config
server.config.update(
    SECRET_KEY=os.urandom(12),
    SQLALCHEMY_DATABASE_URI='sqlite:///SubscriptionDatabase.sqlite',
    SQLALCHEMY_TRACK_MODIFICATIONS=False
)
db.init_app(server)
# Setup the LoginManager for the server
login_manager = LoginManager()
login_manager.init_app(server)
login_manager.login_view = '/login'
#User as base
# Create User class with UserMixin
class Users(UserMixin, Users):
    pass
app.layout= html.Div([
            html.Div(id='page-content', className='content')
            ,  dcc.Location(id='url', refresh=False)
        ])
# callback to reload the user object
@login_manager.user_loader
def load_user(user_id):
    return Users.query.get(int(user_id))

Обратите внимание: os.urandom используется для создания SECRET_KEY. Flask-Login по умолчанию использует сеансы для аутентификации. Это означает, что конфигурация должна включать секретный ключ.

login_manager позволяет приложению Dash выполнять такие действия, как загрузка пользователя по идентификатору и проверка пользователя в процессе входа в систему. Обратный вызов login_manager необходим для завершения процессов входа в систему. Этот обратный вызов будет использоваться с остальными обратными вызовами Dash. app.layout - это то, что позволяет страницам отображаться при запуске приложения.

Обратный вызов содержимого страницы

Обратный вызов используется для управления тем, какой макет будет возвращен как дочерний для компонента html.Div с идентификатором page-content. путь в URL-адресе определяет, какой макет возвращается. Для каждого макета есть путь.

@app.callback(
    Output('page-content', 'children')
    , [Input('url', 'pathname')])
def display_page(pathname):
    if pathname == '/':
        return create_user()
    
    elif pathname == '/login':
        return user_login()
    
    elif pathname == '/success':
        if current_user.is_authenticated:
            cuid = current_user.get_id()
            conn = sqlite3.connect('SubscriptionDatabase.sqlite')
            c = conn.cursor()
            c.execute(f"select subscribed from users where id = {cuid}")
            sub = c.fetchone()
if sub[0] ==1:
                return login_success_sub()
            else:
                return login_success2()
        else:
            return failed_login()
    
    elif pathname =='/data':
        if current_user.is_authenticated:
            return data_page()
    
    elif pathname == '/logout':
        if current_user.is_authenticated:
            logout_user()
            return logout_page()
        else:
            return logout_page()
    else:
        return '404'

Обратите внимание: в пути / success есть логика, которая определяет, какой макет успеха вернет. Если текущий пользователь аутентифицирован, SQL-запрос проверяет базу данных, чтобы увидеть значение в столбце подписан для этого пользователя. Если он равен 1, возвращается макет login_success_sub. Если он не равен 1, возвращается макет login_success2.

Кнопка подписки Обратный звонок

Когда пользователь создает учетную запись и входит в систему, он видит кнопку Подписаться. Если они нажмут кнопку подписки, PayPal откроется в компоненте iFrame Dash и предложит пользователю завершить транзакцию подписки. Этот обратный вызов использует API PayPal и передает plan_ID подписки в класс SubscriptionRequest в теле запроса.

@app.callback(
    Output("iframe-div", "children"),
    [Input('sub-button', 'n_clicks')]
)
def sub(n):
    environment = SandboxEnvironment(client_id=client_id, client_secret=client_secret)
    client = PayPalHttpClient(environment)
    
    cuid = current_user.get_id()
    print(current_user.get_id())
    sub = SubscriptionRequest()
sub.prefer('return=representation')
sub.request_body({
      "plan_id": plan_id,
      "application_context": {
        "brand_name": "your_brand",
        "locale": "en-US",
        "shipping_preference": "SET_PROVIDED_ADDRESS",
        "user_action": "SUBSCRIBE_NOW",
        "payment_method": {
          "payer_selected": "PAYPAL",
          "payee_preferred": "IMMEDIATE_PAYMENT_REQUIRED"
        },
        "return_url": "http://127.0.0.1:9000/login",
        "cancel_url": "http://127.0.0.1:9000/login"
      }
    })
    if n is None:
        n = 0
        alink = ''
    if n > 0:
        response = client.execute(sub)
        subid = response.result.id
        alink = response.result.links[0].href
        status = response.result.status
        upd = update(Users).where(Users.id == cuid).values(orderID=subid, status= status)
        
        conn = engine.connect()
        conn.execute(upd)
        conn.close()
return [html.Iframe(src=alink)]

Обратите внимание, что sub.request_body содержит полезные данные, отправленные в API PayPal в запросе POST. return_url - это путь входа в систему. Это означает, что пользователь должен снова войти в систему после завершения транзакции. Это позволяет мне встроить логику в функцию кнопки входа в систему, которая проверяет статус подписки пользователя.

Если пользователь успешно завершает транзакцию, идентификатор заказа и статус сохраняются в таблице Пользователи в базе данных.

Кнопка входа в систему Обратный звонок

Этот обратный вызов передает пользователя на одну из страниц успеха. Обратные вызовы используют функцию check_password_hash из библиотеки Werkzeug. В рамках этого обратного вызова выполняется соединение с API PayPal, который проверяет статус подписки пользователя с помощью SubscriptionActivate ().

@app.callback(
    Output('url_login', 'pathname')
    , [Input('login-button', 'n_clicks')]
    , [State('uname-box', 'value')
    , State('pwd-box', 'value')])
def successful(n_clicks, username, password):
    conn = sqlite3.connect('SubscriptionDatabase.sqlite')
    c = conn.cursor()
    environment = SandboxEnvironment(client_id=client_id, client_secret=client_secret)
    client = PayPalHttpClient(environment)   
    user = Users.query.filter_by(username=username).first()
    
    if user:
        if check_password_hash(user.password, password):
            login_user(user)
            cuid = current_user.get_id()
            c.execute(f"select orderID from users where id = {cuid}")
            oid = c.fetchone()
            
            if oid[0] is not None:
                act = SubscriptionActivate(oid[0])
                response = client.execute(act)
              
                if response.result.status == 'ACTIVE':
                    upd = update(Users).where(Users.id == cuid).values(subscribed=1, status= 'ACTIVE')
                    conn = engine.connect()
                    conn.execute(upd)
                    conn.close()
                else:
                    upd = update(Users).where(Users.id == cuid).values(subscribed=0)
                    conn = engine.connect()
                    conn.execute(upd)
                    conn.close()   
                return '/success'
            else:
                upd = update(Users).where(Users.id == cuid).values(subscribed=0)
                conn = engine.connect()
                conn.execute(upd)
                conn.close()
            return '/success'
        else:
            pass
    else:
        pass

Обратите внимание, когда пользователь успешно входит в систему, SQL-запрос получает его orderID. OrderID отправляется в API, чтобы узнать, активен ли он. Если он активен, столбец подписан в таблице пользователи обновляется до 1. Если подписка неактивна, для столбца устанавливается значение 0.

Обратные вызовы служебных программ и полное приложение app.py

Ниже приведен полный код файла app.py. Есть несколько дополнительных обратных вызовов, используемых для создания пользователя, кнопки возврата, функции выхода и графа данных. Если вы следовали Руководству по аутентификации пользователя, вы уже будете знакомы с остальными обратными вызовами.

import dash_core_components as dcc
import dash_html_components as html
import dash
from dash.dependencies import Input, Output, State
from sqlalchemy import Table, create_engine, update, select, MetaData
from sqlalchemy.sql import select
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3
import warnings
import os
from flask_login import login_user, logout_user, current_user, LoginManager, UserMixin
import configparser
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from config import client_id, client_secret, plan_id
from layouts import create_user, user_login, login_success_sub,login_success2, data_page, failed_login, logout_page
import dash_bootstrap_components as dbc
import requests
import json
from SubscriptionRequests import SubscriptionRequest, environment, client, SubscriptionActivate
# Creating an environment
environment = SandboxEnvironment(client_id=client_id, client_secret=client_secret)
client = PayPalHttpClient(environment)
warnings.filterwarnings("ignore")
conn = sqlite3.connect('SubscriptionDatabase.sqlite')
engine = create_engine('sqlite:///SubscriptionDatabase.sqlite')
db = SQLAlchemy()
config = configparser.ConfigParser()
class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(15), unique=True, nullable = False)
    email = db.Column(db.String(50), unique=True)
    password = db.Column(db.String(80))
    subscribed = db.Column(db.Integer)
    status = db.Column(db.String(80))
    orderID = db.Column(db.String(100))
Users_tbl = Table('users', Users.metadata)
def create_users_table():
    Users.metadata.create_all(engine)
#create the table
create_users_table()
app = dash.Dash(__name__)
server = app.server
app.config.suppress_callback_exceptions = True
# server config
server.config.update(
    SECRET_KEY=os.urandom(12),
    SQLALCHEMY_DATABASE_URI='sqlite:///SubscriptionDatabase.sqlite',
    SQLALCHEMY_TRACK_MODIFICATIONS=False
)
db.init_app(server)
# Setup the LoginManager for the server
login_manager = LoginManager()
login_manager.init_app(server)
login_manager.login_view = '/login'
#User as base
# Create User class with UserMixin
class Users(UserMixin, Users):
    pass
app.layout= html.Div([
            html.Div(id='page-content', className='content')
            ,  dcc.Location(id='url', refresh=False)
        ])
# callback to reload the user object
@login_manager.user_loader
def load_user(user_id):
    return Users.query.get(int(user_id))
@app.callback(
    Output('page-content', 'children')
    , [Input('url', 'pathname')])
def display_page(pathname):
    if pathname == '/':
        return create_user()
    elif pathname == '/login':
        return user_login()
    elif pathname == '/success':
        if current_user.is_authenticated:
            cuid = current_user.get_id()
            conn = sqlite3.connect('SubscriptionDatabase.sqlite')
            c = conn.cursor()
            c.execute(f"select subscribed from users where id = {cuid}")
            sub = c.fetchone()
if sub[0] ==1:
                return login_success_sub()
            else:
                return login_success2()
        else:
            return failed_login()
    elif pathname =='/data':
        if current_user.is_authenticated:
            return data_page()
    elif pathname == '/logout':
        if current_user.is_authenticated:
            logout_user()
            return logout_page()
        else:
            return logout_page()
    else:
        return '404'
#set the callback for the dropdown interactivity
@app.callback(
   [Output('container-button-basic', "children")]
    , [Input('submit-val', 'n_clicks')]
    , [State('username', 'value')
    , State('password', 'value')
    , State('email', 'value')])
def insert_users(n_clicks, un, pw, em):
    hashed_password = generate_password_hash(pw, method='sha256')
    if un is not None and pw is not None and em is not None:
        ins = Users_tbl.insert().values(username=un
                ,  password=hashed_password
                , email=em,)
        conn = engine.connect()
        conn.execute(ins)
        conn.close()
        return [user_login()]
    else:
        return [html.Div([html.H2('Already have a user account?')
                , dcc.Link('Click here to Log In', href='/login')])]
@app.callback(
    Output('url_login', 'pathname')
    , [Input('login-button', 'n_clicks')]
    , [State('uname-box', 'value')
    , State('pwd-box', 'value')])
def successful(n_clicks, username, password):
    conn = sqlite3.connect('SubscriptionDatabase.sqlite')
    c = conn.cursor()
    environment = SandboxEnvironment(client_id=client_id, client_secret=client_secret)
    client = PayPalHttpClient(environment)   
    user = Users.query.filter_by(username=username).first()
    
    if user:
        if check_password_hash(user.password, password):
            login_user(user)
            cuid = current_user.get_id()
            c.execute(f"select orderID from users where id = {cuid}")
            oid = c.fetchone()
            
            if oid[0] is not None:
                act = SubscriptionActivate(oid[0])
                response = client.execute(act)
              
                if response.result.status == 'ACTIVE':
                    upd = update(Users).where(Users.id == cuid).values(subscribed=1, status= 'ACTIVE')
                    conn = engine.connect()
                    conn.execute(upd)
                    conn.close()
                else:
                    upd = update(Users).where(Users.id == cuid).values(subscribed=0)
                    conn = engine.connect()
                    conn.execute(upd)
                    conn.close()   
                return '/success'
            else:
                upd = update(Users).where(Users.id == cuid).values(subscribed=0)
                conn = engine.connect()
                conn.execute(upd)
                conn.close()
            return '/success'
        else:
            pass
    else:
        pass
@app.callback(
    Output('output-state', 'children')
    , [Input('login-button', 'n_clicks')]
    , [State('uname-box', 'value'), State('pwd-box', 'value')])
def update_output(n_clicks, input1, input2):
    if n_clicks > 0:
        user = Users.query.filter_by(username=input1).first()
        if user:
            if check_password_hash(user.password, input2):
                return ''
            else:
                return 'Incorrect username or password'
        else:
            return 'Incorrect username or password'
    else:
        return ''
@app.callback(
    Output('url_login_success', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
@app.callback(
    Output('url_login_success2', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
@app.callback(
    Output('url_login_df', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
# Create callbacks
@app.callback(
    Output('url_logout', 'pathname')
    , [Input('back-button', 'n_clicks')])
def logout_dashboard(n_clicks):
    if n_clicks > 0:
        return '/'
@app.callback(
    Output("iframe-div", "children"),
    [Input('sub-button', 'n_clicks')]
)
def sub(n):
    environment = SandboxEnvironment(client_id=client_id, client_secret=client_secret)
    client = PayPalHttpClient(environment)
    
    cuid = current_user.get_id()
    print(current_user.get_id())
    sub = SubscriptionRequest()
sub.prefer('return=representation')
sub.request_body({
      "plan_id": plan_id,
      "application_context": {
        "brand_name": "your_brand",
        "locale": "en-US",
        "shipping_preference": "SET_PROVIDED_ADDRESS",
        "user_action": "SUBSCRIBE_NOW",
        "payment_method": {
          "payer_selected": "PAYPAL",
          "payee_preferred": "IMMEDIATE_PAYMENT_REQUIRED"
        },
        "return_url": "http://127.0.0.1:9000/login",
        "cancel_url": "http://127.0.0.1:9000/login"
      }
    })
    if n is None:
        n = 0
        alink = ''
    if n > 0:
        response = client.execute(sub)
        subid = response.result.id
        alink = response.result.links[0].href
        status = response.result.status
        upd = update(Users).where(Users.id == cuid).values(orderID=subid, status= status)
        
        conn = engine.connect()
        conn.execute(upd)
        conn.close()
return [html.Iframe(src=alink)]
if __name__ == '__main__':
    app.run_server(port = 9000, debug=True)

Теперь, когда все файлы готовы, войдите в консоль и выполните следующее:

$ python app.py

Отобразится экран Создать учетную запись пользователя, и вы сможете продолжить рабочий процесс.

Когда будет предложено создать подписку, убедитесь, что вы используете доступную учетную запись пользователя песочницы вместо личной.

Поздравляю! Вы только что реализовали API подписки PayPal в приложении Dash.

Заключительные мысли и полный код

Работа с API PayPal временами была сложной, но после создания дополнительных классов Python для API подписки все сошлось. В этой статье рассматривается простая реализация API подписки PayPal, и она может не подходить для всех вариантов использования. Хотя я не рассматриваю это в этой статье, перед тем, как начать работу, я рекомендую проверить библиотеку Flask-Security-Too, чтобы понять, как реализовать функцию сброса пароля для ваших пользователей.

Если вам нужен полный код, посмотрите репозиторий Github здесь. Просто добавьте файл config.py и запустите его для проверки:



Благодарю вас!

- Эрик Клеппен