Skip to content

feat(app-events): add Meta App Events and Objective-C facades (SDK 18.0.2) - #7

Merged
Kapusch merged 18 commits into
masterfrom
feat/app-events-v2
Oct 27, 2026
Merged

Kapusch merged 18 commits into
masterfrom
feat/app-events-v2

Conversation

@Kapusch

@Kapusch Kapusch commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Summary

Replaces #6. Same feature: Kapusch.Facebook.iOS 1.2.0 (first published as 1.2.0-rc.1) adds Meta App Events (FacebookAppEvents) and moves the Login and Share facades from Swift to Objective-C, so the build no longer depends on the Swift compiler Meta used.

Differences from #6:

  • The bundled Facebook iOS SDK stays 18.0.2. This is the owner's decision. The 19.0.0 bump commit is dropped, along with what only SDK 19 has: the metadata and product-improvement switches and the arm64e slice handling.
  • The history is rebuilt from master. No commit contains the encoded private pattern that feat: App Events + Facebook iOS SDK 19.0.0 (1.2.0) #6's scripts/verify-public.py carried. The scanner now follows the shared approach of the Android repository:
    • only generic patterns are tracked;
    • private terms are read at run time from KAPUSCH_PRIVATE_TERMS (a repository secret in CI) or from the untracked .git/info/private-terms;
    • a failure names only the file and private term #N.
  • This PR adds the independent review that feat: App Events + Facebook iOS SDK 19.0.0 (1.2.0) #6 never got, and its fixes. The commits after fix(login) are those fixes.

Review findings and fixes

Checked against the Facebook iOS SDK 18.0.2 sources (FBSDKAppEvents.m, Settings*.swift, ApplicationDelegate.swift, FBSDKUserDataStore.m, FBSDKTimeSpentData.m).

  1. Calls made before the SDK is initialized were silently lost (fix(app-events): refuse calls…). Before FBSDKApplicationDelegate configures it, the SDK:

    • drops logged events, with only a log line;
    • ignores user data;
    • overwrites a user ID set earlier with the persisted one;
    • answers setting getters with defaults, and caches some of them for the whole process. For example, reading IsSKAdNetworkReportEnabled before initialization pinned it to true whatever the Info.plist said.

    Now:

    • App Events, Login and Share report initialization to a shared registry, and the new IsSdkInitialized reads it.
    • The event methods, the identity methods and the getters throw InvalidOperationException until then, as Android does.
    • HandleOpenUrl and ContinueUserActivity stop forwarding until then. Forwarding stores campaign IDs and feeds AEM, so it must wait for consent.
  2. API alignment with Kapusch.Facebook.Android 1.2.0-rc.1 (feat(app-events): align…):

    • InitializeSdk(dataProcessingOptions, dataProcessingCountry, dataProcessingState).
    • Data processing options are recorded and applied again after initialization. Before, options set before init were lost at the next launch.
    • FacebookAppEventsUserData.ExternalId. SetUserData now replaces every field, as on Android.
    • decimal and ulong parameter values are sent as exact text.
    • float values keep their shortest form. Before, 0.1f was sent as 0.10000000149011612.
  3. Consent semantics were partly inaccurate (docs(app-events): describe SDK 18.0.2…):

    • In SDK 18.0.2, an automatic-logging value set at Meta overrides the local FacebookAutoLogAppEventsEnabled / SetAutoLogAppEventsEnabled once the app configuration is cached. SDK 19 changed this.
    • The docs now separate what stops at once on withdrawal from what only stops later.
    • They no longer claim that purchase amounts are rounded to three decimals: the SDK does not do that. The activation threshold is Meta's session timeout, 60 s by default.
    • A parity table lists the intentional gaps with Android:
      • no auto-init on iOS;
      • no SDK 19 metadata switches in 18.0.2;
      • iOS-only forwarding, advertiser tracking, SKAdNetwork and single-field user data.
  4. The sample's withdrawal did not match the recipe (fix(sample)): it now sets explicit-only flushing and skips the identity calls when the SDK is not initialized.

  5. Packaging guards (ci: commits):

    • validate-package.py checks the SDK slices and privacy manifests that the targets reference. An SDK slice rename, like 19's arm64e, would otherwise break every consuming build.
    • CI builds the sample, and scripts/verify-app-symbols.py checks that the app keeps every kfb_* entry point. Only a built app showed the dead-stripping bug that fix(login) fixes.

