1+ # Copyright (c) 2025 Apple Inc. Licensed under MIT License.
2+
3+ import datetime
4+ from typing import Any , Dict , Optional
5+ import base64
6+ import json
7+ import jwt
8+ import uuid
9+
10+ from cryptography .hazmat .backends import default_backend
11+ from cryptography .hazmat .primitives import serialization
12+
13+ from appstoreserverlibrary .models .LibraryUtility import _get_cattrs_converter
14+
15+ class AdvancedCommerceAPIInAppRequest :
16+ def __init__ (self ):
17+ pass
18+
19+ class JWSSignatureCreator :
20+ def __init__ (self , audience : str , signing_key : bytes , key_id : str , issuer_id : str , bundle_id : str ):
21+ self ._audience = audience
22+ self ._signing_key = serialization .load_pem_private_key (signing_key , password = None , backend = default_backend ())
23+ self ._key_id = key_id
24+ self ._issuer_id = issuer_id
25+ self ._bundle_id = bundle_id
26+
27+ def _create_signature (self , feature_specific_claims : Dict [str , Any ]) -> str :
28+ claims = feature_specific_claims
29+ current_time = datetime .datetime .now (datetime .timezone .utc )
30+
31+ claims ["bid" ] = self ._bundle_id
32+ claims ["iss" ] = self ._issuer_id
33+ claims ["aud" ] = self ._audience
34+ claims ["iat" ] = current_time
35+ claims ["nonce" ] = str (uuid .uuid4 ())
36+
37+ return jwt .encode (claims ,
38+ self ._signing_key ,
39+ algorithm = "ES256" ,
40+ headers = {"kid" : self ._key_id },
41+ )
42+
43+ class PromotionalOfferV2SignatureCreator (JWSSignatureCreator ):
44+ def __init__ (self , signing_key : bytes , key_id : str , issuer_id : str , bundle_id : str ):
45+ """
46+ Create a PromotionalOfferV2SignatureCreator
47+
48+ :param signing_key: Your private key downloaded from App Store Connect
49+ :param key_id: Your private key ID from App Store Connect
50+ :param issuer_id: Your issuer ID from the Keys page in App Store Connect
51+ :param bundle_id: Your app's bundle ID
52+ """
53+ super ().__init__ (audience = "promotional-offer" , signing_key = signing_key , key_id = key_id , issuer_id = issuer_id , bundle_id = bundle_id )
54+
55+ def create_signature (self , product_id : str , offer_identifier : str , transaction_id : Optional [str ]) -> str :
56+ """
57+ Create a promotional offer V2 signature.
58+ https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests
59+
60+ :param product_id: The unique identifier of the product
61+ :param offer_identifier: The promotional offer identifier that you set up in App Store Connect
62+ :param transaction_id: The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app. This field is optional, but recommended.
63+ :return: The signed JWS.
64+ """
65+ if product_id is None :
66+ raise ValueError ("product_id cannot be null" )
67+ if offer_identifier is None :
68+ raise ValueError ("offer_identifier cannot be null" )
69+ feature_specific_claims = {
70+ "productId" : product_id ,
71+ "offerIdentifier" : offer_identifier
72+ }
73+ if transaction_id is not None :
74+ feature_specific_claims ["transactionId" ] = transaction_id
75+ return self ._create_signature (feature_specific_claims = feature_specific_claims )
76+
77+ class IntroductoryOfferEligibilitySignatureCreator (JWSSignatureCreator ):
78+ def __init__ (self , signing_key : bytes , key_id : str , issuer_id : str , bundle_id : str ):
79+ """
80+ Create an IntroductoryOfferEligibilitySignatureCreator
81+
82+ :param signing_key: Your private key downloaded from App Store Connect
83+ :param key_id: Your private key ID from App Store Connect
84+ :param issuer_id: Your issuer ID from the Keys page in App Store Connect
85+ :param bundle_id: Your app's bundle ID
86+ """
87+ super ().__init__ (audience = "introductory-offer-eligibility" , signing_key = signing_key , key_id = key_id , issuer_id = issuer_id , bundle_id = bundle_id )
88+
89+ def create_signature (self , product_id : str , allow_introductory_offer : bool , transaction_id : str ) -> str :
90+ """
91+ Create an introductory offer eligibility signature.
92+ https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests
93+
94+ :param product_id: The unique identifier of the product
95+ :param allow_introductory_offer: A boolean value that determines whether the customer is eligible for an introductory offer
96+ :param transaction_id: The unique identifier of any transaction that belongs to the customer. You can use the customer's appTransactionId, even for customers who haven't made any In-App Purchases in your app.
97+ :return: The signed JWS.
98+ """
99+ if product_id is None :
100+ raise ValueError ("product_id cannot be null" )
101+ if allow_introductory_offer is None :
102+ raise ValueError ("allow_introductory_offer cannot be null" )
103+ if transaction_id is None :
104+ raise ValueError ("transaction_id cannot be null" )
105+ feature_specific_claims = {
106+ "productId" : product_id ,
107+ "allowIntroductoryOffer" : allow_introductory_offer ,
108+ "transactionId" : transaction_id
109+ }
110+ return self ._create_signature (feature_specific_claims = feature_specific_claims )
111+
112+ class AdvancedCommerceAPIInAppSignatureCreator (JWSSignatureCreator ):
113+ def __init__ (self , signing_key : bytes , key_id : str , issuer_id : str , bundle_id : str ):
114+ """
115+ Create an AdvancedCommerceAPIInAppSignatureCreator
116+
117+ :param signing_key: Your private key downloaded from App Store Connect
118+ :param key_id: Your private key ID from App Store Connect
119+ :param issuer_id: Your issuer ID from the Keys page in App Store Connect
120+ :param bundle_id: Your app's bundle ID
121+ """
122+ super ().__init__ (audience = "advanced-commerce-api" , signing_key = signing_key , key_id = key_id , issuer_id = issuer_id , bundle_id = bundle_id )
123+
124+ def create_signature (self , advanced_commerce_in_app_request : AdvancedCommerceAPIInAppRequest ) -> str :
125+ """
126+ Create an Advanced Commerce in-app signed request.
127+ https://developer.apple.com/documentation/storekit/generating-jws-to-sign-app-store-requests
128+
129+ :param advanced_commerce_in_app_request: The request to be signed.
130+ :return: The signed JWS.
131+ """
132+ if advanced_commerce_in_app_request is None :
133+ raise ValueError ("advanced_commerce_in_app_request cannot be null" )
134+ c = _get_cattrs_converter (type (advanced_commerce_in_app_request ))
135+ request = c .unstructure (advanced_commerce_in_app_request )
136+ encoded_request = base64 .b64encode (json .dumps (request ).encode (encoding = 'utf-8' )).decode ('utf-8' )
137+ feature_specific_claims = {
138+ "request" : encoded_request
139+ }
140+ return self ._create_signature (feature_specific_claims = feature_specific_claims )
0 commit comments