diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f176d9 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..54c7fda --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bfce45b --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/README.md b/README.md index 85ddeb1..656f12c 100644 --- a/README.md +++ b/README.md @@ -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 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 diff --git a/build.gradle.kts b/build.gradle.kts index 452ffb6..c7c3281 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") } @@ -20,6 +21,13 @@ android { kotlinOptions { jvmTarget = "17" } + lint { + abortOnError = true + warningsAsErrors = false + } + testOptions { + unitTests.isReturnDefaultValues = true + } publishing { singleVariant("release") { withSourcesJar() @@ -27,6 +35,15 @@ android { } } +ktlint { + android.set(true) + ignoreFailures.set(false) +} + +dependencies { + testImplementation("junit:junit:4.13.2") +} + afterEvaluate { publishing { publications { diff --git a/src/main/java/com/tapapplink/sdk/TapAppLink.kt b/src/main/java/com/tapapplink/sdk/TapAppLink.kt index 72cb49e..ccc3f09 100644 --- a/src/main/java/com/tapapplink/sdk/TapAppLink.kt +++ b/src/main/java/com/tapapplink/sdk/TapAppLink.kt @@ -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() { @@ -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) diff --git a/src/test/java/com/tapapplink/sdk/TapAppLinkTest.kt b/src/test/java/com/tapapplink/sdk/TapAppLinkTest.kt new file mode 100644 index 0000000..561cd49 --- /dev/null +++ b/src/test/java/com/tapapplink/sdk/TapAppLinkTest.kt @@ -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) + } +}