Skip to content

feat(business): add Kapusch.TikTok.Business.Android (TikTok Business SDK 1.7.1) - #5

Merged
Kapusch merged 10 commits into
masterfrom
feat/business-sdk-v2
Oct 27, 2026
Merged

Kapusch merged 10 commits into
masterfrom
feat/business-sdk-v2

Conversation

@Kapusch

@Kapusch Kapusch commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Summary

Adds Kapusch.TikTok.Business.Android, a .NET Android package for the TikTok Business SDK (App Events) 1.7.1, independent of the existing Kapusch.TikTok.Android Share package. Replaces #4 with a clean history: no commit contains any encoded private pattern.

  • Packaging: the checksum-locked upstream AAR ships unchanged next to a thin Kotlin JNI facade. Shared AndroidX and Kotlin classes come from managed NuGets. Google Play Billing and Install Referrer stay consumer-owned. Nothing is bundled twice.
  • Public API aligned with Kapusch.TikTok.Business.iOS (TikTokApisForiOSComponents PR feat(business): add Kapusch.TikTok.Business.Android (TikTok Business SDK 1.7.1) #4): same types, members, config record (AppId, TikTokAppIds, AccessToken and the same opt-outs), TikTokBusinessInitResult(Status, int ErrorCode, ErrorMessage) with the shared 1001/1002 codes, and the same semantics. A deferred start returns InitializedTrackingDisabled with ErrorCode 0. Calls return false before initialization. Calls run synchronously in call order. Platform gaps are documented in Docs/Business/Integration.md ("Differences from the iOS package").
  • Initialization is bounded and deterministic. SDK 1.7.1 returns silently from every initializeSdk call after the first, and its async init step swallows Throwables without calling back. The facade enters the SDK only once per process and answers later attempts with InitIncompleteErrorCode (1104). A managed 30 s timer that does not depend on the main looper reports InitTimeoutErrorCode (1105). A late success still sets the initialized state.
  • Releases: one publish workflow, with v* tags for Share (unchanged contract) and business-v* tags for Business.
  • Public tree scan: scripts/verify-public.py follows the shared approach. Private terms come at run time from KAPUSCH_PRIVATE_TERMS (CI secret) or .git/info/private-terms, and failures only say "private term #N".

Review findings addressed

  • [blocker] Encoded private names in scripts/verify-public.py: replaced by the run-time private-terms scanner. Checked git log -p origin/master..HEAD and all commit messages for plain, hex, \x escaped, base64 and URL-encoded forms of every configured term: none found.
  • [major] Initialization could hang forever, and a retry could re-enter a half-built SDK: fixed as described above.
  • The log level mapping now follows the Android SDK's real order (NONE < INFO < WARN < DEBUG, with errors logged at INFO).
  • Identity calls use the SDK's synchronous identify/logout. getAppEventLogger() returns null outside SDK call stacks, so the SDK event queue cannot be used. The docs state the remaining Android ordering gap honestly.
  • Currency validation now matches iOS (ISO 4217 shape, non-negative value, currency required with a value). JSON serialization accepts the same value types as iOS.

Validation

  • No client keys, signing material, or secrets committed
  • Business: cold Native/Android/build.sh, dotnet pack, scripts/validate-business-package.sh (now also checks that the Kotlin and managed error codes match)
  • Share: built, packed and scripts/validate-package.sh passed. Share sources are unchanged.
  • Sample Release builds with R8, with and without Billing / Install Referrer
  • python3 -I scripts/verify-public.py with the local private-terms file
  • Runtime check on an Android 16 (API 36) x86_64 emulator, offline:
    • an invalid config throws ArgumentException;
    • concurrent calls share one task;
    • DisableAutoStart gives InitializedTrackingDisabled with code 0;
    • a repeated init returns the current state;
    • StartTrack gives Started;
    • identify, track, logout, flush and token update all return true;
    • after SetTrackingEnabled(false), manual calls return false;
    • with native assets disabled, init gives 1102 and no crash.
  • An app that references both packages builds in Release with R8 when KapuschTikTokAndroidIncludeBundledRuntimeDependencies=false. With the bundled graph it fails on duplicate AndroidX classes, as documented.

🤖 Generated with Claude Code

Kapusch and others added 10 commits October 27, 2026 22:19
Pin the upstream release AAR (byte-identical to the JitPack artifact),
its license and the Gradle distribution by SHA-256 in a lock file
separate from the Share package lock, and add a restore script that
fails on any checksum mismatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The businessinterop module compiles against the locked upstream classes
only and ships as its own AAR next to the unchanged upstream AAR. The
Kotlin object maps a Bundle configuration to TTConfig, initializes on
the main thread and reports the outcome at most once through a
ValueCallback, with numeric wrapper codes shared with the iOS package.

SDK 1.7.1 returns silently from every initializeSdk call after the
first, and its async init step swallows Throwables without calling
back. The facade therefore records its single native attempt and
answers later attempts with an init-incomplete code instead of
re-entering a half-built SDK; the managed caller bounds the wait.

Track, identify, logout, flush, startTrack and updateAccessToken run
synchronously, return whether the SDK accepted them and never throw
into the host. Log levels map to the closest Android SDK level.
Consumer R8 rules keep the facade, com.tiktok.** and the reflectively
loaded AdvertisingIdClient, and tolerate absent Billing and Install
Referrer. The Gradle verification metadata covers cold caches and the
Linux aapt2 artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add THIRD_PARTY_NOTICES.Business.md for the Business package so the
Share package keeps its notices file unchanged, and document the data
the SDK collects for privacy and Play Data Safety reviews.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a second packable project that ships the interop AAR, the locked
upstream AAR, its license and the Business notices, with
buildTransitive props/targets that inject only these two vendor AARs
and support a source-mode switch. Shared AndroidX and Kotlin classes
come from managed NuGets aligned on the AndroidX lifecycle 2.10 family
(minSdk 23), so the package unifies with Billing 8.3 and AppCompat
instead of duplicating classes. Billing and Install Referrer stay
consumer-owned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expose the same public API as Kapusch.TikTok.Business.iOS: a
TikTokBusinessConfig record with required AppId, TikTokAppIds and
AccessToken, InitializeAsync returning TikTokBusinessInitResult
(Started, InitializedTrackingDisabled or Failed, int ErrorCode 0 on
success, shared 1001/1002 codes), StartTrack, SetTrackingEnabled,
TrackEvent and typed StartTrial/Subscribe/Purchase/Registration/Login
helpers, Identify, Logout, Flush, UpdateAccessToken, IsInitialized,
IsTrackingEnabled, SdkVersion and TestEventCode. Every call returns
false before a successful initialization and nothing native loads
until InitializeAsync.

InitializeAsync validates the values and throws ArgumentException like
iOS, shares one native initialization, returns the current state after
a success, and is bounded to 30 seconds by a managed timer that does
not depend on the main looper. A success that arrives after the
timeout still latches the initialized state; retries are deterministic.
A deferred start reports InitializedTrackingDisabled with ErrorCode 0.

Android-only codes (11xx), constants and the SetTrackingEnabled(false)
limitation of SDK 1.7.1 are documented on the members.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
validate-business-package.sh checks the required entries, that exactly
two vendor AARs ship with no bundled runtime dependencies, that the
upstream AAR matches its lock byte for byte, the JNI signatures the
managed layer calls, that the Kotlin wrapper error codes match the
managed constants, the consumer R8 rules, the notices, the managed
NuGet dependencies and that Billing / Install Referrer stay
consumer-owned. The Share validator now also rejects any Business
artifact.

verify-public.py checks tracked files for pasted App Secrets and filled
sample templates, and rejects private terms read at run time from
KAPUSCH_PRIVATE_TERMS or the untracked .git/info/private-terms file.
No private term is tracked, and failures name only "private term #N".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain .NET Android app restores the packed package from
artifacts/nuget and shows the consent-gated recipe: the SDK is only
initialized after the user accepts. Credentials come from an untracked
asset copied from a template. Release builds run R8; an MSBuild switch
drops Billing and Install Referrer to prove the package links without
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a public-tree scan job fed by the KAPUSCH_PRIVATE_TERMS secret and
a Business job on the same pinned runner and toolchain as the Share
job: native build, pack, package validation, then a clean-consumer
Release build of the sample with R8, once with Billing 8.3 and Install
Referrer and once without them. The Share job is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep a single publish workflow so the existing NuGet Trusted Publishing
policy covers both packages. v* tags still release Kapusch.TikTok.Android;
business-v* tags release Kapusch.TikTok.Business.Android, which must be
X.Y.Z or X.Y.Z-rc.N. Manual runs pick the package explicitly and a tag
of the other family is refused. Releases refuse to overwrite an existing
NuGet.org version, pack with the source commit, push only the selected
nupkg and upload it as an artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README lists the Share and Business packages with their tag families.
Docs/Business/Integration.md covers requirements, the consent-gated
initialization recipe, the bounded and deterministic initialization,
threading and call order, result codes, the configuration and event
reference, the automatic purchase double counting warning, Android
permissions, R8 and Data Safety, the differences from the iOS package
and coexistence with the Share package. Docs/Release.md documents
independent releases, and AGENTS.md, CONTRIBUTING.md and the PR
template gain the Business rules, validation commands and the
private-terms scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kapusch
Kapusch merged commit 5596f5d into master Oct 27, 2026
3 checks passed
@Kapusch
Kapusch deleted the feat/business-sdk-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