Category: swiftui

  • Decode Apple Receipt

    Decode Apple Receipt

    Decode Apple Receipt

    Verify with the server-side click here

    /**
     * ***********************************************************************
     *  SMINRANA CONFIDENTIAL
     *   __________________
     *
     * Copyright 2020  SMINRANA
     * All Rights Reserved.
     *
     * NOTICE:  All information contained herein is, and remains
     * the property of SMINRANA and its suppliers,
     * if any.  The intellectual and technical concepts contained
     * herein are proprietary to SMINRANA
     * and its suppliers and may be covered by U.S. and Foreign Patents,
     * patents in process, and are protected by trade secret or copyright law.
     * Dissemination of this information or reproduction of this material
     * is strictly forbidden unless prior written permission is obtained
     * from SMINRANA.
     * www.sminrana.com
     *
     */
    
    import SwiftUI
    import StoreKit
    import Combine
    
    class AppStorageManager: NSObject, ObservableObject, SKProductsRequestDelegate, SKPaymentTransactionObserver {
    
        @AppStorage("username") var username: String = ""
        @AppStorage("password") var password: String = ""
        
        override init() {
            super.init()
            
            SKPaymentQueue.default().add(self)
        }
        
        @Published var products = [SKProduct]()
        
        func getProdcut(indetifiers: [String]) {
            print("Start requesting products ...")
            let request = SKProductsRequest(productIdentifiers: Set(indetifiers))
            request.delegate = self
            request.start()
        }
        
    
        // SKProductsRequestDelegate
    
        func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
            print("Did receive response \(response.products)")
                    
            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.")
            }
        }
    
        func restorePurchase() {
            SKPaymentQueue.default().restoreCompletedTransactions()
        }
        
        struct PaymentReceiptResponseModel: Codable {
            var status: Int
            var email: String?
            var password: String?
            var message: String?
        }
        
        // SKPaymentTransactionObserver
    
    
        // This gets called when transaction purchased by user
        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: [])
                            
                           // Send receiptString to server for further verification
                        }
                        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)
                }
            }
        }
        
        // This gets called when a transaction restored by user
        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: [])
                    
                    // Send receiptString to server for further verification
                }
                catch { print("Couldn't read receipt data with error: " + error.localizedDescription) }
            }
        }
        
      
    }
    
    Spread the love
  • Auto-renewable subscriptions with SwiftUI

    Auto-renewable subscriptions with SwiftUI

    Auto-renewable subscriptions with SwiftUI

    Read about App Store Server Integration in Laravel

    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.

    To offer subscriptions, youʼll need to configure them in App Store Connect and use StoreKit APIs in your app. You’ll also need to assign each subscription to a subscription group (a group of subscriptions with different access levels, prices, and durations that people can choose from), then add details such as a name, price, and description. This information displays in the In-App Purchases section of your app’s product page on the App Store. Ensure that the subscriptions are available across all device types that your app supports. Consider allowing a way for subscribers to see the status of their subscription within your app, along with upgrade, crossgrade, and downgrade options, as well as a way to easily manage or turn off their auto-renewable subscription. Make sure to follow our design and review guidelines.

    To get ready:

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

    Auto-renewable subscriptions with SwiftUI

    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.

    Auto-renewable subscriptions with SwiftUI

    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:

    https://youtu.be/e7Tflo6AUH4

    Server side with PHP and Laravel:

    Create app specific shared secret

    Auto-renewable subscriptions with SwiftUI

    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

    Spread the love
  • My thoughts on SwiftUI

    My thoughts on SwiftUI

    I recently completed one medium size SwiftUI project and, it was a wonderful experience with MVVM architecture. I took some time to grab everything, but my struggles been paid off.

    I no longer want to do a new project on the storyboard. You should not do it either. No fear just start you will finish it soon.

    Storyboard was some improvements than single xib file, at least you did not have to open many tabs for xib which always slowed down Xcode. But it was always like this on Apple platform since Xcode 3.2, developing app means you have to handle xib file.

    I always liked Android’s XML way than a xib file on a Xcode.

    I never liked the Auto Layout either, but it always worked on all devices. But I hated it when I had to break a few constraints to add a few more to make space for a new TextField or Image. It has never been easy.

    The most useful thing about SwiftUI that how easy it is to create a view and reuse it anywhere if you can make it in that way. The @Binding and @State are really helpful.

    And the ViewModel updates a model which your view observes always. So my ViewModel has a property @Published whenever it changes its all observers will get the update by calling the property @ObservedObject or @StateObject.

    The SwiftUI is very new but it is the best thing for app developers on Apple platforms. I even converting few more apps in SwiftUI in the coming months.

    Download a demo login app I’m working on from github.

    Spread the love