iOS অ্যাপে ইন অ্যাপ পারচেস

SwiftUI অ্যাপে ইন অ্যাপ পারচেস যোগ করা খুব সহজ। তবে অনেক গুলো প্রসেস ফলো করতে হয়। প্রসেস গুলো নিয়েই লিখব এই আর্টিকেলে।

প্রজেক্ট সেটআপ

একটা নতুন প্রজেক্ট তৈরি করে নিব। বা আগের কোন প্রজেক্টেও কাজ করতে পারি। প্রজেক্টের Signing & Capabilities থেকে ইন অ্যাপ পারচেস ক্যাপাবিলিটি যোগ করতে হবে।

Targets > Signing & Capabilities > Capability এ গিয়ে In-App Purchase ক্যাপাবিলিটি যোগ করে নেওয়া যাবে।

ক্লিক করে যোগ করার পর আর কিছু করতে হবে না এখানে।

স্টোরকিট কনফিগারেশন ফাইল

ইন-অ্যাপ পারচেসের জন্য স্টোরকিট কনফিগারেশন ফাইল তৈরি করে নিতে হয়। এই ফাইল মূলত টেস্ট করার জন্য। লাইভ অ্যাপের ক্ষেত্রে প্রোডাক্ট বা সাবস্ক্রিপশন সিঙ্ক্রোনাইজ করে নেওয়া যায়। আবার লোকাল প্রোডাক্ট বা সাবস্ক্রিপশন তৈরি করা যায় টেস্ট এর জন্য। আমরা প্রথমে দেখব কিভাবে লোকাল প্রোডাক্ট তৈরি করা যায়।

এই জন্য এক্সকোডের ফাইল মেন্যু থেকে File > New > File from template এ ক্লিক করে StoreKit Configuration File তৈরি করে নিব।

এই ফাইলের যে কোন নাম দিতে পারব। টার্গেট হিসেবে প্রজেক্ট যেন সিলেক্ট করা থাকে, তা দেখে নিব।

অ্যাপ স্টোর কানেক্টে ইন অ্যাপ পার্সেচ প্রোডাক্ট যোগ করা থাকলে Sync this file with an app in App Store Connect এ ক্লিক করলে ঐ অ্যাপের আন্ডারে যদি কোন ইন অ্যাপ পার্সেচ তৈরি করা থাকে, সেগুলো সিংক্রোনাইজ হয়ে যাবে।

সদ্য তৈরি করা .storekit ফাইলে ক্লিক করে ওপেন করব। নিচের দিকে বাম কোনায় ছোট্ট একটা প্লাস আইকন দেখতে পাবো। এখানে ক্লিক করে আমরা বিভিন্ন ধরণের ইন-অ্যাপ পার্সেচ যোগ করে নিতে পারব। কোনটা কিসের জন্য ব্যবহার করব, তা নিয়ে বিস্তারিত লেখার শেষে যোগ করে দিব। এই উদাহরণের জন্য আমরা Add Non-Consumable In-App Purchase বাটনে ক্লিক করে Non-Consumable In-App Purchase প্রোডাক্ট যোগ করে নিব।

এরপর এই প্রোডাক্টের নাম, প্রোডাক্ট আইডি (সাধারণত বান্ডেল আইডি + প্রোডাক্টের নাম), প্রাইস, লোকালাইজেশন ইত্যাদি যোগ করব।

এগুলো যোগ করার পর আমাদের কনফিগারেশন শেষ। এবার আমরা প্রজেক্টে এই প্রোডাক্ট দেখাতে পারব। তার আগে দেখে নেই কিভাবে অ্যাপ স্টোর কানেক্টে ইন অ্যাপ পার্চেস প্রোডাক্ট যোগ করা যায়।

অ্যাপ স্টোর কানেক্টে ইন অ্যাপ পার্চেস প্রোডাক্টঃ

