Package: inapppy
InAppPyTable of contentsIntroductionInstallationGoogle Play (receipt+signature)Google Play (verification)Google Play (verification with result)App Store (receipt+ using optionalshared-secret)App Store Response (validation_result/raw_response) exampleApp Store,asyncioversion (available in the inapppy.asyncio package)DevelopmentDonate1. IntroductionIn-app purchase validation library forApple AppStoreandGooglePlay(App Storevalidator haveasyncsupport!). Works on python3.6+2. Installationpip install inapppy3. Google Play (validatesreceiptagainst providedsignatureusing RSA)frominapppyimportGooglePlayValidator,InAppPyValidationErrorbundle_id='com.yourcompany.yourapp'api_key='API key from the developer console'validator=GooglePlayValidator(bundle_id,api_key)try:# receipt means `androidData` in result of purchase# signature means `signatureAndroid` in result of purchasevalidation_result=validator.validate('receipt','signature')exceptInAppPyValidationError:# handle validation errorpassAn additional example showing how to authenticate using dict credentials instead of loading from a fileimportjsonfrominapppyimportGooglePlayValidator,InAppPyValidationErrorbundle_id='com.yourcompany.yourapp'# Avoid hard-coding credential data in your code. This is just an example.api_credentials=json.loads('{''   "type": "service_account",''   "project_id": "xxxxxxx",''   "private_key_id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",''   "private_key": "-----BEGIN PRIVATE KEY-----\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXX==\n-----END PRIVATE KEY-----\n",''   "client_email": "XXXXXXXXX@XXXXXXXX.XXX",''   "client_id": "XXXXXXXXXXXXXXXXXX",''   "auth_uri": "https://accounts.google.com/o/oauth2/auth",''   "token_uri": "https://oauth2.googleapis.com/token",''   "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",''   "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/XXXXXXXXXXXXXXXXX.iam.gserviceaccount.com"'' }')validator=GooglePlayValidator(bundle_id,api_credentials)try:# receipt means `androidData` in result of purchase# signature means `signatureAndroid` in result of purchasevalidation_result=validator.validate('receipt','signature')exceptInAppPyValidationError:# handle validation errorpass4. Google Play verificationfrominapppyimportGooglePlayVerifier,errorsdefgoogle_validator(receipt):"""
    Accepts receipt, validates in Google.
    """purchase_token=receipt['purchaseToken']product_sku=receipt['productId']verifier=GooglePlayVerifier(GOOGLE_BUNDLE_ID,GOOGLE_SERVICE_ACCOUNT_KEY_FILE,)response={'valid':False,'transactions':[]}try:result=verifier.verify(purchase_token,product_sku,is_subscription=True)response['valid']=Trueresponse['transactions'].append((result['orderId'],product_sku))excepterrors.GoogleErrorasexc:logging.error('Purchase validation failed{}'.format(exc))returnresponse5. Google Play verification (with result)Alternative to.verifymethod, instead of raising an error result class will be returned.frominapppyimportGooglePlayVerifier,errorsdefgoogle_validator(receipt):"""
    Accepts receipt, validates in Google.
    """purchase_token=receipt['purchaseToken']product_sku=receipt['productId']verifier=GooglePlayVerifier(GOOGLE_BUNDLE_ID,GOOGLE_SERVICE_ACCOUNT_KEY_FILE,)response={'valid':False,'transactions':[]}result=verifier.verify_with_result(purchase_token,product_sku,is_subscription=True)# result contains dataraw_response=result.raw_responseis_canceled=result.is_canceledis_expired=result.is_expiredreturnresult6. App Store (validatesreceiptusing optionalshared-secretagainst iTunes service)frominapppyimportAppStoreValidator,InAppPyValidationErrorbundle_id='com.yourcompany.yourapp'auto_retry_wrong_env_request=False# if True, automatically query sandbox endpoint if# validation fails on production endpointvalidator=AppStoreValidator(bundle_id,auto_retry_wrong_env_request=auto_retry_wrong_env_request)try:exclude_old_transactions=False# if True, include only the latest renewal transactionvalidation_result=validator.validate('receipt','optional-shared-secret',exclude_old_transactions=exclude_old_transactions)exceptInAppPyValidationErrorasex:# handle validation errorresponse_from_apple=ex.raw_response# contains actual response from AppStore service.pass7. App Store Response (validation_result/raw_response) example{"latest_receipt":"MIIbngYJKoZIhvcNAQcCoIIbj...","status":0,"receipt":{"download_id":0,"receipt_creation_date_ms":"1486371475000","application_version":"2","app_item_id":0,"receipt_creation_date":"2017-02-06 08:57:55 Etc/GMT","original_purchase_date":"2013-08-01 07:00:00 Etc/GMT","request_date_pst":"2017-02-06 04:41:09 America/Los_Angeles","original_application_version":"1.0","original_purchase_date_pst":"2013-08-01 00:00:00 America/Los_Angeles","request_date_ms":"1486384869996","bundle_id":"com.yourcompany.yourapp","request_date":"2017-02-06 12:41:09 Etc/GMT","original_purchase_date_ms":"1375340400000","in_app":[{"purchase_date_ms":"1486371474000","web_order_line_item_id":"1000000034281189","original_purchase_date_ms":"1486371475000","original_purchase_date":"2017-02-06 08:57:55 Etc/GMT","expires_date_pst":"2017-02-06 01:00:54 America/Los_Angeles","original_purchase_date_pst":"2017-02-06 00:57:55 America/Los_Angeles","purchase_date_pst":"2017-02-06 00:57:54 America/Los_Angeles","expires_date_ms":"1486371654000","expires_date":"2017-02-06 09:00:54 Etc/GMT","original_transaction_id":"1000000271014363","purchase_date":"2017-02-06 08:57:54 Etc/GMT","quantity":"1","is_trial_period":"false","product_id":"com.yourcompany.yourapp","transaction_id":"1000000271014363"}],"version_external_identifier":0,"receipt_creation_date_pst":"2017-02-06 00:57:55 America/Los_Angeles","adam_id":0,"receipt_type":"ProductionSandbox"},"latest_receipt_info":[{"purchase_date_ms":"1486371474000","web_order_line_item_id":"1000000034281189","original_purchase_date_ms":"1486371475000","original_purchase_date":"2017-02-06 08:57:55 Etc/GMT","expires_date_pst":"2017-02-06 01:00:54 America/Los_Angeles","original_purchase_date_pst":"2017-02-06 00:57:55 America/Los_Angeles","purchase_date_pst":"2017-02-06 00:57:54 America/Los_Angeles","expires_date_ms":"1486371654000","expires_date":"2017-02-06 09:00:54 Etc/GMT","original_transaction_id":"1000000271014363","purchase_date":"2017-02-06 08:57:54 Etc/GMT","quantity":"1","is_trial_period":"true","product_id":"com.yourcompany.yourapp","transaction_id":"1000000271014363"},{"purchase_date_ms":"1486371719000","web_order_line_item_id":"1000000034281190","original_purchase_date_ms":"1486371720000","original_purchase_date":"2017-02-06 09:02:00 Etc/GMT","expires_date_pst":"2017-02-06 01:06:59 America/Los_Angeles","original_purchase_date_pst":"2017-02-06 01:02:00 America/Los_Angeles","purchase_date_pst":"2017-02-06 01:01:59 America/Los_Angeles","expires_date_ms":"1486372019000","expires_date":"2017-02-06 09:06:59 Etc/GMT","original_transaction_id":"1000000271014363","purchase_date":"2017-02-06 09:01:59 Etc/GMT","quantity":"1","is_trial_period":"false","product_id":"com.yourcompany.yourapp","transaction_id":"1000000271016119"}],"environment":"Sandbox"}8. App Store, asyncio version (available in the inapppy.asyncio package)frominapppyimportInAppPyValidationErrorfrominapppy.asyncioimportAppStoreValidatorbundle_id='com.yourcompany.yourapp'auto_retry_wrong_env_request=False# if True, automatically query sandbox endpoint if# validation fails on production endpointvalidator=AppStoreValidator(bundle_id,auto_retry_wrong_env_request=auto_retry_wrong_env_request)try:exclude_old_transactions=False# if True, include only the latest renewal transactionvalidation_result=awaitvalidator.validate('receipt','optional-shared-secret',exclude_old_transactions=exclude_old_transactions)exceptInAppPyValidationErrorasex:# handle validation errorresponse_from_apple=ex.raw_response# contains actual response from AppStore service.pass9. Development# run checks and teststox# setup projectmakesetup# check for lint errorsmakelint# run testsmaketest# run blackmakeblack10. DonateYou can support development of this project by buying me a coffee ;)CoinWalletEURhttps://paypal.me/LukasSalkauskasDOGEDGjSG3T6g9h2k6iSku7mtKCynCpmwowpyNBTC1LZAiWmLYzZae4hq3ai9hFYD3e3qcwjDsUETH0xD62245986345130edE10e4b545fF577Bd5BaE3E4