Checked with no change needed:

  • The Login and Share port matches the old Swift sources line by line:
    • result mapping (failure, then cancelled, then success);
    • the empty-token check;
    • nonce handling;
    • the callback guards;
    • native dialog mode;
    • the pasteboard handling, which never reads the pasteboard;
    • the URL scheme filter.
  • The facades match the 18.0.2 headers: selectors, nullability, threading (activateApp and InitializeSdk hop to the main thread) and the flush-behaviour values.

Validation (local, Xcode 26.3, .NET 10.0.203)

  • bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh: OK. Every commit of the branch builds natively and in managed code (-warnaserror).
  • dotnet pack and python3 scripts/validate-package.py: OK.
  • bash tests/validate-feature-selection.sh: OK.
  • python3 scripts/verify-public.py: OK, with the private terms configured. The commit messages and patches of the branch are clean too, in plain, hex and base64 forms.
  • The sample builds for the simulator with Login;AppEvents, AppEvents and Share. All 37 kfb_* entry points are present in each build.
  • An x64 simulator run launches the sample and initializes the SDK through Login.

🤖 Generated with Claude Code

Kapusch and others added 18 commits October 27, 2026 21:57
…ive-C facades

Meta now builds the Facebook iOS SDK binaries with Xcode releases newer
than the ones available on CI runners, and a Swift facade can only import
the SDK's Swift interfaces with the exact Swift compiler Meta used.

- Port the Login and Share facades from Swift to Objective-C. They call the
  SDK through its Objective-C headers (including the generated -Swift.h
  headers), keep the same C entry points and keep the same behaviour.
- Replace the SwiftPM dependency with release archives pinned by URL and
  SHA-256 in DependencyLocks/iOS/lockstate.txt (still SDK 18.0.2). The
  hashes equal the upstream Package.swift remoteChecksum values.
- Build each facade with clang + libtool per slice, then wrap it in the
  same kfb/kfbshare xcframeworks as before.
- Port the Share source invariants to the Objective-C implementation.
- Cache the pinned archives in CI instead of the SwiftPM scratch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NativeFacebookShare binds its entry points with static __Internal imports,
so the .NET iOS build passes them to the native linker as required symbols
for every app. A Login-only build (the default selection) therefore failed
to link with "Undefined symbols: _kfb_facebook_share_*" since 1.1.0.

When Share is not selected, inject kfbsharestub.xcframework instead of
kfbshare.xcframework. It defines the same three entry points on top of
Foundation only: configuring logs an error, sharing completes with
Failed/share_feature_not_selected and URL handling returns false. Share
selections are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add kfbappevents.xcframework, an Objective-C facade over FBSDKAppEvents,
FBSDKSettings and FBSDKApplicationDelegate exported as kfb_appevents_* C
symbols:

- SDK lifecycle: initialize, finished-launching hook, URL and user-activity
  forwarding (AEM / deep links), SDK version.
- Settings: auto-log app events, advertiser ID collection, advertiser
  tracking (ignored by the SDK on iOS 17+), SKAdNetwork reporting, data
  processing options (LDU) and app-events debug logging.
- Logging: activateApp, logEvent (optional valueToSum), logPurchase, flush
  and flush behaviour. Parameters travel as a JSON object of string or
  number values.
- Identity: anonymous ID, user ID and advanced-matching user data.

It only depends on FBSDKCoreKit, which every feature selection already
links, so the package injects it for every selection: the managed App
Events entry points will be static __Internal imports and must always
resolve. The facade never changes a setting on its own. Values set through
it are recorded and applied again after SDK initialization so the SDK
persists them even when they were set before initialization.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expose the App Events facade as Kapusch.Facebook.iOS.FacebookAppEvents,
bound with static __Internal LibraryImport entry points:

- InitializeSdk / FinishedLaunching, HandleOpenUrl and
  ContinueUserActivity forwarding for AEM and deep links, SdkVersion.
- Set/Is pairs for automatic logging, advertiser ID collection,
  advertiser tracking (ignored by the SDK on iOS 17+) and SKAdNetwork
  reporting; SetDataProcessingOptions and SetDebugLoggingEnabled.
- ActivateApp, LogEvent (parameters + optional valueToSum), LogPurchase,
  typed LogStartTrial / LogSubscribe / LogCompleteRegistration helpers,
  Flush and flush behaviour.
- GetAnonymousId, Set/Clear/GetUserId and advanced-matching user data.

Parameters accept strings, numbers and booleans (sent as "1"/"0") and are
serialized with Utf8JsonWriter (trim and AOT safe). Add constants for every
event name, parameter name, parameter value and user-data type declared by
the bundled SDK headers, plus the Android-public activate/deactivate and
_valueToSum names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Parse AppEvents in KapuschFacebookFeatures. App Events only needs
  FBSDKCoreKit, which every selection links, so its facade is always
  linked; selecting AppEvents alone produces a CoreKit-only build without
  LoginKit or ShareKit. The default selection stays Login.
