GithubHelp home page GithubHelp logo

russell-archer / storehelper Goto Github PK

View Code? Open in Web Editor NEW
392.0 9.0 47.0 201.2 MB

Implementing In-App Purchases with StoreKit2 in Xcode 13 - 15 using SwiftUI, Swift 5.7 - 5.9, iOS 15 - 17 and macOS 12 - 14. Also supports tvOS and visionOS.

License: MIT License

Swift 100.00%
swiftui storekit ios15 in-app-purchases swift swift5 macos12 ios macos xcode

storehelper's Introduction

I’m a software developer based in London and Sevenoaks, UK, where I’m currently creating iOS and macOS apps with Swift, SwiftUI and UIKit. I have over 35 years experience in the industry, in many different roles and with a range of technologies.

storehelper's People

Contributors

aehlke avatar chrmod avatar emilpedersen avatar hengyu avatar russell-archer avatar youkchansim avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

storehelper's Issues

isPurchased() can't be called right after start()

I'm trying to determine if the user has purchased a subscription, e.g. on a previous run of the app. So I call:

            await storeHelper.start()
            
            // Update our knowledge of what products are purchased
            do {
                try await storeHelper.isPurchased(productId: PRO_PRODUCT_ID)

However, start() starts an async task, and isAppStoreAvailable isn't true until the task has finished, meaning that isPurchased fails right away, because isAppStoreAvailable is false.

I need to know if the subscription has been purchased to show or hide a feature in the UI without waiting for a network request, so I make these calls at app startup so that the purchase will be cached in purchasedProductsFallback, which I can check quickly.

What's the right way to do this with StoreHelper?

isPurchased with subscriptions

Hi,

I wanted to ask/suggest what happens to the function isPurchased when dealing with a subscription that may be cancelled or/and expired. The desirable behaviour is that isPurchased would return true when the subscription is autorenewing normally, or when it has been cancelled but still hasn't reached it's expiration date. And false otherwise, that is to say, when the subscribed content should be no longer available to the user. I haven't seen any status confirmation in the function, so I assume it returns true if it has been purchased at any time, even if it has been cancelled and expired.

Am I correct? If I am, what would be an optimal way to check this circumstance? The idea is to have a function that returns true if the user should have access to that premium content and false otherwise.

Thank you.

Compile Issue

Hi,

XCode is throwing a compilation error:

Missing arguments for parameters 'content', 'publisher', 'action' in call

This from the MainView on the following line from the demo app:

NavigationLink(destination: SubscriptionView()) { Text("Subscriptions").font(.largeTitle).padding()}

SubscriptionView does not appear to take any parameters.

C

Prepare for Swift 6.0

Swift 6.0 is expected to be released with Xcode 16 in September 2024. The swift6-support branch will be used to prepare StoreHelper for some major changes that will be required with regards to concurrency.

Updated documentation regarding restore purchases [suggestion]

First off, thanks for this amazing project.
I'm using this project already in one app that is live at the App Store and the first build was rejected by the reviewer because I didn't had a restore purchases button. I didn't put that button because I read in the documentation that with StoreKit 2 it's not required to have that button.
Maybe you should updated the project document regarding that topic, because without a restore purchases button apps are being rejected. Also read on twitter about other people with apps rejected because of the same reason.

To restore I'm just using:
StoreKit.sync()

don't know if there's something more appropriate...

Thanks again for your work in the project!

Question about localized content

Hiya. Have you had experience with localized content ?

I have localized my in app purchase item in App Store connect but am yet to see them in the other languages , even when using via testFlight and switching languages.

Is there something I'm missing ? Or will I only see them when I update to the appStore ?

Thanks

Compile errors

Seeing this when building with this package in my project:

/Users/ray/Library/Developer/Xcode/DerivedData/Foo-dbwvioleuojlekfcjzuhvmapqlxu/SourcePackages/checkouts/StoreHelper/Sources/StoreHelper/Views/Shared/TermsOfServiceView.swift:20:38: error: variable binding in a condition requires an initializer
            if let termsOfServiceUrl { Link("Terms of Service", destination: termsOfServiceUrl) }
                                     ^
/Users/ray/Library/Developer/Xcode/DerivedData/Foo-dbwvioleuojlekfcjzuhvmapqlxu/SourcePackages/checkouts/StoreHelper/Sources/StoreHelper/Views/Shared/TermsOfServiceView.swift:22:37: error: variable binding in a condition requires an initializer
            if let privacyPolicyUrl { Link("Privacy Policy", destination: privacyPolicyUrl) }
                                    ^
/Users/ray/Library/Developer/Xcode/DerivedData/Foo-dbwvioleuojlekfcjzuhvmapqlxu/SourcePackages/checkouts/StoreHelper/Sources/StoreHelper/Views/Shared/TermsOfServiceView.swift:28:34: error: variable binding in a condition requires an initializer
            if let termsOfService, let tosUrl = URL(string: termsOfService) { termsOfServiceUrl = tosUrl }
                                 ^
/Users/ray/Library/Developer/Xcode/DerivedData/Foo-dbwvioleuojlekfcjzuhvmapqlxu/SourcePackages/checkouts/StoreHelper/Sources/StoreHelper/Views/Shared/TermsOfServiceView.swift:29:33: error: variable binding in a condition requires an initializer
            if let privacyPolicy, let ppUrl = URL(string: privacyPolicy) { privacyPolicyUrl =  ppUrl }

"Writerly" hard-coded into StoreHelper

In the Redeem an Offer section of Manage Purchases I get "Have an offer code for Writerly? Tap "Redeem an Offer" to...

This should be a configurable param rather than a string hard-coded into the package.

How to customize Product View

this package provider a Products list view, but I need a simpler view for only one subscription, just like Apple Docs:

image

so I write a view using PurchaseButton:

    var body: some View {
        if storeHelper.hasSubscriptionProducts, let subscriptions = storeHelper.subscriptionProducts {
            ForEach(subscriptions, id: \.id) { subscription in
                VStack {
                    Text(subscription.displayName)
                    Text(subscription.description)
                    PurchaseButton(purchaseState: $purchaseState, productId: productId, price: subscription.displayPrice)
                }
            }
        }
    }

but error:

'PurchaseButton' initializer is inaccessible due to 'internal' protection level

image

so, how to customize Product View?

Certificate issue on device

iOS 16.4 (though same problem with iOS 16.3), Xcode 14.2 and 14.3.

Doing a non-consumable in-app purchase.
Code was running fine a few months ago, and recently started on an update, and encounter this issue. Running on simulator works as before - purchase validates and updates just fine. Running on a device (iPhone 12 Pro with 16.3 or 16.4) fails validation with:

2023-04-01 00:16:39.288226-0700 XXXXX[27827:2726419] [Default] Failed to verify certificate chain due to client recoverable failure:
Error Domain=NSOSStatusErrorDomain Code=-67843 "“StoreKit Testing in Xcode” certificate is not trusted" UserInfo={NSLocalizedDescription=“StoreKit Testing in Xcode” certificate is not trusted, NSUnderlyingError=0x28259a550 {Error Domain=NSOSStatusErrorDomain Code=-67843 "Certificate 0 “StoreKit Testing in Xcode” has errors: Root is not trusted;" UserInfo={NSLocalizedDescription=Certificate 0 “StoreKit Testing in Xcode” has errors: Root is not trusted;}}}
2023-04-01 00:16:39.289279-0700 XXXXXX[27827:2726418] [Default] Failed to verify signature for Transaction, will assume invalid: failedToVerifyCertificateChain

An issue that unconditionally returns true in shouldAddStorePayment method

/// Lets us know a user initiated an in-app purchase direct from the App Store, rather than via the app itself.
/// This method is required if you have IAP promotions.
public func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
        // Return true to continue the transaction. If this IAP has been previously purchased it'll be picked up
        // by StoreKit1 and re-purchase prevented
        return true
}

