Auto-renewable subscriptions with SwiftUI

Auto-renewable subscriptions provide access to content, services, or premium features in your app on an ongoing basis. They automatically renew at the end of their duration until the user chooses to cancel. Subscriptions are available on iOS, iPadOS, macOS, watchOS, and tvOS.

Great subscription apps justify the recurring payment by providing ongoing value and continually innovating the app experience. If you’re considering implementing the subscription model, plan to regularly update your app with feature enhancements or expanded content.

Before creating any subscription make sure you have done all these steps and all are showing Active status. 

Go here https://appstoreconnect.apple.com/agreements/#

Creating subscriptions

Go to app detail page and click on the In-App purchase link under Feature. 

Select Auto-Renewable Subscription and click the Create button.

SwiftUI View-Model

//
//  AppStoreManager.swift
//  ARSubscription
//
//  Created by Smin Rana on 2/1/22.
//

import SwiftUI
import StoreKit
import Combine

class AppStoreManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {

    @Published var products = [SKProduct]()
    
    override init() {
        super.init()
        
        SKPaymentQueue.default().add(self)
    }
    
    func getProdcut(indetifiers: [String]) {
        print("Start requesting products ...")
        let request = SKProductsRequest(productIdentifiers: Set(indetifiers))
        request.delegate = self
        request.start()
    }
    
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        print("Did receive response")
                
        if !response.products.isEmpty {
            for fetchedProduct in response.products {
                DispatchQueue.main.async {
                    self.products.append(fetchedProduct)
                }
            }
        }
        
        for invalidIdentifier in response.invalidProductIdentifiers {
            print("Invalid identifiers found: \(invalidIdentifier)")
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        print("Request did fail: \(error)")
    }
    
    // Transaction
    
    @Published var transactionState: SKPaymentTransactionState?
    
    func purchaseProduct(product: SKProduct) {
        if SKPaymentQueue.canMakePayments() {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(payment)
        } else {
            print("User can't make payment.")
        }
    }
    
    struct PaymentReceiptResponseModel: Codable {
        var status: Int
        var email: String?
        var password: String?
        var message: String?
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        for transaction in transactions {
            switch transaction.transactionState {
            case .purchasing:
                self.transactionState = .purchasing
            case .purchased:
                print("===============Purchased================")
                UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)
                if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
                    FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {

                    do {
                        let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
                        let receiptString = receiptData.base64EncodedString(options: [])
                        
                        // TODO: Send your receiptString to the server and verify with Apple
                        // receiptString should be sent to server as JSON
                        // {
                        //    "receipt" : receiptString
                        // }
                        
                        self.transactionState = .purchased // only if server sends successful response
                    }
                    catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
                }
            case .restored:
                UserDefaults.standard.setValue(true, forKey: transaction.payment.productIdentifier)

                queue.finishTransaction(transaction)
                print("==================RESTORED State=============")
                self.transactionState = .restored
            case .failed, .deferred:
                print("Payment Queue Error: \(String(describing: transaction.error))")
                queue.finishTransaction(transaction)
                self.transactionState = .failed
            default:
                print(">>>> something else")
                queue.finishTransaction(transaction)
            }
        }
    }
    
    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        print("===============Restored================")
        if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
            FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {

            do {
                let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
                let receiptString = receiptData.base64EncodedString(options: [])
                
                // TODO: Send your receiptString to the server and verify with Apple
                // receiptString should be sent to server as JSON
                // {
                //    "receipt" : receiptString
                // }
                
                
                self.transactionState = .purchased // only if server sends successful response
            }
            catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
        }
    }
    
    func restorePurchase() {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
}

Testing with .storekit file on simulator:

Server side with PHP and Laravel:

Create app specific shared secret

PHP and Laravel

<?php 

// Laravel and GuzzleHTTP\Client

function verifyReceiptWithApple() {
        $input = file_get_contents('php://input');
        $request = json_decode($input);

        $d = $request->receipt;
        $secret = 'your_app_purchase_secret';

        //$url = 'https://sandbox.itunes.apple.com/verifyReceipt';
        $url = 'https://buy.itunes.apple.com/verifyReceipt';

        // Replace with curl if you are not using Laravel
        $client = new Client([
            'headers' => [ 'Content-Type' => 'application/json' ]
        ]);
        
        $response = $client->post($url,
            ['body' => json_encode(
                [
                    'receipt-data' => $d,
                    'password' => $secret,
                    'exclude-old-transactions' => false
                ]
            )]
        );

        $json = json_decode($response->getBody()->getContents());
        if ($json->status == 0) {

            $email = "";

            // Get original transaction id
            $receipts = $json->receipt->in_app;
            if (!empty($receipts) && count($receipts) > 0) {
                $first_receipt = $receipts[0];
                if ($first_receipt->in_app_ownership_type == "PURCHASED") {
                    $original_transaction_id = $first_receipt->original_transaction_id;

                    // Create email address with transaction id 
                    // Create new user if not exists
                    $email = $original_transaction_id.'@domain.com';
                    $have_user = "check_with_your_database";
                    if (!$have_user) {
                        // New purchase -> user not found
                    } else {
                        // Restore purchase -> user found 
                        
                    }
                }
            }

            return response()->json(["status" => 1, "message" => "Receipt is verified"]);
        } else {
            return response()->json(["status" => 0, "message" => "Invalid receipt"]);
        }
    }

Things to include in-app purchase

  • Value proposition
  • Call to action
  • Clear terms
  • Signup
  • Multiple tiers
  • Log in
  • Restore
  • Terms and conditions

Read more

App store distribution and marketing: https://developer.apple.com/videos/app-store-distribution-marketing

Auto-Renewable Subscriptions: https://developer.apple.com/design/human-interface-guidelines/in-app-purchase/overview/auto-renewable-subscriptions/

Architecting for subscriptions: https://developer.apple.com/videos/play/wwdc2020/10671/

Download full source code

Competitive Programming: Taking Input

Taking input

In most cases, standard streams are used for reading input and writing output. In C++, the  streams are cin>> for input and cout<< for output. 

The inputs are most of the time string or numbers and separated with spaces and newlines. So every space or newline is new input.

For following input -> one line two inputs separated by space:

8 9

We can get it like

int a,b;
cin>>a>>b;

cout<<a<<b;

For following input -> two lines three inputs, separated by space and new line:

8 9

ABCD

int a,b;
string s;

cin>>a>>b;
cin>>s;

cout<<a<<b;
cout<<s;

Firebase getToken is deprecated

If you are still using old style to get FireBase token, it is time to change it.

Old way

String token = FirebaseInstanceId.getInstance().getToken();

New way

FirebaseInstanceId.getInstance().getInstanceId()
				.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
					@Override
					public void onComplete(@NonNull Task<InstanceIdResult> task) {
						if (!task.isSuccessful()) {
							Log.w("FB token e: ", "getInstanceId failed", task.getException());
							return;
						}

						// Get new Instance ID token
						String token = task.getResult().getToken();
						Log.d("FB token", token);
						if (!token.equals("")) {
// send to your server						}
					}
				});