Skip to content

3.0.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 13:35
· 3 commits to main since this release
c18668d

Changed

  • TurboModule migration. The SDK is now a real TurboModule generated by React Native's codegen, with full backwards compatibility for the legacy architecture. Previously the package shipped no codegenConfig at all, so codegen never ran and the module was reached through React Native's legacy interop layer even on the New Architecture. The public JavaScript API is unchanged.
    • Added a codegenConfig block (RNAppstackSdkSpec) and a real TurboModule spec at src/NativeAppstackReactNative.ts; the JavaScript entry point now resolves the native module through TurboModuleRegistry, which works on both architectures.
    • iOS: AppstackReactNative conforms to the generated NativeAppstackReactNativeSpec protocol and implements getTurboModule: under RCT_NEW_ARCH_ENABLED, falling back to RCTBridgeModule on the legacy architecture.
    • Android: the module now extends the generated NativeAppstackReactNativeSpec on the New Architecture (with a BaseReactPackage that reports isTurboModule), and keeps the ReactContextBaseJavaModule implementation on the legacy architecture. Shared logic moved to AppstackReactNativeModuleImpl.
  • Breaking: configure() now rejects a fractional logLevel. logLevel: 1.5 previously passed validation and was silently truncated to 1 by the native casts (NSInteger on iOS, Double.toInt() on Android); only the documented levels 0, 1, 2 and 3 are accepted. The error message is now logLevel must be one of 0, 1, 2, or 3 (was logLevel must be a number between 0 and 3, which read as though 1.5 were valid).
  • Breaking: sendEvent resolves void instead of true. The old true was misleading — it only ever meant "the call reached native", and was returned even when the native SDK went on to drop the event (automatic-only type, SDK disabled, buffer full, no connectivity). Nothing about delivery is observable from JavaScript, so the return value no longer implies it. Code branching on the result should stop doing so; awaiting still works unchanged.
  • sendEvent now resolves the event type in JavaScript, sending native an explicit (type, name) pair: a standard type with a null name, or the CUSTOM category with a name. This fixes a cross-platform divergence in which the same call behaved differently per platform: sendEvent('MY_CUSTOM_EVENT') — an unknown type with no separate name — was sent by iOS as a custom event named MY_CUSTOM_EVENT, while Android collapsed it to CUSTOM and then rejected it with INVALID_EVENT_NAME because no name had been supplied. Both platforms now receive identical arguments, and the native wrappers' guessing branches are no longer reachable from JavaScript.
  • A manual INSTALL is dropped before reaching native, logging an error and resolving normally rather than throwing. Installs are recorded automatically and sending them by hand inflates install counts, which is why iOS already discarded them natively. The same guard covers the SDK's internal lifecycle events, which are not part of the public EventType API: those exist only in iOS's enum, so sending one by hand previously became a bogus custom event on Android alone. Resolving rather than rejecting keeps callers who harmlessly send a manual INSTALL today — a silent discard since 2.5.0 — from suddenly seeing failures.
  • Parameter values of null or undefined are stripped before the call reaches native. Android already filtered them out (filterValues { it != null }) while iOS forwarded NSNull, so identical JavaScript produced a different payload per platform. Stripping is shallow, matching Android's behaviour; an empty result is sent as no parameters at all. 0, false and '' are real values and are preserved.
  • parameters is typed against a JSON-safe value type (Record<string, JsonValue | undefined>) instead of Record<string, any>. Values outside that set — a Date, a class instance, a function — never survived the bridge intact; this makes that a compile error instead of a runtime surprise. AppstackEventParameters and JsonValue are exported for annotating your own payloads.
  • Android: the native wrapper now treats an unrecognised event type as a custom event named after that type, matching iOS, instead of rejecting with INVALID_EVENT_NAME. Defence in depth only — the JavaScript wrapper no longer sends an unrecognised type, so this path is unreachable from JS; it exists so the divergence above cannot return if that invariant is ever broken.

