Skip to content

Commit f742126

Browse files
Merge pull request #65 from alexanderjordanbaker/ASSNv2.10
Updating for App Store Server Notifications v2.10
2 parents 77058c4 + 09ab944 commit f742126

File tree

8 files changed

+158
-7
lines changed

8 files changed

+158
-7
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright (c) 2024 Apple Inc. Licensed under MIT License.
2+
from typing import Optional
3+
4+
from attr import define
5+
import attr
6+
7+
from .LibraryUtility import AttrsRawValueAware
8+
9+
@define
10+
class ExternalPurchaseToken(AttrsRawValueAware):
11+
"""
12+
The payload data that contains an external purchase token.
13+
14+
https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken
15+
"""
16+
17+
externalPurchaseId: Optional[str] = attr.ib(default=None)
18+
"""
19+
The field of an external purchase token that uniquely identifies the token.
20+
21+
https://developer.apple.com/documentation/appstoreservernotifications/externalpurchaseid
22+
"""
23+
24+
tokenCreationDate: Optional[int] = attr.ib(default=None)
25+
"""
26+
The field of an external purchase token that contains the UNIX date, in milliseconds, when the system created the token.
27+
28+
https://developer.apple.com/documentation/appstoreservernotifications/tokencreationdate
29+
"""
30+
31+
appAppleId: Optional[int] = attr.ib(default=None)
32+
"""
33+
The unique identifier of an app in the App Store.
34+
35+
https://developer.apple.com/documentation/appstoreservernotifications/appappleid
36+
"""
37+
38+
bundleId: Optional[str] = attr.ib(default=None)
39+
"""
40+
The bundle identifier of an app.
41+
42+
https://developer.apple.com/documentation/appstoreservernotifications/bundleid
43+
"""

appstoreserverlibrary/models/NotificationTypeV2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ class NotificationTypeV2(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):
2727
TEST = "TEST"
2828
RENEWAL_EXTENSION = "RENEWAL_EXTENSION"
2929
REFUND_REVERSED = "REFUND_REVERSED"
30+
EXTERNAL_PURCHASE_TOKEN = "EXTERNAL_PURCHASE_TOKEN"

appstoreserverlibrary/models/ResponseBodyV2DecodedPayload.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
from attr import define
55
import attr
6-
from .Data import Data
76

7+
from .Data import Data
8+
from .ExternalPurchaseToken import ExternalPurchaseToken
89
from .LibraryUtility import AttrsRawValueAware
910
from .NotificationTypeV2 import NotificationTypeV2
1011
from .Subtype import Subtype
@@ -53,7 +54,7 @@ class ResponseBodyV2DecodedPayload(AttrsRawValueAware):
5354
data: Optional[Data] = attr.ib(default=None)
5455
"""
5556
The object that contains the app metadata and signed renewal and transaction information.
56-
The data and summary fields are mutually exclusive. The payload contains one of the fields, but not both.
57+
The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields.
5758
5859
https://developer.apple.com/documentation/appstoreservernotifications/data
5960
"""
@@ -75,7 +76,15 @@ class ResponseBodyV2DecodedPayload(AttrsRawValueAware):
7576
summary: Optional[Summary] = attr.ib(default=None)
7677
"""
7778
The summary data that appears when the App Store server completes your request to extend a subscription renewal date for eligible subscribers.
78-
The data and summary fields are mutually exclusive. The payload contains one of the fields, but not both.
79+
The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields.
7980
8081
https://developer.apple.com/documentation/appstoreservernotifications/summary
82+
"""
83+
84+
externalPurchaseToken: Optional[ExternalPurchaseToken] = attr.ib(default=None)
85+
"""
86+
This field appears when the notificationType is EXTERNAL_PURCHASE_TOKEN.
87+
The data, summary, and externalPurchaseToken fields are mutually exclusive. The payload contains only one of these fields.
88+
89+
https://developer.apple.com/documentation/appstoreservernotifications/externalpurchasetoken
8190
"""

appstoreserverlibrary/models/Subtype.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ class Subtype(str, Enum, metaclass=AppStoreServerLibraryEnumMeta):
2626
PRODUCT_NOT_FOR_SALE = "PRODUCT_NOT_FOR_SALE"
2727
SUMMARY = "SUMMARY"
2828
FAILURE = "FAILURE"
29+
UNREPORTED = "UNREPORTED"

appstoreserverlibrary/signed_data_verifier.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
22

