Commit Graph
33 Commits
Author SHA1 Message Date
Antonio Pallares 81aaf6a051 Offering hasPaywall getter (#4847)
* feat: add `hasPaywall` getter to `Offering`

Returns `true` if the offering has either a v1 or a v2 paywall

* docs: update some methods references changing `PaywallData` with "paywall", as `PaywallData` only refers to v1 paywalls

* docs: improve reference of `Offering`'s `paywall` and `paywallComponents` to mention the new `hasPaywall` property

* lint

* tests: add unit tests that check the `hasPaywall` property of `Offering`

* api tests: add `paywallComponents` and `hasPaywall` to SwiftAPITester
2025-03-04 12:09:53 +01:00
Facundo Menzella 6b5c430082 chore: Enable force_unwrapping for SwiftLint (#4820)
* enable force_unwrapping

* avoid force unwrap

* fixed all errors

* remove extra disable
2025-02-26 12:40:49 +01:00
Josh Holtz 3d86d7dbbb [Paywalls V2] Remove/replace PAYWALL_COMPONENTS compiler flag and fix OS/platform compile issues (#4727)
* We can't just remove PAYWALL_COMPONENT because we don't support macos and tv and things were failing

* Move PartialComponent outside of PaywallComponent extension

* Fix some lint errors

* Wrong thing

* Undid things i shouldn't have done

* Removed more

* Added comment

* Removed/fix some places for the check thingies

* RevenueCat module compiles on all the things

* RevenueCatUI compiles in all the platforms now

* Make all the things compile again

* Make work for visionOS

* Fix some platform and xcode version issues

* Fix failing spm ios 15 test

* Undo circleci changes

* Replace smooth with easeInOut

* Only run the thing i want

* Attempt to remove circular

* Explicit returns

* Does this pass

* This is it

* Swift 5.9 for this

* All tests again

* Undid public testing processor stuff

* Undo this too

* Swift compiler check out pill style circular

* Fixes from PR review

* Fix compile for mac
2025-01-29 11:53:25 -06:00
Cesar de la Vega 5b20a4deac Fixes double callbacks when using PaywallViewController (#4333) 2024-10-02 08:40:23 +02:00
Josh Holtz cc12dd93f8 [Paywall Components] Match properties and behaviors from dashboard (#4261)
* A mixture of changes while putting the demo together

* Doing some manual fixes for max image height

* Removed unused file and renamed struct

* Fixed lint

* Fix tests

* Image default value on component and not localization

* Fixed sample paywall

* A few more small fixes

* Fix lint
2024-09-26 14:43:11 -05:00
41d3541451 Paywalls with custom purchase and restore logic handlers (#3973)
## tl;dr

PaywallView has a new constructor that takes `performPurchase` and
`performRestore` blocks, which are called to preform purchasing/restore
directly by the customer's app when the purchase/restore buttons are
tapped on the paywall. This makes it possible to use RC Paywalls when
`Purchases` has been configured `.with(purchasesAreCompletedBy:
.myApp)`.

Example usage:
```swift
PaywallView(performPurchase: {
    var userCancelled = false
    var error: Error?
    
    // use StoreKit to perform purchase

    return (userCancelled: userCancelled, error: error)
}, performRestore: {
    var success = false
    var error: Error?

    // use StoreKit to perform restore

    return (success: success, error: error)
})
```

## Description

When a `PaywallView` is constructed, a new `PurchaseHandler` is created.
The `PurchaseHandler` (an internal RevenueCatUI class) is owned by the
`PaywallView`, and it is responsible for executing new purchases and
restores.

When a `PaywallView` is constructed **without** `performPurchase` and
`performRestore` blocks, the `PaywallView` creates a `PurchaseHandler`
capable of preforming purchases using RevenueCat. When a `PaywallView`
is constructed with `performPurchase` and `performRestore` blocks, it
can also make purchases using the customer-supplied closures.

The `PurchaseHandler` is invoked when the user taps the
`PurchaseButton`, calling `purchaseHandler.purchase(package:
self.selectedPackage.content)`, which branches to either the internal or
external purchase code, as defined by `purchasesAreCompletedBy`:

```swift
@MainActor
func purchase(package: Package) async throws {
    switch self.purchases.purchasesAreCompletedBy {
    case .revenueCat:
        try await performPurchase(package: package)
    case .myApp:
        try await performExternalPurchaseLogic(package: package)
    }
}
```


Purchase and Restore blocks can also be assigned for paywall footers:

```swift
MyAppDefinedPaywall()
    .paywallFooter(myAppPurchaseLogic: MyAppPurchaseLogic(performPurchase: { packageToPurchase in
        var userCancelled = false
        var error: Error?

        // use StoreKit to perform purchase

        return (userCancelled: userCancelled, error: error)
    }, performRestore: {
        var success = false
        var error: Error?

        // use StoreKit to perform restore

        return (success: success, error: error)
    }))
 ```

and via `.presentPaywallIfNeeded`:

```swift
MyAppDefinedPaywal()
.presentPaywallIfNeeded(requiredEntitlementIdentifier: "test",
myAppPurchaseLogic: MyAppPurchaseLogic(performPurchase: {
packageToPurchase in
        return (userCancelled: false, error: nil)
    }, performRestore: {
        return (success: true, error: nil)
    }))
```



## Notes

### Testing

A lot needs to be mocked, the purchase/restore blocks that are passed in need to be assigned directly to the `PurchaseHandler`, which is then passed in as part of a `PaywallConfiguration`, both of which are normally constructed internally in the PaywallView constructor.

```swift
    func testHandleExternalRestoreWithPurchaaseHandlers() throws {
        var completed = false
        var customRestoreCodeExecuted = false

        let purchasHandler = Self.externalPurchaseHandler { _ in
            return (userCancelled: true, error: nil)
        } performRestore: {
            customRestoreCodeExecuted = true
            return (success: true, error: nil)
        }

let config = PaywallViewConfiguration(purchaseHandler: purchasHandler)

        try PaywallView(configuration: config).addToHierarchy()

        Task {
            _ = try await purchasHandler.restorePurchases()
            completed = true
        }

        expect(completed).toEventually(beTrue())
        expect(customRestoreCodeExecuted) == true
    }
```

### Error Handling

For the external code blocks to be called, `purchasesAreCompletedBy` needs to be set to `.myApp` AND the blocks need to be defined. So we need to handle cases when these are not consistent:

1. If someone configures purchasesAreCompletedBy to `.myApp`, and then displays a PaywallView **without** purchase/restore handlers, the SDK will:
* Log an error when the PaywallView is constructed
* When the PaywallView is displayed, replace it with a big red banner when in debug mode, fatalError() in release mode.
* If you purchase directly via the purchase handler (which is an internal class but this is done for testing), it will throw.

2. If someone configures purchases to use `.revenueCat`, and then displays a PaywallView **with** purchase/restore handler, the SDK will:

* Log a warning when the PaywallView is constructed

Rationale:
In case 1, there could be no (acceptable) way to make a purchase, so this is very bad and it needs to be dealt with.

In case 2, they've over-specified the paywall, and while this might be confusing (why isn't my code being called??), it's not as problematic, and we want to make it easy for people to switch from using their purchase logic to our purchase logic.

These checks are made in PaywallView.swift, method `checkForConfigurationConsitency()`.

### UIKit

Not yet supported, will add in a follow-up PR, unlikely to be complicated.

### Paywall Footer and Paywall If Needed
The `performPurchase` and `performRestore` parameters for the paywall footer are contained in a struct rather than as loose parameters of closure types, because doing the latter would create a very poor experience with regards to code completion, where Xcode will always offer complete your code as a trailing closure, but will always use the first closure where the function signature matches.

The following illustrates the problem of using loose closure parameters:

The trailing closure that Xcode auto-creates here does _not_ get called for `performPurchase`, but rather `purchaseStarted`, because it also has a `package` as its input parameter, and comes first in the parameter list 😱.

https://github.com/RevenueCat/purchases-ios/assets/109382862/1f809e7c-b661-4844-89e3-fd846a029531

This is the problematic function signature:

```swift
public func paywallFooter(
        offering: Offering,
        condensed: Bool = false,
        fonts: PaywallFontProvider = DefaultPaywallFontProvider(),
        purchaseStarted: PurchaseOfPackageStartedHandler? = nil,
        purchaseCompleted: PurchaseOrRestoreCompletedHandler? = nil,
        purchaseCancelled: PurchaseCancelledHandler? = nil,
        restoreStarted: RestoreStartedHandler? = nil,
        restoreCompleted: PurchaseOrRestoreCompletedHandler? = nil,
        purchaseFailure: PurchaseFailureHandler? = nil,
        restoreFailure: PurchaseFailureHandler? = nil,
        performPurchase: PerformPurchase? = nil,
        performRestore: PerformRestore? = nil
    ) -> some View
 ```

To fix this, get rid of the two new parameters: 
```swift
        performPurchase: PerformPurchase? = nil,
        performRestore: PerformRestore? = nil
```
 
and embed them in a struct:

```swift
public struct MyAppPurchaseLogic {
    public let performPurchase: PerformPurchase
    public let performRestore: PerformRestore

    public init(performPurchase: @escaping PerformPurchase, performRestore: @escaping PerformRestore) {
        self.performPurchase = performPurchase
        self.performRestore = performRestore
    }
}
```

which we use as a last parameter:

```swift
        myAppPurchaseLogic: MyAppPurchaseLogic? = nil
```

 Now the code completes as so:


https://github.com/RevenueCat/purchases-ios/assets/109382862/332ac42d-ccba-42bc-bfc3-702d7870c99c

The `MyAppPurchaseLogic` doesn't complete in perfectly, but once you
initialize one of those it goes nicely the rest of the way, including
the error messages that instruct you on what you need to return.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>
Co-authored-by: Cesar de la Vega <cesarvegaro@gmail.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Will Taylor <wtaylor151@gmail.com>
Co-authored-by: Andy Boedo <andresboedo@gmail.com>
2024-07-05 12:05:44 -07:00
Toni Rico 7bab5e4ff8 Add option to intercept touch events in PaywallViewController (#3801)
### Description
This will help dealing with
https://github.com/RevenueCat/purchases-flutter/issues/1023

We will conditionally not propagate touch events from the
`PaywallViewController` to fix this issue.
2024-04-11 17:48:51 +02:00
Cesar de la Vega 8bc8a013dc PaywallViewController: option to not automatically dismiss and add onRequestedDismissal (#3738)
Please read
https://github.com/RevenueCat/react-native-purchases/pull/913
2024-03-13 08:41:34 -05:00
Cesar de la Vega aaee3317f8 Paywalls: add updateWithDisplayCloseButton to PaywallViewController (#3708)
Add function to be used from the hybrids
(https://github.com/RevenueCat/react-native-purchases/pull/913)
2024-02-29 16:05:09 +01:00
Cesar de la Vega 3442d40e5a Paywalls: new .onPurchaseStarted(package) modifier (#3693)
Similar to #3627.

This API is available in:
- `PaywallView()`
- `.presentPaywallIfNeeded`
- `.paywallFooter`
- `PaywallViewControllerDelegate`
2024-02-21 18:52:57 +01:00
Cesar de la VegaandMark Villacampa b7ecce223f Paywalls: new .onRestoreStarted modifier (#3694)
Similar to #3627.

This API is available in:
- `PaywallView()`
- `.presentPaywallIfNeeded`
- `.paywallFooter`
- `PaywallViewControllerDelegate`

---------

Co-authored-by: Mark Villacampa <m@markvillacampa.com>
2024-02-19 17:21:31 +01:00
NachoSoto 4735b6b466 Paywalls: new .onPurchaseStarted modifier (#3627)
Similar to #3622.

This API is available in:
- `PaywallView()`
- `.presentPaywallIfNeeded`
- `.paywallFooter`
- `PaywallViewControllerDelegate`
2024-02-02 16:37:49 -08:00
NachoSotoandJustin Yang 0be29ee97d PaywallViewController: expose fontName for CustomFontProvider (by @Jjastiny) (#3628)
Original PR: #3626.

This is the iOS counterpart to
https://github.com/RevenueCat/purchases-android/pull/1589
This exposes the `fontName` as a parameter and sets
`CustomFontProvider`.

### Changes:
- Created `updateFontWithFontName` for `PaywallViewController`
- Created init function for `offerIdentifier` and `fontName`

<img width="450" alt="301765252-95f61abb-10a1-4903-a2ef-12572ae3eaff"
src="https://github.com/RevenueCat/purchases-ios/assets/6516487/56061916-0391-447a-ae74-57f2dddcf7a7">

---------

Co-authored-by: Justin Yang <jjastiny@gmail.com>
2024-02-02 14:46:46 -08:00
NachoSoto fc566cd850 Paywalls: new .onPurchaseFailure and .onRestoreFailure modifiers (#3622) 2024-02-01 11:16:49 -08:00
NachoSoto 637acaa2b9 PaywallViewController: methods for reconfiguring paywall with new offering (#3592)
For use by `react-native-purchases`.

This is not the most efficient, but it's required because we don't get
the props on React Native until after we've already returned the
`UIView`.
2024-01-24 10:18:07 -08:00
NachoSoto 2f1a4e0be8 PaywallViewController: new initializer with Offering identifier (#3587)
Depends on #3586.

This will be used by the hybrid SDKs to be able to load paywalls with an
offering identifier.
2024-01-22 09:00:36 -08:00
NachoSoto 25008b0e68 Paywalls: extracted PaywallViewConfiguration (#3586) 2024-01-22 07:12:46 -08:00
NachoSoto 0098ee8fe0 Paywalls: new .onPurchaseCancelled and paywallViewControllerDidCancelPurchase: (#3578)
This allows apps to be able to receive notifications when users cancel a
purchase.
2024-01-17 09:08:54 -08:00
NachoSoto adf06efef6 PaywallViewController: add PaywallFontProvider parameter (#3567) 2024-01-11 15:28:28 -08:00
NachoSoto fc0568fcb8 PaywallViewControllerDelegate: fixed typo in @objc method name (#3569)
Follow up to #3563.
2024-01-11 13:22:08 -08:00
Cesar de la Vega 280964f9b5 Paywalls: new PaywallViewControllerDelegate.paywallViewController(_:didChangeSizeTo:) (#3563)
- Add `paywallViewControlleSizeDidChange` to notify of size changes
- Added back top anchor to footers for auto layout to size correctly
2024-01-10 16:10:05 +01:00
Andy Boedo 3bd3b7f101 Paywalls: improve footer view UIKit support for hybrid SDKs (#3547)
### Motivation
Improves support for FooterView by adding layout constraints so that the
footer can automatically size itself, and adding a clear background for
the cutout to show properly


![Simulator Screenshot - iPhone 15 - 2023-12-29 at 18 31
38](https://github.com/RevenueCat/purchases-ios/assets/3922667/24a68ece-d03c-4a91-87e3-05e7fee85801)
2024-01-03 10:28:27 -03:00
Toni RicoandNachoSoto de31133733 Paywalls: add PaywallFooterViewController (#3486)
### Description
This adds a new `PaywallFooterViewController`. This VC will be used to
render the footer view in the hybrids.

---------

Co-authored-by: NachoSoto <ignaciosoto90@gmail.com>
2023-12-19 12:48:51 -08:00
Toni Rico 61a38d3a64 Paywalls: Add dismissal method in PaywallViewControllerDelegate (#3493)
### Description
This adds a new method to the `PaywallViewControllerDelegate` to
indicate when the `PaywallViewController` is dismissed.

This will be useful to listen to responses in the hybrids, since we need
to present the paywall and wait until it's been purchased/dismissed
before returning a value for the hybrids.

This will be needed to fix:
https://github.com/RevenueCat/purchases-flutter/issues/886
2023-12-18 18:23:10 +01:00
noncenz b946db9391 Paywalls: fix PaywallViewControllerDelegate access from Objective-C (#3510)
Public variables must be exposed with `@objc` to be accessible outside
of Swift. This change is necessary to set delegate from host project so
that we may dismiss the paywall.

### Motivation
Paywall doesn't seem to be usable in an Objective-C project as there is
no way to receive the callback to drop the paywall other than by setting
a delegate.

### Description
Added code to expose the public variable `delegate` in
PaywallViewController to Objective-C.
Tested by setting delegate and receiving delegate method callbacks in
Obj-C code.
2023-12-10 11:57:58 -08:00
NachoSoto 610f971163 Paywalls: add displayCloseButton to PaywallViewController (#3391)
See #3359.
This brings support for that parameter to the `UIKit` type.
2023-11-08 09:45:14 -08:00
NachoSotoandAndy Boedo af08736775 RevenueCatUI: added support to other deployment targets (#3392)
Follow up to #3378.

This allows us to link both `RevenueCat` and `RevenueCatUI` from
`PurchasesHybridCommon` supporting the same platforms.
The actual types might not be available in some platforms / versions,
but we can detect that at compile time.

See
https://app.circleci.com/pipelines/github/RevenueCat/purchases-hybrid-common/2564/workflows/ea6a4e24-e1eb-4c7a-a930-173945732999/jobs/10352

---------

Co-authored-by: Andy Boedo <andresboedo@gmail.com>
2023-11-08 07:35:19 -08:00
NachoSoto 4a9b9e4bf8 Paywalls: new .onPurchaseCompleted overload with StoreTransaction (#3323) 2023-10-23 20:43:41 -07:00
NachoSoto 66ef49d02e Paywalls: new onRestoreCompleted handler (#3190)
This is available on `.paywallFooter()`, `.presentPaywallIfNeeded`, and
as a `.onRestoreCompleted` modifier.
2023-09-14 18:28:11 -07:00
NachoSoto 2103b04762 Paywalls: added MIT license to all headers (#3084)
`Xcode` doesn't allow customizing the header (looks like maybe it worked
at some point but not anymore:
https://stackoverflow.com/questions/67614785/changing-the-default-header-comment-license-in-swift-package).

At least this adds the license to all the initial files in the package.
2023-09-14 18:28:11 -07:00
NachoSoto a566f6313f Paywalls: finished iOS 15 support (#3043) 2023-09-14 18:28:11 -07:00
NachoSoto cc0152f96f Paywalls: added support for custom fonts (#2988)
This adds a new `PaywallFontProvider` `protocol` as well as 2
implementations:
- `DefaultPaywallFontProvider`: exactly equivalent to the existing font
behavior
- `CustomPaywallFontProvider` allows using a custom font name and still
use dynamic type

![image](https://github.com/RevenueCat/purchases-ios/assets/685609/3659576c-a957-4f33-86ba-e3633520aa17)

![image](https://github.com/RevenueCat/purchases-ios/assets/685609/250304ae-c2e7-4726-888c-bb454651664e)
2023-09-14 18:28:11 -07:00
NachoSoto cd5f85f844 Paywalls: UIKit PaywallViewController (#2934) 2023-09-14 18:28:11 -07:00