- Warn about unknown (or wrongly cased) feature names, which were silently
  ignored before.
- Cover the AppEvents, Login;Share;AppEvents and default selections in the
  feature-selection test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NativeFacebookShare.ConfigureAndInitialize forces automatic app events
logging off and advertiser ID collection / advertiser tracking to
trackingAllowed. Once the app manages these settings through
FacebookAppEvents (for example after a consent decision), Share must not
silently override them.

- The App Events facade records explicit values in the Objective-C class
  KFBAppEventsSettingsRegistry. Share and Login look it up through the
  Objective-C runtime, so there is no link-time dependency.
- Share only applies its historical defaults to settings the app did not
  set explicitly. Apps that never call FacebookAppEvents keep the exact
  previous behaviour.
- After Share or Login initializes the SDK, the recorded values are
  applied again so the SDK persists them (no-op when nothing was set).
- Guard the behaviour in the Share source invariants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- scripts/validate-package.py replaces the inline layout check in CI and
  publish. On top of the layout (now including kfbsharestub and
  kfbappevents) it checks that both slices of each wrapper export every
  entry point of its header, that FacebookAppEvents binds every
  kfb_appevents_* symbol with a static __Internal import, that the managed
  constants cover every App Events name of the bundled SDK headers and that
  each constant value exists in the bundled FBSDKCoreKit binary.
- scripts/verify-public.py fails on real-looking Meta app IDs, fb<appId>
  URL schemes, client tokens and email addresses in tracked files. Private
  terms are never tracked: they are read at run time from the
  KAPUSCH_PRIVATE_TERMS environment variable (a repository secret in CI)
  or the untracked .git/info/private-terms file, and failures only name
  the file and the term's position. The check is skipped with a notice
  when no term is configured.
- Move FacebookDataProcessingOptions next to the other App Events types:
  it is a caller-supplied value, not an SDK constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Integration.md: AppEvents feature selection; Info.plist switches with SDK
  defaults (FacebookAutoLogAppEventsEnabled,
  FacebookAdvertiserIDCollectionEnabled, FacebookSKAdNetworkReportEnabled)
  and the missing iOS AutoInit counterpart; a consent recipe (switches off
  in Info.plist, initialize after consent, manual ActivateApp, withdrawal
  steps); AEM URL / Universal Link forwarding; the double-counting rule for
  server-side purchase events; single SKAdNetwork owner; diagnostics and
  platform gaps.
- README, NuGet readme, SourceMode and third-party notices: App Events,
  new xcframeworks, pinned archives and the CoreKit tracking manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Select Login;AppEvents in the sample (overridable with
  KapuschFacebookFeatures) so it links the App Events facade explicitly.
- Ship the App Events switches off in Info.template.plist and add buttons
  for the consent recipe of Docs/Integration.md: grant consent (initialize,
  enable advertiser ID collection and debug logging, manual activation),
  log StartTrial and a custom event, flush and withdraw consent. Log
  activations from OnActivated after consent and forward Universal Links.
- Without a FacebookAppID in the local, gitignored Info.plist the App
  Events buttons only print a message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NativeFacebookLogin resolved kfb_facebook_* with a run-time lookup in the
main program. Nothing referenced those symbols at link time, so the native
linker dead-stripped them: Initialize and HandleOpenUrl silently did
nothing and SignInAsync could not reach the SDK. A Login;Share build of
master showed only the three Share symbols in the app executable.

- Bind the four Login entry points with [LibraryImport("__Internal")], like
  Share and App Events. The public API and its best-effort semantics are
  unchanged.
- When Login is not selected, link kfbloginstub.xcframework (Foundation
  only), which defines the same entry points: SignInAsync completes with
  Failed/login_feature_not_selected.
- Validate the static imports and package the placeholder.

Verified with nm on simulator builds for Login;AppEvents, AppEvents and
Share: every kfb_* entry point is present in the executable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before FBSDKApplicationDelegate configured it, the Facebook iOS SDK
silently drops logged events (only a log line), ignores advanced-matching
user data, overwrites a user ID set earlier with the persisted one, and
answers setting getters with defaults. Some of those defaults are cached
for the whole process: reading IsSKAdNetworkReportEnabled before
initialization pinned it to true whatever FacebookSKAdNetworkReportEnabled
said, and IsAdvertiserTrackingEnabled could pin "unspecified" on iOS 15-16.

- The App Events registry now records whether a wrapper of this package
  initialized the SDK. App Events, Login and Share report it right after
  application:didFinishLaunchingWithOptions: (KFBNotifyAppEventsSdkInitialized,
  which also re-applies the recorded settings as before).