3-
from typing import List
3+
from typing import List, Optional
44
from base64 import b64decode
55
from enum import IntEnum
66
import time
@@ -94,11 +94,21 @@ def verify_and_decode_notification(self, signed_payload: str) -> ResponseBodyV2D
9494
bundle_id = decoded_signed_notification.summary.bundleId
9595
app_apple_id = decoded_signed_notification.summary.appAppleId
9696
environment = decoded_signed_notification.summary.environment
97+
elif decoded_signed_notification.externalPurchaseToken:
98+
bundle_id = decoded_signed_notification.externalPurchaseToken.bundleId
99+
app_apple_id = decoded_signed_notification.externalPurchaseToken.appAppleId
100+
if decoded_signed_notification.externalPurchaseToken.externalPurchaseId and decoded_signed_notification.externalPurchaseToken.externalPurchaseId.startswith("SANDBOX"):
101+
environment = Environment.SANDBOX
102+
else:
103+
environment = Environment.PRODUCTION
104+
self._verify_notification(bundle_id, app_apple_id, environment)
105+
return decoded_signed_notification
106+
107+
def _verify_notification(self, bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]):
97108
if bundle_id != self._bundle_id or (self._environment == Environment.PRODUCTION and app_apple_id != self._app_apple_id):
98109
raise VerificationException(VerificationStatus.INVALID_APP_IDENTIFIER)
99110
if environment != self._environment:
100111
raise VerificationException(VerificationStatus.INVALID_ENVIRONMENT)
101-
return decoded_signed_notification
102112

