Commit Graph
22 Commits
Author SHA1 Message Date
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
NachoSoto 2bcd0ce328 5.0.0 changes: support SK2 by default 2024-07-01 14:37:21 -05:00
NachoSoto 2b87c3016d Created PostedTransactionCache (#2911)
This will be used to skip posting the same transaction multiple times,
as a way to work around StoreKit reporting SK1 notifications from SK2
APIs.
2023-08-07 15:41:53 -07:00
NachoSoto b28f9fbf05 Removed preventPurchasePopupCallFromTriggeringCacheRefresh, update caches on willEnterForeground (#2623)
This method was a hack to deal with apps getting `didBecomeActive`
notifications in the middle of a purchase when the popups were
displayed.
This had several problems:
- It was only being used for StoreKit 1 purchases
- It wasn't used for StoreKit 2 purchases through the SDK
- It wasn't used for purchases initiated outside the SDK (like using the
upcoming paywall screens)
- It updated the cache timestamp to "now", including offerings, despite
not actually updating data
- It was an indirect and implicit way of solving a problem instead of an
explicit one

This solves the same by observing `willEnterForeground`, which only
happens when the app actually comes back from the background, and not
when it simply loses focus.
2023-06-12 16:26:14 -07:00
NachoSoto 99c8a34e62 Offline Entitlements: use offline-computed CustomerInfo when server is down (#2368)
### Changes:
- Created `PurchasedProductsFetcherType` to be able to mock and hold
fetchers, even on older devices
- Created `ProductEntitlementMappingFetcher` to abstract injecting
`ProductEntitlementMapping`
- Added logic to `CustomerInfoResponseHandler` to compute offline
`CustomerInfo` after a server error (see #2367). **This is used by all
`CustomerInfo` requests as well as `PostReceiptDataOperation`**.
- Wrote comprehensive `CustomerInfoResponseHandler` tests

### TODO:
- [x] Don't cache offline computed `CustomerInfo`: #2378
- [x] Integration tests: #2501.
2023-05-19 10:27:44 -07:00
NachoSoto aeb87adbe3 OfferingsManager: return Offerings from new disk cache when server is down (#2495)
This new cache is used only when the server is down, so paywalls can
continue to work in that case.

### Example:
```
- DEBUG: ℹ️ Configuring SDK using RevenueCat's UserDefaults suite.
- DEBUG: ℹ️ No cached Offerings, fetching from network
- DEBUG: ℹ️ Offerings cache is stale, updating from network in foreground
- WARN: ⚠️ Error fetching offerings: server appears down
- DEBUG: ℹ️ MockProductsManager: no stubbed products, returning fake products for ["monthly_freetrial"]
- DEBUG: ℹ️ Vending Offerings from disk cache
```
2023-05-17 19:21:17 -04:00
NachoSoto 94860be79f DeviceCache: refactored cache keys (#2494) 2023-05-16 09:58:19 -04:00
Andy Boedo eeb0ced8bd Fix deadlock when sending notification center in customerInfoListener (#2407)
Fixes an issue where a deadlock could happen if you fire a notification
in the customerInfoListener.

The issue stems from our usage of NotificationCenter to detect deletion
of userDefaults.

Since we've changed the default UserDefaults anyway, I don't feel like
this adds much value anymore, and we've spent wayyyy too much time
dealing with race conditions caused by it, so I'm just getting rid of
it.

I couldn't get it to reproduce on my device, but @codykerns was able to
get a consistent repro and confirmed the fix with this branch.
2023-04-14 18:07:13 -03:00
NachoSoto 3246a58978 DeviceCache: workaround for potential deadlock (#2375)
Fixes #2252, #2364, [SDKONCALL-214], [SDKONCALL-241].
See also #2078 and #2079.

### Background

#2078 solved a potential deadlock in `DeviceCache`. This could happen
when a background thread was in the middle of writing to `UserDefaults`
through `DeviceCache`, and at the same time, the main thread wanted to
read from it.
This is normally fine, except that if there are any `NotificationCenter`
observations on the `UserDefaults` used, the background thread would
then post that notification and wait for the main thread, leading to a
deadlock. What #2078 did was to use a different method to make sure that
the observation was received in the calling thread.
Additionally, #2079 avoided that observation whenever we used a
different `UserDefaults`.

### Problem

When the SDK changed to a custom `UserDefaults` (#2046), we made the
choice to only do this if there was no prior data.
However, if an app has prior RevenueCat data saved in
`UserDefaults.standard`, that instance is used.

This potential deadlock is possible for any app with prior data, that
also contains observations to `UserDefaults`, either manually through
`NotificationCenter`, or using something like `@AppStorage` with
`SwiftUI`.

### Solution

`DeviceCache.cachedAppUserID` is a very commonly used method. This PR
changes it to an in-memory cache, initialized to the `UserDefaults`
value.
By doing that, most code paths that rely on this value won't risk
deadlocking with `UserDefaults` writes that jump threads.

### Long term

I've filed SDK-3034 to fully migrate data from `UserDefaults.standard`
to avoid this problem altogether in the future.


[SDKONCALL-214]:
https://revenuecats.atlassian.net/browse/SDKONCALL-214?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SDKONCALL-238]:
https://revenuecats.atlassian.net/browse/SDKONCALL-238?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SDKONCALL-241]:
https://revenuecats.atlassian.net/browse/SDKONCALL-241?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2023-03-24 16:15:26 -07:00
NachoSoto b495540308 Offline Entitlements: store ProductEntitlementMapping in cache (#2355)
## Changes:
- `DeviceCache` support for caching `ProductsEntitlementsResponse` as
well as "last updated" date
- Added `JSONEncoder.encode` helper matching `JSONDecoder.decode`
version, with an optional `logErrors` parameter
- `OfferingsManager. updateProductsEntitlementsCacheIfStale`
- `Purchases.updateAllCaches` also updates this new cache
- Added `OfferingsManager` / `DeviceCache` / `Purchases` tests
- Integration test for fetching mapping
2023-03-23 13:30:19 -07:00
NachoSoto 0dbb108226 DeviceCache: simplified constructor (#2354) 2023-03-21 06:33:48 -07:00
Toni Rico fdd79cdcb0 Fix issue with missing subscriber attributes if set after login but before login callback (#2313)
### Description
Deals with
[SDK-2817](https://linear.app/revenuecat/issue/SDK-2817/fix-issue-with-missing-subscriber-attributes-on-purchases-[ios])
This is the iOS counterpart to
https://github.com/RevenueCat/purchases-android/pull/809

There was an issue where subscriber attributes could be delayed to be
synced to the backend. This happened when setting a subscriber attribute
AFTER the `logIn` call but BEFORE the `logIn` operation was completed.

This issue happens because we start syncing subscriber attributes
immediately when calling `logIn`, but if we set any after that but
before the login is complete, they will be assigned to the old user id.
We would sync those the next time the app is backgrounded/foregrounded
but might be missed if there is a purchase before that happens. When
there is a purchase, we only sync the current user's unsynced
attributes, but not attributes of other users.

This PR copies all unsynced attributes from the old user id to the new
one upon a successful login. This way, the unsynced attributes will be
sent if the user tries to purchase.

Note that we only copy these attributes when the old user id was
anonymous. This is because there is a possible edge case of copying
attributes from a user to a different user if the sync request fails and
remains unsynced when changing between user ids.
2023-02-23 15:52:58 +01:00
NachoSoto 43e53870cc DeviceCache: added verbose logs for init/deinit (#2101) 2022-12-07 14:37:57 -08:00
NachoSoto e5d72936ae DeviceCache: removed fatalError for users not overriding UserDefaults (#2079) 2022-11-22 13:50:51 -08:00
NachoSoto 6474066e88 DeviceCache: changed NotificationCenter observation to be received on posting thread (#2078)
This could solve [CSDK-519], and at the very least it makes the
semantics simpler to understand.

The traditional `NotificationCenter.addObserver` seems to always lead to
a jump to the main thread when modifying `UserDefaults` from any other
thread.
See:
```
Thread 1:
0 libsystem_kernel.dylib 0x1fa81041c __psynch_cvwait + 8
1 libsystem_pthread.dylib 0x20aa5406c _pthread_cond_wait + 1232
2 Foundation 0x1b85b3800 -[NSOperation waitUntilFinished] + 508
3 CoreFoundation 0x1be16fe0c _CFXNotificationPost + 772
4 Foundation 0x1b85d04dc -[NSNotificationCenter postNotificationName:object:userInfo:] + 92
5 AudioMemos2 0x102ed18a4 specialized closure #1 in DeviceCache.cache(customerInfo:appUserID:) + 972964 (<compiler-generated>:0)
```

There were no potential race condition (and as far as we can tell, no
deadlocks possible) because of how synchronization is done, but using
the modern `NotificationCenter.addObserver` simplifies this, because
`handleUserDefaultsChanged` will now be invoked synchronously in the
calling thread.

[CSDK-519]:
https://revenuecats.atlassian.net/browse/CSDK-519?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-11-22 13:39:07 -08:00
NachoSoto ecce3ff982 Changed default UserDefaults from .standard to our own Suite (#2046)
Fixes [CSDK-16].
See
https://docs.google.com/document/d/1DDyed98RFpIvUwUPJcetc7X4fBhDzf-r1trC4g3zf4c/edit#
for reasoning and documentation.

### Motivation

The RevenueCat SDK has always used `UserDefaults.standard` as the
default value. The advantage of this was simplicity, however, this
brings a few challenges.

### Issues

- While `UserDefaults` is itself thread-safe, our users might be
observing `UserDefaults.defaults` changes via `KVO`,
`NotificationCenter`, or `@AppStorage` (see
https://github.com/RevenueCat/purchases-ios/issues/1527). We update
`UserDefaults` from background threads, and users might be getting
updates on those threads without handling thread-safety correctly.
- Users can accidentally delete data from the shared `UserDefaults` (see
https://rev.cat/userdefaults-crash).

Using a custom `UserDefaults` solves all these issues.

### Change

The new `UserDefaults.revenueCatSuite` is used when:
- Only if users aren’t overriding the `UserDefaults` parameter
- Only if `UserDefaults.standard` has no value for
`com.revenuecat.userdefaults.appUserID.new` (the user hasn't used the
SDK in that app before).

#### Logged during app start:

> PurchaseTester[35110:6323140] DEBUG: ℹ️ Configuring SDK using
RevenueCat's UserDefaults suite.

### Alternatives Considered
See
https://docs.google.com/document/d/1c2B4MNAFKs_DcFJ3OE_KnmryHCArBkjlZbAD9_kSXu0/edit#

We considered also migrating data for everyone into the new
`UserDefaults`.
This new approach is much simpler, less error prone, easier to
implement, and maintain.
The only downside is that existing users won’t benefit from this change,
but @aboedo correctly pointed out: if the app is working for them, no
reason to change anything.

So in this way, this becomes a purely forward-looking improvement.

[CSDK-16]:
https://revenuecats.atlassian.net/browse/CSDK-16?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-11-22 11:32:09 -08:00
NachoSoto 9f7632178d DeviceCache: Sendable conformance and fixed thread-safety (#1823)
For [CSDK-379].
Extracted #1795 to make it easier to review this fix in isolation.

`handleUserDefaultsChanged` isn't guaranteed to be called from any particular thread, so its access to `appUserIDHasBeenSet` wasn't thread-safe.

This is now guaranteed by `Sendable`.

_In theory. In practice we are required to use `@unchecked` for now because of `NotificationCenter` and the class not being `final`. But without those I verified that the class does compile with `Sendable`, which means that there aren't any other potential issues._

[CSDK-379]: https://revenuecats.atlassian.net/browse/CSDK-379?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-08-15 11:04:48 -07:00
343ba7eaf4 AdServices (#1727)
* [CF-106] Fetch AdServices Token (#1519)

Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>

* [CF-553] Post AdServices token (#1534)

Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Co-authored-by: Josh Holtz <me@joshholtz.com>

* Update ios 14 snapshots tets

* Reworked adservices to use new Purchases.shared.attribution API

* Updated with some comments from PR

* Add iAd code back (#1739)

Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>

* use new mapKeys dictionary extension for device cache

* update for some pr comments

* remove purchasestests

* generate missing snapshots

* fix deprecation messages to clarify method now called on shared instance

* Fix for inserting nonstring into userdefaults

* Add tests for bug

* fix lint

* Added availablility checks for tests and removed unused varialbes

* Whoops... wrong class

Co-authored-by: beylmk <maddie@revenuecat.com>
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
2022-08-08 07:42:42 -05:00
NachoSoto a2fc04c09e SubscriberAttribute: converted into struct (#1648)
### Motivation:

Initially I wanted to make `SubscriberAttribute.asDictionary` work using `Codable`. However, this isn't the right refactor because we need the actual raw types to be encoded inside `UserDefaults`.
At least this refactor provides several improvements:

### Changes:

- `SubscriberAttribute` now has value semantics
- ⚠️  `SubscriberAttribute.isSynced` used to be mutable, with reference semantics, which was dangerous
- `SubscriberAttribute: Equatable` implementation is now automatic
- Cleaned up `SubscriberAttribute.asDictionary` implementation, extracting keys into an `enum`
- Moved `Dictionary` -> `SubscriberAttribute` creation to the type itself
- ⚠️ `SubscriberAttribute.init(dictionary:)` returns an `Optional` instead of using force-unwrap, avoiding potential crashes
- Added more serialization / deserialization tests
- Removed `SubscriberAttributesMarshaller` and moved method into `SubscriberAttribute` extension
- Moved `SubscriberAttributeDict` into `SubscriberAttribute.Dictionary`
2022-06-17 10:00:51 -07:00
Juanpe Catalán 9482e8db82 Clear cached offerings and products after Storefront changes (#1583)
Follow up to #1557. Subtask of #1382 

### Description

This PR aims to clear the stuff in the cache when the Storefront changes.

Changes:
- Add `clearCachedOfferings` method to `DeviceCache`.
- Add `clearCache` method to `ProductFetcherSK1`.
- Make `internal` the `clearCache` method in `ProductsFetcherSK2`.
- Handle the **Storefront** changes in the `PurchasesOrchestator`.
- Add tests to cover some scenarios.
2022-06-16 10:29:07 -07:00
NachoSoto 2cbb71eb20 Extracted SandboxEnvironmentDetector (#1645)
### Motivation:

For [CSDK-53], `EntitlementInfos` will need access to the environment. This provides a lighter interface for that, as well as a simple implementation to inject that doesn't require the entire `SystemInfo`

[CSDK-53]: https://revenuecats.atlassian.net/browse/CSDK-53?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2022-06-09 11:13:41 -07:00
aboedo f5697de730 renamed Purchases folder to Sources (#1416)
* renamed `Purchases` folder to `Sources`

* temporarily enabling installation test jobs

* re-disabling installation test jobs
2022-03-28 11:53:24 -03:00