Removed

  • Breaking: the positional configure(apiKey, isDebug?, endpointBaseUrl?, logLevel?, customerUserId?) signature, deprecated in 2.6.0, is gone. Only configure(apiKey, { logLevel, customerUserId }) remains. isDebug and endpointBaseUrl are removed outright — neither was ever forwarded to the native SDKs, so no behaviour is lost by dropping them. A 2.x-style call now throws an error naming the replacement instead of silently ignoring the extra arguments, which would otherwise discard the logLevel and customerUserId that follow them.
    // 2.x
    await AppstackSDK.configure(apiKey, false, undefined, 0, 'user_123');
    // 3.0
    await AppstackSDK.configure(apiKey, { logLevel: 0, customerUserId: 'user_123' });
  • Breaking: sendEvent(eventType, eventName, parameters) is now sendEvent(event, parameters). The middle eventName argument is gone. Pass a standard EventType for a standard event, or your own name for a custom one — there is no separate "custom" mode:
    // before                                          // after
    sendEvent('PURCHASE', null, { revenue: 4.99 })    sendEvent(EventType.PURCHASE, { revenue: 4.99 })
    sendEvent('CUSTOM', 'user_attributes', params)    sendEvent('user_attributes', params)
    sendEvent('CUSTOM', 'APP_OPENED')                 sendEvent('APP_OPENED')
    A three-argument call now rejects with a message naming the replacement, rather than silently binding the event name to parameters and shipping an event whose payload is its own name. (sendEvent is async, so every validation failure rejects the returned promise rather than throwing synchronously.) The two-argument sendEvent('CUSTOM', 'my_event') form — a custom event with no parameters — is rejected on the same grounds, since its name would otherwise land in parameters. EventType itself stays, and remains the recommended form; a plain string is accepted and resolved case-insensitively.
  • Breaking: EventType.CUSTOM was removed. In the two-argument API you pass a custom event's name directly, so CUSTOM had no caller-facing meaning and was actively a footgun: sendEvent(EventType.CUSTOM, params) would have resolved as "standard type CUSTOM with no name", which iOS drops outright and Android sends with a null event_name. Removing it turns that into a compile error; the literal string 'CUSTOM' is also rejected at runtime, with a message pointing at the custom-name form.
  • Breaking: isDebug and endpointBaseUrl are removed from the exported AppstackConfig type.
  • iOS: The native module no longer subclasses RCTEventEmitter. It never emitted any events (supportedEvents returned an empty array), and the inherited addListener / removeListeners methods were already unavailable on the New Architecture. Any code calling new NativeEventEmitter(NativeModules.AppstackReactNative) on the legacy architecture should be removed.
  • The two deprecation console.warns that 2.6.0 added for positional isDebug / endpointBaseUrl are gone with the signature they warned about.
  • Deleted a stale, unwrapped duplicate of two AppstackSDK.framework slices that was committed directly under ios/ (plus its orphaned Info.plist). It was not referenced by the podspec but was published in every npm tarball. The vendored framework at ios/AppstackSDK.xcframework is unaffected.

Added

  • In development builds (__DEV__), sendEvent logs a warning when the event string is not a recognised standard type, naming the custom event it became. This is the only place a misspelled standard event is catchable: a typo like 'PURCAHSE' otherwise becomes a custom event with no error anywhere, silently losing the standard-event semantics that enhanced app campaigns optimise against. It is a heuristic — it also fires on legitimate custom names — so it is advisory, dev-only, and never gates the send.

Fixed

  • iOS: The SDK now builds when the host app forces our pod below iOS 15.0, instead of failing to compile. AppstackASAAttribution in the vendored framework requires iOS 15.0, and the podspec asks for a 15.0 deployment target — but React Native 0.74 writes its own minimum (13.4) onto every pod target after install, and that setting wins over the podspec. The build then stopped with 'AppstackASAAttribution' is only available in iOS 15.0 or newer. The two Apple Search Ads calls are now behind a runtime if #available(iOS 15.0, *) check. Nothing changes for apps: the Objective-C layer already rejects both methods with UNSUPPORTED_IOS_VERSION below iOS 15.0, and Apple Search Ads attribution cannot work there anyway. iOS 15.0 remains the SDK's requirement.
  • iOS: The podspec depended on React-Codegen, which is an unrelated third-party pod on the public CocoaPods trunk (React Native's pod is ReactCodegen). Dependencies are now installed via React Native's own install_modules_dependencies helper, which also supplies the codegen and static-framework header search paths that cannot be reproduced by hand. As a side effect, the React-Core header search path fix from 2.1.0 now applies on both architectures instead of only the legacy one.
  • Android: Removed cmakeListsPath: null from react-native.config.js. It never disabled codegen autolinking as its comment claimed (null is falsy, so autolinking always fell through to the default generated path), and it obscured the real reason codegen never ran.
  • Removed three hand-written files under android/src/main/jni/ that imitated codegen output (AppstackReactNativeSpec.{h,cpp} and a CMakeLists.txt declaring a spec target). They contained no TurboModule code, were never built, and are now replaced by genuine generated artifacts.