Hi~
I'm trying to call a custom purchase page without calling the purchase bottom sheet of the iOS.
But, since the above method is returning true, the purchase process of the system is called unconditionally.
A property that can return false is expected.

Or, it would be nice to be able to control it directly through a closure function.
Please review.
thank you.

Handling consumable example "Buy me a coffee"

Hiya,

is it possible to simply remove the restore button for a "buy me a coffee" situation ? I know this passes app review as I have done it before in UIKit with SwiftyStoreKit .

(edit : I think I understand now. I have copied the source package and made it editable)

Thanks for such. useful framework!

James

Subscriptions renewed when the MacOS app was closed are not picked up by Storekit2

I'm seeing a really strange behavior in Xcode 14.3 (14E222a, Ventura 13.3) for a MacOS app with auto-renewable subscriptions.

Scenario:

  1. Launch the app (local Xcode testing) and purchase a subscription -> works OK.
  2. Let the app run and after a while the subscription renews and StoreHelper picks up the renewal alright.
  3. Close the app and let the subscription renew a few times "in the background."
  4. When reopening the app, the latest transaction retrieved by a call to storeHelper.subscriptionHelper.subscriptionInfo(for: "Monthly") is the last one seen when the app was running. All the transactions happening while the app was closed are not there.
  5. The Storekit 1 call in AppStoreHelper is picking up the "ghost" transactions quite well, but it only passes down the stream the ProductID(s) associated with those transactions, which is quite useless without a proper subscriptionInfo object.

