Skip to content
Open
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
64 changes: 64 additions & 0 deletions .github/workflows/build-native-apk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Build Native Android APK

on:
push:
branches:
- 'claude/native-android-*'
- 'native-app'
paths:
- 'android-native/**'
- '.github/workflows/build-native-apk.yml'
pull_request:
branches: [master]
paths:
- 'android-native/**'
- '.github/workflows/build-native-apk.yml'
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow grants contents: write permissions, but the job only builds and uploads an artifact. Consider reducing to contents: read (or removing the explicit permissions block) to follow least-privilege.

Suggested change
contents: write
contents: read

Copilot uses AI. Check for mistakes.

defaults:
run:
working-directory: android-native

steps:
- uses: actions/checkout@v4

- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17

- name: Setup Android SDK
uses: android-actions/setup-android@v3

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
gradle-version: 8.11.1
cache-read-only: ${{ github.ref != 'refs/heads/native-app' }}

- name: Generate Gradle wrapper
run: gradle wrapper --gradle-version 8.11.1

Comment on lines +46 to +48
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow generates the Gradle wrapper at runtime and then runs ./gradlew. This is fragile (it hides missing wrapper scripts/JARs in the repo and adds work each run). Prefer committing the wrapper scripts + gradle-wrapper.jar under android-native/ and removing the wrapper-generation step so builds are reproducible locally and in CI.

Suggested change
- name: Generate Gradle wrapper
run: gradle wrapper --gradle-version 8.11.1

Copilot uses AI. Check for mistakes.
- name: Compile Kotlin (fast feedback)
run: ./gradlew compileDebugKotlin
env:
MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }}

- name: Build debug APK
run: ./gradlew assembleDebug
env:
MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }}

- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: communauto-native-debug
path: android-native/app/build/outputs/apk/debug/app-debug.apk
retention-days: 30
14 changes: 14 additions & 0 deletions android-native/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
/app/build
/app/release
*.apk
*.aab
87 changes: 87 additions & 0 deletions android-native/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}

android {
namespace = "com.communauto.tools"
compileSdk = 35

defaultConfig {
applicationId = "com.communauto.tools"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"

// Google Maps API key — gradle.properties, local.properties, or MAPS_API_KEY env var
manifestPlaceholders["MAPS_API_KEY"] =
project.findProperty("MAPS_API_KEY") as? String
?: System.getenv("MAPS_API_KEY")
?: ""
}

buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}

buildFeatures {
compose = true
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}
}

dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.12.01")
implementation(composeBom)

// Compose
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.navigation:navigation-compose:2.8.5")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")

// Networking
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

// Google Maps Compose
implementation("com.google.maps.android:maps-compose:6.2.1")
implementation("com.google.android.gms:play-services-maps:19.0.0")
implementation("com.google.android.gms:play-services-location:21.3.0")

// WebView
implementation("androidx.webkit:webkit:1.12.1")

// Accompanist (permissions)
implementation("com.google.accompanist:accompanist-permissions:0.36.0")

// DataStore for preferences
implementation("androidx.datastore:datastore-preferences:1.1.1")

// Pull-to-refresh
implementation("androidx.compose.material3:material3-android")

Comment on lines +83 to +85
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

androidx.compose.material3:material3 is already included above, and PullToRefreshBox lives in that artifact. The extra androidx.compose.material3:material3-android dependency looks redundant and can be removed unless there’s a specific reason to keep both.

Suggested change
// Pull-to-refresh
implementation("androidx.compose.material3:material3-android")

Copilot uses AI. Check for mistakes.
debugImplementation("androidx.compose.ui:ui-tooling")
}
14 changes: 14 additions & 0 deletions android-native/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Retrofit
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.communauto.tools.data.model.** { *; }

# Gson
-keep class com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}

# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
31 changes: 31 additions & 0 deletions android-native/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<application
android:name=".CommunautoApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.CommunautoNative"
android:usesCleartextTraffic="false">

<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />

<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.communauto.tools

import android.app.Application
import android.webkit.CookieManager
import com.communauto.tools.data.auth.AuthManager
import com.communauto.tools.data.repository.VehicleRepository

class CommunautoApp : Application() {

lateinit var authManager: AuthManager
private set

lateinit var vehicleRepository: VehicleRepository
private set

override fun onCreate() {
super.onCreate()

// Enable cookies for WebView (shared with OkHttp via WebViewCookieJar)
CookieManager.getInstance().setAcceptCookie(true)

authManager = AuthManager(this)
vehicleRepository = VehicleRepository()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.communauto.tools

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.viewmodel.compose.viewModel
import com.communauto.tools.ui.navigation.AppNavigation
import com.communauto.tools.ui.theme.CommunautoTheme
import com.communauto.tools.viewmodel.AuthViewModel

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
CommunautoTheme {
val authViewModel: AuthViewModel = viewModel()
AppNavigation(authViewModel = authViewModel)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.communauto.tools.data.api

import com.communauto.tools.data.model.BookingResponse
import com.communauto.tools.data.model.VehiclesResponse
import com.communauto.tools.data.model.WcfResponse
import retrofit2.http.GET
import retrofit2.http.Query

interface CommunautoApi {

@GET("WCF/LSI/LSIBookingServiceV3.svc/GetAvailableVehicles")
suspend fun getAvailableVehicles(
@Query("BranchID") branchId: Int = 1,
@Query("LanguageID") languageId: Int = 2,
@Query("CityID") cityId: Int? = null,
): WcfResponse<VehiclesResponse>

@GET("WCF/LSI/LSIBookingServiceV3.svc/CreateBooking")
suspend fun createBooking(
@Query("CustomerID") customerId: Int,
@Query("CarID") carId: Int,
): WcfResponse<BookingResponse>

@GET("WCF/LSI/LSIBookingServiceV3.svc/CancelBooking")
suspend fun cancelBooking(
@Query("CustomerID") customerId: Int,
@Query("BranchID") branchId: Int = 1,
): WcfResponse<BookingResponse>

@GET("WCF/LSI/LSIBookingServiceV3.svc/GetCurrentBooking")
suspend fun getCurrentBooking(
@Query("CustomerID") customerId: Int,
): WcfResponse<BookingResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.communauto.tools.data.api

import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

/**
* CookieJar that delegates to Android's [CookieManager].
*
* This is the key to the "cookie trick": the WebView login flow sets cookies
* in CookieManager. Because OkHttp shares the same CookieManager, all API
* calls automatically include the session cookies — no manual extraction needed.
*/
class WebViewCookieJar : CookieJar {

private val cookieManager: CookieManager = CookieManager.getInstance()

override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val urlString = url.toString()
for (cookie in cookies) {
cookieManager.setCookie(urlString, cookie.toString())
}
}

override fun loadForRequest(url: HttpUrl): List<Cookie> {
val urlString = url.toString()
val cookieString = cookieManager.getCookie(urlString) ?: return emptyList()
return cookieString.split(";").mapNotNull { raw ->
Cookie.parse(url, raw.trim())
}
}
}

object NetworkModule {

private const val BASE_URL = "https://www.reservauto.net/"

private val cookieJar = WebViewCookieJar()

private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.cookieJar(cookieJar)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
}
)
Comment on lines +51 to +55
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpLoggingInterceptor is enabled unconditionally. Even at BASIC, this can leak sensitive URLs/metadata in production logs and adds overhead. Consider enabling logging only for debug builds (e.g., via BuildConfig.DEBUG) and disabling it in release.

Copilot uses AI. Check for mistakes.
.build()
}

val api: CommunautoApi by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CommunautoApi::class.java)
}
}
Loading
Loading