Skip to content

Commit a2bfa94

Browse files
authored
Merge pull request #663 from qonversion/release/6.10.0
Release 6.10.0
2 parents aec3850 + 3dd9edd commit a2bfa94

17 files changed

Lines changed: 339 additions & 18 deletions

.github/workflows/checks.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,37 @@ jobs:
4040
run: |
4141
swift build
4242
43+
common-checks:
44+
runs-on: ubuntu-latest
45+
46+
steps:
47+
- uses: actions/checkout@v2
48+
49+
- name: Verify API base URL
50+
run: |
51+
EXPECTED_URL="https://api2.qonversion.io/"
52+
53+
MAIN_URL=$(grep 'kAPIBase = @"' Sources/Qonversion/Qonversion/Constants/QNAPIConstants/QNAPIConstants.m | sed 's/.*@"//;s/".*//')
54+
NOCODES_URL=$(grep 'baseURL = "' Sources/NoCodes/Assemblies/ServicesAssembly.swift | sed 's/.*= "//;s/".*//')
55+
56+
FAILED=0
57+
58+
if [ "$MAIN_URL" != "$EXPECTED_URL" ]; then
59+
echo "::error::Main API URL mismatch: expected '$EXPECTED_URL', got '$MAIN_URL'"
60+
FAILED=1
61+
fi
62+
63+
if [ "$NOCODES_URL" != "$EXPECTED_URL" ]; then
64+
echo "::error::NoCodes API URL mismatch: expected '$EXPECTED_URL', got '$NOCODES_URL'"
65+
FAILED=1
66+
fi
67+
68+
if [ "$FAILED" -eq 1 ]; then
69+
exit 1
70+
fi
71+
72+
echo "API URLs verified: $EXPECTED_URL"
73+
4374
tests:
4475
runs-on: macos-latest
4576

Framework/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<key>CFBundlePackageType</key>
1616
<string>FMWK</string>
1717
<key>CFBundleShortVersionString</key>
18-
<string>6.9.0</string>
18+
<string>6.10.0</string>
1919
<key>CFBundleSignature</key>
2020
<string>????</string>
2121
<key>CFBundleVersion</key>

Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
PODS:
22
- OCMock (3.9.4)
3-
- Qonversion (6.7.0):
4-
- Qonversion/Main (= 6.7.0)
5-
- Qonversion/Main (6.7.0)
3+
- Qonversion (6.8.0):
4+
- Qonversion/Main (= 6.8.0)
5+
- Qonversion/Main (6.8.0)
66

77
DEPENDENCIES:
88
- OCMock
@@ -18,7 +18,7 @@ EXTERNAL SOURCES:
1818

1919
SPEC CHECKSUMS:
2020
OCMock: 589f2c84dacb1f5aaf6e4cec1f292551fe748e74
21-
Qonversion: 6dee5747c1bc682774b897fcc1917cf631ca3611
21+
Qonversion: dc8d5a02ec83a4bfe1f2a73f08d9c00ebb254063
2222

2323
PODFILE CHECKSUM: 039889b49d515621e653ab1d97888c4c2744c482
2424

Qonversion.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Pod::Spec.new do |s|
33
idfa_exclude_files = ['Sources/Qonversion/IDFA']
44
s.name = 'Qonversion'
55
s.swift_version = '5.5'
6-
s.version = '6.9.0'
6+
s.version = '6.10.0'
77
s.summary = 'qonversion.io'
88
s.description = <<-DESC
99
Deep Analytics for iOS Subscriptions

Qonversion.xcodeproj/project.pbxproj

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
176D395BD9D9A09A56F417D7 /* QONDeferredPurchasesListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 61B460B7A476E7C352C3B615 /* QONDeferredPurchasesListener.h */; settings = {ATTRIBUTES = (Public, ); }; };
11+
202B79E6100384DE55BACD10 /* QONEntitlementsUpdateListenerAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = C4DB5F155FE9F52DC87B68F6 /* QONEntitlementsUpdateListenerAdapter.h */; settings = {ATTRIBUTES = (Project, ); }; };
12+
0A17440D00C09DABC1E2B1D3 /* QONEntitlementsUpdateListenerAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B69D8BEFE710A9EF0697B28 /* QONEntitlementsUpdateListenerAdapter.m */; };
1013
15EE5F2E2F6CBF096CB24CBA /* NoCodesLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F40A7683B495F16AD590A5EC /* NoCodesLoadingView.swift */; };
1114
207A91DAC28DE776D419F8DC /* NoCodesHTMLInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59914571FB7D583A6205C7A /* NoCodesHTMLInjector.swift */; };
1215
218480F803075431AD24186B /* ScreenEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2865A58C95F0B475399053F2 /* ScreenEvent.swift */; };
@@ -341,6 +344,9 @@
341344
/* End PBXCopyFilesBuildPhase section */
342345

343346
/* Begin PBXFileReference section */
347+
61B460B7A476E7C352C3B615 /* QONDeferredPurchasesListener.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = QONDeferredPurchasesListener.h; sourceTree = "<group>"; };
348+
C4DB5F155FE9F52DC87B68F6 /* QONEntitlementsUpdateListenerAdapter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = QONEntitlementsUpdateListenerAdapter.h; sourceTree = "<group>"; };
349+
0B69D8BEFE710A9EF0697B28 /* QONEntitlementsUpdateListenerAdapter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = QONEntitlementsUpdateListenerAdapter.m; sourceTree = "<group>"; };
344350
02B82604A89181D262616958 /* NoCodesContextBuilder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NoCodesContextBuilder.swift; sourceTree = "<group>"; };
345351
1037791CABEE4F1611A8561E /* Pods-Watch Sample Watch App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Watch Sample Watch App.release.xcconfig"; path = "Target Support Files/Pods-Watch Sample Watch App/Pods-Watch Sample Watch App.release.xcconfig"; sourceTree = "<group>"; };
346352
2865A58C95F0B475399053F2 /* ScreenEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenEvent.swift; sourceTree = "<group>"; };
@@ -1343,6 +1349,7 @@
13431349
895731CC26DD03A2009507A6 /* QONIntroEligibility.m */,
13441350
895731CD26DD03A2009507A6 /* QONUser.m */,
13451351
895731CE26DD03A2009507A6 /* QONEntitlementsUpdateListener.h */,
1352+
61B460B7A476E7C352C3B615 /* QONDeferredPurchasesListener.h */,
13461353
895731CF26DD03A2009507A6 /* QONAutomations.h */,
13471354
895731D126DD03A2009507A6 /* QONExperimentGroup.h */,
13481355
895731D226DD03A2009507A6 /* QONEntitlement.m */,
@@ -1562,6 +1569,8 @@
15621569
8957324926DD03A3009507A6 /* QONLaunchResult+Protected.h */,
15631570
A1839F615CCAF96B67D8A9F2 /* QONUserProperty+Protected.h */,
15641571
A18396B586B0A77331A3A346 /* QONUserProperties+Protected.h */,
1572+
C4DB5F155FE9F52DC87B68F6 /* QONEntitlementsUpdateListenerAdapter.h */,
1573+
0B69D8BEFE710A9EF0697B28 /* QONEntitlementsUpdateListenerAdapter.m */,
15651574
);
15661575
path = Protected;
15671576
sourceTree = "<group>";
@@ -1826,6 +1835,7 @@
18261835
700B9D502E742E5D0031B056 /* QONPurchaseResult+Protected.h in Headers */,
18271836
6A21BF532AB2059F005BDA7C /* QONRequest.h in Headers */,
18281837
8957329026DD03A3009507A6 /* QONEntitlementsUpdateListener.h in Headers */,
1838+
176D395BD9D9A09A56F417D7 /* QONDeferredPurchasesListener.h in Headers */,
18291839
8957329326DD03A3009507A6 /* QONExperimentGroup.h in Headers */,
18301840
7098564D2A370F270023DFAB /* QONRemoteConfigMapper.h in Headers */,
18311841
6A121DB12BBB10370073B330 /* QONRemoteConfigListRequestData.h in Headers */,
@@ -2341,6 +2351,7 @@
23412351
708C1A3A2E4A21BC00252C6E /* NetworkErrorHandlerInterface.swift in Sources */,
23422352
F5890274B1E90C6993376869 /* ScreenEventType.swift in Sources */,
23432353
218480F803075431AD24186B /* ScreenEvent.swift in Sources */,
2354+
0A17440D00C09DABC1E2B1D3 /* QONEntitlementsUpdateListenerAdapter.m in Sources */,
23442355
3521C548C169154D45835C38 /* ScreenEventsServiceInterface.swift in Sources */,
23452356
48FCBE392ED00F2022C0823A /* ScreenEventsService.swift in Sources */,
23462357
6A121DB32BBB10AA0073B330 /* QONRemoteConfigListRequestData.m in Sources */,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#import <XCTest/XCTest.h>
2+
#import <OCMock/OCMock.h>
3+
#import "QNProductCenterManager.h"
4+
#import "QNAPIClient.h"
5+
#import "QNUserInfoService.h"
6+
#import "QNIdentityManager.h"
7+
#import "QNLocalStorage.h"
8+
#import "QONFallbackService.h"
9+
#import "QONDeferredPurchasesListener.h"
10+
#import "QONEntitlementsUpdateListener.h"
11+
#import "QONEntitlementsUpdateListenerAdapter.h"
12+
#import "QONEntitlement.h"
13+
#import "QONPurchaseResult.h"
14+
#import "QONPurchaseResult+Protected.h"
15+
#import "QNTestConstants.h"
16+
17+
@interface QNProductCenterManager (DeferredTestPrivate)
18+
19+
@property (nonatomic, strong) id<QONDeferredPurchasesListener> deferredPurchasesListener;
20+
21+
@end
22+
23+
@interface DeferredPurchaseListenerTests : XCTestCase
24+
25+
@property (nonatomic) QNProductCenterManager *manager;
26+
27+
@end
28+
29+
@implementation DeferredPurchaseListenerTests
30+
31+
- (void)setUp {
32+
id mockUserInfoService = OCMClassMock([QNUserInfoService class]);
33+
id mockIdentityManager = OCMClassMock([QNIdentityManager class]);
34+
id mockLocalStorage = OCMProtocolMock(@protocol(QNLocalStorage));
35+
id mockFallbackService = OCMClassMock([QONFallbackService class]);
36+
_manager = [[QNProductCenterManager alloc] initWithUserInfoService:mockUserInfoService identityManager:mockIdentityManager localStorage:mockLocalStorage fallbackService:mockFallbackService];
37+
}
38+
39+
- (void)tearDown {
40+
_manager = nil;
41+
}
42+
43+
#pragma mark - Setter Tests
44+
45+
- (void)testSetDeferredPurchaseListenerStoresListener {
46+
id mockListener = OCMProtocolMock(@protocol(QONDeferredPurchasesListener));
47+
48+
[_manager setDeferredPurchasesListener:mockListener];
49+
50+
XCTAssertEqual(_manager.deferredPurchasesListener, mockListener);
51+
}
52+
53+
- (void)testSetDeferredPurchaseListenerToNilClearsListener {
54+
id mockListener = OCMProtocolMock(@protocol(QONDeferredPurchasesListener));
55+
[_manager setDeferredPurchasesListener:mockListener];
56+
57+
[_manager setDeferredPurchasesListener:nil];
58+
59+
XCTAssertNil(_manager.deferredPurchasesListener);
60+
}
61+
62+
- (void)testSetDeferredPurchaseListenerReplacesExisting {
63+
id firstListener = OCMProtocolMock(@protocol(QONDeferredPurchasesListener));
64+
id secondListener = OCMProtocolMock(@protocol(QONDeferredPurchasesListener));
65+
[_manager setDeferredPurchasesListener:firstListener];
66+
67+
[_manager setDeferredPurchasesListener:secondListener];
68+
69+
XCTAssertEqual(_manager.deferredPurchasesListener, secondListener);
70+
}
71+
72+
#pragma mark - Adapter Pattern Tests
73+
74+
- (void)testSetPurchasesDelegateWrapsInAdapter {
75+
id mockLegacyListener = OCMProtocolMock(@protocol(QONEntitlementsUpdateListener));
76+
77+
[_manager setPurchasesDelegate:mockLegacyListener];
78+
79+
XCTAssertNotNil(_manager.deferredPurchasesListener);
80+
XCTAssertTrue([_manager.deferredPurchasesListener isKindOfClass:[QONEntitlementsUpdateListenerAdapter class]]);
81+
}
82+
83+
- (void)testSetPurchasesDelegateToNilClearsListener {
84+
id mockLegacyListener = OCMProtocolMock(@protocol(QONEntitlementsUpdateListener));
85+
[_manager setPurchasesDelegate:mockLegacyListener];
86+
87+
[_manager setPurchasesDelegate:nil];
88+
89+
XCTAssertNil(_manager.deferredPurchasesListener);
90+
}
91+
92+
- (void)testSetDeferredListenerReplacesAdapter {
93+
id mockLegacyListener = OCMProtocolMock(@protocol(QONEntitlementsUpdateListener));
94+
[_manager setPurchasesDelegate:mockLegacyListener];
95+
XCTAssertTrue([_manager.deferredPurchasesListener isKindOfClass:[QONEntitlementsUpdateListenerAdapter class]]);
96+
97+
id mockDeferredListener = OCMProtocolMock(@protocol(QONDeferredPurchasesListener));
98+
[_manager setDeferredPurchasesListener:mockDeferredListener];
99+
100+
XCTAssertEqual(_manager.deferredPurchasesListener, mockDeferredListener);
101+
}
102+
103+
#pragma mark - Adapter Forwarding Tests
104+
105+
- (void)testAdapterForwardsEntitlementsFromPurchaseResult {
106+
// The adapter should extract entitlements from QONPurchaseResult
107+
// and forward them to the wrapped legacy listener.
108+
id mockLegacyListener = OCMProtocolMock(@protocol(QONEntitlementsUpdateListener));
109+
QONEntitlementsUpdateListenerAdapter *adapter = [[QONEntitlementsUpdateListenerAdapter alloc] initWithLegacyListener:mockLegacyListener];
110+
111+
NSDictionary *entitlements = @{@"premium": OCMClassMock([QONEntitlement class])};
112+
QONPurchaseResult *purchaseResult = [QONPurchaseResult successWithEntitlements:entitlements transaction:nil];
113+
114+
#pragma clang diagnostic push
115+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
116+
OCMExpect([mockLegacyListener didReceiveUpdatedEntitlements:entitlements]);
117+
#pragma clang diagnostic pop
118+
119+
[adapter deferredPurchaseCompleted:purchaseResult];
120+
121+
OCMVerifyAll(mockLegacyListener);
122+
}
123+
124+
@end

Sources/Qonversion/Public/QONConfiguration.h

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#import "QONLaunchMode.h"
1111
#import "QONEntitlementsCacheLifetime.h"
1212
#import "QONEntitlementsUpdateListener.h"
13+
#import "QONDeferredPurchasesListener.h"
1314
#import "QONEnvironment.h"
1415
#import "QONPromoPurchasesDelegate.h"
1516

@@ -54,9 +55,16 @@ NS_SWIFT_NAME(Qonversion.Configuration)
5455
@property (nonatomic, copy, readonly) NSString *baseURL;
5556

5657
/**
58+
@deprecated Use deferredPurchasesListener instead.
5759
Listener to handle entitlements update. For example, when pending purchases like SCA, Ask to buy, etc., happened.
5860
*/
59-
@property (nonatomic, weak, readonly) id<QONEntitlementsUpdateListener> entitlementsUpdateListener;
61+
@property (nonatomic, weak, readonly) id<QONEntitlementsUpdateListener> entitlementsUpdateListener __attribute__((deprecated("Use deferredPurchasesListener instead")));
62+
63+
/**
64+
Listener to be notified when a deferred purchase completes.
65+
Provides transaction-level details, including for consumable products without entitlements.
66+
*/
67+
@property (nonatomic, weak, readonly) id<QONDeferredPurchasesListener> deferredPurchasesListener;
6068

6169
/**
6270
Delegate to handle App Store Promo purchases.
@@ -104,10 +112,18 @@ NS_SWIFT_NAME(Qonversion.Configuration)
104112
- (void)setEntitlementsCacheLifetime:(QONEntitlementsCacheLifetime)cacheLifetime;
105113

106114
/**
115+
@deprecated Use setDeferredPurchasesListener: instead.
107116
Set this listener to handle entitlements update. For example, when pending purchases like SCA, Ask to buy, etc., happened.
108117
@param entitlementsUpdateListener - listener for handling entitlements update
109118
*/
110-
- (void)setEntitlementsUpdateListener:(id<QONEntitlementsUpdateListener>)entitlementsUpdateListener;
119+
- (void)setEntitlementsUpdateListener:(id<QONEntitlementsUpdateListener>)entitlementsUpdateListener __attribute__((deprecated("Use setDeferredPurchasesListener: instead")));
120+
121+
/**
122+
Set this listener to be notified when a deferred purchase completes.
123+
Provides transaction-level details, including for consumable products without entitlements.
124+
@param listener - listener for handling deferred purchase completions
125+
*/
126+
- (void)setDeferredPurchasesListener:(id<QONDeferredPurchasesListener>)listener;
111127

112128
/**
113129
Set this delegate to handle AppStore promo purchases

Sources/Qonversion/Public/QONConfiguration.m

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#import "QONConfiguration.h"
1010
#import "QNAPIConstants.h"
1111

12-
static NSString *const kSDKVersion = @"6.9.0";
12+
static NSString *const kSDKVersion = @"6.10.0";
1313

1414
@interface QONConfiguration ()
1515

@@ -63,6 +63,10 @@ - (void)setEntitlementsUpdateListener:(id<QONEntitlementsUpdateListener>)entitle
6363
_entitlementsUpdateListener = entitlementsUpdateListener;
6464
}
6565

66+
- (void)setDeferredPurchasesListener:(id<QONDeferredPurchasesListener>)listener {
67+
_deferredPurchasesListener = listener;
68+
}
69+
6670
- (void)setPromoPurchasesDelegate:(id<QONPromoPurchasesDelegate>)delegate {
6771
_promoPurchasesDelegate = delegate;
6872
}
@@ -84,7 +88,11 @@ - (id)copyWithZone:(NSZone *)zone {
8488
[copyConfig setVersion:_version];
8589
[copyConfig setEnvironment:_environment];
8690
[copyConfig setEntitlementsCacheLifetime:_entitlementsCacheLifetime];
91+
#pragma clang diagnostic push
92+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
8793
[copyConfig setEntitlementsUpdateListener:_entitlementsUpdateListener];
94+
#pragma clang diagnostic pop
95+
[copyConfig setDeferredPurchasesListener:_deferredPurchasesListener];
8896
[copyConfig setCustomUserDefaults:_customUserDefaults];
8997
[copyConfig setProxyURL:_baseURL];
9098

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// QONDeferredPurchasesListener.h
3+
// Qonversion
4+
//
5+
6+
#import <Foundation/Foundation.h>
7+
8+
@class QONPurchaseResult;
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
NS_SWIFT_NAME(Qonversion.DeferredPurchasesListener)
13+
@protocol QONDeferredPurchasesListener <NSObject>
14+
15+
/**
16+
Called when a deferred purchase completes.
17+
For example, when pending purchases like SCA, Ask to Buy, etc., are approved and finalized.
18+
Provides the purchase result with entitlements.
19+
@param purchaseResult - the result of the completed deferred purchase, containing entitlements.
20+
*/
21+
- (void)deferredPurchaseCompleted:(QONPurchaseResult * _Nonnull)purchaseResult;
22+
23+
@end
24+
25+
NS_ASSUME_NONNULL_END

Sources/Qonversion/Public/QONEntitlementsUpdateListener.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
NS_SWIFT_NAME(Qonversion.EntitlementsUpdateListener)
1313
@protocol QONEntitlementsUpdateListener <NSObject>
1414

15-
- (void)didReceiveUpdatedEntitlements:(NSDictionary<NSString *, QONEntitlement *> * _Nonnull)entitlements;
15+
/**
16+
Use QONDeferredPurchasesListener instead.
17+
DeferredPurchasesListener provides full transaction details including product ID,
18+
transaction ID, and value - critical for consumable products without entitlements.
19+
*/
20+
- (void)didReceiveUpdatedEntitlements:(NSDictionary<NSString *, QONEntitlement *> * _Nonnull)entitlements __attribute__((deprecated("Use QONDeferredPurchasesListener instead")));
1621

1722
@end

0 commit comments

Comments
 (0)