Am I the only one seeing this? Is this going to happen in production too, or is it just a bug in local testing?

How to disable logs?

Greeting from Ukraine! 🇺🇦

@russell-archer Thank you for this amazing project!

I just started to use it and while creating a SwiftUI views, in debug I see enormous logs like:

Request products from the App Store started
Request products from the App Store success
Request all products purchase status started
Request products from the App Store success
Request all products purchase status started
Request all products purchase status success
Request all products purchase status success

Which is really mixing with all other logs for my debug..

So I wonder, is there way to disable such logs from StoreHelper?
Or maybe I'm doing something wrong if even in views where I'm not using StoreHelper I still see such logs..

Auto-renew subscription eligibility

Hi,
in StoreDemoApp Product storekit file I changed the Gold subscription introductory offer to offer a 1 week free trial period.
I also changed the Subscription Renewal Rate of 1 month = 5 minutes to make local tests.

Running the app, Gold subscription buy product button caption correctly shows the 1 week free and I successfully bought it.

Then, without waiting the 5 minutes for the auto-renew, in the app I cancelled the subscription, and under Gold subscription I can see "Subscription cancelled. Will expire at..." with the date of expiring. So it knows that the trial period is still active.

But once the subscription has expired, under Gold I can see just a green mark. I expected to find some button saying "Renew" or "Buy" (obviously without the free trial because the user already used it and is no more eligible for the introductory offer...).

Is this normal? I mean, if a user cancels his subscription while in trial, at the end of the trial he should be able to buy the subscription again (without trial), or am I wrong?
Thanks

Screenshot 2023-06-20 alle 10 58 16

Subscription group

Hi,

I am encountering an issue retrieving subsection info. Since I already have my app on app store, there's not way for me to adopt the subscriptions Id to the give format (com.{developer}.subscription.{subscriptionGroupName}.{productName})
Isn't there an alternative way to get the info for a subscription group?

Products not loading fast enough ..

Hiya,

I have everything pretty much working with your framework now so thanks for your hard work, and great documentation.
I haven't been able load the products as any point in the life Cycle early enough to not get a "need to refresh message" showing for a second or two. Is this normal ?
And if so , would it be possible to change the message to a ProgressView instead?
I hacked one myself :) Which is more reassuring than the nasty red message :-)

Thanks very much

James

Use custom colors

Is it possible to use custom colors? I see you are using predefined colors like .primary and .secondary, i will need to customize those to match the colors of my app.

Question about using StoreHelper in Safari Extension project

Is it possible to use StoreHelper into a Xcode project created with SafariExtension multiplatform template?
Do you have some sample or guide (like the QuickStart) ?
In the meantime I'm trying it, but there are some discrepancies. For example, StoreHelper is present in Frameworks, Libraries... for iOS but not for MacOs. Before to going on I thought was better and safer to ask you.
Thanks

Could not fetch Products array (tvOS)

In requestProductsFromAppStore on tvOS, debug mode (not yet live on the App Store) I get a failure of requestProductsFailure either by having nil Products or an empty array of Products.

The same happens on watchOS production mode, while it works ok in debug.

I narrowed down the issue to the line try? await Product.products(for: productIds)

It seems that there's a bug in StoreKit2 🤔

Any thoughts?

AppStore.showManageSubscriptions() sync issue

Hi,

When using AppStore.showManageSubscriptions() to cancel or change subscriptions, I'm not getting any subsequent Transaction updates and no change in the isPurchased() status regardless of what I do in the subscription sheet. Tested in Simulator with 16.1 and a local Product store configuration file. Any guidance would be appreciated.

Thanks.

support Xcode 14 for iOS 15

image

https://developer.apple.com/support/app-store/

now 82% iOS users is using iOS 15, I have saw StoreHelper docs:

using Xcode 14 to easily add in-app purchase support to iOS 16/macOS 13 SwiftUI apps. Xcode 13 with iOS 15/macOS 12 are also supported.

I am using Xcode 14 on macOS 13, I try to download Xcode 13, but run failed.

image

image

so I have to use Xcode 14 to develop app for iOS 15, does StoreHelper support it?

how to check subscribed on first View

I want show content or notice subscription on the App's first View, so I need check subscribed first.

but storeHelper.start() is a async function, so the storeHelper.subscriptionProductIds is empty, and can not check subscribed.

image

Missing products in production ready app

