Skip to content

Conversation

@Sonal-Kachare
Copy link
Contributor

@Sonal-Kachare Sonal-Kachare commented Jan 29, 2026

@coderabbitai

Summary by CodeRabbit

Release Notes

  • New Features

    • Added in-action in-app content fetch capability
    • Enhanced server-side in-app notification scheduling with improved storage handling
    • Added fresh app launch tracking to batch requests
  • Refactoring

    • Updated in-app scheduler and timer management infrastructure
    • Refined public API surface with optimized header exposure

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • ^(task|feat|feature|fix)/

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

This PR refactors the in-app notification scheduling system, replacing the legacy CTInAppDelayManager with a new modular CTInAppScheduler framework. It introduces duration-based partitioning, timer-based scheduling with multiple storage and data extraction strategies, adds fresh app launch tracking, and enables in-action in-app fetch capabilities.

Changes

Cohort / File(s) Summary
Pod Specification & Public API Exposure
CleverTap-iOS-SDK.podspec
Removes ~35 legacy public headers (CleverTap.h, UI headers, custom templates, etc.) and adds new headers for in-app store, switch user delegate, and scheduler components for both iOS and tvOS.
Core In-App Scheduler Framework
CleverTapSDK/InApps/InAppsScheduler/InAppScheduler.swift, InAppTimerManager.swift
Introduces CTInAppScheduler for unified timer-based scheduling with storage strategies and data extraction; InAppTimerManager provides thread-safe, lifecycle-aware timer management with background/foreground rescheduling.
In-App Scheduling Strategies
InAppsScheduler/DelayedInAppStorageStrategy.swift, InactionInAppStorageStrategy.swift, InAppSchedulingStrategy.swift
Defines InAppSchedulingStrategy protocol and implements two storage strategies (delayed and in-action) with prepare, retrieve, clear operations.
In-App Data Extraction & Selection
InAppsScheduler/InAppDataExtractor.swift, InAppSelectionStrategy.swift
Introduces data extractors for delayed and in-action in-apps (extracting delays, creating result types) and selection strategies for immediate/delayed in-app prioritization with suppression awareness.
Result Type Definitions
InAppsScheduler/CTTimerResult.h/.m, CTDelayedInAppResult.h/.m, CTInActionResult.h/.m
Adds typed result objects encapsulating timer outcomes (completed, error, discarded) and delayed/in-action in-app results with factory constructors.
Duration Partitioning & Configuration
InApps/DurationPartitionedInApps.swift, InAppDurationPartitioner.swift
Introduces duration-partitioned in-app containers (ImmediateAndDelayed, UnknownAndInAction, InActionOnly) and constants/partitioner logic for classifying in-apps by delay/in-action duration.
Removed Legacy Delay System
InApps/CTInAppDelayManager.h, CTInAppDelayManager.m, CleverTapInternal.h
Completely removes 414 lines of CTInAppDelayManager and its delegate protocol from internal interface.
SDK Core Updates
CleverTap.h, CleverTap.m, CTConstants.h
Adds fetchInactionInApps: public method, fresh app launch tracking (isFreshAppLaunch, freshAppLaunchSent), batch header "fl" field, and three new in-app meta key constants.
In-App Store Updates
InApps/CTInAppStore.h, CTInAppStore.m
Adds server-side in-action metadata storage, delayed in-app store/clear/dequeue methods; refactors persistence with encryption/decryption for in-app data using new key suffixes.
In-App Evaluation & Processing
InApps/CTInAppEvaluationManager.h/.m, CleverTap+InAppsResponseHandler.m
Introduces selectAndProcessEligibleInApps:withStrategy:withTTL: for unified processing with suppression and TTL; updates response handler to partition and route in-apps through new schedulers and evaluators instead of direct queuing.
In-App Display & Scheduling
InApps/CTInAppDisplayManager.h/.m
Replaces CTInAppDelayManager with CTInAppScheduler for both delayed and in-action in-apps; adds scheduleDelayedInAppsForAllModes: and scheduleInActionInApps: public methods; removes template registration check.
Project Configuration
CleverTapSDK.xcodeproj/project.pbxproj
Adds InAppsScheduler group with new Swift scheduler files and result types; removes CTInAppDelayManager references; updates header build phases and file references (+52/-8 lines).

Sequence Diagram

sequenceDiagram
    participant Client as App/Client
    participant Scheduler as CTInAppScheduler
    participant Timer as InAppTimerManager
    participant Storage as StorageStrategy
    participant Extractor as DataExtractor
    participant Callback as Completion Handler

    Client->>Scheduler: schedule(inApps:onComplete:)
    Scheduler->>Storage: prepareForScheduling(inApps:)
    Storage-->>Scheduler: prepared (true/false)
    
    loop For each in-app with delay > 0
        Scheduler->>Timer: scheduleTimer(id:delay:callback:)
        Timer-->>Scheduler: DispatchWorkItem
    end

    Note over Timer: Wait for delay...
    Timer->>Storage: retrieveAfterTimer(id:)
    Storage-->>Timer: in-app data (or nil)
    
    Timer->>Extractor: createSuccessResult(id:data:)<br/>or createErrorResult(id:)<br/>or createDiscardedResult(id:)
    Extractor-->>Timer: CTTimerResult / CTDelayedInAppResult / CTInActionResult
    
    Timer->>Callback: callback(result)
    Callback-->>Client: onComplete with result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #448: Modifies CleverTap+InAppsResponseHandler to skip in-app processing during user-switch scenarios, aligning with similar control-flow changes in this PR's response handler refactoring.
  • PR #495: Introduces CTInAppDelayManager and timer-based delay support; this PR replaces that system with the new CTInAppScheduler framework.
  • PR #491: Directly related—also modifies the delayed in-apps subsystem (CTInAppDelayManager, CTInAppStore, CTInAppDisplayManager, CTConstants) that this PR refactors.

Suggested reviewers

  • nishant-clevertap
  • nzagorchev
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: implementing InAction in-app notifications and refactoring in-app scheduling with a new CTInAppScheduler component, which aligns with the extensive changes throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch task/SDK-5440_inapp_inactions

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Reviews will stop working after February 8, 2026 if the new IP is not added to your allowlist.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@francispereira
Copy link

francispereira commented Jan 29, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@akashvercetti akashvercetti changed the title SDK-5440 Implement InAction inApps SDK-5440 Implement InAction inApps @codearabbitai Jan 29, 2026
@akashvercetti
Copy link
Collaborator

akashvercetti commented Jan 29, 2026

@coderabbitai review