- New FacebookAppEvents.IsSdkInitialized (kfb_appevents_is_sdk_initialized).
- The event methods, the identity methods and the setting getters throw
  InvalidOperationException until then, like Kapusch.Facebook.Android.
  Setters, data processing options, flush behaviour, debug logging, the
  anonymous ID and the SDK version keep working at any time.
- HandleOpenUrl and ContinueUserActivity return false without forwarding
  before initialization: forwarding stores campaign IDs and feeds AEM,
  which must wait for consent.
- Guard the Login and Share notifications in the source invariants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Android

Kapusch.Facebook.Android 1.2.0-rc.1 already ships the shared App Events
API. A few iOS signatures and encodings still differed:

- InitializeSdk(dataProcessingOptions, dataProcessingCountry,
  dataProcessingState), like Android without its Context. The options are
  set before initialization, so they apply to its first requests, and
  again right after it.
- SetDataProcessingOptions is now recorded like the boolean settings and
  re-applied after initialization. Before, options set before
  initialization stayed in memory only and were lost at the next launch.
- FacebookAppEventsUserData.ExternalId. SetUserData now replaces every
  field, external ID included (set or cleared after the ten-field SDK
  setter, which keeps the stored one), so a null property clears it as on
  Android. SetUserDataValue stays available for single fields.
- Event parameters: decimal and ulong values are sent as exact text and
  floats keep their shortest form (a float 0.1 was sent as
  0.10000000149011612), as on Android. Unsupported values are reported
  against the public "parameters" argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Server override: with SDK 18.0.2 an automatic-logging value set for the
  app at Meta overrides FacebookAutoLogAppEventsEnabled and
  SetAutoLogAppEventsEnabled once the app configuration is cached (SDK 19
  changed this). Tell integrators to turn it off at Meta too and document
  IsAutoLogAppEventsEnabled as the effective value.
- Initialization: what the SDK does before it is initialized, which
  FacebookAppEvents calls now throw or return false, and which work.
- Withdrawal: what stops at once (app logging, IDFA, user ID and user data
  also for queued events, flushing with ExplicitOnly) and what only stops
  later (server override, StoreKit observation, the initialized SDK, the
  non-persisted flush behaviour when Login or Share initialize the SDK).
- Remove claims the 18.0.2 sources do not support: purchase amounts are
  not rounded to three decimals, the activation threshold is Meta's
  session timeout (60 s by default), the advertiser tracking flag is also
  ignored before iOS 17 once Meta's domain configuration enables ATT
  handling.
- Document the event parameter encoding and an API parity table with
  Kapusch.Facebook.Android, with the intentional gaps: no auto-init on
  iOS, no SDK 19 metadata switches in 18.0.2, iOS-only forwarding,
  advertiser tracking, SKAdNetwork and single-field user data.
- CoreKit links FBAEMKit for every feature; Login and Share privacy
  manifests declare tracking too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Withdrawal switches flushing to explicit only, so events queued before
  it stay on the device, and skips the identity calls when the SDK is not
  initialized (they now throw; nothing is sent in that case anyway).
- Granting consent again restores automatic flushing and the log line
  shows IsSdkInitialized.
- The README states that the sample initializes the SDK at launch for its
  Login button, unlike a consent-first app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kapusch.Facebook.iOS.targets bundles each SDK framework's
PrivacyInfo.xcprivacy from hard-coded ios-arm64 and
ios-arm64_x86_64-simulator slices. An SDK release that renames a slice
(SDK 19 ships ios-arm64_arm64e) would make every consuming build fail on
a missing BundleResource. validate-package.py now fails when a packaged
SDK xcframework lacks one of those slices or its privacy manifest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Login dead-stripping bug fixed earlier in this branch was only
visible in a built app: the package and its static libraries were fine.
CI now builds the sample for the simulator (Login;AppEvents, so the Share
placeholder is linked too) and scripts/verify-app-symbols.py checks that
the executable contains every kfb_* entry point declared by the wrapper
headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AEMKit

App Events made CoreKit, and with it FBAEMKit, part of every feature
selection; the README still described it as a Share-only dependency and
did not name the bundled SDK release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.2.0 adds App Events and keeps Facebook iOS SDK 18.0.2. It is first
published as 1.2.0-rc.1 from a v1.2.0-rc.1 tag on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kapusch
Kapusch merged commit f4185cf into master Oct 27, 2026
1 check passed
@Kapusch
Kapusch deleted the feat/app-events-v2 branch October 27, 2026 21:36
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.

1 participant