Hi, I have published my app with StoreHelper integration. During tests in dev mode, when I fetch information about the product all seem to work perfectly. Whenever I publish the app via TestFlight (so it's using the production version) the product information is not available. What am I missing here?

To mention, I have added Products.plist and Products.storekit to the bundle resources. Without this product info enabled, I can't properly display the subscription screen, which blocks me from being approved by Apple Review Team.

Should I provide a fallback screen with fixed information about the product (like price, and name) so Apple can review it and accept it? My first thoughts are that my subscription is not reviewed yet (in prod mode) that's why it's not fetched.

Appreciate any help.

Do we have any idea on the next release

Hey team,

The package includes lots of major changes since the first release. I'm wondering if we could create a new tag for the next release. Since it's a common way to use version management in the Swift Package Manager.

Best,
Hengyu

Xcode Reports missing update task?

Xcode is reporting this warning at runtime (in Debug mode):
Sources/StoreHelper/Core/StoreHelper.swift:361 Making a purchase without listening for transaction updates risks missing successful purchases. Create a Task to iterate Transaction.updates at launch.

Cancellation not detected

Hi,

I'm revisiting an issue that I thought had seemed to be resolved, specifically, cancelling auto-renewable subscriptions using either XCode->Storekit->Manage Transactions or AppStore.showManageSubscriptions() from within an app. I've tested this with the StoreHelperDemo app and with my own code, and both consistently fail for me.

I'm on iOS 16.2 and the latest version of StoreHelper. While I can correctly monitor subscription status using: Product.SubscriptionInfo.Status.updates, the various transactions in StoreHelper don't seem to pick this up consistently.

What I believe needs to happen is that Status updates need to be monitored and the StoreHelper caches updated appropriately... Something along the lines of:

`for await status in Product.SubscriptionInfo.Status.updates {

            let result: VerificationResult<Product.SubscriptionInfo.RenewalInfo> = status.renewalInfo
            switch result {
                case .unverified(let unverifiedTransaction, let error):
                    print("unverified")
                
                case .verified(let verifiedTransaction):
                    let productId = verifiedTransaction.currentProductID
                    print("\(pid) verified")
            
                   if status.state == .subscribed || status.state == .inGracePeriod {
                      print("status subscribed or in grace")
                  } else if status.state == .revoked || status.state == .expired {
                     print("status cancelled")
                     updatePurchasedIdentifiers(productId, purchased: false)
                 } 
            }                

}`

Thanks.

show two URL: Terms of Service, Privacy Policy

Guideline 3.1.2 - Business - Payments - Subscriptions

We noticed that your app did not meet all the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, section 3.8(b) of the Paid Applications agreement.

We were unable to find the following required information in your app's binary:

– A functional link to the Terms of Use (EULA)

– A functional link to the privacy policy

demo:

image

code:

var body: some View {
        if storeHelper.hasSubscriptionProducts, let subscriptions = storeHelper.subscriptionProducts {
            ForEach(subscriptions, id: \.id) { subscription in
                VStack {
                    Text("Upgrade to " + subscription.displayName).font(.title)
                    Text(subscription.description).font(.title2)
                    PurchaseButton(purchaseState: $purchaseState, productId: productId, price: subscription.displayPrice + " / " + period)
                }
                .padding()
                .task { await purchaseState(for: productId)}
                .onChange(of: storeHelper.purchasedProducts) { _ in
                    Task.init {
                        await purchaseState(for: productId)
                    }
                }
                .onAppear() {
                    self.productId = subscription.id
                    self.period = subscription.subscription?.subscriptionPeriod.unit.localizedDescription ?? ""
                    let periodValue = subscription.subscription?.subscriptionPeriod.value ?? 1
                    if periodValue != 1 {
                        // TODO Month to Months
                        self.period = String(periodValue) + " " + self.period
                    }
                }
            }
        }
        Text("[Terms of Service](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/) and [Privacy Policy](https://example.com/)")
        Button(action: {
            Task.init {
                try? await AppStore.sync()
                purchasesRestored = true
            }
        }) {
            Text(purchasesRestored ? "Purchases Restored" : "Restore Purchases")
        }
        .padding()
        .disabled(purchasesRestored)
    }

Any support of Privacy Manifest?

Privacy Updates for AppStore submission

Expected Behavior

Developers are responsible for all code included in their apps. At WWDC23, Apple introduced new privacy manifests and signatures for commonly used third-party SDKs and announced that developers will need to declare approved reasons for using a set of APIs in their app’s privacy manifest.

Current Behavior

The library doesn't provide Privacy Manifest file

Possible Solution

The library should provide Privacy Manifest if it collects information or access "Required Reasons API"

Additional Info

Screenshot 2024-03-08 at 09 26 02

Can't compile on visionOS

Hey!

Will there be support here for a visionOS?

I know it's totally new, but it's really what we would need for a future, so better to start adopting now..

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.