যে অ্যাপে ইন-অ্যাপ পার্চেস যোগ করব, তার নিচের দিকে in-App Purchases অপশন পাবো। এখানে + আইকনে ক্লিক করলে নতুন প্রোডাক্ট যোগ করতে পারব। যদিও টেস্ট করার জন্য এই স্টেপের দরকার নেই। অ্যাপে সাবমিট করার ক্ষেত্রে দরকার হবে। যদিও লোকাল স্টোরকিট ফাইলও সাবমিট করা যাবে।

লোকাল স্টোর কিটের মতই এখানেও একই ধরণের তথ্য দিয়ে প্রোডাক্ট তৈরি করতে হবে। সব গুলো ফিল্ড যদি না পূরণ করি, এবং Ready for Submit স্ট্যাটাস যদি না দেখায়, তাহলে এক্সকোডের স্টোরকিট ফাইলের সাথে সিংক্রোনাইজ হবে না।

অ্যাপে প্রোডাক্ট দেখানো

অ্যাপে প্রোডাক্ট দেখানো অনেক সহজ। অনেক। ProductView এর ভেতর প্রোডাক্ট আইডি পাস করলেই হবে। লোকাল স্টোরকিটের আইডি হোক বা অ্যাপ স্টোর কানেক্টে তৈরি প্রোডাক্ট হোক, এতটুকু কোড লিখলেই হবেঃ

import SwiftUI
import StoreKit

struct ContentView: View {
    var body: some View {
        
        ProductView(id: "com.example.appdemo.pro")
        
    }
}
#Preview {
    ContentView()
}

রান করে দেখুন। এখনো দেখাচ্ছে না তাই না? এর কারণ হচ্ছে রান করার সময় রান কনফিগারেশনে স্টোরকিট ফাইলটা দেখিয়ে দিতে হবে।

তার জন্য এক্সকোডের উপড়ে স্ট্যাটাসবারে প্রজেক্টের উপর ক্লিক করলে Edit Scheme অপশন দেখাবে। এরপর এখানে ক্লিক করব। তাহলে নিচের মত অপশন আসবেঃ

এখানে StoreKit Configuration হিসেবে লোকাল স্টোরকিট ফাইলটা দেখিয়ে দিতে হবে। ডিভাগের ক্ষেত্রে লাইভ প্রোডাক্ট দেখাবে না। এই জন্য লোকাল স্টোরকিট থেকে ডামি প্রোডাক্ট দেখানোর জন্যই এই স্টেপ।

সব কিছু ঠিকঠাক মত করতে পারলে নিচের মত করে দেখতে পাবোঃ

প্রোডাক্টের একটা বাটন তৈরি করে দিবে। এরপর ঐ বাটনে ক্লিক করলে উপরের মত কেনার অপশন দিবে। এমনকি Purchase বাটনে ক্লিক করে কিনতেও পারব। টাকা কাটবে না।

একটা ইউজার ইন-অ্যাপ পার্সেচ করার উপর ভিত্তি করে আমরা কোন কন্টেন্ট দেখাবো। তার জন্য আমাদের যাচাই করে নিতে হবে ইউজার নির্দিষ্ট প্রোডাক্টটা কিনেছে কিনা। এর জন্য আমরা একটা ObservableObject তৈরি করে নিব।

import Foundation
import StoreKit

class StoreManager: ObservableObject {
    @Published var purchasedProducts: Set<String> = []
    
    init() {
        Task {
            await fetchPurchasedProducts()
        }
    }
    
    @MainActor
    func fetchPurchasedProducts() async {
        // Get all the transactions for the app
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                // Add the product identifier to the purchased set
                purchasedProducts.insert(transaction.productID)
            }
        }
    }
}

এবার নির্দিষ্ট প্রোডাক্ট পার্চেসের উপর ভিত্তি করে কন্টেন্ট দেখাতে পারি এভাবেঃ

import SwiftUI
import StoreKit
 