103113
def verify_and_decode_app_transaction(self, signed_app_transaction: str) -> AppTransaction:
104114
"""
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"notificationType": "EXTERNAL_PURCHASE_TOKEN",
3+
"subtype": "UNREPORTED",
4+
"notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20",
5+
"version": "2.0",
6+
"signedDate": 1698148900000,
7+
"externalPurchaseToken": {
8+
"externalPurchaseId": "b2158121-7af9-49d4-9561-1f588205523e",
9+
"tokenCreationDate": 1698148950000,
10+
"appAppleId": 55555,
11+
"bundleId": "com.example"
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"notificationType": "EXTERNAL_PURCHASE_TOKEN",
3+
"subtype": "UNREPORTED",
4+
"notificationUUID": "002e14d5-51f5-4503-b5a8-c3a1af68eb20",
5+
"version": "2.0",
6+
"signedDate": 1698148900000,
7+
"externalPurchaseToken": {
8+
"externalPurchaseId": "SANDBOX_b2158121-7af9-49d4-9561-1f588205523e",
9+
"tokenCreationDate": 1698148950000,
10+
"appAppleId": 55555,
11+
"bundleId": "com.example"
12+
}
13+
}

tests/test_decoded_payloads.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
22

3+
from typing import Optional
34
import unittest
45
from appstoreserverlibrary.models.AutoRenewStatus import AutoRenewStatus
56
from appstoreserverlibrary.models.Environment import Environment
@@ -15,7 +16,7 @@
1516
from appstoreserverlibrary.models.TransactionReason import TransactionReason
1617
from appstoreserverlibrary.models.Type import Type
1718

18-
from tests.util import create_signed_data_from_json, get_default_signed_data_verifier
19+
from tests.util import create_signed_data_from_json, get_default_signed_data_verifier, get_signed_data_verifier
1920

2021
class DecodedPayloads(unittest.TestCase):
2122
def test_app_transaction_decoding(self):
@@ -122,6 +123,7 @@ def test_notificaiton_decoding(self):
122123
self.assertEqual(1698148900000, notification.signedDate)
123124
self.assertIsNotNone(notification.data)
124125
self.assertIsNone(notification.summary)
126+
self.assertIsNone(notification.externalPurchaseToken)
125127
self.assertEqual(Environment.LOCAL_TESTING, notification.data.environment)
126128
self.assertEqual("LocalTesting", notification.data.rawEnvironment)
127129
self.assertEqual(41234, notification.data.appAppleId)
@@ -148,6 +150,7 @@ def test_summary_notification_decoding(self):
148150
self.assertEqual(1698148900000, notification.signedDate)
149151
self.assertIsNone(notification.data)
150152
self.assertIsNotNone(notification.summary)
153+
self.assertIsNone(notification.externalPurchaseToken)
151154
self.assertEqual(Environment.LOCAL_TESTING, notification.summary.environment)
152155
self.assertEqual("LocalTesting", notification.summary.rawEnvironment)
153156
self.assertEqual(41234, notification.summary.appAppleId)
@@ -156,4 +159,62 @@ def test_summary_notification_decoding(self):
156159
self.assertEqual("efb27071-45a4-4aca-9854-2a1e9146f265", notification.summary.requestIdentifier)
157160
self.assertEqual(["CAN", "USA", "MEX"], notification.summary.storefrontCountryCodes)
158161
self.assertEqual(5, notification.summary.succeededCount)
159-
self.assertEqual(2, notification.summary.failedCount)
162+
self.assertEqual(2, notification.summary.failedCount)
163+
164+
def test_external_purchase_token_notification_decoding(self):
165+
signed_external_purchase_token_notification = create_signed_data_from_json('tests/resources/models/signedExternalPurchaseTokenNotification.json')
166+
167+
signed_data_verifier = get_default_signed_data_verifier()
168+
169+
def check_environment_and_bundle_id(bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]):
170+
self.assertEqual("com.example", bundle_id)
171+
self.assertEqual(55555, app_apple_id)
172+
self.assertEqual(Environment.PRODUCTION, environment)
173+
174+
signed_data_verifier._verify_notification = check_environment_and_bundle_id
175+
176+
notification = signed_data_verifier.verify_and_decode_notification(signed_external_purchase_token_notification)
177+
178+
self.assertEqual(NotificationTypeV2.EXTERNAL_PURCHASE_TOKEN, notification.notificationType)
179+
self.assertEqual("EXTERNAL_PURCHASE_TOKEN", notification.rawNotificationType)
180+
self.assertEqual(Subtype.UNREPORTED, notification.subtype)
181+
self.assertEqual("UNREPORTED", notification.rawSubtype)
182+
self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID)
183+
self.assertEqual("2.0", notification.version)
184+
self.assertEqual(1698148900000, notification.signedDate)
185+
self.assertIsNone(notification.data)
186+
self.assertIsNone(notification.summary)
187+
self.assertIsNotNone(notification.externalPurchaseToken)
188+
self.assertEqual("b2158121-7af9-49d4-9561-1f588205523e", notification.externalPurchaseToken.externalPurchaseId)
189+
self.assertEqual(1698148950000, notification.externalPurchaseToken.tokenCreationDate)
190+
self.assertEqual(55555, notification.externalPurchaseToken.appAppleId)
191+
self.assertEqual("com.example", notification.externalPurchaseToken.bundleId)
192+
193+
def test_external_purchase_token_sandbox_notification_decoding(self):
194+
signed_external_purchase_token_notification = create_signed_data_from_json('tests/resources/models/signedExternalPurchaseTokenSandboxNotification.json')
195+
196+
signed_data_verifier = get_default_signed_data_verifier()
197+
198+
def check_environment_and_bundle_id(bundle_id: Optional[str], app_apple_id: Optional[int], environment: Optional[Environment]):
199+
self.assertEqual("com.example", bundle_id)
200+
self.assertEqual(55555, app_apple_id)
201+
self.assertEqual(Environment.SANDBOX, environment)
202+
203+
signed_data_verifier._verify_notification = check_environment_and_bundle_id
204+
205+
notification = signed_data_verifier.verify_and_decode_notification(signed_external_purchase_token_notification)
206+
207+
self.assertEqual(NotificationTypeV2.EXTERNAL_PURCHASE_TOKEN, notification.notificationType)
208+
self.assertEqual("EXTERNAL_PURCHASE_TOKEN", notification.rawNotificationType)
209+
self.assertEqual(Subtype.UNREPORTED, notification.subtype)
210+
self.assertEqual("UNREPORTED", notification.rawSubtype)
211+
self.assertEqual("002e14d5-51f5-4503-b5a8-c3a1af68eb20", notification.notificationUUID)
212+
self.assertEqual("2.0", notification.version)
213+
self.assertEqual(1698148900000, notification.signedDate)
214+
self.assertIsNone(notification.data)
215+
self.assertIsNone(notification.summary)
216+
self.assertIsNotNone(notification.externalPurchaseToken)
217+
self.assertEqual("SANDBOX_b2158121-7af9-49d4-9561-1f588205523e", notification.externalPurchaseToken.externalPurchaseId)
218+
self.assertEqual(1698148950000, notification.externalPurchaseToken.tokenCreationDate)
219+
self.assertEqual(55555, notification.externalPurchaseToken.appAppleId)
220+
self.assertEqual("com.example", notification.externalPurchaseToken.bundleId)

0 commit comments

Comments
 (0)