@akashvercetti akashvercetti changed the title SDK-5440 Implement InAction inApps @codearabbitai SDK-5440 Implement InAction inApps @coderabbitai Jan 29, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai bot changed the title SDK-5440 Implement InAction inApps @coderabbitai SDK-5440 Implement InAction inApps Refactor in-app notification scheduling with CTInAppScheduler Jan 29, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CleverTapSDK/InApps/CTInAppEvaluationManager.m (1)

144-150: Fix server-side queueing and align TTL intent.

  1. _addInAppNotificationsToQueue: is passed @[ssInApps], which nests the array and likely enqueues a single array element instead of in-app dictionaries.
  2. The comment says server-side should not update TTL, but withTTL:true can still update TTL depending on strategy.
🔧 Proposed fix
-    NSMutableArray *ssInApps = [self selectAndProcessEligibleInApps: eligibleInApps withStrategy:ImmediateInAppSelectionStrategy.shared withTTL: true];
-    [self.inAppDisplayManager _addInAppNotificationsToQueue:@[ssInApps]];
+    NSArray<NSDictionary *> *ssInApps = [self selectAndProcessEligibleInApps:eligibleInApps
+                                                                withStrategy:ImmediateInAppSelectionStrategy.shared
+                                                                     withTTL:NO];
+    [self.inAppDisplayManager _addInAppNotificationsToQueue:ssInApps];
🤖 Fix all issues with AI agents
In `@CleverTapSDK/InApps/CleverTap`+InAppsResponseHandler.m:
- Around line 93-111: The second storeClientSideInApps: call overwrites the
first; instead, when partitionImmediateDelayedInApps (ImmediateAndDelayed)
returns both immediate and delayed in-apps, merge the arrays (e.g., concatenate
partitionedClientSideInApps.immediateInApps and .delayedInApps preserving
desired order) into a single NSArray and call [self.inAppStore
storeClientSideInApps:mergedArray] once; keep the separate downloadMediaURLs:
and downloadCustomTemplatesFileURLs: invocations for each group but remove the
second storeClientSideInApps: call so storeClientSideInApps: (which assigns
_clientSideInApps) receives the combined list.

In `@CleverTapSDK/InApps/CTInAppEvaluationManager.m`:
- Around line 196-202: The server-side path in
evaluateOnAppLaunchedInActionServerSide: creates ssInAppsCopy using a shallow
mutableCopy so its NSDictionary payloads stay immutable; change the code to
deep-copy each NSDictionary into NSMutableDictionary (same approach as the
delayed path) after receiving ssInApps from
selectAndProcessEligibleInApps:withStrategy:withTTL:, then pass that array of
NSMutableDictionary objects to scheduleInActionInApps: to avoid mutation crashes
at runtime.

In `@CleverTapSDK/InApps/CTInAppStore.m`:
- Around line 438-459: The method storeServerSideInactionInApps: incorrectly
assigns the payload to _serverSideInApps, overwriting the regular server-side
in-apps cache; change the assignment to the intended in-action metadata property
(_serverSideInActionInAppsMetaData) before encrypting and persisting using
storageKeyWithSuffix:CLTAP_PREFS_INACTION_INAPP_KEY_SS, keeping the existing
encryption (cryptManager) and CTPreferences putString flow and preserving the
exception handling and logging in the same method.
- Around line 299-329: The function dequeueDelayedInAppWithCampaignId: currently
returns the matched inApp from inside the for-loop before removing it from the
delayedInAppsQueue; modify the logic in dequeueDelayedInAppWithCampaignId: so
that when a matching queuedCampaignId is found you remove the item from the
mutable delayedInAppsQueue and call storeDelayedInApps: to persist the updated
queue (using the same delayedInAppsQueue NSMutableArray), then return the
removed inApp; ensure these changes are inside the `@synchronized`(self) block and
reference the existing symbols delayedInAppsQueue, storeDelayedInApps:, and
CLTAP_INAPP_ID so the item is actually removed before returning.

In `@CleverTapSDK/InApps/InAppsScheduler/CTTimerResult.h`:
- Around line 1-14: Add the missing Foundation import and nullability
annotations to the CTTimerResult.h header: include `#import`
<Foundation/Foundation.h> at the top of the file and wrap the public
declarations (the CTTimerResultType enum) with NS_ASSUME_NONNULL_BEGIN and
NS_ASSUME_NONNULL_END to match other headers (e.g., CTInActionResult.h) so
NS_ENUM and other Foundation symbols resolve correctly.

In `@CleverTapSDK/InApps/InAppsScheduler/InactionInAppStorageStrategy.swift`:
- Around line 17-43: prepareForScheduling currently writes to inActionCache
without synchronization causing data races with retrieveAfterTimer; wrap the
write loop into cacheQueue.sync(flags: .barrier) to ensure exclusive access and
that the method only returns after caching completes, keeping the weak self
guard (guard let self = self else { return true }) and updating
cachedCount/inActionCache inside that barrier block while preserving the same
inActionId logic.

In `@CleverTapSDK/InApps/InAppsScheduler/InAppDataExtractor.swift`:
- Around line 61-71: In InAppDataExtractor.extractDelay the code claims the
stored value is milliseconds but returns it unchanged; update extractDelay to
convert milliseconds to seconds before returning (divide the extracted
Int/Double by 1000.0), and handle numeric types robustly (Int, Double, NSNumber)
so the method returns a TimeInterval in seconds consistent with
DelayedInAppDataExtractor; reference the InAppDataExtractor.extractDelay method
to locate and change the conversion logic.

In `@CleverTapSDK/InApps/InAppsScheduler/InAppScheduler.swift`:
- Around line 67-71: The loop over newInApps uses the wrong key ("id") when
extracting the in-app identifier which causes nil/incorrect IDs to be passed to
dataExtractor?.createErrorResult(id:message:) and onComplete(result); update the
guard in the loop that currently reads let id = inApp["id"] to use the
consistent key "ti" (i.e., guard let id = inApp["ti"] as? String else { continue
}) so the error callback receives the correct identifier (ensure you update the
extraction in the for-in loop where newInApps,
dataExtractor?.createErrorResult(id:message:), and onComplete(result) are
referenced).

In `@CleverTapSDK/InApps/InAppsScheduler/InAppSelectionStrategy.swift`:
- Around line 137-138: The constant INAPP_DELAY_AFTER_TRIGGER in
InAppSelectionStrategy (currently set to "delay") mismatches
InAppDelayConstants.INAPP_DELAY_AFTER_TRIGGER (which is "delayAfterTrigger"),
causing key inconsistencies; update INAPP_DELAY_AFTER_TRIGGER to use the exact
same key ("delayAfterTrigger") as InAppDelayConstants.INAPP_DELAY_AFTER_TRIGGER,
or refactor usages to reference InAppDelayConstants.INAPP_DELAY_AFTER_TRIGGER
directly (search for INAPP_DELAY_AFTER_TRIGGER, InAppSelectionStrategy, and
InAppDurationPartitioner to align all usages).