struct ContentView: View {
    @StateObject var storeManager = StoreManager()
     
    var body: some View {
        VStack {
            if storeManager.purchasedProducts.contains("com.example.appdemo.pro") {
                Text("Congratulations! You are enjoying pro feature of the App.")
            } else {
                ProductView(id: "com.example.appdemo.pro")
            }
        }
    }
}
 
#Preview {
    ContentView()
}

ম্যানুয়ালি প্রোডাক্ট দেখানো

ProductView শুধু মাত্র iOS 17+ এর ক্ষেতে কাজ করবে। এর নিচের ভার্সনের iOS টার্গেট করলে আমাদের ম্যানুয়ালি প্রোডাক্ট গুলো দেখাতে হবে। তার জন্য প্রথমে প্রোডাক্ট ফেচ করে নিতে হবে। আমরা একটা StoreManager তৈরি করে নিবঃ

import Foundation
import StoreKit

class StoreManager: ObservableObject {
    
    // product you have added on .storekit file or on App Store Connect
    private var productIDs = ["com.example.appdemo.pro"]
    
    // fetched products array
    @Published var products = [Product]()
  
    init() {
        Task {
            await requestProducts()
        }
    }

    @MainActor
    func requestProducts() async {
        do {
            products = try await Product.products(for: productIDs)
        } catch {
            print(error.localizedDescription)
    }
  }
}

এবার UI তে এভাবে দেখাতে পারবঃ

import SwiftUI
import StoreKit

struct ContentView: View {
    @StateObject var storeManager = StoreManager()
    
    var body: some View {
        VStack {
            ForEach(storeManager.products, id: \.id) { product in
                
                Text(product.displayName)
                    .font(.title)
                Text(product.description)
                Button("\(product.displayPrice)") {
                    // handle purchase
                }
            }
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

ProductView পার্চেসও ম্যানেজ করত। কিন্তু ম্যানুয়ালি দেখানোর ক্ষেত্রে পার্চেসও আমাদের হ্যান্ডেল করতে হবে। StoreManager এ নিচের মেথড যোগ করিঃ

    @MainActor
    func purchase(_ product: Product) async throws -> Transaction? {
        
        let result = try await product.purchase()
        switch result {
        case .success(.verified(let transaction)):
            purchasedProducts.insert(product.id)
            await transaction.finish()
            return transaction
        case .userCancelled, .pending:
            return nil
        default:
            return nil
        }
    }

UI তে বাটনে পার্চেস মেথড এভাবে কল করতে পারিঃ

Button("\(product.displayPrice)") {
                    Task {
                        try await storeManager.purchase(product)
                    }
                }

পার্চেস করার পর ঐটা ভেরিফাইড পার্চেস কিনা, তাও যাচাই করে নিতে হবে। সব কিছু একত্রেঃ

import Foundation
import StoreKit
 

class StoreManager: ObservableObject {
     
    private var updateListenerTask: Task<Void, Error>? = nil
     
    // product you have added on .storekit file or on App Store Connect
    private var productIDs = ["com.example.appdemo.pro"]
     
    @Published var purchasedProducts: Set<String> = []
     
    // fetched products array
    @Published var products = [Product]()
     
    init() {
        // Start a transaction listener as soon as the app launches.
        updateListenerTask = listenForTransactions()
        Task {
            await fetchPurchasedProducts()
            await requestProducts()
        }
    }
     
    deinit {
        updateListenerTask?.cancel()
    }
     
     
    @MainActor
    func requestProducts() async {
        do {
            products = try await Product.products(for: productIDs)
        } catch {
            print(error.localizedDescription)
        }
    }
     
    @MainActor
    func purchase(_ product: Product) async throws -> Transaction? {
        // 1:
        let result =
        try await product.purchase()
        switch result {
        case .success(.verified(let transaction)):
            purchasedProducts.insert(product.id)
            await transaction.finish()
            return transaction
        default:
            return nil
        }
    }
     
     
    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            for await result in Transaction.updates {
                switch result {
                case .verified(let transaction):
                    // Handle successful transactions
                    await self.handle(transaction)
                case .unverified(_, _):
                    // Handle failed transactions
                    break
                }
            }
        }
    }
     
     
    @MainActor
    private func handle(_ transaction: Transaction) async {
        purchasedProducts.insert(transaction.productID)
        await transaction.finish()
    }
     
     
    @MainActor
    func fetchPurchasedProducts() async {
         
        // Get all the transactions for the app
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result {
                // Add the product identifier to the purchased set
                purchasedProducts.insert(transaction.productID)
            }
        }
    }
     
}

