Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

[*.{kt,kts}]
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ktlint_code_style = intellij_idea

[*.md]
trim_trailing_whitespace = false
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: Build, test, lint
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
cache: gradle

- name: Set up Android SDK
uses: android-actions/setup-android@v4

- name: Grant Gradle wrapper execute permission
run: chmod +x gradlew

- name: Build
run: ./gradlew --no-daemon assembleRelease

- name: Unit tests
run: ./gradlew --no-daemon test

- name: ktlint
run: ./gradlew --no-daemon ktlintCheck

- name: Android Lint
run: ./gradlew --no-daemon lintRelease
144 changes: 144 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Release

on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-*"
- "v[0-9]+.[0-9]+.[0-9]+"
- "v[0-9]+.[0-9]+.[0-9]+-*"

permissions:
contents: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
check:
name: Build, test, lint
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
cache: gradle

- name: Set up Android SDK
uses: android-actions/setup-android@v4

- name: Grant Gradle wrapper execute permission
run: chmod +x gradlew

- name: Build
run: ./gradlew --no-daemon assembleRelease

- name: Unit tests
run: ./gradlew --no-daemon test

- name: ktlint
run: ./gradlew --no-daemon ktlintCheck

- name: Android Lint
run: ./gradlew --no-daemon lintRelease

github-release:
name: GitHub Release
needs: check
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true


jitpack:
name: Trigger JitPack build
needs: check
runs-on: ubuntu-latest
timeout-minutes: 15
continue-on-error: true

steps:
- name: Resolve tag without optional v prefix
id: tag
run: |
REF="${GITHUB_REF_NAME}"
echo "raw=${REF}" >> "$GITHUB_OUTPUT"
echo "bare=${REF#v}" >> "$GITHUB_OUTPUT"

- name: Request JitPack build
run: |
TAG="${{ steps.tag.outputs.raw }}"
URL="https://jitpack.io/com/github/tapapplink/tapapplink-android/${TAG}/build.log"
echo "Requesting ${URL}"
# JitPack builds on first request; allow time for the queue to accept it.
curl --fail --silent --show-error --location \
--retry 3 --retry-delay 5 \
--max-time 120 \
"${URL}" -o /tmp/jitpack-build.log || true
head -n 40 /tmp/jitpack-build.log || true
echo "JitPack page: https://jitpack.io/#tapapplink/tapapplink-android/${TAG}"

maven-central:
name: Publish to Maven Central (optional)
needs: check
runs-on: ubuntu-latest
timeout-minutes: 30
# Disabled until a repo admin adds the Maven Central secrets listed in README.
# Flip this to true (and keep the secret check) once publishing is configured.
if: ${{ false }}

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
cache: gradle
server-id: central
server-username: MAVEN_CENTRAL_USERNAME
server-password: MAVEN_CENTRAL_PASSWORD
gpg-private-key: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
gpg-passphrase: SIGNING_GPG_PASSPHRASE

- name: Set up Android SDK
uses: android-actions/setup-android@v4

- name: Grant Gradle wrapper execute permission
run: chmod +x gradlew

- name: Publish to Maven Central
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
SIGNING_GPG_PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
run: |
if [ -z "${MAVEN_CENTRAL_USERNAME}" ] || [ -z "${MAVEN_CENTRAL_PASSWORD}" ]; then
echo "Maven Central secrets are not configured; skipping publish."
exit 0
fi
echo "Maven Central publish is wired but the Gradle publishing block is not enabled yet."
echo "See README Publishing section for the one-time setup, then enable this job."
exit 1
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,38 @@ TapAppLink.applyCode("SARAH10") { }

Purchases are attributed through billing webhooks. Leave out a client `trackPurchase` call.

## CI and releases

GitHub Actions runs on every pull request and push to `main`: Gradle assemble, unit tests, ktlint, and Android Lint.

Pushing a semver tag (`0.2.0`, `v0.2.0`, or a prerelease suffix) runs the same checks, creates a GitHub Release, and requests a JitPack build for that tag. `jitpack.yml` pins OpenJDK 17 for JitPack.

### Local checks

```bash
./gradlew assembleRelease test ktlintCheck lintRelease
```

## Publishing

**JitPack (current):** consumers depend on `com.github.tapapplink:tapapplink-android:<tag>`. Tag a release; CI triggers JitPack. No extra secrets are required for that path.

**Maven Central (optional, disabled):** the release workflow includes a `maven-central` job left behind `if: false` until a repository admin finishes the one-time setup below. Prefer JitPack until you need Central discovery or a non-`com.github` coordinate.

One-time setup (repository admin with write access to secrets and to the Sonatype / Central Portal account):