In `@CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift`:
- Around line 114-120: Remove all leftover commented-out code blocks in
InAppTimerManager (e.g., the commented Thread.current.isCancelled check and
other commented sections around timer handling and job storage). Clean up
comments near symbols like InAppTimerManager,
storeCancelledJob(id:delay:scheduledAt:callback:), removeActiveJob(id:), and any
prints referencing self.tag so only relevant inline comments remain; ensure no
functional change is introduced—only delete the commented-out code fragments
mentioned in the review.
- Around line 157-168: cancelAllTimers() currently unlocks before cancelling,
which allows scheduleTimer() to add new timers that might be incorrectly
cancelled and leaves stale entries; fix by acquiring lock, copying the current
workItems from activeJobs (e.g., map values' workItem), clearing activeJobs
(activeJobs.removeAll()) while still holding the lock, then release the lock and
iterate to cancel the copied workItems (workItem.cancel()), and finally log the
cancelledCount; update the method to reference cancelAllTimers(), activeJobs,
workItem, scheduleTimer(), and cleanup() accordingly.
🟡 Minor comments (13)
CleverTapSDK.xcodeproj/project.pbxproj-332-342 (1)

332-342: Confirm header visibility for new InApp result types.

Lines 339-341 and 2317-2319 add the new headers without ATTRIBUTES, so they default to Project visibility. The codebase convention is to explicitly set header visibility (as seen in other header entries with settings = {ATTRIBUTES = (Public, ); } or ATTRIBUTES = (Private, ); }). If these types are meant to be visible to Swift or exposed in the framework module map, set the visibility explicitly.

🔧 Suggested update if these headers must be exposed
-57B7E7772F2B33C000CF22E1 /* CTInActionResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B7E7702F2B33C000CF22E1 /* CTInActionResult.h */; };
-57B7E7782F2B33C000CF22E1 /* CTTimerResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B7E7722F2B33C000CF22E1 /* CTTimerResult.h */; };
-57B7E7792F2B33C000CF22E1 /* CTDelayedInAppResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B7E76E2F2B33C000CF22E1 /* CTDelayedInAppResult.h */; };
+57B7E7772F2B33C000CF22E1 /* CTInActionResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B7E7702F2B33C000CF22E1 /* CTInActionResult.h */; settings = {ATTRIBUTES = (Public, ); }; };
+57B7E7782F2B33C000CF22E1 /* CTTimerResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B7E7722F2B33C000CF22E1 /* CTTimerResult.h */; settings = {ATTRIBUTES = (Public, ); }; };
+57B7E7792F2B33C000CF22E1 /* CTDelayedInAppResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B7E76E2F2B33C000CF22E1 /* CTDelayedInAppResult.h */; settings = {ATTRIBUTES = (Public, ); }; };
CleverTapSDK/InApps/InAppDurationPartitioner.swift-1-7 (1)

1-7: File header comment doesn't match filename.

The header comment says InAppDelayConstants.swift but the actual filename is InAppDurationPartitioner.swift.

✏️ Suggested fix
 //
-//  InAppDelayConstants.swift
+//  InAppDurationPartitioner.swift
 //  CleverTapSDK
 //
CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift-1-7 (1)

1-7: File header comment doesn't match filename.

The header says InAppSchedulingStrategy.swift but the actual filename is InAppTimerManager.swift.

✏️ Suggested fix
 //
-//  InAppSchedulingStrategy.swift
+//  InAppTimerManager.swift
 //  CleverTapSDK
 //
CleverTapSDK/InApps/InAppDurationPartitioner.swift-43-50 (1)

43-50: Documentation lists methods that don't exist.

The class documentation mentions partitionLegacyInApps, partitionClientSideInApps, and partitionAppLaunchServerSideInApps but the actual method names are partitionImmediateDelayedInApps, partitionLegacyMetaInApps, partitionServerSideMetaInApps, and partitionAppLaunchServerSideMetaInApps.

✏️ Suggested fix
  * Provides specialized partition functions for different in-app sources:
- * - `partitionLegacyInApps`: immediate + delayed
+ * - `partitionImmediateDelayedInApps`: immediate + delayed (used for legacy, CS, and app-launch SS)
  * - `partitionLegacyMetaInApps`: inAction only
- * - `partitionClientSideInApps`: immediate + delayed
  * - `partitionServerSideMetaInApps`: unknown + inAction
- * - `partitionAppLaunchServerSideInApps`: immediate + delayed
  * - `partitionAppLaunchServerSideMetaInApps`: inAction only
CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift-387-402 (1)

387-402: Remove unused CTCancelledJobData class.

CTCancelledJobData duplicates the private CancelledJobData struct and is not used anywhere in the codebase. Despite the @objc annotation suggesting Objective-C interop, the complete absence of references indicates it is dead code.

CleverTapSDK/CleverTap.m-145-146 (1)

145-146: Add synchronization to prevent concurrent writes to the fresh‑launch flag.

freshAppLaunchSent is a file-static variable shared across all CleverTap instances, but isFreshAppLaunch is an instance method with no synchronization. Since multiple SDK instances can exist and call this method concurrently, freshAppLaunchSent can be read and written without ordering guarantees, violating the one-shot-per-process intent. Wrap the read-modify-write with @synchronized([CleverTap class]).

Fix
- (BOOL)isFreshAppLaunch {
-    BOOL isInitialTimeRecorded = (initialAppEnteredForegroundTime > 0);
-    BOOL result = isInitialTimeRecorded && !freshAppLaunchSent;
-    freshAppLaunchSent = isInitialTimeRecorded;
-    return result;
-}
+ (BOOL)isFreshAppLaunch {
+    `@synchronized`([CleverTap class]) {
+        BOOL isInitialTimeRecorded = (initialAppEnteredForegroundTime > 0);
+        BOOL result = isInitialTimeRecorded && !freshAppLaunchSent;
+        freshAppLaunchSent = isInitialTimeRecorded;
+        return result;
+    }
+}

Also applies to: 839-841, 856-861

CleverTapSDK/CleverTap.m-3696-3703 (1)

3696-3703: Add input validation to fetchInactionInApps: to handle non-numeric campaign IDs.

The integerValue method returns 0 for non-numeric strings, which silently enqueues a fetch for campaign ID 0 instead of failing or logging the issue. Consider validating that inAppId is numeric before conversion, or short-circuit with an early return for invalid inputs.

