From 7b965e4121f6f4ddb536635db9d54f16842bc126 Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sat, 18 Apr 2020 20:47:11 +0200
Subject: [PATCH 01/10] Add APPLE_API_SECRET param
---
app/config.py | 3 +++
example.env | 5 ++++-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/app/config.py b/app/config.py
index 7d8d8f88..7b7c31e1 100644
--- a/app/config.py
+++ b/app/config.py
@@ -243,3 +243,6 @@ with open(get_abs_path(DISPOSABLE_FILE_PATH), "r") as f:
DISPOSABLE_EMAIL_DOMAINS = [
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")
\ No newline at end of file
diff --git a/example.env b/example.env
index a2747523..f4265bdb 100644
--- a/example.env
+++ b/example.env
@@ -122,4 +122,7 @@ FACEBOOK_CLIENT_SECRET=to_fill
# LOCAL_FILE_UPLOAD=true
# The landing page
-# LANDING_PAGE_URL=https://simplelogin.io
\ No newline at end of file
+# LANDING_PAGE_URL=https://simplelogin.io
+
+# Used when querying info on Apple API
+# APPLE_API_SECRET=secret
\ No newline at end of file
From b0118e615a6d22d4ce3ccc396d34cccad327f0b6 Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sat, 18 Apr 2020 20:47:33 +0200
Subject: [PATCH 02/10] Add AppleSubscription model
---
app/models.py | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/app/models.py b/app/models.py
index 0456a352..dc5f3575 100644
--- a/app/models.py
+++ b/app/models.py
@@ -940,6 +940,25 @@ class ManualSubscription(db.Model, ModelMixin):
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)
+
+
class DeletedAlias(db.Model, ModelMixin):
"""Store all deleted alias to make sure they are NOT reused"""
From f7f1e7f35850f111f3478116b440085cc7a6f3ac Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sun, 19 Apr 2020 10:54:05 +0200
Subject: [PATCH 03/10] replace user.next_bill_date() by
sub.next_bill_date.strftime("%Y-%m-%d")
---
app/dashboard/templates/dashboard/billing.html | 2 +-
app/dashboard/templates/dashboard/pricing.html | 15 ++++++++-------
app/models.py | 10 ----------
cron.py | 12 ++++++++++--
.../emails/transactional/subscription-end.html | 2 +-
.../emails/transactional/subscription-end.txt | 2 +-
templates/header.html | 6 ++++--
7 files changed, 25 insertions(+), 24 deletions(-)
diff --git a/app/dashboard/templates/dashboard/billing.html b/app/dashboard/templates/dashboard/billing.html
index d2a74a17..6c6cbfb7 100644
--- a/app/dashboard/templates/dashboard/billing.html
+++ b/app/dashboard/templates/dashboard/billing.html
@@ -14,7 +14,7 @@
{% if sub.cancelled %}
You are on the {{ sub.plan_name() }} plan.
- 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") }}
diff --git a/app/dashboard/templates/dashboard/pricing.html b/app/dashboard/templates/dashboard/pricing.html
index 0ce5320c..fd50e965 100644
--- a/app/dashboard/templates/dashboard/pricing.html
+++ b/app/dashboard/templates/dashboard/pricing.html
@@ -57,13 +57,14 @@
- {% if current_user.is_cancel() %}
-
- You have an active subscription until {{current_user.next_bill_date()}}.
- Please note that if you re-subscribe now, this will be a completely
- new subscription and
- your payment method will be charged immediately.
-
+ {% set sub = current_user.get_subscription() %}
+ {% if sub and sub.cancelled %}
+
+ You have an active subscription until {{ sub.next_bill_date.strftime("%Y-%m-%d") }}.
+ Please note that if you re-subscribe now, this will be a completely
+ new subscription and
+ your payment method will be charged immediately.
+
{% endif %}
diff --git a/app/models.py b/app/models.py
index dc5f3575..05a850ab 100644
--- a/app/models.py
+++ b/app/models.py
@@ -262,16 +262,6 @@ class User(db.Model, ModelMixin, UserMixin):
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"""
diff --git a/cron.py b/cron.py
index 912d69a7..651092c6 100644
--- a/cron.py
+++ b/cron.py
@@ -63,8 +63,16 @@ def notify_premium_end():
send_email(
user.email,
f"Your subscription will end soon {user.name}",
- render("transactional/subscription-end.txt", user=user),
- render("transactional/subscription-end.html", user=user),
+ render(
+ "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"),
+ ),
)
diff --git a/templates/emails/transactional/subscription-end.html b/templates/emails/transactional/subscription-end.html
index 6d4d5a2b..dcb4536f 100644
--- a/templates/emails/transactional/subscription-end.html
+++ b/templates/emails/transactional/subscription-end.html
@@ -7,7 +7,7 @@
{{ render_text("Hi,") }}
{% 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:") }}
diff --git a/templates/emails/transactional/subscription-end.txt b/templates/emails/transactional/subscription-end.txt
index 32c6b881..b84c90fd 100644
--- a/templates/emails/transactional/subscription-end.txt
+++ b/templates/emails/transactional/subscription-end.txt
@@ -1,6 +1,6 @@
Hi {{user.name}}
-Your subscription will end on {{ user.next_bill_date() }}.
+Your subscription will end on {{ next_bill_date }}.
When the subscription ends:
diff --git a/templates/header.html b/templates/header.html
index 7161fe40..973c8860 100644
--- a/templates/header.html
+++ b/templates/header.html
@@ -28,9 +28,11 @@
{% if current_user.in_trial() %}
Trial ends {{ current_user.trial_end|dt }}
{% elif current_user.is_premium() %}
+
Premium
- {% if current_user.is_cancel() %}
- until {{ current_user.next_bill_date() }}
+ {% set sub = current_user.get_subscription() %}
+ {% if sub and sub.cancelled %}
+ until {{ sub.next_bill_date.strftime("%Y-%m-%d") }}
{% endif %}
{% endif %}
From 2a837f9213a7c897cb9507fa1f8db33fac17d39e Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sun, 19 Apr 2020 10:54:15 +0200
Subject: [PATCH 04/10] remove user.is_cancel()
---
app/models.py | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/app/models.py b/app/models.py
index 05a850ab..2018bd93 100644
--- a/app/models.py
+++ b/app/models.py
@@ -262,15 +262,6 @@ class User(db.Model, ModelMixin, UserMixin):
return True
- 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:
"""
user is premium if they:
From 85fd4412ba36e3cdcf5ae0b53041f38b1434e615 Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sun, 19 Apr 2020 10:58:32 +0200
Subject: [PATCH 05/10] take into account AppleSubscription in premium formula
---
app/models.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/app/models.py b/app/models.py
index 2018bd93..f821dbd4 100644
--- a/app/models.py
+++ b/app/models.py
@@ -217,6 +217,10 @@ class User(db.Model, ModelMixin, UserMixin):
if sub:
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)
if manual_sub and manual_sub.end_at > arrow.now():
return True
@@ -251,6 +255,10 @@ class User(db.Model, ModelMixin, UserMixin):
if sub and not sub.cancelled:
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)
# user who has giveaway premium can decide to upgrade
if (
@@ -939,6 +947,10 @@ class AppleSubscription(db.Model, ModelMixin):
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):
"""Store all deleted alias to make sure they are NOT reused"""
From 1bba38edb618396fef69f9f995108ba374783e8c Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sun, 19 Apr 2020 11:13:38 +0200
Subject: [PATCH 06/10] Add POST /apple/process_payment
---
README.md | 8 ++
app/api/__init__.py | 1 +
app/api/views/apple.py | 286 ++++++++++++++++++++++++++++++++++++++++
app/config.py | 2 +-
tests/api/test_apple.py | 27 ++++
5 files changed, 323 insertions(+), 1 deletion(-)
create mode 100644 app/api/views/apple.py
create mode 100644 tests/api/test_apple.py
diff --git a/README.md b/README.md
index 4aac854b..98a62f17 100644
--- a/README.md
+++ b/README.md
@@ -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`
diff --git a/app/api/__init__.py b/app/api/__init__.py
index 182ac59e..a272e69c 100644
--- a/app/api/__init__.py
+++ b/app/api/__init__.py
@@ -6,4 +6,5 @@ from .views import (
auth,
auth_mfa,
alias,
+ apple,
)
diff --git a/app/api/views/apple.py b/app/api/views/apple.py
new file mode 100644
index 00000000..ac36c024
--- /dev/null
+++ b/app/api/views/apple.py
@@ -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
diff --git a/app/config.py b/app/config.py
index 7b7c31e1..b0fbad06 100644
--- a/app/config.py
+++ b/app/config.py
@@ -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")
\ No newline at end of file
+APPLE_API_SECRET = os.environ.get("APPLE_API_SECRET")
diff --git a/tests/api/test_apple.py b/tests/api/test_apple.py
new file mode 100644
index 00000000..8bb17477
--- /dev/null
+++ b/tests/api/test_apple.py
@@ -0,0 +1,27 @@
+from flask import url_for
+
+from app.extensions import db
+from app.models import User, ApiKey
+
+
+def test_apple_process_payment(flask_client):
+ user = User.create(
+ email="a@b.c", password="password", name="Test User", activated=True
+ )
+ db.session.commit()
+
+ # create api_key
+ api_key = ApiKey.create(user.id, "for test")
+ db.session.commit()
+
+ receipt_data = """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"""
+
+ r = flask_client.post(
+ url_for("api.apple_process_payment"),
+ headers={"Authentication": api_key.code},
+ json={"receipt_data": receipt_data},
+ )
+
+ # will fail anyway as there's apple secret is not valid
+ assert r.status_code == 400
+ assert r.json == {"ok": False}
From 71d53d16dacb2c15d7cd6fa9792796e7dfa8062a Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sun, 19 Apr 2020 11:18:27 +0200
Subject: [PATCH 07/10] add poll_apple_subscription(), call it everyday
---
cron.py | 16 ++++++++++++++++
crontab.yml | 6 ++++++
2 files changed, 22 insertions(+)
diff --git a/cron.py b/cron.py
index 651092c6..c63d5c57 100644
--- a/cron.py
+++ b/cron.py
@@ -3,6 +3,7 @@ import argparse
import arrow
from app import s3
+from app.api.views.apple import verify_receipt
from app.config import IGNORED_EMAILS, ADMIN_EMAIL
from app.email_utils import send_email, send_trial_end_soon_email, render
from app.extensions import db
@@ -17,6 +18,7 @@ from app.models import (
Client,
ManualSubscription,
RefusedEmail,
+ AppleSubscription,
)
from server import create_app
@@ -105,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():
"""send admin stats everyday"""
if not ADMIN_EMAIL:
@@ -206,6 +218,7 @@ if __name__ == "__main__":
"notify_manual_subscription_end",
"notify_premium_end",
"delete_refused_emails",
+ "poll_apple_subscription"
],
)
args = parser.parse_args()
@@ -228,3 +241,6 @@ if __name__ == "__main__":
elif args.job == "delete_refused_emails":
LOG.d("Deleted refused emails")
delete_refused_emails()
+ elif args.job == "poll_apple_subscription":
+ LOG.d("Poll Apple Subscriptions")
+ poll_apple_subscription()
diff --git a/crontab.yml b/crontab.yml
index 0c453757..fc61a44f 100644
--- a/crontab.yml
+++ b/crontab.yml
@@ -28,3 +28,9 @@ jobs:
shell: /bin/bash
schedule: "0 11 * * *"
captureStderr: true
+
+ - name: SimpleLogin Poll Apple Subscriptions
+ command: python /code/cron.py -j poll_apple_subscription
+ shell: /bin/bash
+ schedule: "0 12 * * *"
+ captureStderr: true
From 11772d35e1ac59306a1b3ca01dcd2bb235a04d85 Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sun, 19 Apr 2020 11:18:42 +0200
Subject: [PATCH 08/10] Add sql migration
---
.../versions/2020_041911_dd911f880b75_.py | 47 +++++++++++++++++++
1 file changed, 47 insertions(+)
create mode 100644 migrations/versions/2020_041911_dd911f880b75_.py
diff --git a/migrations/versions/2020_041911_dd911f880b75_.py b/migrations/versions/2020_041911_dd911f880b75_.py
new file mode 100644
index 00000000..674b71e6
--- /dev/null
+++ b/migrations/versions/2020_041911_dd911f880b75_.py
@@ -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 ###
From b33ec7d025b3065238a1e093096b83221a73035c Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sun, 19 Apr 2020 11:20:44 +0200
Subject: [PATCH 09/10] fix reformatting
---
README.md | 4 ++--
cron.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 98a62f17..8f160437 100644
--- a/README.md
+++ b/README.md
@@ -1102,8 +1102,8 @@ If success, 200.
Process payment receipt
Input:
-- `Authentication` header that contains the api key
-- `receipt_data` the receipt_data base64Encoded returned by StoreKit, i.e. `rawReceiptData.base64EncodedString`
+- `Authentication` in header: the api key
+- `receipt_data` in body: the receipt_data base64Encoded returned by StoreKit, i.e. `rawReceiptData.base64EncodedString`
### Database migration
diff --git a/cron.py b/cron.py
index c63d5c57..e1715d07 100644
--- a/cron.py
+++ b/cron.py
@@ -218,7 +218,7 @@ if __name__ == "__main__":
"notify_manual_subscription_end",
"notify_premium_end",
"delete_refused_emails",
- "poll_apple_subscription"
+ "poll_apple_subscription",
],
)
args = parser.parse_args()
From bf55ba052178b6bb9ce03a838b70f0518584e4ba Mon Sep 17 00:00:00 2001
From: Son NK <>
Date: Sun, 19 Apr 2020 11:22:05 +0200
Subject: [PATCH 10/10] Add output doc for POST /apple/process_payment
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 8f160437..9aa637ea 100644
--- a/README.md
+++ b/README.md
@@ -1105,6 +1105,10 @@ 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
The database migration is handled by `alembic`