1. Create a [Maven Central Portal](https://central.sonatype.com/) account and claim the `com.tapapplink` namespace (or your chosen groupId).
2. Generate a Portal user token (username + password pair).
3. Create a GPG key for signing artefacts and export the private key (ASCII-armoured).
4. In this GitHub repo, add Actions secrets:
- `MAVEN_CENTRAL_USERNAME` – Portal token username
- `MAVEN_CENTRAL_PASSWORD` – Portal token password
- `SIGNING_GPG_PRIVATE_KEY` – ASCII-armoured private key
- `SIGNING_GPG_PASSPHRASE` – passphrase for that key
5. Wire Gradle publishing to the Central Portal (for example via the `com.vanniktech.maven.publish` plugin or the official `maven-publish` + signing setup), then set the `maven-central` job `if` to run when those secrets are present.

Until steps 1–5 are done, leave the job disabled so tagged releases only ship via GitHub Releases and JitPack.

## License

MIT
17 changes: 17 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("com.android.library") version "8.7.3"
id("org.jetbrains.kotlin.android") version "2.0.21"
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
id("maven-publish")
}

Expand All @@ -20,13 +21,29 @@ android {
kotlinOptions {
jvmTarget = "17"
}
lint {
abortOnError = true
warningsAsErrors = false
}
testOptions {
unitTests.isReturnDefaultValues = true
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}

ktlint {
android.set(true)
ignoreFailures.set(false)
}

dependencies {
testImplementation("junit:junit:4.13.2")
}

afterEvaluate {
publishing {
publications {
Expand Down
15 changes: 5 additions & 10 deletions src/main/java/com/tapapplink/sdk/TapAppLink.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,20 +102,16 @@ object TapAppLink {
fun getAppUserId(): String? = lastAppUserId

@JvmStatic
fun linkRevenueCatUser(appUserId: String, callback: (JSONObject) -> Unit) =
setAppUserId(appUserId, callback)
fun linkRevenueCatUser(appUserId: String, callback: (JSONObject) -> Unit) = setAppUserId(appUserId, callback)

@JvmStatic
fun linkAdaptyUser(customerUserId: String, callback: (JSONObject) -> Unit) =
setAppUserId(customerUserId, callback)
fun linkAdaptyUser(customerUserId: String, callback: (JSONObject) -> Unit) = setAppUserId(customerUserId, callback)

@JvmStatic
fun linkSuperwallUser(appUserId: String, callback: (JSONObject) -> Unit) =
setAppUserId(appUserId, callback)
fun linkSuperwallUser(appUserId: String, callback: (JSONObject) -> Unit) = setAppUserId(appUserId, callback)

@JvmStatic
fun linkQonversionUser(userId: String, callback: (JSONObject) -> Unit) =
setAppUserId(userId, callback)
fun linkQonversionUser(userId: String, callback: (JSONObject) -> Unit) = setAppUserId(userId, callback)

@JvmStatic
fun resetForTesting() {
Expand All @@ -142,8 +138,7 @@ object TapAppLink {
return post(cfg, "/ingestIdentify", body)
}

private fun requireConfig(): TapAppLinkConfig =
config ?: throw IllegalStateException("TapAppLink.configure() must be called first")
private fun requireConfig(): TapAppLinkConfig = config ?: throw IllegalStateException("TapAppLink.configure() must be called first")

private fun isoNow(): String {
val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
Expand Down
58 changes: 58 additions & 0 deletions src/test/java/com/tapapplink/sdk/TapAppLinkTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.tapapplink.sdk

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test

class TapAppLinkTest {
@Before
fun setUp() {
TapAppLink.resetForTesting()
}

@Test
fun configureLeavesOfferAndIdsUnset() {
TapAppLink.configure(
TapAppLinkConfig(
publicKey = "etk_test",
environment = TapAppLinkEnvironment.SANDBOX,
),
)
assertNull(TapAppLink.getOffer())
assertNull(TapAppLink.getAttributionId())
assertNull(TapAppLink.getAppUserId())
}

@Test
fun resetForTestingClearsCachedState() {
TapAppLink.configure(
TapAppLinkConfig(
publicKey = "etk_test",
environment = TapAppLinkEnvironment.PRODUCTION,
),
)
TapAppLink.resetForTesting()
assertNull(TapAppLink.getOffer())
assertNull(TapAppLink.getAttributionId())
assertNull(TapAppLink.getAppUserId())
}

@Test
fun environmentValuesMatchApi() {
assertEquals("production", TapAppLinkEnvironment.PRODUCTION.value)
assertEquals("sandbox", TapAppLinkEnvironment.SANDBOX.value)
}

@Test
fun offerHoldsBillingFields() {
val offer = TapAppLinkOffer(
creatorName = "Sarah",
promoCode = "SARAH10",
billingOfferId = "offer_1",
)
assertEquals("Sarah", offer.creatorName)
assertEquals("SARAH10", offer.promoCode)
assertEquals("offer_1", offer.billingOfferId)
}
}
Loading