✅ Input validation + safe conversion
- (void)fetchInactionInApps:(NSString *)inAppId {
-    NSNumber *campaignId = @([inAppId integerValue]);
-    NSDictionary *eventData = @{
-        @"t": @(6),
-        @"tgtId": campaignId  // Use inAppId directly if it's already NSNumber or typecast to NSString
-    };
-    [self queueEvent:@{CLTAP_EVENT_NAME: CLTAP_WZRK_FETCH_EVENT, CLTAP_EVENT_DATA: eventData} withType:CleverTapEventTypeFetch];
-}
+ (void)fetchInactionInApps:(NSString *)inAppId {
+    if (inAppId.length == 0) {
+        CleverTapLogDebug(self.config.logLevel, @"%@: fetchInactionInApps called with empty inAppId", self);
+        return;
+    }
+    NSScanner *scanner = [NSScanner scannerWithString:inAppId];
+    NSInteger campaignValue;
+    if (![scanner scanInteger:&campaignValue] || !scanner.isAtEnd) {
+        CleverTapLogDebug(self.config.logLevel, @"%@: fetchInactionInApps called with non-numeric inAppId: %@", self, inAppId);
+        return;
+    }
+    NSDictionary *eventData = @{
+        @"t": @(6),
+        @"tgtId": @(campaignValue)
+    };
+    [self queueEvent:@{CLTAP_EVENT_NAME: CLTAP_WZRK_FETCH_EVENT, CLTAP_EVENT_DATA: eventData} withType:CleverTapEventTypeFetch];
+}
CleverTapSDK/InApps/InAppsScheduler/CTInActionResult.m-12-18 (1)

12-18: Missing [inActionId copy] in readyToFetchWithId:data:.

The inActionId is assigned directly without copying, unlike the message property in other factory methods (lines 24, 32, 40). For consistency and safety with NSString properties, use [inActionId copy].

🐛 Proposed fix
 + (instancetype)readyToFetchWithId:(NSString *)inActionId data:(NSDictionary<NSString *, id> *)data {
     CTInActionResult *result = [[CTInActionResult alloc] init];
     result->_type = CTInActionResultTypeReadyToFetch;
-    result->_inActionId = inActionId;
+    result->_inActionId = [inActionId copy];
     result->_data = data;
     return result;
 }
CleverTapSDK/InApps/InAppsScheduler/CTInActionResult.m-20-42 (1)

20-42: Inconsistent inActionId handling across factory methods.

errorWithId:, discardedWithId:, and cancelledWithId: also assign inActionId directly without [copy]. Apply the same fix to all factory methods for consistency.

🐛 Proposed fix for all factory methods
 + (instancetype)errorWithId:(NSString *)inActionId message:(NSString *)message {
     CTInActionResult *result = [[CTInActionResult alloc] init];
     result->_type = CTInActionResultTypeError;
-    result->_inActionId = inActionId;
+    result->_inActionId = [inActionId copy];
     result->_message = [message copy];
     return result;
 }

 + (instancetype)discardedWithId:(NSString *)inActionId message:(NSString *)message {
     CTInActionResult *result = [[CTInActionResult alloc] init];
     result->_type = CTInActionResultTypeDiscarded;
-    result->_inActionId = inActionId;
+    result->_inActionId = [inActionId copy];
     result->_message = [message copy];
     return result;
 }

 + (instancetype)cancelledWithId:(NSString *)inActionId message:(NSString *)message {
     CTInActionResult *result = [[CTInActionResult alloc] init];
     result->_type = CTInActionResultTypeCancelled;
-    result->_inActionId = inActionId;
+    result->_inActionId = [inActionId copy];
     result->_message = [message copy];
     return result;
 }
CleverTapSDK/InApps/InAppsScheduler/InAppScheduler.swift-123-136 (1)

123-136: Potential nil assignment when dataExtractor is nil.

When dataExtractor is nil, createErrorResult and createSuccessResult return nil via optional chaining. The local result variable is typed as Any (non-optional) but will receive nil, which then gets passed to onComplete. This could cause unexpected behavior downstream.

Consider adding a guard or providing a fallback result when dataExtractor is unavailable.

🛡️ Proposed fix with guard
             case .completed:
                 // Timer completed, retrieve and process
-                let result: Any
                 guard let resultId = timerResult.resultId else {
-                    result = self.dataExtractor?.createErrorResult(id: id, message: "Data not found")
-                    onComplete(result)
+                    if let errorResult = self.dataExtractor?.createErrorResult(id: id, message: "Data not found") {
+                        onComplete(errorResult)
+                    }
                     return
                 }
                 let data = self.storageStrategy?.retrieveAfterTimer(id: resultId)
                 
+                let result: Any?
                 if let data = data as? [String: Any] {
                     result = self.dataExtractor?.createSuccessResult(id: resultId, data: data)
                 } else {
                     result = self.dataExtractor?.createErrorResult(id: resultId, message: "Data not found")
                 }
-                onComplete(result)
+                if let result = result {
+                    onComplete(result)
+                }
CleverTapSDK/InApps/CTInAppDisplayManager.m-206-223 (1)

206-223: Dead statement and redundant mutableCopy calls.

Line 214 [result.data mutableCopy]; is a dead statement with no effect. Lines 216-217 each create a new mutable copy of the same data. This is both wasteful and potentially error-prone if modifications are expected to persist.

🐛 Proposed fix
         switch (result.type) {
             case CTDelayedInAppResultTypeSuccess:
-                [result.data mutableCopy];
-                NSLog(@"Updating TTL");
-                [strongSelf.inAppStore updateTTL:[result.data mutableCopy]];
-                [strongSelf _addInAppNotificationsToQueue:@[[result.data mutableCopy]]];
+                {
+                    NSMutableDictionary *inAppData = [result.data mutableCopy];
+                    CleverTapLogInternal(strongSelf.config.logLevel, @"%@: Updating TTL for delayed in-app", strongSelf);
+                    [strongSelf.inAppStore updateTTL:inAppData];
+                    [strongSelf _addInAppNotificationsToQueue:@[inAppData]];
+                }
                 break;
             case CTDelayedInAppResultTypeError: break;
             case CTDelayedInAppResultTypeDiscarded: break;
         }
CleverTapSDK/InApps/CTInAppStore.m-480-501 (1)

480-501: storeServerSideInActionMetaData: is not declared in the header and appears to be dead code.

The method is defined at line 480 in the implementation but has no declaration in CTInAppStore.h and is never called anywhere in the codebase. Consider removing this unused method. Note: the header does contain a similarly-named method storeServerSideInactionInApps: which may have been intended here instead.

CleverTapSDK/InApps/InAppsScheduler/InAppDataExtractor.swift-26-36 (1)

26-36: Correct the misleading variable naming and comment — delayAfterTrigger is already in seconds.