UI তে এভাবে দেখতে পারিঃ

import SwiftUI
import StoreKit

struct ContentView: View {
    @StateObject var storeManager = StoreManager()
    
    var body: some View {

        if storeManager.purchasedProducts.contains("com.example.appdemo.pro") {
            Text("Congratulations! You are enjoying pro feature of the App.")
        } else {

            ForEach(storeManager.products, id: \.id) { product in
                
                Text(product.displayName)
                    .font(.title)
                Text(product.description)
                Button("\(product.displayPrice)") {
                    Task {
                        try await storeManager.purchase(product)
                    }
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

এই তো। এবার যে কোন অ্যাপে আমরা ইন-অ্যাপ পার্সেচ যোগ করতে পারব। উপরে আমরা শুধুমাত্র Non-Consumable প্রোডাক্ট তৈরি করেছি। যা সাধারণত লাইফ টাইম পার্চেস বা যার মেয়াদ সারা জীবন থাকে। নিচে বিভিন্ন ইন-অ্যাপ পার্চেস সম্পর্কে বিস্তারিত লিখছিঃ

Consumable

  • একবার ব্যবহার করলে শেষ হয়ে যায়।
  • বারবার কেনা যায়।
  • অ্যাপ রিস্টোর করলে আবার কিনতে হয়।

উদাহরণ: গেমের জন্য ১০০টি কয়েন, এক্সট্রা লাইফ, বা হিন্ট।

Non-Consumable

  • একবার কেনার পর সবসময় ব্যবহার করা যায়।
  • অ্যাপ রিস্টোর করলে আবার পাওয়া যায়।

উদাহরণ: অ্যাপের প্রিমিয়াম ফিচার আনলক।

Auto-Renewable Subscription

  • নির্দিষ্ট সময় পরপর চার্জ হয় (মাসিক/বার্ষিক ইত্যাদি)।
  • অটো রিনিউ হয় যতক্ষণ না ব্যবহারকারী বন্ধ করে দেয়।

উদাহরণ: Netflix, Apple Music, বা একটি নিউজ অ্যাপের প্রিমিয়াম সাবস্ক্রিপশন।

Non-Renewing Subscription

  • নির্দিষ্ট সময়ের জন্য অ্যাক্সেস দেয়।
  • মেয়াদ শেষ হলে আবার কিনতে হয়, অটোমেটিক রিনিউ হয় না।

উদাহরণ: তিন মাসের কোর্স সাবস্ক্রিপশন বা সিজনাল গেম পাস।

প্রয়োজন অনুযায়ী উপরের যে মডেল আপনার অ্যাপের জন্য ভালো হয়, সেই মডেলের ইন-অ্যাপ পার্চেস যোগ করতে পারেন। কোন সমস্যায় পড়লে চ্যাটজিপিটি সহ বিভিন্ন AI এসিস্টেন্ট তো আছেই।

SwiftUI ব্যবহার করে iOS অ্যাপ ডেভেলপমেন্টের জন্য একটা বই লিখেছি। যার ইবুক ভার্সন এখান থেকে কেনা যাবেঃ প্র্যাক্টিক্যাল SwiftUI ই-বুক

Leave a Comment