diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..232d659c --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,216 @@ +version: 2.1 + +orbs: + android: circleci/android@2.3.0 + ruby: circleci/ruby@2.1.0 + +aliases: + - &workspace + ~/passkeys-sdk + - &unit-test-folder-result + ~/test-results/junit + +executors: + android-executor: + docker: + # https://circleci.com/developer/images/image/cimg/android#image-tags + - image: cimg/android:2023.12.1 + resource_class: large + working_directory: *workspace + + mac-os-executor: + macos: + xcode: "15.1.0" + resource_class: macos.m1.medium.gen1 + working_directory: *workspace + + ruby-executor: + docker: + - image: cimg/ruby:3.3.0 + working_directory: *workspace + +commands: + test-results-folder: + parameters: + folder: + default: *unit-test-folder-result + type: string + steps: + - run: + name: Copying tests results to <> + command: | + mkdir -p <> + find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} <> \; + when: always + working_directory: *workspace + + install-jdk-17: + steps: + - run: + name: Install OpenJDK 17 + command: | + sudo apt-get update && sudo apt-get install openjdk-17-jdk + sudo update-alternatives --set java /usr/lib/jvm/java-17-openjdk-amd64/bin/java + sudo update-alternatives --set javac /usr/lib/jvm/java-17-openjdk-amd64/bin/javac + java -version + + danger-check: + parameters: + danger_params: + type: string + steps: + - run: + name: Danger check + command: bundle exec danger <> --verbose --remove-previous-comments + +jobs: + assemble: + executor: android-executor + + steps: + - checkout + - android/restore-build-cache + - android/restore-gradle-cache + - ruby/install-deps + - run: + name: Assemble build + command: ./gradlew assembleDebug assembleDebugAndroidTest + - danger-check: + danger_params: --danger_id=assemble --dangerfile=dangerfiles/dangerfile_pr_validation + - android/save-gradle-cache + - android/save-build-cache + + lint-check: + executor: ruby-executor + + steps: + - checkout + - android/restore-build-cache + - android/restore-gradle-cache + - ruby/install-deps + - install-jdk-17 + - run: + name: Kotlin lint + command: ./gradlew ktlintCheck + - danger-check: + danger_params: --danger_id=lint --dangerfile=dangerfiles/dangerfile_ktlint + - android/save-gradle-cache + - android/save-build-cache + + detekt-check: + executor: android-executor + + steps: + - checkout + - android/restore-build-cache + - android/restore-gradle-cache + - ruby/install-deps + - run: + name: Detekt check + command: ./gradlew detekt + - danger-check: + danger_params: --danger_id=detekt --dangerfile=dangerfiles/dangerfile_detekt + - android/save-gradle-cache + - android/save-build-cache + + android-unit-test: + executor: android-executor + + steps: + - checkout + - android/restore-build-cache + - android/restore-gradle-cache + - ruby/install-deps + - run: + name: Kover code coverage report + command: ./gradlew :shared:koverXmlReportDebug + - danger-check: + danger_params: --danger_id=unit_tests --dangerfile=dangerfiles/dangerfile_code_coverage + - run: + name: Kover code coverage rule + command: ./gradlew :shared:koverVerify + - android/save-gradle-cache + - android/save-build-cache + - test-results-folder + - store_test_results: + path: *unit-test-folder-result + - store_artifacts: + path: /home/circleci/test-results/ + + ios-unit-test: + executor: mac-os-executor + + steps: + - checkout + - android/restore-build-cache + - android/restore-gradle-cache + - run: + name: Run Unit tests + command: ./gradlew :shared:iosSimulatorArm64Test + - android/save-gradle-cache + - android/save-build-cache + - test-results-folder + - store_test_results: + path: *unit-test-folder-result + - store_artifacts: + path: /Users/distiller/test-results/ + + ios-generate-framework: + executor: mac-os-executor + + steps: + - checkout + - run: + name: Create Framework + command: ./gradlew iosArm64Binaries + - run: + name: Compress Artifacts + command: | + tar -cvzf debugFramework.tar.gz shared/build/bin/iosArm64/debugFramework + tar -cvzf releaseFramework.tar.gz shared/build/bin/iosArm64/releaseFramework + - store_artifacts: + path: debugFramework.tar.gz + - store_artifacts: + path: releaseFramework.tar.gz + + android-generate-aar: + executor: android-executor + + steps: + - checkout + - run: + name: Create aars + command: | + ./gradlew :shared:assembleDebug + mv shared/build/outputs/aar/shared-debug.aar TwilioPasskeys-debug.aar + ./gradlew :shared:assembleRelease + mv shared/build/outputs/aar/shared-release.aar TwilioPasskeys.aar + - store_artifacts: + path: TwilioPasskeys-debug.aar + - store_artifacts: + path: TwilioPasskeys.aar + +workflows: + build-all: + jobs: + - assemble + - lint-check: + requires: + - assemble + - detekt-check: + requires: + - assemble + - android-unit-test: + requires: + - lint-check + - detekt-check + - ios-unit-test: + requires: + - lint-check + - detekt-check + - ios-generate-framework: + requires: + - ios-unit-test + - android-generate-aar: + requires: + - android-unit-test diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..cd0b4f54 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +max_line_length = 150 + +[*.{kt, kts, xml}] +ktlint_code_style = ktlint_official +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +# Don't allow any wildcard imports +ij_kotlin_packages_to_use_import_on_demand = unset +ktlint_code_style = ktlint_official + +[*.xml] +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false +max_line_length = unset + +[*.yml] +ij_yaml_spaces_within_brackets = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e510fa99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +.idea +.DS_Store +build +captures +.externalNativeBuild +.cxx +local.properties +xcuserdata \ No newline at end of file diff --git a/AndroidApp/build.gradle.kts b/AndroidApp/build.gradle.kts new file mode 100644 index 00000000..167f9695 --- /dev/null +++ b/AndroidApp/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.hilt) + alias(libs.plugins.kotlinSerialization) + kotlin("kapt") +} + +android { + namespace = "com.twilio.passkeys.android" + compileSdk = 34 + defaultConfig { + applicationId = "com.twilio.passkeys.android" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } +} + +dependencies { + implementation(projects.shared) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + implementation(libs.androidx.activity.compose) + implementation(libs.hilt.android) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.androidx.constraintlayout.compose) + implementation(libs.androidx.constraintlayout) + kapt(libs.hilt.android.compiler) + // Retrofit + implementation(libs.retrofit) + implementation(libs.retrofit2.kotlinx.serialization.converter) + implementation(libs.logging.interceptor) + implementation(libs.kotlinx.serialization.json) + + debugImplementation(libs.compose.ui.tooling) +} diff --git a/AndroidApp/detekt-baseline.xml b/AndroidApp/detekt-baseline.xml new file mode 100644 index 00000000..0d75593a --- /dev/null +++ b/AndroidApp/detekt-baseline.xml @@ -0,0 +1,23 @@ + + + + + LongMethod:HomePage.kt$@Suppress("FunctionName") @Composable @Preview fun HomePage( number: String = "+1 1111 1111", onDisconnect: () -> Unit = {}, ) + LongParameterList:AuthenticateRepository.kt$AuthenticateRepository$( rawId: String, id: String, clientDataJson: String, userHandle: String?, signature: String?, authenticatorData: String?, ) + LongParameterList:CreateRepository.kt$CreateRepository$( rawId: String, id: String, clientDataJson: String, attestationObject: String, type: String, transports: List<String>, ) + MagicNumber:Color.kt$0xFF0263E0 + MagicNumber:Color.kt$0xFFF22F46 + MagicNumber:HomePage.kt$0x08000000 + MagicNumber:HomePage.kt$0xB2FFFFFF + MagicNumber:HomePage.kt$0xFF0385EB + MagicNumber:HomePage.kt$0xFF05090A + MagicNumber:HomePage.kt$0xFFFFFFFF + MagicNumber:HomePage.kt$400 + MagicNumber:HomePage.kt$500 + MagicNumber:LoginPage.kt$0xFF121C2D + MagicNumber:LoginPage.kt$0xFFFFFFFF + MagicNumber:LoginPage.kt$400 + MagicNumber:LoginPage.kt$600 + UnusedParameter:MyApplicationTheme.kt$darkTheme: Boolean = isSystemInDarkTheme() + + diff --git a/AndroidApp/src/main/AndroidManifest.xml b/AndroidApp/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f16ef8a8 --- /dev/null +++ b/AndroidApp/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/LoginViewModel.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/LoginViewModel.kt new file mode 100644 index 00000000..95f5331e --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/LoginViewModel.kt @@ -0,0 +1,156 @@ +package com.twilio.passkeys.android + +import android.app.Activity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.twilio.passkeys.AppContext +import com.twilio.passkeys.AuthenticatePasskeyResult +import com.twilio.passkeys.CreatePasskeyResult +import com.twilio.passkeys.TwilioPasskey +import com.twilio.passkeys.android.model.RegistrationStartResponse +import com.twilio.passkeys.android.repository.AuthenticateRepository +import com.twilio.passkeys.android.repository.CreateRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel +@Inject +constructor( + private val twilioPasskey: TwilioPasskey, + private val authenticateRepository: AuthenticateRepository, + private val createRepository: CreateRepository, +) : ViewModel() { + private val _state: MutableSharedFlow = + MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + val state = _state.asSharedFlow() + + fun create( + username: String, + activity: Activity, + ) { + viewModelScope.launch { + try { + val registrationStartResponse: RegistrationStartResponse = + createRepository.start( + username, + ) + val json = + Json { + encodeDefaults = true + ignoreUnknownKeys = true + explicitNulls = false + } + val challengePayload = json.encodeToString(registrationStartResponse) + when (val createPasskeyResult = twilioPasskey.create(challengePayload, AppContext(activity))) { + is CreatePasskeyResult.Error -> { + setErrorState(createPasskeyResult.error.message.toString()) + } + + is CreatePasskeyResult.Success -> { + try { + createRepository.verification( + rawId = createPasskeyResult.createPasskeyResponse.rawId, + id = createPasskeyResult.createPasskeyResponse.id, + clientDataJson = createPasskeyResult.createPasskeyResponse.clientDataJSON, + attestationObject = createPasskeyResult.createPasskeyResponse.attestationObject, + type = createPasskeyResult.createPasskeyResponse.type, + transports = createPasskeyResult.createPasskeyResponse.transports, + ) + _state.emit(LoginState.PasskeySuccess(username)) + } catch (e: Exception) { + setErrorState(e.message.toString()) + } + } + } + } catch (e: Exception) { + setErrorState(e.message.toString()) + } + } + } + + fun authenticate(activity: Activity) { + viewModelScope.launch { + try { + val authenticationStartResponse = authenticateRepository.start() + val json = Json { encodeDefaults = true } + val challengePayload = json.encodeToString(authenticationStartResponse) + when (val authenticatePasskeyResult = twilioPasskey.authenticate(challengePayload, AppContext(activity))) { + is AuthenticatePasskeyResult.Error -> { + + setErrorState( + authenticatePasskeyResult.error.message, + ) + } + + is AuthenticatePasskeyResult.Success -> { + try { + val response = + authenticatePasskeyResult.authenticatePasskeyResponse + authenticateRepository.verification( + rawId = response.rawId, + id = response.id, + clientDataJson = response.clientDataJSON, + userHandle = response.userHandle, + signature = response.signature, + authenticatorData = response.authenticatorData, + ) + _state.emit( + LoginState.PasskeySuccess( + "test", + ), + ) + } catch (e: Exception) { + setErrorState(e.message.toString()) + } + } + } + } catch (e: Exception) { + setErrorState(e.message.toString()) + } + } + } + + fun logout() { + viewModelScope.launch { + _state.emit(LoginState.Logout) + } + } + + fun areFieldsValid(phoneNumber: String): Boolean { + if (!android.util.Patterns.PHONE.matcher(phoneNumber).matches()) { + _state.tryEmit(LoginState.NumberError) + return false + } + return true + } + + private fun setErrorState(message: String) { + _state.tryEmit( + LoginState.PasskeyError( + message, + ), + ) + } +} + +sealed interface LoginState { + data object Initial : LoginState + + data object NumberError : LoginState + + data object Logout : LoginState + + data class PasskeySuccess(val number: String) : LoginState + + data class PasskeyError(val message: String) : LoginState +} diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/MainActivity.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/MainActivity.kt new file mode 100644 index 00000000..f8ab4c00 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/MainActivity.kt @@ -0,0 +1,106 @@ +package com.twilio.passkeys.android + +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.twilio.passkeys.android.pages.HomePage +import com.twilio.passkeys.android.pages.LoginPage +import com.twilio.passkeys.android.ui.MyApplicationTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + private var name = "" + + @OptIn(ExperimentalComposeUiApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MyApplicationTheme { + Surface( + modifier = + Modifier + .fillMaxSize() + .semantics { testTagsAsResourceId = true }, + color = MaterialTheme.colorScheme.background, + ) { + ScreenMain() + } + } + } + } + + @Composable + @Suppress("FunctionName") + fun ScreenMain(loginViewModel: LoginViewModel = hiltViewModel()) { + val navController = rememberNavController() + val uiState = loginViewModel.state.collectAsState(initial = LoginState.Initial).value + + LaunchedEffect(uiState) { + when (uiState) { + LoginState.Initial -> { + loginViewModel.authenticate(this@MainActivity) + } + + LoginState.NumberError -> {} + LoginState.Logout -> { + navController.navigate("login") + } + + is LoginState.PasskeyError -> { + Toast.makeText( + this@MainActivity, + uiState.message, + Toast.LENGTH_SHORT, + ).show() + } + + is LoginState.PasskeySuccess -> { + navController.navigate("home/${uiState.number}") + } + } + } + NavHost(navController = navController, startDestination = "login") { + composable("login") { + LoginPage( + onNumberEntered = { phoneNumber -> + name = phoneNumber + if (loginViewModel.areFieldsValid(phoneNumber)) { + loginViewModel.create(name, this@MainActivity) + } + }, + fetchPasskeys = { + loginViewModel.authenticate(this@MainActivity) + }, + numberError = uiState is LoginState.NumberError, + ) + } + composable( + "home/{number}", + arguments = listOf(navArgument("number") { type = NavType.StringType }), + ) { backStackEntry -> + val number = backStackEntry.arguments?.getString("number")!! + HomePage(number = number, onDisconnect = { + loginViewModel.logout() + }) + } + } + } +} diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/PasskeyApp.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/PasskeyApp.kt new file mode 100644 index 00000000..c1885ce3 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/PasskeyApp.kt @@ -0,0 +1,7 @@ +package com.twilio.passkeys.android + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class PasskeyApp : Application() diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/api/AuthenticateApi.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/api/AuthenticateApi.kt new file mode 100644 index 00000000..d2c96f79 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/api/AuthenticateApi.kt @@ -0,0 +1,27 @@ +package com.twilio.passkeys.android.api + +import com.twilio.passkeys.android.model.AuthenticateStartResponse +import com.twilio.passkeys.android.model.AuthenticateVerificationResponse +import kotlinx.serialization.Serializable +import retrofit2.http.Body +import retrofit2.http.POST + +interface AuthenticateApi { + @POST("/authentication/start") + suspend fun authenticateStart(): AuthenticateStartResponse + + @POST("/authentication/verification") + suspend fun authenticateVerification( + @Body authenticateVerificationRequest: AuthenticateVerificationRequest, + ): AuthenticateVerificationResponse +} + +@Serializable +data class AuthenticateVerificationRequest( + val rawId: String, + val id: String, + val clientDataJson: String, + val userHandle: String?, + val signature: String?, + val authenticatorData: String?, +) diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/api/RegistrationApi.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/api/RegistrationApi.kt new file mode 100644 index 00000000..f2e273ea --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/api/RegistrationApi.kt @@ -0,0 +1,32 @@ +package com.twilio.passkeys.android.api + +import com.twilio.passkeys.android.model.RegistrationStartResponse +import com.twilio.passkeys.android.model.RegistrationVerificationResponse +import kotlinx.serialization.Serializable +import retrofit2.http.Body +import retrofit2.http.POST + +interface RegistrationApi { + @POST("/registration/start") + suspend fun registrationStart( + @Body registrationStartRequest: RegistrationStartRequest, + ): RegistrationStartResponse + + @POST("/registration/verification") + suspend fun registrationVerification( + @Body registrationVerificationRequest: RegistrationVerificationRequest, + ): RegistrationVerificationResponse +} + +@Serializable +data class RegistrationStartRequest(val username: String) + +@Serializable +data class RegistrationVerificationRequest( + val rawId: String, + val id: String, + val clientDataJson: String, + val attestationObject: String, + val type: String, + val transports: List, +) diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/di/TwilioPasskeyModule.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/di/TwilioPasskeyModule.kt new file mode 100644 index 00000000..a2860356 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/di/TwilioPasskeyModule.kt @@ -0,0 +1,56 @@ +package com.twilio.passkeys.android.di + +import android.content.Context +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.twilio.passkeys.TwilioPasskey +import com.twilio.passkeys.android.api.AuthenticateApi +import com.twilio.passkeys.android.api.RegistrationApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit + +@Module +@InstallIn(ViewModelComponent::class) +class TwilioPasskeyModule { + @Provides + fun provideTwilioPasskey( + @ApplicationContext context: Context, + ): TwilioPasskey { + return TwilioPasskey(context) + } + + private val json = + Json { + ignoreUnknownKeys = true + explicitNulls = false + } + + @Provides + fun provideRetrofit(): Retrofit { + val contentType = "application/json".toMediaType() + val client = + OkHttpClient.Builder() + .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) + .build() + return Retrofit.Builder().baseUrl("https://your_backend/") + .addConverterFactory(json.asConverterFactory(contentType)).client(client) + .build() + } + + @Provides + fun provideRegistrationApi(retrofit: Retrofit): RegistrationApi { + return retrofit.create(RegistrationApi::class.java) + } + + @Provides + fun provideAuthenticateApi(retrofit: Retrofit): AuthenticateApi { + return retrofit.create(AuthenticateApi::class.java) + } +} diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/model/AuthenticateStartResponse.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/model/AuthenticateStartResponse.kt new file mode 100644 index 00000000..d093cbe4 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/model/AuthenticateStartResponse.kt @@ -0,0 +1,24 @@ +package com.twilio.passkeys.android.model + +import kotlinx.serialization.Serializable + +@Serializable +data class AuthenticateStartResponse( + val publicKey: PublicKeyCredential, +) + +@Serializable +data class PublicKeyCredential( + val challenge: String, + val timeout: Int, + val rpId: String, + val allowCredentials: List, + val userVerification: String, +) + +@Serializable +data class AllowCredential( + val id: String, + val type: String, + val transports: List, +) diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/model/AuthenticateVerificationResponse.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/model/AuthenticateVerificationResponse.kt new file mode 100644 index 00000000..e690b1aa --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/model/AuthenticateVerificationResponse.kt @@ -0,0 +1,6 @@ +package com.twilio.passkeys.android.model + +import kotlinx.serialization.Serializable + +@Serializable +data class AuthenticateVerificationResponse(val status: String) diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/model/RegistrationStartResponse.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/model/RegistrationStartResponse.kt new file mode 100644 index 00000000..7c4acd46 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/model/RegistrationStartResponse.kt @@ -0,0 +1,49 @@ +package com.twilio.passkeys.android.model + +import kotlinx.serialization.Serializable + +@Serializable +data class RegistrationStartResponse( + val rp: RP, + val user: User, + val challenge: String, + val pubKeyCredParams: List, + val timeout: Int, + val excludeCredentials: List, + val authenticatorSelection: AuthenticatorSelection, + val attestation: String, +) + +@Serializable +data class RP( + val id: String, + val name: String, +) + +@Serializable +data class User( + val id: String, + val name: String, + val displayName: String?, +) + +@Serializable +data class PublicKeyCredentialParam( + val type: String, + val alg: Int, +) + +@Serializable +data class ExcludeCredential( + val id: String, + val type: String, + val transports: List, +) + +@Serializable +data class AuthenticatorSelection( + val authenticatorAttachment: String, + val requireResidentKey: Boolean, + val residentKey: String, + val userVerification: String, +) diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/model/RegistrationVerificationResponse.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/model/RegistrationVerificationResponse.kt new file mode 100644 index 00000000..8dde93e7 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/model/RegistrationVerificationResponse.kt @@ -0,0 +1,6 @@ +package com.twilio.passkeys.android.model + +import kotlinx.serialization.Serializable + +@Serializable +data class RegistrationVerificationResponse(val status: String) diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/pages/HomePage.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/pages/HomePage.kt new file mode 100644 index 00000000..7cbcfba2 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/pages/HomePage.kt @@ -0,0 +1,280 @@ +package com.twilio.passkeys.android.pages + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import com.twilio.passkeys.android.R +import com.twilio.passkeys.android.ui.blue + +@Suppress("FunctionName") +@Composable +@Preview +fun HomePage( + number: String = "+1 1111 1111", + onDisconnect: () -> Unit = {}, +) { + ConstraintLayout(modifier = Modifier.fillMaxSize()) { + val (column1, column2, spacer) = createRefs() + Column( + modifier = + Modifier + .fillMaxWidth() + .constrainAs(column1) { + height = Dimension.value(340.dp) + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .background(blue), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + painter = painterResource(id = R.drawable.owl_inverted), + tint = Color.White, + contentDescription = "Owl", + modifier = Modifier.padding(32.dp), + ) + IconButton( + onClick = onDisconnect, + modifier = + Modifier + .padding(32.dp) + .testTag("logout"), + ) { + Icon( + painter = painterResource(id = R.drawable.menu), + tint = Color.White, + contentDescription = "Logout", + ) + } + } + + Text( + text = "Hello +$number", + style = + TextStyle( + fontSize = 32.sp, + lineHeight = 48.sp, + fontWeight = FontWeight(500), + color = Color(0xFFFFFFFF), + ), + modifier = Modifier.padding(horizontal = 32.dp), + ) + Text( + text = "Welcome to OwlBank", + style = + TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontWeight = FontWeight(400), + color = Color(0xB2FFFFFF), + ), + modifier = Modifier.padding(horizontal = 32.dp), + ) + OutlinedTextField( + leadingIcon = { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = "Search", + tint = Color.White, + ) + }, + value = "", + onValueChange = {}, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color(0xFF0385EB), + unfocusedBorderColor = Color(0xFF0385EB), + ), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp, vertical = 24.dp), + shape = RoundedCornerShape(100.dp), + ) + } + + Spacer( + modifier = + Modifier + .height(80.dp) + .constrainAs(spacer) { + linkTo(top = column1.bottom, bottom = column1.bottom) + }, + ) + Column( + modifier = + Modifier + .padding(horizontal = 32.dp) + .constrainAs(column2) { + top.linkTo(spacer.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + ) { + Card( + elevation = + CardDefaults.cardElevation( + defaultElevation = 8.dp, + ), + modifier = + Modifier + .width(336.dp) + .height(80.dp), + shape = RoundedCornerShape(size = 2.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxSize() + .background(color = Color(0xFFFFFFFF)), + ) { + Icon( + modifier = Modifier.padding(start = 24.dp), + painter = painterResource(id = R.drawable.copy), + contentDescription = "Open account", + ) + Text( + modifier = Modifier.padding(start = 24.dp), + text = "Open an account", + style = + TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight(500), + color = Color(0xFF05090A), + letterSpacing = 0.4.sp, + ), + ) + } + } + Card( + elevation = + CardDefaults.cardElevation( + defaultElevation = 8.dp, + ), + modifier = + Modifier + .width(336.dp) + .height(80.dp), + shape = RoundedCornerShape(size = 2.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .shadow( + elevation = 28.dp, + spotColor = Color(0x08000000), + ambientColor = Color(0x08000000), + ) + .padding(top = 8.dp) + .width(336.dp) + .height(80.dp) + .background( + color = Color(0xFFFFFFFF), + shape = RoundedCornerShape(size = 2.dp), + ), + ) { + Icon( + modifier = Modifier.padding(start = 24.dp), + painter = painterResource(id = R.drawable.credit_card), + contentDescription = "Open account", + ) + Text( + modifier = Modifier.padding(start = 24.dp), + text = "Get a credit card", + style = + TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight(500), + color = Color(0xFF05090A), + letterSpacing = 0.4.sp, + ), + ) + } + } + + Card( + elevation = + CardDefaults.cardElevation( + defaultElevation = 8.dp, + ), + modifier = + Modifier + .width(336.dp) + .height(80.dp), + shape = RoundedCornerShape(size = 2.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .shadow( + elevation = 28.dp, + spotColor = Color(0x08000000), + ambientColor = Color(0x08000000), + ) + .padding(top = 8.dp) + .width(336.dp) + .height(80.dp) + .background( + color = Color(0xFFFFFFFF), + shape = RoundedCornerShape(size = 2.dp), + ), + ) { + Icon( + modifier = Modifier.padding(start = 24.dp), + painter = painterResource(id = R.drawable.dollar), + contentDescription = "Open account", + ) + Text( + modifier = Modifier.padding(start = 24.dp), + text = "Apply for a loan", + style = + TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight(500), + color = Color(0xFF05090A), + letterSpacing = 0.4.sp, + ), + ) + } + } + } + } +} diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/pages/LoginPage.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/pages/LoginPage.kt new file mode 100644 index 00000000..46037cb1 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/pages/LoginPage.kt @@ -0,0 +1,144 @@ +package com.twilio.passkeys.android.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.twilio.passkeys.android.R +import com.twilio.passkeys.android.ui.button_color +import com.twilio.passkeys.android.ui.twilio + +@Suppress("FunctionName") +@Composable +@Preview +fun LoginPage( + onNumberEntered: (phoneNumber: String) -> Unit = {}, + fetchPasskeys: () -> Unit = {}, + numberError: Boolean = false, +) { + val number = remember { mutableStateOf(TextFieldValue()) } + Column( + modifier = + Modifier + .fillMaxSize() + .padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.owl_image), + contentDescription = "owl", + contentScale = ContentScale.Fit, + modifier = + Modifier + .padding(top = 100.dp) + .clickable { fetchPasskeys() }, + ) + Text( + text = + buildAnnotatedString { + append("Welcome to ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, color = Color.Black)) { + append("OwlBank") + } + }, + modifier = Modifier.padding(top = 32.dp), + style = + TextStyle( + fontSize = 18.sp, + lineHeight = 28.sp, + fontWeight = FontWeight(400), + color = Color(0xFF121C2D), + ), + ) + Text( + text = "What's your phone number?", + modifier = Modifier.padding(top = 8.dp), + style = + TextStyle( + fontSize = 24.sp, + lineHeight = 32.sp, + fontWeight = FontWeight(600), + color = Color(0xFF121C2D), + textAlign = TextAlign.Center, + ), + ) + OutlinedTextField( + label = { Text(text = "Phone number") }, + value = number.value, + onValueChange = { number.value = it }, + isError = numberError, + supportingText = { if (numberError) Text(text = "Invalid input, please type a valid phone number") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), + singleLine = true, + modifier = + Modifier + .testTag("phone_number") + .fillMaxWidth() + .padding(top = 4.dp), + trailingIcon = { + Icon( + painter = painterResource(R.drawable.twilio), + contentDescription = "Twilio", + tint = twilio, + ) + }, + shape = RoundedCornerShape(8.dp), + ) + Button( + modifier = + Modifier + .testTag("submit") + .fillMaxWidth() + .height(72.dp) + .padding(top = 16.dp), + onClick = { onNumberEntered(number.value.text) }, + colors = + ButtonDefaults.buttonColors( + containerColor = button_color, + ), + shape = RoundedCornerShape(8.dp), + ) { + Text( + text = "Get started", + style = + TextStyle( + fontSize = 16.sp, + lineHeight = 24.sp, + fontWeight = FontWeight(600), + color = Color(0xFFFFFFFF), + textAlign = TextAlign.Center, + ), + ) + } + } +} diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/repository/AuthenticateRepository.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/repository/AuthenticateRepository.kt new file mode 100644 index 00000000..957c76cb --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/repository/AuthenticateRepository.kt @@ -0,0 +1,35 @@ +package com.twilio.passkeys.android.repository + +import com.twilio.passkeys.android.api.AuthenticateApi +import com.twilio.passkeys.android.api.AuthenticateVerificationRequest +import com.twilio.passkeys.android.model.AuthenticateStartResponse +import com.twilio.passkeys.android.model.AuthenticateVerificationResponse +import javax.inject.Inject + +class AuthenticateRepository + @Inject + constructor(private val authenticateApi: AuthenticateApi) { + suspend fun start(): AuthenticateStartResponse { + return authenticateApi.authenticateStart() + } + + suspend fun verification( + rawId: String, + id: String, + clientDataJson: String, + userHandle: String?, + signature: String?, + authenticatorData: String?, + ): AuthenticateVerificationResponse { + return authenticateApi.authenticateVerification( + AuthenticateVerificationRequest( + rawId = rawId, + id = id, + clientDataJson = clientDataJson, + userHandle = userHandle, + signature = signature, + authenticatorData = authenticatorData, + ), + ) + } + } diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/repository/CreateRepository.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/repository/CreateRepository.kt new file mode 100644 index 00000000..652c80f9 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/repository/CreateRepository.kt @@ -0,0 +1,36 @@ +package com.twilio.passkeys.android.repository + +import com.twilio.passkeys.android.api.RegistrationApi +import com.twilio.passkeys.android.api.RegistrationStartRequest +import com.twilio.passkeys.android.api.RegistrationVerificationRequest +import com.twilio.passkeys.android.model.RegistrationStartResponse +import com.twilio.passkeys.android.model.RegistrationVerificationResponse +import javax.inject.Inject + +class CreateRepository + @Inject + constructor(private val registrationApi: RegistrationApi) { + suspend fun start(username: String): RegistrationStartResponse { + return registrationApi.registrationStart(RegistrationStartRequest(username)) + } + + suspend fun verification( + rawId: String, + id: String, + clientDataJson: String, + attestationObject: String, + type: String, + transports: List, + ): RegistrationVerificationResponse { + return registrationApi.registrationVerification( + RegistrationVerificationRequest( + rawId = rawId, + id = id, + clientDataJson = clientDataJson, + attestationObject = attestationObject, + type = type, + transports = transports, + ), + ) + } + } diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/ui/Color.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/ui/Color.kt new file mode 100644 index 00000000..48babff5 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/ui/Color.kt @@ -0,0 +1,7 @@ +package com.twilio.passkeys.android.ui + +import androidx.compose.ui.graphics.Color + +val twilio = Color(0xFFF22F46) +val button_color = Color(0xFF0263E0) +val blue = Color(0xFF0263E0) diff --git a/AndroidApp/src/main/java/com/twilio/passkeys/android/ui/MyApplicationTheme.kt b/AndroidApp/src/main/java/com/twilio/passkeys/android/ui/MyApplicationTheme.kt new file mode 100644 index 00000000..1361a6a7 --- /dev/null +++ b/AndroidApp/src/main/java/com/twilio/passkeys/android/ui/MyApplicationTheme.kt @@ -0,0 +1,14 @@ +package com.twilio.passkeys.android.ui + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable + +@Suppress("FunctionName") +@Composable +fun MyApplicationTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + MaterialTheme(content = content) +} diff --git a/AndroidApp/src/main/res/drawable-hdpi/owl_image.png b/AndroidApp/src/main/res/drawable-hdpi/owl_image.png new file mode 100644 index 00000000..52a64e4e Binary files /dev/null and b/AndroidApp/src/main/res/drawable-hdpi/owl_image.png differ diff --git a/AndroidApp/src/main/res/drawable-mdpi/owl_image.png b/AndroidApp/src/main/res/drawable-mdpi/owl_image.png new file mode 100644 index 00000000..8adcd8e7 Binary files /dev/null and b/AndroidApp/src/main/res/drawable-mdpi/owl_image.png differ diff --git a/AndroidApp/src/main/res/drawable-xhdpi/owl_image.png b/AndroidApp/src/main/res/drawable-xhdpi/owl_image.png new file mode 100644 index 00000000..e2b6445e Binary files /dev/null and b/AndroidApp/src/main/res/drawable-xhdpi/owl_image.png differ diff --git a/AndroidApp/src/main/res/drawable-xxhdpi/owl_image.png b/AndroidApp/src/main/res/drawable-xxhdpi/owl_image.png new file mode 100644 index 00000000..e47e5f08 Binary files /dev/null and b/AndroidApp/src/main/res/drawable-xxhdpi/owl_image.png differ diff --git a/AndroidApp/src/main/res/drawable/copy.xml b/AndroidApp/src/main/res/drawable/copy.xml new file mode 100644 index 00000000..df66e051 --- /dev/null +++ b/AndroidApp/src/main/res/drawable/copy.xml @@ -0,0 +1,10 @@ + + + diff --git a/AndroidApp/src/main/res/drawable/credit_card.xml b/AndroidApp/src/main/res/drawable/credit_card.xml new file mode 100644 index 00000000..4ecca75a --- /dev/null +++ b/AndroidApp/src/main/res/drawable/credit_card.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/AndroidApp/src/main/res/drawable/dollar.xml b/AndroidApp/src/main/res/drawable/dollar.xml new file mode 100644 index 00000000..67848541 --- /dev/null +++ b/AndroidApp/src/main/res/drawable/dollar.xml @@ -0,0 +1,10 @@ + + + diff --git a/AndroidApp/src/main/res/drawable/ic_launcher_background.xml b/AndroidApp/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..e477184d --- /dev/null +++ b/AndroidApp/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/AndroidApp/src/main/res/drawable/ic_launcher_foreground.xml b/AndroidApp/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..6cebd2cc --- /dev/null +++ b/AndroidApp/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/AndroidApp/src/main/res/drawable/menu.xml b/AndroidApp/src/main/res/drawable/menu.xml new file mode 100644 index 00000000..b164b9f9 --- /dev/null +++ b/AndroidApp/src/main/res/drawable/menu.xml @@ -0,0 +1,12 @@ + + + + diff --git a/AndroidApp/src/main/res/drawable/owl.xml b/AndroidApp/src/main/res/drawable/owl.xml new file mode 100644 index 00000000..ecadec8a --- /dev/null +++ b/AndroidApp/src/main/res/drawable/owl.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/AndroidApp/src/main/res/drawable/owl_inverted.xml b/AndroidApp/src/main/res/drawable/owl_inverted.xml new file mode 100644 index 00000000..f6093b35 --- /dev/null +++ b/AndroidApp/src/main/res/drawable/owl_inverted.xml @@ -0,0 +1,10 @@ + + + diff --git a/AndroidApp/src/main/res/drawable/twilio.xml b/AndroidApp/src/main/res/drawable/twilio.xml new file mode 100644 index 00000000..773ba5b8 --- /dev/null +++ b/AndroidApp/src/main/res/drawable/twilio.xml @@ -0,0 +1,9 @@ + + + diff --git a/AndroidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/AndroidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/AndroidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/AndroidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/AndroidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..7353dbd1 --- /dev/null +++ b/AndroidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/AndroidApp/src/main/res/mipmap-hdpi/ic_launcher.png b/AndroidApp/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..31024474 Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/AndroidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/AndroidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..4b8d2672 Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/AndroidApp/src/main/res/mipmap-mdpi/ic_launcher.png b/AndroidApp/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..22318edd Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/AndroidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/AndroidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..cc7c5797 Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher.png b/AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..24695d8e Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..1e23def5 Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..37873ae5 Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..63ed69e9 Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..bc411209 Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..d1082560 Binary files /dev/null and b/AndroidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/AndroidApp/src/main/res/values/ic_launcher_background.xml b/AndroidApp/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..0f55997d --- /dev/null +++ b/AndroidApp/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #0878FF + \ No newline at end of file diff --git a/AndroidApp/src/main/res/values/strings.xml b/AndroidApp/src/main/res/values/strings.xml new file mode 100644 index 00000000..d67614ee --- /dev/null +++ b/AndroidApp/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + PasskeysDemo + diff --git a/AndroidApp/src/main/res/values/styles.xml b/AndroidApp/src/main/res/values/styles.xml new file mode 100644 index 00000000..6b4fa3d0 --- /dev/null +++ b/AndroidApp/src/main/res/values/styles.xml @@ -0,0 +1,3 @@ + +