mjpablo23
mjpablo23

Reputation: 731

iOS in-app purchase subscription get free trial period length from SKProduct

I am working on in-app purchases with subscriptions. In swift, you can get price and price locale from the SKProduct like so:

weeklyProduct.price.doubleValue  
weeklyProduct.priceLocale.currencySymbol

where weeklyProduct is a SKProduct.

Is it possible to get the free trial length? For example, I specified a two week free trial for the product. can I get this from the SKProduct?

Upvotes: 26

Views: 16241

Answers (11)

Manabu Nakazawa
Manabu Nakazawa

Reputation: 2365

If you use SwiftyStoreKit, localizedSubscriptionPeriod is the easiest way

import SwiftyStoreKit

product.introductoryPrice?.localizedSubscriptionPeriod // "1 week"

This is the implementation: https://github.com/bizz84/SwiftyStoreKit/blob/master/Sources/SwiftyStoreKit/SKProductDiscount+LocalizedPrice.swift

Upvotes: 1

Ofir Malachi
Ofir Malachi

Reputation: 1286

OBJECTIVE C

#import "SKProduct+SKProduct.h"

-(NSString*_Nullable)localizedTrialDuraion{

if (@available(iOS 11.2, *)) {
    
    NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init];
    [formatter setUnitsStyle:NSDateComponentsFormatterUnitsStyleFull]; //e.g 1 month
    formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorDropAll;
    NSDateComponents * dateComponents = [[NSDateComponents alloc]init];
    [dateComponents setCalendar:[NSCalendar currentCalendar]];
    
    switch (self.introductoryPrice.subscriptionPeriod.unit) {
        case SKProductPeriodUnitDay:{
            formatter.allowedUnits = NSCalendarUnitDay;
            [dateComponents setDay:self.introductoryPrice.subscriptionPeriod.numberOfUnits];
            break;
        }
        case SKProductPeriodUnitWeek:{
            formatter.allowedUnits = NSCalendarUnitWeekOfMonth;
            [dateComponents setWeekOfMonth:self.introductoryPrice.subscriptionPeriod.numberOfUnits];
            break;
        }
        case SKProductPeriodUnitMonth:{
            formatter.allowedUnits = NSCalendarUnitMonth;
            [dateComponents setMonth:self.introductoryPrice.subscriptionPeriod.numberOfUnits];
            break;
        }
        case SKProductPeriodUnitYear:{
            formatter.allowedUnits = NSCalendarUnitYear;
            [dateComponents setYear:self.introductoryPrice.subscriptionPeriod.numberOfUnits];
            break;
        }
        default:{
            return nil;
            break;
        }
            break;
    }
    [dateComponents setValue:self.introductoryPrice.subscriptionPeriod.numberOfUnits forComponent:formatter.allowedUnits];
    return [formatter stringFromDateComponents:dateComponents];
} else {
    // Fallback on earlier versions
}

return nil;

}

Upvotes: 0

bodich
bodich

Reputation: 2235

Here is more compact and short in use version for swift 5, extending SKProductSubscriptionPeriod

Usage:

print("\(period.localizedDescription) free trial")

//Printed example "1 week free trial"

Implementation:

extension SKProductSubscriptionPeriod {
    public var localizedDescription: String {
        let period:String = {
            switch self.unit {
            case .day: return "day"
            case .week: return "week"
            case .month: return "month"
            case .year: return "year"
            @unknown default:
                return "unknown period"
            }
        }()
    
        let plural = numberOfUnits > 1 ? "s" : ""
        return "\(numberOfUnits) \(period)\(plural)"
    }
}

Upvotes: -1

Roman Solodyashkin
Roman Solodyashkin

Reputation: 829

