diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..5dfafeef2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,129 @@ +# CLAUDE.md - Iterable Android SDK + +## Project Overview + +The Iterable Android SDK integrates Android apps with [Iterable](https://www.iterable.com), a growth marketing platform. The SDK provides push notifications (FCM), in-app messaging, embedded messaging, mobile inbox, event tracking, deep linking, and user management. + +**Min SDK:** 21 (Android 5.0+) | **Language:** Java (75%) + Kotlin (25%) + +## Quick Reference + +### Build & Test + +```bash +# Build (incremental, fast) +./build.sh + +# Build (clean) +./build.sh --clean + +# Run all unit tests +./test.sh + +# Run a specific test class +./test.sh IterableApiTest + +# Run a specific test method +./test.sh "IterableApiTest.testSetEmail" + +# List all available test classes +./test.sh --list +``` + +The wrapper scripts (`build.sh`, `test.sh`) provide formatted output and error summaries. You can also use Gradle directly: + +```bash +./gradlew build -x test # build +./gradlew :iterableapi:testDebugUnitTest --tests "*IterableApiTest*" # test +``` + +### Requirements + +- JDK 17+ +- Android SDK with compileSdk 34 +- Gradle 8.0+ (use the wrapper: `./gradlew`) + +## Project Structure + +``` +iterableapi/ # Core SDK module (main deliverable) +iterableapi-ui/ # UI components module (inbox, embedded views) +app/ # Internal test application for SDK integration testing +sample-apps/ # Example apps demonstrating SDK usage (inbox-customization) +integration-tests/ # End-to-end integration tests (requires emulator) +tools/ # CI/CD utilities (emulator wait script) +``` + +## Module Details + +### iterableapi (core SDK) +- **Source:** `iterableapi/src/main/java/com/iterable/iterableapi/` +- **Tests:** `iterableapi/src/test/java/com/iterable/iterableapi/` +- ~94 source files, ~48 test files +- Mostly Java with Kotlin for newer features (embedded messaging, encryption, keychain) + +### iterableapi-ui (UI components) +- **Source:** `iterableapi-ui/src/main/java/com/iterable/iterableapi/ui/` +- Inbox UI (Java) and Embedded views (Kotlin) +- No unit tests in this module + +## Key Classes + +| Class | Purpose | +|-------|---------| +| `IterableApi.java` | Main SDK interface (singleton entry point) | +| `IterableConfig.java` | SDK configuration builder | +| `IterableApiClient.java` | Network communication / API calls | +| `IterableAuthManager.java` | JWT authentication management | +| `IterableKeychain.kt` | Secure token/credential storage | +| `IterableInAppManager.java` | In-app message lifecycle management | +| `IterableEmbeddedManager.kt` | Embedded message management | +| `IterableConstants.java` | API endpoint paths and constants | +| `IterableRequestTask.java` | Network request execution | +| `IterableFirebaseMessagingService.java` | FCM push notification handling | +| `IterableDeeplinkManager.java` | Deep link resolution | +| `IterableNotificationHelper.java` | Push notification display | + +## Common Development Tasks + +### Adding a new API endpoint +1. Add endpoint path constant to `IterableConstants.java` +2. Add request method to `IterableApiClient.java` +3. Add public-facing method to `IterableApi.java` +4. Add tests in `iterableapi/src/test/java/` + +### Modifying authentication +- Auth flow: `IterableAuthManager.java` +- Token storage: `IterableKeychain.kt` +- Auth failure handling: `AuthFailure.java`, `AuthFailureReason.java` + +### Adding a new model class +- Create in `iterableapi/src/main/java/com/iterable/iterableapi/` +- Implement `Parcelable` if passed between components +- Add JSON serialization for network transport + +## Code Style + +- Checkstyle enforced (see `checkstyle.xml`) +- No star imports +- Max method length: 200 lines +- Standard Java naming conventions (camelCase methods/vars, PascalCase types) +- New features tend to use Kotlin; existing Java code stays Java unless refactored + +## CI/CD + +GitHub Actions workflows in `.github/workflows/`: +- `build.yml` - Main build and test +- `integration-tests.yml` - Full integration test suite +- `inapp-e2e-tests.yml` - E2E tests for in-app messaging +- `codeql.yml` - Code quality analysis +- `prepare-release.yml` / `validate-release.yml` / `publish.yml` - Release pipeline + +## Testing Notes + +- Unit tests use Robolectric for Android framework simulation +- Network tests use OkHttp MockWebServer +- Mocking via Mockito (core + inline) +- JSON assertions via JSONAssert +- Test base class: `BaseTest.java` +- Test utilities: `IterableTestUtils.java`, `InAppTestUtils.java`, `EmbeddedTestUtils.java` diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..7459cb91f --- /dev/null +++ b/build.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# This script is to be used by LLMs and AI agents to build the Iterable Android SDK. +# It uses Gradle to build the project and shows errors in a clean format. +# It also checks if the build is successful and exits with the correct status. +# +# Usage: ./build.sh [--clean] +# --clean: Force a clean build (slower, but ensures clean state) + +# Note: Not using set -e because we need to handle build failures gracefully + +echo "Building Iterable Android SDK..." + +# Create a temporary file for the build output +TEMP_OUTPUT=$(mktemp) + +# Function to clean up temp file on exit +cleanup() { + rm -f "$TEMP_OUTPUT" +} +trap cleanup EXIT + +# Check if we have Android SDK +if [ -z "$ANDROID_HOME" ] && [ -z "$ANDROID_SDK_ROOT" ]; then + echo "โš ๏ธ Warning: ANDROID_HOME or ANDROID_SDK_ROOT not set. Build may fail if Android SDK is not in PATH." +fi + +# Parse command line arguments for clean build option +CLEAN_BUILD=false +if [[ "$1" == "--clean" ]]; then + CLEAN_BUILD=true + echo "๐Ÿงน Clean build requested" +fi + +# Run the build and capture all output +if [ "$CLEAN_BUILD" = true ]; then + echo "๐Ÿ”จ Clean building all modules..." + ./gradlew clean build -x test --no-daemon --console=plain > "$TEMP_OUTPUT" 2>&1 + BUILD_STATUS=$? +else + echo "๐Ÿ”จ Building all modules (incremental)..." + ./gradlew build -x test --no-daemon --console=plain > "$TEMP_OUTPUT" 2>&1 + BUILD_STATUS=$? +fi + +# Show appropriate output based on build result +if [ $BUILD_STATUS -eq 0 ]; then + echo "โœ… Iterable Android SDK build succeeded!" + echo "" + echo "๐Ÿ“ฆ Built modules:" + echo " โ€ข iterableapi (core SDK)" + echo " โ€ข iterableapi-ui (UI components)" + echo " โ€ข app (sample app)" +else + echo "โŒ Iterable Android SDK build failed with status $BUILD_STATUS" + echo "" + echo "๐Ÿ” Build errors:" + + # Extract and show compilation errors with file paths and line numbers + grep -E "\.java:[0-9]+: error:|\.kt:[0-9]+: error:|error:|Error:|FAILURE:|Failed|Exception:" "$TEMP_OUTPUT" | head -20 + + echo "" + echo "โš ๏ธ Build warnings:" + grep -E "\.java:[0-9]+: warning:|\.kt:[0-9]+: warning:|warning:|Warning:" "$TEMP_OUTPUT" | head -10 + + # If no specific errors found, show the failure section + if ! grep -q -E "\.java:[0-9]+: error:|\.kt:[0-9]+: error:|error:" "$TEMP_OUTPUT"; then + echo "" + echo "๐Ÿ“‹ Build failure details:" + grep -A 10 -B 2 "FAILURE\|BUILD FAILED" "$TEMP_OUTPUT" | head -15 + fi + + echo "" + echo "๐Ÿ’ก Common solutions:" + echo " โ€ข Check Java version (JDK 17+ required)" + echo " โ€ข Verify ANDROID_HOME is set correctly" + echo " โ€ข Run './gradlew --stop' to kill daemon processes" + echo " โ€ข Check dependencies in build.gradle files" +fi + +exit $BUILD_STATUS \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..61c390c73 --- /dev/null +++ b/test.sh @@ -0,0 +1,269 @@ +#!/bin/bash + +# This script is to be used by LLMs and AI agents to run tests for the Iterable Android SDK. +# It uses Gradle to run tests and provides clean output with filtering capabilities. + +set -e + +# Parse command line arguments +FILTER="" +LIST_TESTS=false + +if [[ $# -eq 1 ]]; then + if [[ "$1" == "--list" ]]; then + LIST_TESTS=true + else + FILTER="$1" + echo "๐ŸŽฏ Running tests with filter: $FILTER" + fi +elif [[ $# -gt 1 ]]; then + echo "โŒ Usage: $0 [filter|--list]" + echo " filter: Test class name (e.g., 'IterableApiTest')" + echo " or specific test (e.g., 'IterableApiTest.testSetEmail')" + echo " or module:test (e.g., 'iterableapi:testDebugUnitTest --tests IterableApiTest')" + echo " --list: List all available test classes" + exit 1 +fi + +# Handle test listing +if [[ "$LIST_TESTS" == true ]]; then + echo "๐Ÿ“‹ Listing available test classes..." + + echo "๐Ÿ“ฆ Available Test Classes:" + + # List test classes from iterableapi module + echo "" + echo "๐Ÿ”ง iterableapi module:" + if [[ -d "iterableapi/src/test/java/com/iterable/iterableapi" ]]; then + find iterableapi/src/test/java/com/iterable/iterableapi -name "*.java" -o -name "*.kt" | while read test_file; do + test_class=$(basename "$test_file" | sed 's/\.[^.]*$//') + # Count test methods in each file + test_count=$(grep -c "fun test\|@Test" "$test_file" 2>/dev/null || echo "0") + echo " โ€ข $test_class ($test_count tests)" + done + fi + + # List test classes from iterableapi-ui module + echo "" + echo "๐ŸŽจ iterableapi-ui module:" + if [[ -d "iterableapi-ui/src/test" ]]; then + find iterableapi-ui/src/test -name "*.java" -o -name "*.kt" 2>/dev/null | while read test_file; do + test_class=$(basename "$test_file" | sed 's/\.[^.]*$//') + test_count=$(grep -c "fun test\|@Test" "$test_file" 2>/dev/null || echo "0") + echo " โ€ข $test_class ($test_count tests)" + done + else + echo " (No unit tests found)" + fi + + # List test classes from app module + echo "" + echo "๐Ÿ“ฑ app module:" + if [[ -d "app/src/test" ]]; then + find app/src/test -name "*.java" -o -name "*.kt" 2>/dev/null | while read test_file; do + test_class=$(basename "$test_file" | sed 's/\.[^.]*$//') + test_count=$(grep -c "fun test\|@Test" "$test_file" 2>/dev/null || echo "0") + echo " โ€ข $test_class ($test_count tests)" + done + else + echo " (No unit tests found)" + fi + + echo "" + echo "๐Ÿ” Example Usage:" + echo " ./test.sh IterableKeychainTest" + echo " ./test.sh \"IterableKeychainTest.testSaveAndGetEmail\"" + echo " ./test.sh \":iterableapi:testDebugUnitTest --tests com.iterable.iterableapi.IterableKeychainTest\"" + + exit 0 +fi + +echo "Running Iterable Android SDK tests..." + +# Build the gradle command +if [[ -n "$FILTER" ]]; then + # If filter contains a colon, use it as-is (already in module:task format) + if [[ "$FILTER" == *":"* ]]; then + GRADLE_CMD="./gradlew $FILTER --no-daemon --console=plain --rerun-tasks" + # If filter contains a dot, convert TestClass.testMethod to wildcard format + elif [[ "$FILTER" == *"."* ]]; then + GRADLE_CMD="./gradlew :iterableapi:testDebugUnitTest --tests \"*$FILTER*\" --no-daemon --console=plain --rerun-tasks" + # Otherwise, assume it's just a test class name and use wildcard + else + GRADLE_CMD="./gradlew :iterableapi:testDebugUnitTest --tests \"*$FILTER*\" --no-daemon --console=plain --rerun-tasks" + fi +else + # Run all tests with detailed test output (force execution to see individual tests) + GRADLE_CMD="./gradlew :iterableapi:testDebugUnitTest --no-daemon --console=plain --rerun-tasks" +fi + +echo "๐Ÿงช Running: $GRADLE_CMD" +echo "๐Ÿ“Š Real-time progress:" +echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + +# Create a temporary file for capturing output while showing progress +TEMP_OUTPUT=$(mktemp) + +# Function to clean up temp file on exit +cleanup() { + rm -f "$TEMP_OUTPUT" +} +trap cleanup EXIT + +# Run the tests with real-time output parsing +eval $GRADLE_CMD 2>&1 | tee "$TEMP_OUTPUT" | while IFS= read -r line; do + # Pretty print different types of Gradle output + case "$line" in + *"> Task "*) + # Task execution - only show important ones, simplify build tasks + task_name=$(echo "$line" | sed 's/.*> Task //' | sed 's/ .*//') + case "$task_name" in + *"compile"*|*"build"*|*"generate"*|*"process"*|*"merge"*|*"package"*) + # Only show building status once per batch of build tasks + if [[ ! -f "/tmp/gradle_building_shown" ]]; then + echo "๐Ÿ”จ Building..." + touch "/tmp/gradle_building_shown" + fi + ;; + esac + ;; + *"BUILD SUCCESSFUL"*) + rm -f "/tmp/gradle_building_shown" 2>/dev/null + echo "โœ… Build completed successfully!" + ;; + *"BUILD FAILED"*) + rm -f "/tmp/gradle_building_shown" 2>/dev/null + echo "โŒ Build failed!" + ;; + # JUnit test execution patterns + *"Test"*"started"*|*"Test"*"STARTED"*) + test_name=$(echo "$line" | sed 's/.*Test //' | sed 's/ started.*//' | sed 's/ STARTED.*//') + echo "๐Ÿ”„ $test_name" + ;; + *"Test"*"passed"*|*"Test"*"PASSED"*) + test_name=$(echo "$line" | sed 's/.*Test //' | sed 's/ passed.*//' | sed 's/ PASSED.*//') + echo "โœ… $test_name" + ;; + *"Test"*"failed"*|*"Test"*"FAILED"*) + test_name=$(echo "$line" | sed 's/.*Test //' | sed 's/ failed.*//' | sed 's/ FAILED.*//') + echo "โŒ $test_name" + ;; + *"Test"*"skipped"*|*"Test"*"SKIPPED"*) + test_name=$(echo "$line" | sed 's/.*Test //' | sed 's/ skipped.*//' | sed 's/ SKIPPED.*//') + echo "โญ๏ธ $test_name" + ;; + # Gradle test output patterns + *" > "*) + if [[ "$line" == *"STARTED"* ]]; then + test_name=$(echo "$line" | sed 's/.*> //' | sed 's/ STARTED.*//') + echo "๐Ÿ”„ $test_name" + elif [[ "$line" == *"PASSED"* ]]; then + test_name=$(echo "$line" | sed 's/.*> //' | sed 's/ PASSED.*//') + echo "โœ… $test_name" + elif [[ "$line" == *"FAILED"* ]]; then + test_name=$(echo "$line" | sed 's/.*> //' | sed 's/ FAILED.*//') + echo "โŒ $test_name" + elif [[ "$line" == *"SKIPPED"* ]]; then + test_name=$(echo "$line" | sed 's/.*> //' | sed 's/ SKIPPED.*//') + echo "โญ๏ธ $test_name" + fi + ;; + # Also catch the full line format for context + *".* > "*) + if [[ "$line" == *"STARTED"* ]]; then + class_and_method=$(echo "$line" | sed 's/ STARTED.*//') + method_name=$(echo "$class_and_method" | sed 's/.*> //') + echo "๐Ÿ”„ $method_name" + elif [[ "$line" == *"PASSED"* ]]; then + class_and_method=$(echo "$line" | sed 's/ PASSED.*//') + method_name=$(echo "$class_and_method" | sed 's/.*> //') + echo "โœ… $method_name" + elif [[ "$line" == *"FAILED"* ]]; then + class_and_method=$(echo "$line" | sed 's/ FAILED.*//') + method_name=$(echo "$class_and_method" | sed 's/.*> //') + echo "โŒ $method_name" + elif [[ "$line" == *"SKIPPED"* ]]; then + class_and_method=$(echo "$line" | sed 's/ SKIPPED.*//') + method_name=$(echo "$class_and_method" | sed 's/.*> //') + echo "โญ๏ธ $method_name" + fi + ;; + # Test summary and counts + *"tests completed"*|*"test passed"*|*"test failed"*|*"test skipped"*) + echo "๐Ÿ“Š $line" + ;; + # Method level test info + *"testPassed"*|*"testFailed"*|*"testSkipped"*|*"testStarted"*) + method_name=$(echo "$line" | sed 's/.*testMethod=//' | sed 's/,.*//') + if [[ "$line" == *"testPassed"* ]]; then + echo "โœ… $method_name" + elif [[ "$line" == *"testFailed"* ]]; then + echo "โŒ $method_name" + elif [[ "$line" == *"testSkipped"* ]]; then + echo "โญ๏ธ $method_name" + elif [[ "$line" == *"testStarted"* ]]; then + echo "๐Ÿ”„ $method_name" + fi + ;; + # General failures and errors + *"FAILED"*|*"ERROR"*|*"Exception"*|*"error"*) + echo "โŒ $line" + ;; + # Success indicators for individual tests + *"PASSED"*) + if [[ "$line" == *"test"* ]] || [[ "$line" == *"Test"* ]]; then + echo "โœ… $line" + fi + ;; + # Timing for tests only + *"seconds"*|*"ms"*) + if [[ "$line" == *"test"* ]] || [[ "$line" == *"Test"* ]]; then + echo "โฑ๏ธ $line" + fi + ;; + *) + # Catch specific test method patterns + if [[ "$line" == *"test"* ]] && ([[ "$line" == *"pass"* ]] || [[ "$line" == *"fail"* ]] || [[ "$line" == *"start"* ]] || [[ "$line" == *"complete"* ]]); then + echo "๐Ÿงช $line" + fi + ;; + esac +done + +# Get the actual exit status from the temp file +if grep -q "BUILD SUCCESSFUL" "$TEMP_OUTPUT"; then + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo "โœ… All tests completed successfully!" + + # Try to extract test summary + if grep -q "tests completed" "$TEMP_OUTPUT"; then + echo "๐Ÿ“Š Test Summary:" + grep "tests completed\|test.*passed\|test.*failed\|test.*skipped" "$TEMP_OUTPUT" | tail -5 + fi + + exit 0 +elif grep -q "BUILD FAILED" "$TEMP_OUTPUT"; then + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo "โŒ Tests failed!" + echo "" + echo "๐Ÿ” Failure Summary:" + + # Show specific test failures + grep -A 3 -B 1 "FAILED\|Failed\|failed.*test" "$TEMP_OUTPUT" | head -15 + + # Show build failures if no test failures + if ! grep -q "test.*FAILED\|test.*failed" "$TEMP_OUTPUT"; then + echo "" + echo "๐Ÿ“‹ Build Error Details:" + grep -A 5 -B 2 "BUILD FAILED\|FAILURE:" "$TEMP_OUTPUT" | head -10 + fi + + exit 1 +else + echo "โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€" + echo "โŒ Test execution failed" + echo "" + echo "๐Ÿ” Error details:" + grep -E "error:|Error:|FAILURE:|Failed|Exception:" "$TEMP_OUTPUT" | head -10 + exit 1 +fi \ No newline at end of file