mirror of
https://github.com/simple-login/app.git
synced 2025-02-20 22:02:54 +08:00
Add POST /apple/process_payment
This commit is contained in:
parent
85fd4412ba
commit
1bba38edb6
5 changed files with 323 additions and 1 deletions
|
@ -1097,6 +1097,14 @@ If success, 200.
|
|||
}
|
||||
```
|
||||
|
||||
#### POST /apple/process_payment
|
||||
|
||||
Process payment receipt
|
||||
|
||||
Input:
|
||||
- `Authentication` header that contains the api key
|
||||
- `receipt_data` the receipt_data base64Encoded returned by StoreKit, i.e. `rawReceiptData.base64EncodedString`
|
||||
|
||||
### Database migration
|
||||
|
||||
The database migration is handled by `alembic`
|
||||
|
|
|
@ -6,4 +6,5 @@ from .views import (
|
|||
auth,
|
||||
auth_mfa,
|
||||
alias,
|
||||
apple,
|
||||
)
|
||||
|
|
286
app/api/views/apple.py
Normal file
286
app/api/views/apple.py
Normal file
|
@ -0,0 +1,286 @@
|
|||
from typing import Optional
|
||||
|
||||
import arrow
|
||||
from flask import g
|
||||
from flask import jsonify
|
||||
from flask import request
|
||||
from flask_cors import cross_origin
|
||||
import requests
|
||||
|
||||
from app.api.base import api_bp, verify_api_key
|
||||
from app.api.serializer import (
|
||||
AliasInfo,
|
||||
serialize_alias_info,
|
||||
get_alias_infos_with_pagination,
|
||||
)
|
||||
from app.config import APPLE_API_SECRET
|
||||
from app.log import LOG
|
||||
from app.models import PlanEnum, AppleSubscription
|
||||
|
||||
_MONTHLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.monthly"
|
||||
_YEARLY_PRODUCT_ID = "io.simplelogin.ios_app.subscription.premium.yearly"
|
||||
|
||||
# Apple API URL
|
||||
_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"
|
||||
_PROD_URL = "https://buy.itunes.apple.com/verifyReceipt"
|
||||
|
||||
|
||||
@api_bp.route("/apple/process_payment", methods=["POST"])
|
||||
@cross_origin()
|
||||
@verify_api_key
|
||||
def apple_process_payment():
|
||||
"""
|
||||
Process payment
|
||||
Input:
|
||||
receipt_data: in body
|
||||
Output:
|
||||
200 of the payment is successful, i.e. user is upgraded to premium
|
||||
|
||||
"""
|
||||
user = g.user
|
||||
receipt_data = request.get_json().get("receipt_data")
|
||||
|
||||
apple_sub = verify_receipt(receipt_data, user)
|
||||
if apple_sub:
|
||||
return jsonify(ok=True), 200
|
||||
|
||||
return jsonify(ok=False), 400
|
||||
|
||||
|
||||
def verify_receipt(receipt_data, user) -> Optional[AppleSubscription]:
|
||||
"""Call verifyReceipt endpoint and create/update AppleSubscription table
|
||||
Call the production URL for verifyReceipt first,
|
||||
and proceed to verify with the sandbox URL if receive a 21007 status code.
|
||||
|
||||
Return AppleSubscription object if success
|
||||
|
||||
https://developer.apple.com/documentation/appstorereceipts/verifyreceipt
|
||||
"""
|
||||
r = requests.post(
|
||||
_PROD_URL, json={"receipt-data": receipt_data, "password": APPLE_API_SECRET}
|
||||
)
|
||||
|
||||
if r.json() == {"status": 21007}:
|
||||
# try sandbox_url
|
||||
LOG.warning("Use the sandbox url instead")
|
||||
r = requests.post(
|
||||
_SANDBOX_URL,
|
||||
json={"receipt-data": receipt_data, "password": APPLE_API_SECRET},
|
||||
)
|
||||
|
||||
data = r.json()
|
||||
LOG.d("response from Apple %s", data)
|
||||
# data has the following format
|
||||
# {
|
||||
# "status": 0,
|
||||
# "environment": "Sandbox",
|
||||
# "receipt": {
|
||||
# "receipt_type": "ProductionSandbox",
|
||||
# "adam_id": 0,
|
||||
# "app_item_id": 0,
|
||||
# "bundle_id": "io.simplelogin.ios-app",
|
||||
# "application_version": "2",
|
||||
# "download_id": 0,
|
||||
# "version_external_identifier": 0,
|
||||
# "receipt_creation_date": "2020-04-18 16:36:34 Etc/GMT",
|
||||
# "receipt_creation_date_ms": "1587227794000",
|
||||
# "receipt_creation_date_pst": "2020-04-18 09:36:34 America/Los_Angeles",
|
||||
# "request_date": "2020-04-18 16:46:36 Etc/GMT",
|
||||
# "request_date_ms": "1587228396496",
|
||||
# "request_date_pst": "2020-04-18 09:46:36 America/Los_Angeles",
|
||||
# "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
|
||||
# "original_purchase_date_ms": "1375340400000",
|
||||
# "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
|
||||
# "original_application_version": "1.0",
|
||||
# "in_app": [
|
||||
# {
|
||||
# "quantity": "1",
|
||||
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "transaction_id": "1000000653584474",
|
||||
# "original_transaction_id": "1000000653584474",
|
||||
# "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
|
||||
# "purchase_date_ms": "1587227262000",
|
||||
# "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
|
||||
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
|
||||
# "original_purchase_date_ms": "1587227264000",
|
||||
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
|
||||
# "expires_date": "2020-04-18 16:32:42 Etc/GMT",
|
||||
# "expires_date_ms": "1587227562000",
|
||||
# "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
|
||||
# "web_order_line_item_id": "1000000051847459",
|
||||
# "is_trial_period": "false",
|
||||
# "is_in_intro_offer_period": "false",
|
||||
# },
|
||||
# {
|
||||
# "quantity": "1",
|
||||
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "transaction_id": "1000000653584861",
|
||||
# "original_transaction_id": "1000000653584474",
|
||||
# "purchase_date": "2020-04-18 16:32:42 Etc/GMT",
|
||||
# "purchase_date_ms": "1587227562000",
|
||||
# "purchase_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
|
||||
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
|
||||
# "original_purchase_date_ms": "1587227264000",
|
||||
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
|
||||
# "expires_date": "2020-04-18 16:37:42 Etc/GMT",
|
||||
# "expires_date_ms": "1587227862000",
|
||||
# "expires_date_pst": "2020-04-18 09:37:42 America/Los_Angeles",
|
||||
# "web_order_line_item_id": "1000000051847461",
|
||||
# "is_trial_period": "false",
|
||||
# "is_in_intro_offer_period": "false",
|
||||
# },
|
||||
# ],
|
||||
# },
|
||||
# "latest_receipt_info": [
|
||||
# {
|
||||
# "quantity": "1",
|
||||
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "transaction_id": "1000000653584474",
|
||||
# "original_transaction_id": "1000000653584474",
|
||||
# "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
|
||||
# "purchase_date_ms": "1587227262000",
|
||||
# "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
|
||||
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
|
||||
# "original_purchase_date_ms": "1587227264000",
|
||||
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
|
||||
# "expires_date": "2020-04-18 16:32:42 Etc/GMT",
|
||||
# "expires_date_ms": "1587227562000",
|
||||
# "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
|
||||
# "web_order_line_item_id": "1000000051847459",
|
||||
# "is_trial_period": "false",
|
||||
# "is_in_intro_offer_period": "false",
|
||||
# "subscription_group_identifier": "20624274",
|
||||
# },
|
||||
# {
|
||||
# "quantity": "1",
|
||||
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "transaction_id": "1000000653584861",
|
||||
# "original_transaction_id": "1000000653584474",
|
||||
# "purchase_date": "2020-04-18 16:32:42 Etc/GMT",
|
||||
# "purchase_date_ms": "1587227562000",
|
||||
# "purchase_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
|
||||
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
|
||||
# "original_purchase_date_ms": "1587227264000",
|
||||
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
|
||||
# "expires_date": "2020-04-18 16:37:42 Etc/GMT",
|
||||
# "expires_date_ms": "1587227862000",
|
||||
# "expires_date_pst": "2020-04-18 09:37:42 America/Los_Angeles",
|
||||
# "web_order_line_item_id": "1000000051847461",
|
||||
# "is_trial_period": "false",
|
||||
# "is_in_intro_offer_period": "false",
|
||||
# "subscription_group_identifier": "20624274",
|
||||
# },
|
||||
# {
|
||||
# "quantity": "1",
|
||||
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "transaction_id": "1000000653585235",
|
||||
# "original_transaction_id": "1000000653584474",
|
||||
# "purchase_date": "2020-04-18 16:38:16 Etc/GMT",
|
||||
# "purchase_date_ms": "1587227896000",
|
||||
# "purchase_date_pst": "2020-04-18 09:38:16 America/Los_Angeles",
|
||||
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
|
||||
# "original_purchase_date_ms": "1587227264000",
|
||||
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
|
||||
# "expires_date": "2020-04-18 16:43:16 Etc/GMT",
|
||||
# "expires_date_ms": "1587228196000",
|
||||
# "expires_date_pst": "2020-04-18 09:43:16 America/Los_Angeles",
|
||||
# "web_order_line_item_id": "1000000051847500",
|
||||
# "is_trial_period": "false",
|
||||
# "is_in_intro_offer_period": "false",
|
||||
# "subscription_group_identifier": "20624274",
|
||||
# },
|
||||
# {
|
||||
# "quantity": "1",
|
||||
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "transaction_id": "1000000653585760",
|
||||
# "original_transaction_id": "1000000653584474",
|
||||
# "purchase_date": "2020-04-18 16:44:25 Etc/GMT",
|
||||
# "purchase_date_ms": "1587228265000",
|
||||
# "purchase_date_pst": "2020-04-18 09:44:25 America/Los_Angeles",
|
||||
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
|
||||
# "original_purchase_date_ms": "1587227264000",
|
||||
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
|
||||
# "expires_date": "2020-04-18 16:49:25 Etc/GMT",
|
||||
# "expires_date_ms": "1587228565000",
|
||||
# "expires_date_pst": "2020-04-18 09:49:25 America/Los_Angeles",
|
||||
# "web_order_line_item_id": "1000000051847566",
|
||||
# "is_trial_period": "false",
|
||||
# "is_in_intro_offer_period": "false",
|
||||
# "subscription_group_identifier": "20624274",
|
||||
# },
|
||||
# ],
|
||||
# "latest_receipt": "very long string",
|
||||
# "pending_renewal_info": [
|
||||
# {
|
||||
# "auto_renew_product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "original_transaction_id": "1000000653584474",
|
||||
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "auto_renew_status": "1",
|
||||
# }
|
||||
# ],
|
||||
# }
|
||||
|
||||
if data["status"] != 0:
|
||||
LOG.error(
|
||||
"verifyReceipt status !=0, probably invalid receipt. User %s", user,
|
||||
)
|
||||
return None
|
||||
|
||||
# each item in data["receipt"]["in_app"] has the following format
|
||||
# {
|
||||
# "quantity": "1",
|
||||
# "product_id": "io.simplelogin.ios_app.subscription.premium.monthly",
|
||||
# "transaction_id": "1000000653584474",
|
||||
# "original_transaction_id": "1000000653584474",
|
||||
# "purchase_date": "2020-04-18 16:27:42 Etc/GMT",
|
||||
# "purchase_date_ms": "1587227262000",
|
||||
# "purchase_date_pst": "2020-04-18 09:27:42 America/Los_Angeles",
|
||||
# "original_purchase_date": "2020-04-18 16:27:44 Etc/GMT",
|
||||
# "original_purchase_date_ms": "1587227264000",
|
||||
# "original_purchase_date_pst": "2020-04-18 09:27:44 America/Los_Angeles",
|
||||
# "expires_date": "2020-04-18 16:32:42 Etc/GMT",
|
||||
# "expires_date_ms": "1587227562000",
|
||||
# "expires_date_pst": "2020-04-18 09:32:42 America/Los_Angeles",
|
||||
# "web_order_line_item_id": "1000000051847459",
|
||||
# "is_trial_period": "false",
|
||||
# "is_in_intro_offer_period": "false",
|
||||
# }
|
||||
transactions = data["receipt"]["in_app"]
|
||||
latest_transaction = max(transactions, key=lambda t: int(t["expires_date_ms"]))
|
||||
original_transaction_id = latest_transaction["original_transaction_id"]
|
||||
expires_date = arrow.get(int(latest_transaction["expires_date_ms"]) / 1000)
|
||||
plan = (
|
||||
PlanEnum.monthly
|
||||
if latest_transaction["product_id"] == _MONTHLY_PRODUCT_ID
|
||||
else PlanEnum.yearly
|
||||
)
|
||||
|
||||
apple_sub: AppleSubscription = AppleSubscription.get_by(user_id=user.id)
|
||||
|
||||
if apple_sub:
|
||||
LOG.d(
|
||||
"Create new AppleSubscription for user %s, expired at %s, plan %s",
|
||||
user,
|
||||
expires_date,
|
||||
plan,
|
||||
)
|
||||
apple_sub.receipt_data = receipt_data
|
||||
apple_sub.expires_date = expires_date
|
||||
apple_sub.original_transaction_id = original_transaction_id
|
||||
apple_sub.plan = plan
|
||||
else:
|
||||
LOG.d(
|
||||
"Create new AppleSubscription for user %s, expired at %s, plan %s",
|
||||
user,
|
||||
expires_date,
|
||||
plan,
|
||||
)
|
||||
apple_sub = AppleSubscription.create(
|
||||
user_id=user.id,
|
||||
receipt_data=receipt_data,
|
||||
expires_date=expires_date,
|
||||
original_transaction_id=original_transaction_id,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
return apple_sub
|
|
@ -245,4 +245,4 @@ with open(get_abs_path(DISPOSABLE_FILE_PATH), "r") as f:
|
|||
]
|
||||
|
||||
# Used when querying info on Apple API
|
||||
APPLE_API_SECRET = os.environ.get("APPLE_API_SECRET")
|
||||
APPLE_API_SECRET = os.environ.get("APPLE_API_SECRET")
|
||||
|
|
27
tests/api/test_apple.py
Normal file
27
tests/api/test_apple.py
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue