Skip to content

Commit c62d4dd

Browse files
Merge pull request #533 from CleverTap/SDK-5626-v7.6.0
[SDK-5626] Prepare v7.6.0
2 parents ef6f3e6 + 1ee9afc commit c62d4dd

185 files changed

Lines changed: 15035 additions & 413 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Change Log
22
All notable changes to this project will be documented in this file.
33

4+
### [Version 7.6.0](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/7.6.0) (April 17, 2026)
5+
#### Added
6+
- **Picture-in-Picture (PiP) In-App Notifications:** Introduces Picture-in-Picture in-app notifications — a compact, draggable floating overlay that persists over app content. Supports image, GIF, and video media with configurable controls.
7+
- **InApp Media Support:** Adds GIF and video playback support across all in-app notification templates, enabling richer media experiences within the app.
8+
- **App Inbox Default Media View:** Introduces a fallback media view for App Inbox entries that lack a specified media orientation, rendering images, GIFs, video posters, and audio thumbnails at their natural aspect ratio.
9+
- Adds support for backend-driven mute durations via the `X-WZRK-MUTE-DURATION` response header for more precise, server-controlled mute windows.
10+
411
### [Version 7.5.1](https://github.com/CleverTap/clevertap-ios-sdk/releases/tag/7.5.1) (March 4, 2026)
512
#### Added
613
- Adds deep link URL capture in InApp notification click events, providing consistent attribution with Push notifications. Supports button-level deep links for CTA clicks and template-level deep links for image-only and HTML templates.

CleverTapSDK.xcodeproj/project.pbxproj

Lines changed: 398 additions & 2 deletions
Large diffs are not rendered by default.

CleverTapSDK/CTDomainFactory.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ NS_ASSUME_NONNULL_BEGIN
5353
#endif
5454

5555
- (BOOL)isMuted;
56+
- (void)unmute;
5657
- (BOOL)updateDomainFromResponseHeaders:(NSDictionary *)headers;
5758
- (BOOL)updateNotificationViewedDomainFromResponseHeaders:(NSDictionary *)headers;
5859
- (void)updateMutedFromResponseHeaders:(NSDictionary *)headers;

CleverTapSDK/CTDomainFactory.m

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@
1818
NSString *const kREDIRECT_HEADER = @"X-WZRK-RD";
1919
NSString *const kREDIRECT_NOTIF_VIEWED_HEADER = @"X-WZRK-SPIKY-RD";
2020
NSString *const kMUTE_HEADER = @"X-WZRK-MUTE";
21+
NSString *const kMUTE_DURATION_HEADER = @"X-WZRK-MUTE-DURATION";
2122

22-
NSString *const kMUTED_TS_KEY = @"CLTAP_MUTED_TS_KEY";
23+
NSString *const kMUTE_EXPIRY_TS_KEY = @"CLTAP_MUTE_EXPIRY_TS_KEY";
2324
NSTimeInterval const kMUTE_SECONDS = 24 * 60 * 60;
2425

2526
@interface CTDomainFactory ()
2627
@property (nonatomic, strong) CleverTapInstanceConfig *config;
2728
@property (nonatomic, strong) CTRequestSender *requestSender;
2829

29-
@property (nonatomic, assign) NSTimeInterval lastMutedTs;
30+
@property (nonatomic, assign) NSTimeInterval muteExpiryTs;
3031

3132
#if CLEVERTAP_SSL_PINNING
3233
@property(nonatomic, strong) CTPinnedNSURLSessionDelegate *urlSessionDelegate;
@@ -71,22 +72,23 @@ - (void)setRequestSender:(CTRequestSender *)requestSender {
7172

7273
- (void)loadMutedTs {
7374
if (self.config.isDefaultInstance) {
74-
self.lastMutedTs = [CTPreferences getIntForKey:[CTPreferences storageKeyWithSuffix:kMUTED_TS_KEY config: self.config] withResetValue:[CTPreferences getIntForKey:kMUTED_TS_KEY withResetValue:0]];
75+
self.muteExpiryTs = [CTPreferences getIntForKey:[CTPreferences storageKeyWithSuffix:kMUTE_EXPIRY_TS_KEY config: self.config] withResetValue:[CTPreferences getIntForKey:kMUTE_EXPIRY_TS_KEY withResetValue:0]];
7576
} else {
76-
self.lastMutedTs = [CTPreferences getIntForKey:[CTPreferences storageKeyWithSuffix:kMUTED_TS_KEY config: self.config] withResetValue:0];
77+
self.muteExpiryTs = [CTPreferences getIntForKey:[CTPreferences storageKeyWithSuffix:kMUTE_EXPIRY_TS_KEY config: self.config] withResetValue:0];
7778
}
7879
}
7980

8081
- (BOOL)isMuted {
81-
return [NSDate new].timeIntervalSince1970 - self.lastMutedTs < kMUTE_SECONDS;
82+
return [NSDate new].timeIntervalSince1970 < self.muteExpiryTs;
8283
}
8384

8485
- (void)clearRedirectDomain {
8586
self.redirectDomain = nil;
8687
self.redirectNotifViewedDomain = nil;
8788
[self persistRedirectDomain]; // if nil persist will remove
89+
[self persistRedirectNotifViewedDomain]; // if nil persist will remove
8890
self.redirectDomain = [self loadRedirectDomain]; // reload explicit domain if we have one else will be nil
89-
self.redirectNotifViewedDomain = [self loadRedirectNotifViewedDomain]; // reload explicit notification viewe domain if we have one else will be nil
91+
self.redirectNotifViewedDomain = [self loadRedirectNotifViewedDomain]; // reload explicit notification viewed domain if we have one else will be nil
9092
}
9193

9294
- (NSString *)loadRedirectDomain {
@@ -167,8 +169,19 @@ - (void)persistRedirectNotifViewedDomain {
167169
}
168170

169171
- (void)persistMutedTs {
170-
self.lastMutedTs = [NSDate new].timeIntervalSince1970;
171-
[CTPreferences putInt:self.lastMutedTs forKey:[CTPreferences storageKeyWithSuffix:kMUTED_TS_KEY config: self.config]];
172+
NSTimeInterval expiryTs = [NSDate new].timeIntervalSince1970 + kMUTE_SECONDS;
173+
self.muteExpiryTs = expiryTs;
174+
[CTPreferences putInt:self.muteExpiryTs forKey:[CTPreferences storageKeyWithSuffix:kMUTE_EXPIRY_TS_KEY config: self.config]];
175+
}
176+
177+
- (void)persistMutedExpiry:(NSTimeInterval)expiryTs {
178+
self.muteExpiryTs = expiryTs;
179+
[CTPreferences putInt:self.muteExpiryTs forKey:[CTPreferences storageKeyWithSuffix:kMUTE_EXPIRY_TS_KEY config: self.config]];
180+
}
181+
182+
- (void)unmute {
183+
self.muteExpiryTs = 0;
184+
[CTPreferences putInt:0 forKey:[CTPreferences storageKeyWithSuffix:kMUTE_EXPIRY_TS_KEY config:self.config]];
172185
}
173186

174187
#pragma mark - Handshake Handling
@@ -244,7 +257,13 @@ - (void)updateMutedFromResponseHeaders:(NSDictionary *)headers {
244257
NSString *mutedString = headers[kMUTE_HEADER];
245258
BOOL muted = (mutedString == nil ? NO : [mutedString boolValue]);
246259
if (muted) {
247-
[self persistMutedTs];
260+
NSString *muteDurationString = headers[kMUTE_DURATION_HEADER];
261+
if (muteDurationString != nil) {
262+
long long muteExpiryMs = [muteDurationString longLongValue];
263+
[self persistMutedExpiry:muteExpiryMs / 1000.0];
264+
} else {
265+
[self persistMutedTs];
266+
}
248267
if (self.domainResolverDelegate && [self.domainResolverDelegate respondsToSelector:@selector(onMute)]) {
249268
[self.domainResolverDelegate onMute];
250269
}

CleverTapSDK/CTInAppNotification.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
@property (nonatomic, strong, readonly) NSURL *imageUrlLandscape;
3838
@property (nonatomic, copy, readonly) NSString *contentType;
3939
@property (nonatomic, copy, readonly) NSString *mediaUrl;
40+
@property (nonatomic, copy, readonly) NSString *mediaUrlLandscape;
4041
@property (nonatomic, readonly, assign) BOOL mediaIsVideo;
4142
@property (nonatomic, readonly, assign) BOOL mediaIsAudio;
4243
@property (nonatomic, readonly, assign) BOOL mediaIsImage;
@@ -57,6 +58,7 @@
5758

5859
@property (nonatomic, copy, readonly) NSDictionary *jsonDescription;
5960
@property (nonatomic) NSString *error;
61+
@property (nonatomic) NSString *errorLandscape;
6062

6163
@property (nonatomic, copy, readonly) NSDictionary *customExtras;
6264
@property (nonatomic, copy, readwrite) NSDictionary *actionExtras;

CleverTapSDK/CTInAppNotification.m

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ @interface CTInAppNotification() {
2424
@property (nonatomic, copy, readwrite) NSString *contentType;
2525
@property (nonatomic, copy, readwrite) NSString *landscapeContentType;
2626
@property (nonatomic, copy, readwrite) NSString *mediaUrl;
27+
@property (nonatomic, copy, readwrite) NSString *mediaUrlLandscape;
2728
@property (nonatomic, copy, readwrite) NSString *contentDescription;
2829
@property (nonatomic, copy, readwrite) NSString *landscapeContentDescription;
2930

@@ -172,6 +173,8 @@ - (void)configureFromJSON: (NSDictionary *)jsonObject {
172173
if (![self.landscapeContentType isEqualToString:@"image/gif"] ) {
173174
_mediaIsImage = YES;
174175
}
176+
} else if ([self.landscapeContentType hasPrefix:@"video"]) {
177+
self.mediaUrlLandscape = _mediaUrlLandscape;
175178
}
176179
}
177180
}
@@ -203,21 +206,24 @@ - (void)configureFromJSON: (NSDictionary *)jsonObject {
203206
switch (self.inAppType) {
204207
case CTInAppTypeHeader:
205208
case CTInAppTypeFooter:
206-
if (_mediaIsGif || _mediaIsAudio || _mediaIsVideo){
209+
// GIF support added, video and audio not supported
210+
if (_mediaIsAudio || _mediaIsVideo){
207211
self.imageURL = nil;
208212
CleverTapLogStaticDebug(@"unable to download media, wrong media type for template");
209213
}
210214
break;
211215
case CTInAppTypeCoverImage:
212216
case CTInAppTypeInterstitialImage:
213217
case CTInAppTypeHalfInterstitialImage:
214-
if (_mediaIsGif || _mediaIsAudio || _mediaIsVideo || !_mediaIsImage){
218+
// GIF and Video support added for image-only templates
219+
if (_mediaIsAudio){
215220
self.error = [NSString stringWithFormat:@"wrong media type for template"];
216221
}
217222
break;
218223
case CTInAppTypeCover:
219224
case CTInAppTypeHalfInterstitial:
220-
if (_mediaIsGif || _mediaIsAudio || _mediaIsVideo){
225+
// GIF support added, video and audio not supported
226+
if (_mediaIsAudio || _mediaIsVideo){
221227
self.imageURL = nil;
222228
CleverTapLogStaticDebug(@"unable to download media, wrong media type for template");
223229
}
@@ -293,7 +299,7 @@ - (void)setPreparedInAppImage:(UIImage *)inAppImage
293299

294300
- (void)setPreparedInAppImageLandscape:(UIImage *)inAppImageLandscape
295301
inAppImageLandscapeData:(NSData *)inAppImageLandscapeData error:(NSString *)error {
296-
self.error = error;
302+
self.errorLandscape = error;
297303
self.inAppImageLandscape = inAppImageLandscape;
298304
self.imageLandscapeData = inAppImageLandscapeData;
299305
}

CleverTapSDK/CTInAppUtils.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ typedef NS_ENUM(NSUInteger, CTInAppType){
1313
CTInAppTypeInterstitialImage,
1414
CTInAppTypeHalfInterstitialImage,
1515
CTInAppTypeCoverImage,
16-
CTInAppTypeCustom
16+
CTInAppTypeCustom,
17+
CTInAppTypePiP
1718
};
1819

1920
typedef NS_ENUM(NSUInteger, CTInAppActionType){

CleverTapSDK/CTInAppUtils.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ @implementation CTInAppUtils
2626
@"interstitial-image": @(CTInAppTypeInterstitialImage),
2727
@"half-interstitial-image": @(CTInAppTypeHalfInterstitialImage),
2828
@"cover-image": @(CTInAppTypeCoverImage),
29-
@"custom-code": @(CTInAppTypeCustom)
29+
@"custom-code": @(CTInAppTypeCustom),
30+
@"pip": @(CTInAppTypePiP)
3031
};
3132
}
3233
return _inAppTypeMap;

CleverTapSDK/CTUIUtils.m

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,21 @@ + (BOOL)isUserInterfaceIdiomPad {
7070

7171
#if !(TARGET_OS_TV)
7272
+ (BOOL)isDeviceOrientationLandscape {
73-
UIInterfaceOrientation orientation;
74-
if (@available(iOS 15.0, *)) {
75-
orientation = [CTUIUtils getSharedApplication].windows.firstObject.windowScene.interfaceOrientation;
76-
} else if (@available(iOS 13.0, *)) {
77-
orientation = [CTUIUtils getSharedApplication].windows.firstObject.windowScene.interfaceOrientation;
78-
} else {
73+
if (@available(iOS 13.0, *)) {
74+
UIInterfaceOrientation orientation = UIInterfaceOrientationPortrait;
75+
NSSet *connectedScenes = [CTUIUtils getSharedApplication].connectedScenes;
76+
for (UIScene *scene in connectedScenes) {
77+
if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) {
78+
orientation = ((UIWindowScene *)scene).interfaceOrientation;
79+
break;
80+
}
81+
}
82+
return UIInterfaceOrientationIsLandscape(orientation);
83+
}
7984
#pragma clang diagnostic push
8085
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
81-
orientation = [[CTUIUtils getSharedApplication] statusBarOrientation];
86+
return UIInterfaceOrientationIsLandscape([[CTUIUtils getSharedApplication] statusBarOrientation]);
8287
#pragma clang diagnostic pop
83-
}
84-
BOOL landscape = UIInterfaceOrientationIsLandscape(orientation);
85-
return landscape;
8688
}
8789
#endif
8890

CleverTapSDK/CleverTap.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1446,7 +1446,21 @@ extern NSString * _Nonnull const CleverTapProfileDidInitializeNotification;
14461446

14471447
/*!
14481448
@method
1449-
1449+
1450+
@abstract
1451+
Clears any active mute state set by the backend, allowing the SDK to resume
1452+
normal event tracking and network operations immediately.
1453+
1454+
@discussion
1455+
The CleverTap backend can mute a client for a set duration (e.g., during a
1456+
detected abuse scenario). Call this method to override that mute and restore
1457+
normal SDK operation without waiting for the mute period to expire.
1458+
*/
1459+
- (void)unmute;
1460+
1461+
/*!
1462+
@method
1463+
14501464
@abstract
14511465
Checks if a custom CleverTapID is valid
14521466
*/

0 commit comments

Comments
 (0)