+ (NSString*)localizedTitleForSKPeriod:(SKProductSubscriptionPeriod*)period{
    NSDateComponents *comps = [NSDateComponents new];
    NSDateComponentsFormatter *fmt = [NSDateComponentsFormatter new];
    switch (period.unit) {
        case SKProductPeriodUnitDay:{
            fmt.allowedUnits = NSCalendarUnitDay;
            comps.day = period.numberOfUnits;
        }break;
        case SKProductPeriodUnitWeek:{
            fmt.allowedUnits = NSCalendarUnitWeekOfMonth;
            comps.weekOfMonth = period.numberOfUnits;
        }break;
        case SKProductPeriodUnitMonth:{
            fmt.allowedUnits = NSCalendarUnitMonth;
            comps.month = period.numberOfUnits;
        }break;
        case SKProductPeriodUnitYear: {
            fmt.allowedUnits = NSCalendarUnitYear;
            comps.year = period.numberOfUnits;
        }break;
    }
    // 1 Day, 1 Week, 2 Weeks, 1 Month, 2 Months, 3 Months, 6 Months, 1 Year
    fmt.unitsStyle = NSDateComponentsFormatterUnitsStyleFull;
    // One Day, One Week, Two Weeks, etc
    //fmt.unitsStyle = NSDateComponentsFormatterUnitsStyleSpellOut;
    NSString *s = [[fmt stringFromDateComponents:comps] capitalizedString];
    return s;
}

Upvotes: 0

Ty Lertwichaiworawit
Ty Lertwichaiworawit

Reputation: 2950

Swift 5

Using Eslam's and Scott's answers as inspiration:

import StoreKit

extension SKProduct {
    func priceString() -> String {
        let period:String = {
            switch self.subscriptionPeriod?.unit {
            case .day: return "day"
            case .week: return "week"
            case .month: return "month"
            case .year: return "year"
            case .none: return ""
            case .some(_): return ""
            }
        }()

        let price = self.localizedPrice!
        let numUnits = self.subscriptionPeriod?.numberOfUnits ?? 0
        let plural = numUnits > 1 ? "s" : ""
        return String(format: "%@ for %d %@%@", arguments: [price, numUnits, period, plural])
    }
}

To use:

let price = product.priceString()
print(price)

Result:

THB 89.00 for 7 days
THB 149.00 for 1 month

Upvotes: 0

Scott Wood
Scott Wood

Reputation: 424

Using Eslam's answer as inspiration I created an extension to SKProduct.PeriodUnit

extension SKProduct.PeriodUnit {
    func description(capitalizeFirstLetter: Bool = false, numberOfUnits: Int? = nil) -> String {
        let period:String = {
            switch self {
            case .day: return "day"
            case .week: return "week"
            case .month: return "month"
            case .year: return "year"
            }
        }()

        var numUnits = ""
        var plural = ""
        if let numberOfUnits = numberOfUnits {
            numUnits = "\(numberOfUnits) " // Add space for formatting
            plural = numberOfUnits > 1 ? "s" : ""
        }
        return "\(numUnits)\(capitalizeFirstLetter ? period.capitalized : period)\(plural)"
    }
}

To use:

if #available(iOS 11.2, *),
    let period = prod?.introductoryPrice?.subscriptionPeriod
{
    let desc = period.unit.description(capitalizeFirstLetter: true, numberOfUnits: period.numberOfUnits)
} else {
    // Fallback
}

This will create a nicely formatted string (e.g. 1 day, 1 Week, 2 months, 2 Years)

Upvotes: 12

Roel van der Kraan
Roel van der Kraan

Reputation: 521

I've solved it using DateComponentsFormatter, that saves you a lot of time localizing in different languages and handling plurals and whatnot. This might seem like a lot of code, but I hope it will save me time in the future.

import Foundation

class PeriodFormatter {
    static var componentFormatter: DateComponentsFormatter {
        let formatter = DateComponentsFormatter()
        formatter.maximumUnitCount = 1
        formatter.unitsStyle = .full
        formatter.zeroFormattingBehavior = .dropAll
        return formatter
    }

