commit
18e5dffcd7
12
README.md
12
README.md
|
@ -1097,6 +1097,18 @@ If success, 200.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### POST /apple/process_payment
|
||||||
|
|
||||||
|
Process payment receipt
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- `Authentication` in header: the api key
|
||||||
|
- `receipt_data` in body: the receipt_data base64Encoded returned by StoreKit, i.e. `rawReceiptData.base64EncodedString`
|
||||||
|
|
||||||
|
Output:
|
||||||
|
200 if user is upgraded successfully
|
||||||
|
4** if any error.
|
||||||
|
|
||||||
### Database migration
|
### Database migration
|
||||||
|
|
||||||
The database migration is handled by `alembic`
|
The database migration is handled by `alembic`
|
||||||
|
|
|
@ -6,4 +6,5 @@ from .views import (
|
||||||
auth,
|
auth,
|
||||||
auth_mfa,
|
auth_mfa,
|
||||||
alias,
|
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
|
|
@ -243,3 +243,6 @@ with open(get_abs_path(DISPOSABLE_FILE_PATH), "r") as f:
|
||||||
DISPOSABLE_EMAIL_DOMAINS = [
|
DISPOSABLE_EMAIL_DOMAINS = [
|
||||||
d for d in DISPOSABLE_EMAIL_DOMAINS if not d.startswith("#")
|
d for d in DISPOSABLE_EMAIL_DOMAINS if not d.startswith("#")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Used when querying info on Apple API
|
||||||
|
APPLE_API_SECRET = os.environ.get("APPLE_API_SECRET")
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
{% if sub.cancelled %}
|
{% if sub.cancelled %}
|
||||||
<p>
|
<p>
|
||||||
You are on the <b>{{ sub.plan_name() }}</b> plan. <br>
|
You are on the <b>{{ sub.plan_name() }}</b> plan. <br>
|
||||||
You have canceled your subscription and it will end on {{ current_user.next_bill_date() }}
|
You have canceled your subscription and it will end on {{ sub.next_bill_date.strftime("%Y-%m-%d") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
|
@ -57,9 +57,10 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if current_user.is_cancel() %}
|
{% set sub = current_user.get_subscription() %}
|
||||||
|
{% if sub and sub.cancelled %}
|
||||||
<div class="alert alert-primary" role="alert">
|
<div class="alert alert-primary" role="alert">
|
||||||
You have an active subscription until {{current_user.next_bill_date()}}. <br>
|
You have an active subscription until {{ sub.next_bill_date.strftime("%Y-%m-%d") }}. <br>
|
||||||
Please note that if you re-subscribe now, this will be a completely
|
Please note that if you re-subscribe now, this will be a completely
|
||||||
new subscription and
|
new subscription and
|
||||||
your payment method will be charged <b>immediately</b>.
|
your payment method will be charged <b>immediately</b>.
|
||||||
|
|
|
@ -217,6 +217,10 @@ class User(db.Model, ModelMixin, UserMixin):
|
||||||
if sub:
|
if sub:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
apple_sub: AppleSubscription = AppleSubscription.get_by(user_id=self.id)
|
||||||
|
if apple_sub and apple_sub.is_valid():
|
||||||
|
return True
|
||||||
|
|
||||||
manual_sub: ManualSubscription = ManualSubscription.get_by(user_id=self.id)
|
manual_sub: ManualSubscription = ManualSubscription.get_by(user_id=self.id)
|
||||||
if manual_sub and manual_sub.end_at > arrow.now():
|
if manual_sub and manual_sub.end_at > arrow.now():
|
||||||
return True
|
return True
|
||||||
|
@ -251,6 +255,10 @@ class User(db.Model, ModelMixin, UserMixin):
|
||||||
if sub and not sub.cancelled:
|
if sub and not sub.cancelled:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
apple_sub: AppleSubscription = AppleSubscription.get_by(user_id=self.id)
|
||||||
|
if apple_sub and apple_sub.is_valid():
|
||||||
|
return False
|
||||||
|
|
||||||
manual_sub: ManualSubscription = ManualSubscription.get_by(user_id=self.id)
|
manual_sub: ManualSubscription = ManualSubscription.get_by(user_id=self.id)
|
||||||
# user who has giveaway premium can decide to upgrade
|
# user who has giveaway premium can decide to upgrade
|
||||||
if (
|
if (
|
||||||
|
@ -262,25 +270,6 @@ class User(db.Model, ModelMixin, UserMixin):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def next_bill_date(self) -> str:
|
|
||||||
sub: Subscription = self.get_subscription()
|
|
||||||
if sub:
|
|
||||||
return sub.next_bill_date.strftime("%Y-%m-%d")
|
|
||||||
|
|
||||||
LOG.error(
|
|
||||||
f"next_bill_date() should be called only on user with active subscription. User {self}"
|
|
||||||
)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def is_cancel(self) -> bool:
|
|
||||||
"""User has canceled their subscription but the subscription is still active,
|
|
||||||
i.e. next_bill_date > now"""
|
|
||||||
sub: Subscription = self.get_subscription()
|
|
||||||
if sub and sub.cancelled:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_premium(self) -> bool:
|
def is_premium(self) -> bool:
|
||||||
"""
|
"""
|
||||||
user is premium if they:
|
user is premium if they:
|
||||||
|
@ -940,6 +929,29 @@ class ManualSubscription(db.Model, ModelMixin):
|
||||||
user = db.relationship(User)
|
user = db.relationship(User)
|
||||||
|
|
||||||
|
|
||||||
|
class AppleSubscription(db.Model, ModelMixin):
|
||||||
|
"""
|
||||||
|
For users who have subscribed via Apple in-app payment
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_id = db.Column(
|
||||||
|
db.ForeignKey(User.id, ondelete="cascade"), nullable=False, unique=True
|
||||||
|
)
|
||||||
|
|
||||||
|
expires_date = db.Column(ArrowType, nullable=False)
|
||||||
|
|
||||||
|
original_transaction_id = db.Column(db.String(256), nullable=False)
|
||||||
|
receipt_data = db.Column(db.Text(), nullable=False)
|
||||||
|
|
||||||
|
plan = db.Column(db.Enum(PlanEnum), nullable=False)
|
||||||
|
|
||||||
|
user = db.relationship(User)
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
# Todo: take into account grace period?
|
||||||
|
return self.expires_date > arrow.now()
|
||||||
|
|
||||||
|
|
||||||
class DeletedAlias(db.Model, ModelMixin):
|
class DeletedAlias(db.Model, ModelMixin):
|
||||||
"""Store all deleted alias to make sure they are NOT reused"""
|
"""Store all deleted alias to make sure they are NOT reused"""
|
||||||
|
|
||||||
|
|
28
cron.py
28
cron.py
|
@ -3,6 +3,7 @@ import argparse
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
from app import s3
|
from app import s3
|
||||||
|
from app.api.views.apple import verify_receipt
|
||||||
from app.config import IGNORED_EMAILS, ADMIN_EMAIL
|
from app.config import IGNORED_EMAILS, ADMIN_EMAIL
|
||||||
from app.email_utils import send_email, send_trial_end_soon_email, render
|
from app.email_utils import send_email, send_trial_end_soon_email, render
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
@ -17,6 +18,7 @@ from app.models import (
|
||||||
Client,
|
Client,
|
||||||
ManualSubscription,
|
ManualSubscription,
|
||||||
RefusedEmail,
|
RefusedEmail,
|
||||||
|
AppleSubscription,
|
||||||
)
|
)
|
||||||
from server import create_app
|
from server import create_app
|
||||||
|
|
||||||
|
@ -63,8 +65,16 @@ def notify_premium_end():
|
||||||
send_email(
|
send_email(
|
||||||
user.email,
|
user.email,
|
||||||
f"Your subscription will end soon {user.name}",
|
f"Your subscription will end soon {user.name}",
|
||||||
render("transactional/subscription-end.txt", user=user),
|
render(
|
||||||
render("transactional/subscription-end.html", user=user),
|
"transactional/subscription-end.txt",
|
||||||
|
user=user,
|
||||||
|
next_bill_date=sub.next_bill_date.strftime("%Y-%m-%d"),
|
||||||
|
),
|
||||||
|
render(
|
||||||
|
"transactional/subscription-end.html",
|
||||||
|
user=user,
|
||||||
|
next_bill_date=sub.next_bill_date.strftime("%Y-%m-%d"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,6 +107,16 @@ def notify_manual_sub_end():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def poll_apple_subscription():
|
||||||
|
"""Poll Apple API to update AppleSubscription"""
|
||||||
|
# todo: only near the end of the subscription
|
||||||
|
for apple_sub in AppleSubscription.query.all():
|
||||||
|
user = apple_sub.user
|
||||||
|
verify_receipt(apple_sub.receipt_data, user)
|
||||||
|
|
||||||
|
LOG.d("Finish poll_apple_subscription")
|
||||||
|
|
||||||
|
|
||||||
def stats():
|
def stats():
|
||||||
"""send admin stats everyday"""
|
"""send admin stats everyday"""
|
||||||
if not ADMIN_EMAIL:
|
if not ADMIN_EMAIL:
|
||||||
|
@ -198,6 +218,7 @@ if __name__ == "__main__":
|
||||||
"notify_manual_subscription_end",
|
"notify_manual_subscription_end",
|
||||||
"notify_premium_end",
|
"notify_premium_end",
|
||||||
"delete_refused_emails",
|
"delete_refused_emails",
|
||||||
|
"poll_apple_subscription",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -220,3 +241,6 @@ if __name__ == "__main__":
|
||||||
elif args.job == "delete_refused_emails":
|
elif args.job == "delete_refused_emails":
|
||||||
LOG.d("Deleted refused emails")
|
LOG.d("Deleted refused emails")
|
||||||
delete_refused_emails()
|
delete_refused_emails()
|
||||||
|
elif args.job == "poll_apple_subscription":
|
||||||
|
LOG.d("Poll Apple Subscriptions")
|
||||||
|
poll_apple_subscription()
|
||||||
|
|
|
@ -28,3 +28,9 @@ jobs:
|
||||||
shell: /bin/bash
|
shell: /bin/bash
|
||||||
schedule: "0 11 * * *"
|
schedule: "0 11 * * *"
|
||||||
captureStderr: true
|
captureStderr: true
|
||||||
|
|
||||||
|
- name: SimpleLogin Poll Apple Subscriptions
|
||||||
|
command: python /code/cron.py -j poll_apple_subscription
|
||||||
|
shell: /bin/bash
|
||||||
|
schedule: "0 12 * * *"
|
||||||
|
captureStderr: true
|
||||||
|
|
|
@ -123,3 +123,6 @@ FACEBOOK_CLIENT_SECRET=to_fill
|
||||||
|
|
||||||
# The landing page
|
# The landing page
|
||||||
# LANDING_PAGE_URL=https://simplelogin.io
|
# LANDING_PAGE_URL=https://simplelogin.io
|
||||||
|
|
||||||
|
# Used when querying info on Apple API
|
||||||
|
# APPLE_API_SECRET=secret
|
47
migrations/versions/2020_041911_dd911f880b75_.py
Normal file
47
migrations/versions/2020_041911_dd911f880b75_.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: dd911f880b75
|
||||||
|
Revises: 57ef03f3ac34
|
||||||
|
Create Date: 2020-04-19 11:14:19.929910
|
||||||
|
|
||||||
|
"""
|
||||||
|
import sqlalchemy_utils
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'dd911f880b75'
|
||||||
|
down_revision = '57ef03f3ac34'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('apple_subscription',
|
||||||
|
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||||
|
sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
|
||||||
|
sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('expires_date', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
|
||||||
|
sa.Column('original_transaction_id', sa.String(length=256), nullable=False),
|
||||||
|
sa.Column('receipt_data', sa.Text(), nullable=False),
|
||||||
|
sa.Column('plan', sa.Enum('monthly', 'yearly', name='planenum'), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('user_id')
|
||||||
|
)
|
||||||
|
op.alter_column('file', 'user_id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('file', 'user_id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
nullable=False)
|
||||||
|
op.drop_table('apple_subscription')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -7,7 +7,7 @@
|
||||||
{{ render_text("Hi,") }}
|
{{ render_text("Hi,") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ render_text("Your subscription will end on " + user.next_bill_date() + ".") }}
|
{{ render_text("Your subscription will end on " + next_bill_date + ".") }}
|
||||||
|
|
||||||
{{ render_text("When the subscription ends:") }}
|
{{ render_text("When the subscription ends:") }}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Hi {{user.name}}
|
Hi {{user.name}}
|
||||||
|
|
||||||
Your subscription will end on {{ user.next_bill_date() }}.
|
Your subscription will end on {{ next_bill_date }}.
|
||||||
|
|
||||||
When the subscription ends:
|
When the subscription ends:
|
||||||
|
|
||||||
|
|
|
@ -28,9 +28,11 @@
|
||||||
{% if current_user.in_trial() %}
|
{% if current_user.in_trial() %}
|
||||||
<small class="text-success d-block mt-1">Trial ends {{ current_user.trial_end|dt }}</small>
|
<small class="text-success d-block mt-1">Trial ends {{ current_user.trial_end|dt }}</small>
|
||||||
{% elif current_user.is_premium() %}
|
{% elif current_user.is_premium() %}
|
||||||
|
|
||||||
<small class="text-success d-block mt-1">Premium
|
<small class="text-success d-block mt-1">Premium
|
||||||
{% if current_user.is_cancel() %}
|
{% set sub = current_user.get_subscription() %}
|
||||||
until {{ current_user.next_bill_date() }}
|
{% if sub and sub.cancelled %}
|
||||||
|
until {{ sub.next_bill_date.strftime("%Y-%m-%d") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</small>
|
</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
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