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

11

25

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?

Baier answered 22/8, 2017 at 19:1 Comment(3)
No. You can get information from the receipt that indicates whether the subscription is currently in the free trial period, but not the free trial lengthMisinform
ah ok, thanks. got it.Baier
github.com/bizz84/SwiftyStoreKit/issues/85 OR You can use SwiftyStoreKitMagnify
S
32

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
        }
    }
}
Superordinate answered 3/7, 2019 at 12:20 Comment(2)
The most elegant solution as it properly takes into account localization.Dissatisfied
Nice, but this will give a string in the language of the device. If the app is not localized in that language you will end up with UI text in two languages. To solve this replace dateComponents.calendar = Calendar.current with let interfaceLanguage = Bundle.preferredLocalizations(from:Bundle.main.localizations).first; let locale = Locale(identifier: interfaceLanguage ?? "en"); dateComponents.calendar = locale.calendar;Deduction
E
20

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 ""
    }
}
Esparza answered 30/5, 2018 at 12:21 Comment(0)
W
12

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)

Weeper answered 2/8, 2018 at 15:55 Comment(2)
Great answer. If you want to localise, use stringdicts instead of plural variable : developer.apple.com/library/archive/documentation/MacOSX/…Whitebook
Man, it works for English only. Each language may have its own endings. For example in Russian you use different endings for 1, 2-4 and 5+ unit count and 5+ "years" is replaced with another word at all.Glynisglynn
A
2

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.

Ashley answered 16/4, 2019 at 10:47 Comment(0)
S
1

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

Sophey answered 8/8, 2021 at 8:47 Comment(0)
N
0

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.

Nasho answered 11/10, 2017 at 23:1 Comment(0)
O
0

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.

Overdone answered 6/1, 2018 at 15:57 Comment(0)
D
0

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
Displayed answered 15/5, 2020 at 15:21 Comment(0)
V
0
+ (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;
}
Volant answered 2/12, 2020 at 18:41 Comment(0)
E
0

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;

}

Entoblast answered 13/12, 2020 at 21:49 Comment(0)
J
-1

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)"
    }
}
Jemie answered 13/12, 2020 at 14:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.