    static func format(unit: NSCalendar.Unit, numberOfUnits: Int) -> String? {
        var dateComponents = DateComponents()
        dateComponents.calendar = Calendar.current
        componentFormatter.allowedUnits = [unit]
        switch unit {
        case .day:
            dateComponents.setValue(numberOfUnits, for: .day)
        case .weekOfMonth:
            dateComponents.setValue(numberOfUnits, for: .weekOfMonth)
        case .month:
            dateComponents.setValue(numberOfUnits, for: .month)
        case .year:
            dateComponents.setValue(numberOfUnits, for: .year)
        default:
            return nil
        }

        return componentFormatter.string(from: dateComponents)
    }
}

It requires to convert the SKProduct period unit into a NSCalendarUnit

import StoreKit

@available(iOS 11.2, *)
extension SKProduct.PeriodUnit {
    func toCalendarUnit() -> NSCalendar.Unit {
        switch self {
        case .day:
            return .day
        case .month:
            return .month
        case .week:
            return .weekOfMonth
        case .year:
            return .year
        @unknown default:
            debugPrint("Unknown period unit")
        }
        return .day
    }
}

And you can call it from a SubscriptionPeriod like this:

import StoreKit

@available(iOS 11.2, *)
extension SKProductSubscriptionPeriod {
    func localizedPeriod() -> String? {
        return PeriodFormatter.format(unit: unit.toCalendarUnit(), numberOfUnits: numberOfUnits)
    }
}

Which you can in turn call from a SKProductDiscount like so. Please note I didn't implement the other PaymentModes for now.

import StoreKit

@available(iOS 11.2, *)
extension SKProductDiscount {
    func localizedDiscount() -> String? {
        switch paymentMode {
        case PaymentMode.freeTrial:
            return "Free trial for \(subscriptionPeriod.localizedPeriod() ?? "a period")"
        default:
            return nil
        }
    }
}

Upvotes: 34

kakubei
kakubei

Reputation: 5400

Nice one @scott Wood. I would make it a property of SKProduct.PeriodUnit instead of a function. That would keep the behaviour more consistent with enums:

@available(iOS 11.2, *)
extension SKProduct.PeriodUnit {

    var description: String {
        switch self {
        case .day: return "day"
        case .week: return "week"
        case .month: return "month"
        case .year: return "year"
        // support for future values
        default:
            return "N/A"
        }
    }

    func pluralisedDescription(length: Int) -> String {
        let lengthAndDescription = length.description + " " + self.description
        let plural = length > 1 ?  lengthAndDescription + "s" : lengthAndDescription
        return plural
    }
}

And then a function to return the plural, based on the description property.

And yes, as someone else pointed out, you should localise the plurals if your app is available in other languages.

Upvotes: 2

Binshakerr
Binshakerr

Reputation: 209

You can get it, but as mentioned above it works only starting from iOS 11.2, for other versions you'll have to get it from your server via API.

Here is an example code that I've used:

if #available(iOS 11.2, *) {
  if let period = prod.introductoryPrice?.subscriptionPeriod {
     print("Start your \(period.numberOfUnits) \(unitName(unitRawValue: period.unit.rawValue)) free trial")
  }
} else {
  // Fallback on earlier versions
  // Get it from your server via API
}

func unitName(unitRawValue:UInt) -> String {
    switch unitRawValue {
    case 0: return "days"
    case 1: return "weeks"
    case 2: return "months"
    case 3: return "years"
    default: return ""
    }
}

Upvotes: 20

DanSkeel
DanSkeel

Reputation: 4005

Starting from iOS 11.2 you can get info about trials using introductoryPrice property of SKProduct.

It contains instance of SKProductDiscount class, which describes all discount periods including free trials.

Upvotes: 0

Garrett Cox
Garrett Cox

Reputation: 713

Trial length is not included in the SKProduct information and will have to be hardcoded into the app or stored on your server. The only available option for deriving this type of information (currently) is from the receipt itself.

Upvotes: 0

Related Questions