According to CleverTap documentation, delayAfterTrigger is specified in seconds, not milliseconds. The code is correct, but the variable name delayMs and the comment "Assuming delay is stored in milliseconds, convert to seconds" are misleading. Rename delayMs to delay (or delaySecs) and update the comment to reflect that the value is already in seconds.

     `@objc` public func extractDelay(inApp: [String: Any]) -> TimeInterval {
         // Extract delay from in-app dictionary
-        // Assuming delay is stored in milliseconds, convert to seconds
-        if let delayMs = inApp["delayAfterTrigger"] as? Int {
-            return TimeInterval(delayMs)
+        // Delay is specified in seconds by CleverTap
+        if let delay = inApp["delayAfterTrigger"] as? Int {
+            return TimeInterval(delay)
         }
-        if let delayMs = inApp["delayAfterTrigger"] as? Double {
-            return TimeInterval(delayMs)
+        if let delay = inApp["delayAfterTrigger"] as? Double {
+            return TimeInterval(delay)
         }
         return 0
     }
🧹 Nitpick comments (18)
CleverTapSDK/InApps/CTInAppDisplayManager.h (1)

47-48: Add element typing (and clarify nil semantics) for new public APIs.

Because NSArray * _Nullable maps to [Any]? in Swift, callers can pass unexpected payloads silently. Consider documenting the expected element type and (optionally) using Objective‑C generics; also clarify whether nil is valid or if callers should pass an empty array.

♻️ Suggested header refinement (adjust element type as appropriate)
- - (void)scheduleInActionInApps:(NSArray * _Nullable)inappNotifs;
- - (void)scheduleDelayedInAppsForAllModes:(NSArray * _Nullable)inappNotifs;
+/// Schedules in-app payloads; replace NSDictionary with the concrete payload type if different.
+- (void)scheduleInActionInApps:(NSArray<NSDictionary *> * _Nullable)inappNotifs;
+/// Schedules delayed in-app payloads for all display modes.
+- (void)scheduleDelayedInAppsForAllModes:(NSArray<NSDictionary *> * _Nullable)inappNotifs;
CleverTapSDK.xcodeproj/project.pbxproj (1)

1244-1245: Consider nesting InAppDurationPartitioner.swift under the new scheduler group for consistency.

Right now it sits alongside the InAppsScheduler group, while the rest of the scheduler files are grouped together. Moving it under InAppsScheduler keeps the structure coherent.

🧭 Suggested group placement
 			children = (
 				57B7E7542F220A9300CF22E1 /* InAppsScheduler */,
-				57CDEE402F163C4E001161F5 /* InAppDurationPartitioner.swift */,
 				6BB727102B8E455B009CE7D0 /* CustomTemplates */,
@@
 		57B7E7542F220A9300CF22E1 /* InAppsScheduler */ = {
 			isa = PBXGroup;
 			children = (
+				57CDEE402F163C4E001161F5 /* InAppDurationPartitioner.swift */,
 				57B7E76E2F2B33C000CF22E1 /* CTDelayedInAppResult.h */,

Also applies to: 1592-1607

CleverTapSDK/InApps/CleverTap+InAppsResponseHandler.m (1)

83-91: Minor: Inconsistent comment indentation.

Line 85 comment has extra indentation that doesn't align with surrounding code.

✏️ Suggested fix
     // SS in-apps (inapp_notifs_ss -> IN-ACTION + NORMAL in-app campaigns WITH advance display rules)
     UnknownAndInAction *partitionedServerSideInAppsMeta = [InAppDurationPartitioner partitionServerSideMetaInApps:jsonResp[CLTAP_INAPP_SS_JSON_RESPONSE_KEY]];
-        // delayAfterTrigger only comes within inapp_notifs(Legacy SS, with in-app content)
+    // delayAfterTrigger only comes within inapp_notifs(Legacy SS, with in-app content)
     if ([partitionedServerSideInAppsMeta hasUnknownDurationInApps]) {
CleverTapSDK/InApps/InAppsScheduler/CTDelayedInAppResult.h (1)

16-20: Consider prefixing the enum to avoid naming conflicts.

CTErrorReason is a generic name that could conflict with other error enums in the codebase or when integrating with other libraries. Consider a more specific prefix like CTDelayedInAppErrorReason.

✏️ Suggested rename
-typedef NS_ENUM(NSInteger, CTErrorReason) {
-    CTErrorReasonUnknown,
-    CTErrorReasonPreparationFailed,
-    CTErrorReasonDataNotFound
+typedef NS_ENUM(NSInteger, CTDelayedInAppErrorReason) {
+    CTDelayedInAppErrorReasonUnknown,
+    CTDelayedInAppErrorReasonPreparationFailed,
+    CTDelayedInAppErrorReasonDataNotFound
 };

Then update Line 31:

-@property (nonatomic, readonly) CTErrorReason reason;
+@property (nonatomic, readonly) CTDelayedInAppErrorReason reason;
CleverTapSDK/InApps/InAppsScheduler/CTDelayedInAppResult.m (1)

11-17: Consider copying the data dictionary for safety.

The data dictionary is assigned directly without copying. If the caller mutates the dictionary after creating the result, it could lead to unexpected behavior.

✏️ Suggested fix
 + (instancetype)successWithId:(NSString *)resultId data:(NSDictionary<NSString *, id> *)data {
     CTDelayedInAppResult *result = [[CTDelayedInAppResult alloc] init];
     result->_type = CTDelayedInAppResultTypeSuccess;
     result->_resultId = [resultId copy];
-    result->_data = data;
+    result->_data = [data copy];
     return result;
 }
CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift (1)

89-89: Replace print statements with CleverTap logging.

Debug output uses print() statements throughout the file. For consistency with the rest of the SDK and to support log levels, consider using CleverTapLogDebug or the SDK's logging infrastructure.

Also applies to: 105-105, 135-135, 152-152, 167-167, 212-212, 221-221, 266-266, 278-278, 296-298, 318-318

CleverTapSDK/InApps/InAppsScheduler/InAppSelectionStrategy.swift (2)

114-129: Non-deterministic iteration order over dictionary.

The iteration over delayedInAppsByDelay dictionary (Line 114) has no guaranteed order in Swift. While this may not affect correctness since each delay group is independent, it could lead to non-deterministic logging output and potentially affect debugging.

✏️ Suggested fix for deterministic iteration
-        for (delay, inAppsWithSameDelay) in delayedInAppsByDelay {
+        for delay in delayedInAppsByDelay.keys.sorted() {
+            guard let inAppsWithSameDelay = delayedInAppsByDelay[delay] else { continue }
             print( "\(DelayedInAppSelectionStrategy.TAG), message: Processing \(inAppsWithSameDelay.count) in-apps with delay: \(delay)s")

115-115: Replace print statements with CleverTap logging.

Debug output should use the SDK's logging infrastructure for consistency and to respect log level settings.

Also applies to: 127-127

CleverTapSDK/InApps/DurationPartitionedInApps.swift (1)

42-45: Align initializer visibility with the public API.

These classes are public but their initializers are internal, which prevents SDK consumers from constructing non-empty instances. If external construction is intended, make the initializers public; otherwise consider reducing the class visibility to internal.

♻️ Suggested visibility alignment
-    init(immediateInApps: NSArray, delayedInApps: NSArray) {
+    `@objc` public init(immediateInApps: NSArray, delayedInApps: NSArray) {
         self.immediateInApps = immediateInApps
         self.delayedInApps = delayedInApps
         super.init()
     }
@@
-    init(unknownDurationInApps: NSArray, inActionInApps: NSArray) {
+    `@objc` public init(unknownDurationInApps: NSArray, inActionInApps: NSArray) {
         self.unknownDurationInApps = unknownDurationInApps
         self.inActionInApps = inActionInApps
         super.init()
     }
@@
-    init(inActionInApps: NSArray) {
+    `@objc` public init(inActionInApps: NSArray) {
         self.inActionInApps = inActionInApps
         super.init()
     }

Also applies to: 72-75, 103-105

CleverTapSDK/InApps/InAppsScheduler/InAppSchedulingStrategy.swift (1)

11-17: Clean up the commented‑out doc block.

The // / ... lines look like accidental comment remnants. Consider converting them to proper doc comments or removing for clarity.

📝 Tidy doc comments
-//    / Prepare data before scheduling (e.g., save to DB)
-//    / - Parameter inApps: Array of in-app dictionaries
-//    / - Returns: true if preparation successful, false otherwise
+    /// Prepare data before scheduling (e.g., save to DB)
+    /// - Parameter inApps: Array of in-app dictionaries
+    /// - Returns: true if preparation successful, false otherwise
CleverTapSDK/InApps/InAppsScheduler/DelayedInAppStorageStrategy.swift (2)

34-35: Redundant and incorrect optional cast.

The cast (inApp as? [String : Any]?) ?? nil is unnecessarily complex. If dequeueDelayedInApp returns Any?, you should cast directly to the expected type.

♻️ Proposed fix
-        let inApp = store.dequeueDelayedInApp(withCampaignId: id)
-        return (inApp as? [String : Any]?) ?? nil
+        return store.dequeueDelayedInApp(withCampaignId: id) as? [String: Any]

38-40: Empty clear(id:) method may be intentional but warrants clarification.

The method body is empty while InactionInAppStorageStrategy.clear(id:) actually removes items from cache. If this is intentional because dequeueDelayedInApp already removes the item from storage, consider adding a comment to clarify this design decision.

📝 Suggested documentation
     public func clear(id: String) {
-        
+        // No-op: dequeueDelayedInApp already removes the item from persistent storage
     }
CleverTapSDK/InApps/InAppsScheduler/InAppScheduler.swift (1)

147-148: Discarded return values from retrieveAfterTimer in error/discarded cases.

In the error and discarded cases, retrieveAfterTimer is called but its return value is ignored. If the intent is to clean up storage by dequeuing, consider adding a comment to clarify. If actual cleanup is needed, uncomment the clear(id:) calls or use _ = to explicitly discard the value.

Also applies to: 154-155

CleverTapSDK/InApps/InAppsScheduler/CTInActionResult.h (1)

22-22: Consider adding copy attribute to inActionId property.

For consistency with CTTimerResult.h where resultId is declared with copy, and following Objective-C best practices for NSString properties, inActionId should also use the copy attribute to prevent unexpected mutation if a mutable string is passed.

♻️ Proposed fix
-@property (nonatomic, readonly) NSString* inActionId;
+@property (nonatomic, readonly, copy) NSString *inActionId;
CleverTapSDK/InApps/InAppsScheduler/InactionInAppStorageStrategy.swift (2)

45-57: Redundant cast on Line 56.

result is already typed as [String: Any]?, so the cast as? [String : Any] is unnecessary.

♻️ Proposed fix
-        return result as? [String : Any]
+        return result

18-18: Consider using CleverTap's logging system instead of print statements.

The print statements throughout this class will output to console in production builds. For consistency with the rest of the SDK, consider using the CleverTap logging infrastructure (e.g., CleverTapLogInternal or similar) which respects log levels and can be disabled in production.

Also applies to: 38-38, 41-41, 51-51, 53-53, 63-63, 72-72

CleverTapSDK/InApps/InAppsScheduler/InAppDataExtractor.swift (1)

85-87: createCancelledResult duplicates createDiscardedResult logic.

Both methods return CTInActionResult.discarded with the same message. If "cancelled" has different semantics than "discarded", consider using a distinct message or result type. If they're truly equivalent, consider removing one or having createCancelledResult call createDiscardedResult.

CleverTapSDK/InApps/CTInAppDisplayManager.m (1)

208-208: Use CleverTapLogInternal instead of NSLog for consistency.

Multiple NSLog statements are used throughout the new scheduling methods. For consistency with the rest of the SDK and to respect log-level configuration, these should use CleverTapLogInternal or appropriate CleverTap logging macros.

Also applies to: 227-227, 237-237, 240-240, 243-243, 249-249

Comment on lines 196 to 202
- (void)evaluateOnAppLaunchedInActionServerSide:(NSArray *)appLaunchedNotifs {
CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:CLTAP_APP_LAUNCHED_EVENT eventProperties:self.appLaunchedProperties andLocation:self.location];
NSMutableArray *eligibleInApps = [self evaluate:event withInApps:appLaunchedNotifs];
// Server-side evaluations do **NOT** update TTL
NSMutableArray *ssInApps = [self selectAndProcessEligibleInApps: eligibleInApps withStrategy:ImmediateInAppSelectionStrategy.shared withTTL: true];
NSMutableArray<NSMutableDictionary *> *ssInAppsCopy = [ssInApps mutableCopy];
[self.inAppDisplayManager scheduleInActionInApps: ssInAppsCopy];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure in-action scheduling receives mutable dictionaries.

ssInAppsCopy is only a shallow array copy; the dictionaries remain immutable. If scheduleInActionInApps: mutates payloads (as the delayed path suggests), this can throw at runtime. Consider deep-copying the dictionaries as done in the delayed path.

🔧 Proposed fix
-    NSMutableArray<NSMutableDictionary *> *ssInAppsCopy = [ssInApps mutableCopy];
-    [self.inAppDisplayManager scheduleInActionInApps: ssInAppsCopy];
+    NSMutableArray<NSMutableDictionary *> *ssInAppsCopy = [NSMutableArray arrayWithCapacity:ssInApps.count];
+    for (NSDictionary *inApp in ssInApps) {
+        [ssInAppsCopy addObject:[inApp mutableCopy]];
+    }
+    [self.inAppDisplayManager scheduleInActionInApps:ssInAppsCopy];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- (void)evaluateOnAppLaunchedInActionServerSide:(NSArray *)appLaunchedNotifs {
CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:CLTAP_APP_LAUNCHED_EVENT eventProperties:self.appLaunchedProperties andLocation:self.location];
NSMutableArray *eligibleInApps = [self evaluate:event withInApps:appLaunchedNotifs];
// Server-side evaluations do **NOT** update TTL
NSMutableArray *ssInApps = [self selectAndProcessEligibleInApps: eligibleInApps withStrategy:ImmediateInAppSelectionStrategy.shared withTTL: true];
NSMutableArray<NSMutableDictionary *> *ssInAppsCopy = [ssInApps mutableCopy];
[self.inAppDisplayManager scheduleInActionInApps: ssInAppsCopy];
- (void)evaluateOnAppLaunchedInActionServerSide:(NSArray *)appLaunchedNotifs {
CTEventAdapter *event = [[CTEventAdapter alloc] initWithEventName:CLTAP_APP_LAUNCHED_EVENT eventProperties:self.appLaunchedProperties andLocation:self.location];
NSMutableArray *eligibleInApps = [self evaluate:event withInApps:appLaunchedNotifs];
// Server-side evaluations do **NOT** update TTL
NSMutableArray *ssInApps = [self selectAndProcessEligibleInApps: eligibleInApps withStrategy:ImmediateInAppSelectionStrategy.shared withTTL: true];
NSMutableArray<NSMutableDictionary *> *ssInAppsCopy = [NSMutableArray arrayWithCapacity:ssInApps.count];
for (NSDictionary *inApp in ssInApps) {
[ssInAppsCopy addObject:[inApp mutableCopy]];
}
[self.inAppDisplayManager scheduleInActionInApps:ssInAppsCopy];
🤖 Prompt for AI Agents
In `@CleverTapSDK/InApps/CTInAppEvaluationManager.m` around lines 196 - 202, The
server-side path in evaluateOnAppLaunchedInActionServerSide: creates
ssInAppsCopy using a shallow mutableCopy so its NSDictionary payloads stay
immutable; change the code to deep-copy each NSDictionary into
NSMutableDictionary (same approach as the delayed path) after receiving ssInApps
from selectAndProcessEligibleInApps:withStrategy:withTTL:, then pass that array
of NSMutableDictionary objects to scheduleInActionInApps: to avoid mutation
crashes at runtime.

Comment on lines 299 to 329
- (NSDictionary *)dequeueDelayedInAppWithCampaignId:(NSString *)campaignId {
if (!campaignId) return nil;

@synchronized(self) {
NSMutableArray *delayedInAppsQueue = [[NSMutableArray alloc] initWithArray:[self delayedInAppsQueue]];

NSUInteger indexToRemove = NSNotFound;
for (NSUInteger i = 0; i < delayedInAppsQueue.count; i++) {
NSDictionary *inApp = delayedInAppsQueue[i];
NSString *queuedCampaignId = inApp[CLTAP_NOTIFICATION_ID_TAG];

if ([queuedCampaignId isEqualToString:campaignId]) {
id queuedCampaignIdObj = inApp[CLTAP_INAPP_ID];
NSString *queuedCampaignId;

if ([queuedCampaignIdObj isKindOfClass:[NSNumber class]]) {
queuedCampaignId = [(NSNumber *)queuedCampaignIdObj stringValue];
} else if ([queuedCampaignIdObj isKindOfClass:[NSString class]]) {
queuedCampaignId = (NSString *)queuedCampaignIdObj;
} else {
queuedCampaignId = [queuedCampaignIdObj description];
}
if ([queuedCampaignId isEqualToString:campaignId]) {
indexToRemove = i;
break;
return inApp;
}
}

if (indexToRemove != NSNotFound) {
[delayedInAppsQueue removeObjectAtIndex:indexToRemove];
[self storeDelayedInApps:delayedInAppsQueue];
}
return nil;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: dequeueDelayedInAppWithCampaignId: returns before removing the item.

The method returns inApp at line 320 while still inside the loop, before the removal logic at lines 323-326 can execute. This means the delayed in-app is never removed from the queue, causing it to be processed repeatedly.

🐛 Proposed fix
 - (NSDictionary *)dequeueDelayedInAppWithCampaignId:(NSString *)campaignId {
     if (!campaignId) return nil;
     
     `@synchronized`(self) {
         NSMutableArray *delayedInAppsQueue = [[NSMutableArray alloc] initWithArray:[self delayedInAppsQueue]];
         
-        NSUInteger indexToRemove = NSNotFound;
         for (NSUInteger i = 0; i < delayedInAppsQueue.count; i++) {
             NSDictionary *inApp = delayedInAppsQueue[i];
             id queuedCampaignIdObj = inApp[CLTAP_INAPP_ID];
             NSString *queuedCampaignId;
 
             if ([queuedCampaignIdObj isKindOfClass:[NSNumber class]]) {
                 queuedCampaignId = [(NSNumber *)queuedCampaignIdObj stringValue];
             } else if ([queuedCampaignIdObj isKindOfClass:[NSString class]]) {
                 queuedCampaignId = (NSString *)queuedCampaignIdObj;
             } else {
                 queuedCampaignId = [queuedCampaignIdObj description];
             }
-        if ([queuedCampaignId isEqualToString:campaignId]) {
-                indexToRemove = i;
+            if ([queuedCampaignId isEqualToString:campaignId]) {
+                [delayedInAppsQueue removeObjectAtIndex:i];
+                [self storeDelayedInApps:delayedInAppsQueue];
                 return inApp;
             }
         }
-        if (indexToRemove != NSNotFound) {
-            [delayedInAppsQueue removeObjectAtIndex:indexToRemove];
-            [self storeDelayedInApps:delayedInAppsQueue];
-        }
         return nil;
     }
 }
🤖 Prompt for AI Agents
In `@CleverTapSDK/InApps/CTInAppStore.m` around lines 299 - 329, The function
dequeueDelayedInAppWithCampaignId: currently returns the matched inApp from
inside the for-loop before removing it from the delayedInAppsQueue; modify the
logic in dequeueDelayedInAppWithCampaignId: so that when a matching
queuedCampaignId is found you remove the item from the mutable
delayedInAppsQueue and call storeDelayedInApps: to persist the updated queue
(using the same delayedInAppsQueue NSMutableArray), then return the removed
inApp; ensure these changes are inside the `@synchronized`(self) block and
reference the existing symbols delayedInAppsQueue, storeDelayedInApps:, and
CLTAP_INAPP_ID so the item is actually removed before returning.

Comment on lines 1 to 14
//
// CTTimerResult.h
// CleverTapSDK
//
// Created by Sonal Kachare on 23/01/26.
//

// CTTimerResult.h
typedef NS_ENUM(NSInteger, CTTimerResultType) {
CTTimerResultTypeCompleted,
CTTimerResultTypeError,
CTTimerResultTypeDiscarded
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing #import <Foundation/Foundation.h> causes compilation failure.

The header is missing the Foundation import, which causes all the static analysis errors (NS_ENUM, NSObject, NSString, NSError, etc. are undefined). Compare with CTInActionResult.h which correctly imports Foundation.

Additionally, consider adding NS_ASSUME_NONNULL_BEGIN/NS_ASSUME_NONNULL_END for consistency with other headers in this module.

🐛 Proposed fix
 //
 //  CTTimerResult.h
 //  CleverTapSDK
 //
 //  Created by Sonal Kachare on 23/01/26.
 //

-// CTTimerResult.h
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
 typedef NS_ENUM(NSInteger, CTTimerResultType) {
     CTTimerResultTypeCompleted,
     CTTimerResultTypeError,
     CTTimerResultTypeDiscarded
 };

And at the end of the file:

 + (instancetype)discardedWithId:(NSString *)resultId;

 `@end`
+
+NS_ASSUME_NONNULL_END
🧰 Tools
🪛 Clang (14.0.6)

[error] 9-9: function definition declared 'typedef'

(clang-diagnostic-error)


[error] 10-10: use of undeclared identifier 'CTTimerResultTypeCompleted'

(clang-diagnostic-error)


[error] 11-11: use of undeclared identifier 'CTTimerResultTypeError'; did you mean 'CTTimerResultType'?

(clang-diagnostic-error)


[error] 12-12: use of undeclared identifier 'CTTimerResultTypeDiscarded'

(clang-diagnostic-error)

🤖 Prompt for AI Agents
In `@CleverTapSDK/InApps/InAppsScheduler/CTTimerResult.h` around lines 1 - 14, Add
the missing Foundation import and nullability annotations to the CTTimerResult.h
header: include `#import` <Foundation/Foundation.h> at the top of the file and
wrap the public declarations (the CTTimerResultType enum) with
NS_ASSUME_NONNULL_BEGIN and NS_ASSUME_NONNULL_END to match other headers (e.g.,
CTInActionResult.h) so NS_ENUM and other Foundation symbols resolve correctly.

Comment on lines 157 to 168
@objc public func cancelAllTimers() {
lock.lock()
let cancelledCount = activeJobs.count
let jobsToCancel = activeJobs.values.map { $0.workItem }
lock.unlock()

for workItem in jobsToCancel {
workItem.cancel()
}

print("\(tag) Cancelled \(cancelledCount) timers")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all callers of cancelAllTimers
rg -n "cancelAllTimers" --type swift -C 3

Repository: CleverTap/clevertap-ios-sdk

Length of output: 2668


🏁 Script executed:

#!/bin/bash
# Get the full InAppTimerManager.swift file
wc -l CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift

Repository: CleverTap/clevertap-ios-sdk

Length of output: 134


🏁 Script executed:

#!/bin/bash
# Read key sections of the file to understand the implementation
echo "=== Lines 1-60 (Class definition and structure) ==="
sed -n '1,60p' CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift

echo ""
echo "=== Lines 80-105 (scheduleTimer method) ==="
sed -n '80,105p' CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift

echo ""
echo "=== Lines 155-175 (cancelAllTimers and cleanup) ==="
sed -n '155,175p' CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift

echo ""
echo "=== Lines 210-230 (cleanup method) ==="
sed -n '210,230p' CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift

Repository: CleverTap/clevertap-ios-sdk

Length of output: 4326


🏁 Script executed:

#!/bin/bash
# Search entire codebase for cancelAllTimers calls (excluding the definition and cleanup)
rg "cancelAllTimers" --type swift | grep -v "^CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift"

Repository: CleverTap/clevertap-ios-sdk

Length of output: 53


Race condition: cancelAllTimers() releases lock before cancelling work items.

The lock is released at line 162 before the cancellation loop begins at line 164, allowing new timers to be scheduled concurrently via scheduleTimer() during the cancellation window. While scheduleTimer() checks isCancelled before reusing existing timers, this race condition could still cause newly-scheduled timers to be immediately cancelled.

Additionally, cancelAllTimers() doesn't clear activeJobs, leaving stale cancelled entries. Although this is mitigated by scheduleTimer() creating new work items for cancelled entries, and by cleanup() immediately calling activeJobs.removeAll() after cancellation, the public @objc visibility means external callers could invoke cancelAllTimers() directly, accumulating stale entries.

🤖 Prompt for AI Agents
In `@CleverTapSDK/InApps/InAppsScheduler/InAppTimerManager.swift` around lines 157
- 168, cancelAllTimers() currently unlocks before cancelling, which allows
scheduleTimer() to add new timers that might be incorrectly cancelled and leaves
stale entries; fix by acquiring lock, copying the current workItems from
activeJobs (e.g., map values' workItem), clearing activeJobs
(activeJobs.removeAll()) while still holding the lock, then release the lock and
iterate to cancel the copied workItems (workItem.cancel()), and finally log the
cancelledCount; update the method to reference cancelAllTimers(), activeJobs,
workItem, scheduleTimer(), and cleanup() accordingly.

@akashvercetti akashvercetti changed the title SDK-5440 Implement InAction inApps Refactor in-app notification scheduling with CTInAppScheduler SDK-5440: Implement InAction inApps and Refactor in-app notification scheduling with CTInAppScheduler Jan 29, 2026

@discussion
Fetch in-action in-app content from backend after inactionDuration expires
Sends wzrk_fetch event with t=6 and target ID
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this statement needed?

static int currentRequestTimestamp = 0;
static int initialAppEnteredForegroundTime = 0;
static BOOL isAutoIntegrated;
static BOOL freshAppLaunchSent = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it NO

[self.instance fetchInactionInApps:inAppId];
}

- (NSMutableDictionary *)createInActionFetchRequestWithTargetId:(NSNumber *)targetId {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is never called

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed - unused

@akashvercetti
Copy link
Collaborator

@Sonal-Kachare CleverTapSDK.xcodeproj does not build. Fix missing file errors.


@param inAppId the campaign ID (ti) to fetch content for
*/
- (void)fetchInactionInApps:(NSString * _Nonnull)inAppId;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is not a client side public api, move it to an internal file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it to internal header file

@akashvercetti
Copy link
Collaborator

akashvercetti commented Feb 3, 2026

@Sonal-Kachare Refactor NSLog and print to CleverTapDebug

@akashvercetti
Copy link
Collaborator

@Sonal-Kachare fix the warnings in CTInAppScheduler

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants