diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 0599b91..3805133 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -10,7 +10,7 @@ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6dfaa05..126ca8e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,15 +18,42 @@ android:name=".QuilttConnectorActivity" android:launchMode="singleTop" android:exported="true"> + + + - + + + + + + + + + + + + + + + + + + + + - println("Event: $eventType") - println("Metadata: $metadata") - }, - onExit = { eventType, metadata -> - println("Event: $eventType") - println("Metadata: $metadata") - }, - onExitSuccess = { metadata -> - println("Exit success!") - println("Metadata: $metadata") - Toast.makeText(context, metadata.connectionId, Toast.LENGTH_LONG).show() - if (context is Activity) { - context.finish() - } - }, - onExitAbort = { metadata -> - println("Exit abort!") - println("Metadata: $metadata") - if (context is Activity) { - context.finish() - } - }, - onExitError = { metadata -> - println("Exit error!") - println("Metadata: $metadata") - if (context is Activity) { - context.finish() - } - }) - AndroidView(factory = { connectorWebView } ) -} - -@Preview(showBackground = true) -@Composable -fun QuilttConnectorPreview() { - QuilttConnectorContent( - config = QuilttConnectorConnectConfiguration( - connectorId = "", - oauthRedirectUrl = "" - ) - ) -} \ No newline at end of file diff --git a/app_jetpack_compose/src/main/res/values/strings.xml b/app_jetpack_compose/src/main/res/values/strings.xml deleted file mode 100644 index 592f1df..0000000 --- a/app_jetpack_compose/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - App Jetpack Compose - QuilttConnectorActivity - \ No newline at end of file diff --git a/app_jetpack_compose/.gitignore b/app_quiltthub_webview_example/.gitignore similarity index 100% rename from app_jetpack_compose/.gitignore rename to app_quiltthub_webview_example/.gitignore diff --git a/app_jetpack_compose/build.gradle.kts b/app_quiltthub_webview_example/build.gradle.kts similarity index 78% rename from app_jetpack_compose/build.gradle.kts rename to app_quiltthub_webview_example/build.gradle.kts index f385042..49a4db2 100644 --- a/app_jetpack_compose/build.gradle.kts +++ b/app_quiltthub_webview_example/build.gradle.kts @@ -4,11 +4,11 @@ plugins { } android { - namespace = "app.quiltt.app_jetpack_compose" + namespace = "app.quiltt.app_quiltthub_webview_example" compileSdk = 34 defaultConfig { - applicationId = "app.quiltt.app_jetpack_compose" + applicationId = "app.quiltt.app_quiltthub_webview_example" minSdk = 26 targetSdk = 34 versionCode = 1 @@ -18,6 +18,15 @@ android { vectorDrawables { useSupportLibrary = true } + + buildFeatures { + buildConfig = true + } + + val ingressConnectorId = System.getenv("MOBILE_INGRESS_CONNECTOR_ID") + val addConnectorId = System.getenv("MOBILE_ADD_CONNECTOR_ID") + buildConfigField("String", "INGRESS_CONNECTOR_ID", "\"$ingressConnectorId\"") + buildConfigField("String", "ADD_CONNECTOR_ID", "\"$addConnectorId\"") } buildTypes { @@ -70,6 +79,8 @@ dependencies { debugImplementation("androidx.compose.ui:ui-test-manifest") implementation("androidx.core:core-splashscreen:1.0.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") + implementation("androidx.browser:browser:1.4.0") implementation(project(":connector")) // Enable below to use the published version // implementation("app.quiltt:connector:") diff --git a/app_jetpack_compose/proguard-rules.pro b/app_quiltthub_webview_example/proguard-rules.pro similarity index 100% rename from app_jetpack_compose/proguard-rules.pro rename to app_quiltthub_webview_example/proguard-rules.pro diff --git a/app_jetpack_compose/src/androidTest/java/app/quiltt/app_jetpack_compose/ExampleInstrumentedTest.kt b/app_quiltthub_webview_example/src/androidTest/java/app/quiltt/app_quiltthub_webview_example/ExampleInstrumentedTest.kt similarity index 92% rename from app_jetpack_compose/src/androidTest/java/app/quiltt/app_jetpack_compose/ExampleInstrumentedTest.kt rename to app_quiltthub_webview_example/src/androidTest/java/app/quiltt/app_quiltthub_webview_example/ExampleInstrumentedTest.kt index fbca9f9..30c48ad 100644 --- a/app_jetpack_compose/src/androidTest/java/app/quiltt/app_jetpack_compose/ExampleInstrumentedTest.kt +++ b/app_quiltthub_webview_example/src/androidTest/java/app/quiltt/app_quiltthub_webview_example/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package app.quiltt.app_jetpack_compose +package app.quiltt.app_quiltthub_webview_example import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 diff --git a/app_jetpack_compose/src/main/AndroidManifest.xml b/app_quiltthub_webview_example/src/main/AndroidManifest.xml similarity index 68% rename from app_jetpack_compose/src/main/AndroidManifest.xml rename to app_quiltthub_webview_example/src/main/AndroidManifest.xml index bf862b2..2c49c99 100644 --- a/app_jetpack_compose/src/main/AndroidManifest.xml +++ b/app_quiltthub_webview_example/src/main/AndroidManifest.xml @@ -1,45 +1,42 @@ - + + - - - - - + - - - - \ No newline at end of file diff --git a/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/AppConfig.kt b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/AppConfig.kt new file mode 100644 index 0000000..1959733 --- /dev/null +++ b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/AppConfig.kt @@ -0,0 +1,7 @@ +package app.quiltt.app_quiltthub_webview_example + +object AppConfig { + val ingressConnectorId = BuildConfig.INGRESS_CONNECTOR_ID + val addConnectorId = BuildConfig.ADD_CONNECTOR_ID + val oauthRedirectUrl = "https://quiltt.app/mobile/hub/reconnect" +} \ No newline at end of file diff --git a/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/MainActivity.kt b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/MainActivity.kt new file mode 100644 index 0000000..5357141 --- /dev/null +++ b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/MainActivity.kt @@ -0,0 +1,103 @@ +package app.quiltt.app_quiltthub_webview_example + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +// Splash screen +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import androidx.activity.viewModels +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.ViewModelProvider +import app.quiltt.connector.PingResponse +import app.quiltt.connector.QuilttAuthApi +import app.quiltt.connector.QuilttConnector +import app.quiltt.connector.QuilttConnectorConnectConfiguration +import kotlinx.coroutines.Dispatchers + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val token = SharedPreferencesHelper(context = this).getData("token") + val viewModel: MainViewModel by viewModels { MainViewModelFactory(token) } + + installSplashScreen().apply { + setKeepOnScreenCondition { + viewModel.loading.value + } + } + + setContent { + val isValidToken by viewModel.isValidToken.collectAsState() + if (isValidToken) { + val intent = Intent(this@MainActivity, QuilttHubActivity::class.java) + startActivity(intent) + } else { + IngressConnector() + } + } + } +} + +class MainViewModelFactory(private val token: String?) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T = MainViewModel(token) as T +} + +class MainViewModel(private val token:String?) : ViewModel() { + private val _loading = MutableStateFlow(true) + private val _isValidToken = MutableStateFlow(false) + val loading = _loading.asStateFlow() + val isValidToken = _isValidToken.asStateFlow() + + init { + viewModelScope.launch(Dispatchers.IO) {// run background task here + if (token != null) { + val result = QuilttAuthApi(clientId = null).ping(token = token) + if (result is PingResponse.SessionResponse) { + _isValidToken.value = true + _loading.value = false + } else { + _isValidToken.value = false + _loading.value = false + } + } else { + _isValidToken.value = false + _loading.value = false + } + } + } +} + +@Composable +fun IngressConnector() { + val context = LocalContext.current + val quilttConnector = QuilttConnector(context) + val config = QuilttConnectorConnectConfiguration(connectorId = AppConfig.ingressConnectorId) + val connectorWebView = quilttConnector.connect(config = config, onExitSuccess = { metadata -> + val token: String? = metadata.token + if (token != null) { + SharedPreferencesHelper(context).saveData("token", token) + val intent = Intent(context, QuilttHubActivity::class.java) + context.startActivity(intent) + (context as MainActivity).finish() + } + }) + AndroidView(factory = { connectorWebView } ) +} + +@Preview(showBackground = true) +@Composable +fun QuilttHubPreview() { + IngressConnector() +} \ No newline at end of file diff --git a/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/QuilttConnectorActivity.kt b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/QuilttConnectorActivity.kt new file mode 100644 index 0000000..b9d3289 --- /dev/null +++ b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/QuilttConnectorActivity.kt @@ -0,0 +1,106 @@ +package app.quiltt.app_quiltthub_webview_example + +import android.app.Activity +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.viewinterop.AndroidView +import app.quiltt.connector.QuilttConnector +import app.quiltt.connector.QuilttConnectorConnectConfiguration +import app.quiltt.connector.QuilttConnectorReconnectConfiguration + +class QuilttConnectorActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val connectionId = intent.getStringExtra("connectionId") + val connectorId = AppConfig.addConnectorId + val oauthRedirectUrl = AppConfig.oauthRedirectUrl + val token = SharedPreferencesHelper(context = this).getData("token") + if (connectionId != null) { + val config = QuilttConnectorReconnectConfiguration( + connectorId = connectorId, + oauthRedirectUrl = oauthRedirectUrl, + connectionId = connectionId + ) + setContent { + QuilttReconnectContent(config = config, token = token!!) + } + } else { + val config = QuilttConnectorConnectConfiguration( + connectorId = connectorId, + oauthRedirectUrl = oauthRedirectUrl) + setContent { + QuilttConnectContent(config = config, token = token!!) + } + } + } +} + +@Composable +fun QuilttConnectContent(config: QuilttConnectorConnectConfiguration, token: String) { + val context = LocalContext.current + val quilttConnector = QuilttConnector(context) + quilttConnector.authenticate(token) + val connectorWebView = quilttConnector.connect( + config = config, + onExitSuccess = { metadata -> + if (context is Activity) { + context.finish() + } + }, + onExitAbort = { metadata -> + // TODO: Handle abort + if (context is Activity) { + context.finish() + } + }, + onExitError = { metadata -> + // TODO: Handle error + if (context is Activity) { + context.finish() + } + }) + AndroidView(factory = { connectorWebView } ) +} + +@Composable +fun QuilttReconnectContent(config: QuilttConnectorReconnectConfiguration, token: String) { + val context = LocalContext.current + val quilttConnector = QuilttConnector(context) + quilttConnector.authenticate(token) + val connectorWebView = quilttConnector.reconnect( + config = config, + onExitSuccess = { metadata -> + if (context is Activity) { + context.finish() + } + }, + onExitAbort = { metadata -> + // TODO: Handle abort + if (context is Activity) { + context.finish() + } + }, + onExitError = { metadata -> + // TODO: Handle error + if (context is Activity) { + context.finish() + } + }) + AndroidView(factory = { connectorWebView } ) +} + +@Preview(showBackground = true) +@Composable +fun QuilttConnectorPreview() { + QuilttConnectContent( + config = QuilttConnectorConnectConfiguration( + connectorId = "", + oauthRedirectUrl = "" + ), + token = "" + ) +} \ No newline at end of file diff --git a/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/QuilttHubActivity.kt b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/QuilttHubActivity.kt new file mode 100644 index 0000000..bef685e --- /dev/null +++ b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/QuilttHubActivity.kt @@ -0,0 +1,168 @@ +package app.quiltt.app_quiltthub_webview_example + +import android.content.Intent +import android.os.Bundle +import android.view.ViewGroup +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExitToApp +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.viewinterop.AndroidView +import app.quiltt.connector.QuilttAuthApi +import android.app.AlertDialog +import android.net.Uri +import android.webkit.WebResourceRequest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.painterResource + +class QuilttHubActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + QuilttHubContent() + } + } + + override fun onBackPressed() { + showLogoutConfirmationDialog() + } + fun showLogoutConfirmationDialog() { + AlertDialog.Builder(this) + .setTitle("Confirm Logout") + .setMessage("Are you sure you want to logout?") + .setPositiveButton("Yes") { _, _ -> + logout() +// startActivity((Intent(this, MainActivity::class.java))) + } + .setNegativeButton("No", null) + .show() + } + + fun launchWithCustomTab(url: String) { + val builder = CustomTabsIntent.Builder() + val customTabsIntent = builder.build() + customTabsIntent.launchUrl(this, Uri.parse(url)) + } + private fun logout() { + val token = SharedPreferencesHelper(context = this).getData("token") + val apiClient = QuilttAuthApi(clientId = null) + CoroutineScope(Dispatchers.IO).launch { + apiClient.revoke(token = token!!) + } + this.finish() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun QuilttHubContent() { + val context = LocalContext.current + val activity = context as QuilttHubActivity + val token = SharedPreferencesHelper(context = context).getData("token") + val url = "https://www.quiltthub.com/login?mode=webview&token=$token" + Column { + TopAppBar( + title = { + Image( + painter = painterResource(id = R.drawable.quiltt_app_icon), + contentDescription = "Quiltt Hub Logo", + modifier = Modifier.size(40.dp)) + }, + actions = { + Button(onClick = { + val intent = Intent(context, QuilttConnectorActivity::class.java) + context.startActivity(intent) + }, + shape = RoundedCornerShape(25), + colors = ButtonDefaults.buttonColors( + containerColor = colorResource(id = R.color.purple_500), + contentColor = Color.White)) + { + Text("Add Account") + } + IconButton(onClick = { + activity.showLogoutConfirmationDialog() + }) { + Icon(Icons.Filled.ExitToApp, contentDescription = "Logout") + } + } + ) + WebViewComposable(url = url) + } +} + +@Composable +fun WebViewComposable(url: String) { + AndroidView(factory = { context -> + WebView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + webViewClient = object : WebViewClient() { + val activity = context as QuilttHubActivity + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + val url = request?.url?.toString() + if (url != null && url.startsWith("https://www.quiltthub.com/mobile/hub/reconnect")) { + val urlComponents = Uri.parse(url) + val connectionId = urlComponents.getQueryParameter("connectionId") + val intent = Intent(context, QuilttConnectorActivity::class.java) + intent.putExtra("connectionId", connectionId) + activity.startActivity(intent) + return true + } + if (url != null && url.startsWith("https://www.quiltthub.com")) { + return false + } + // TODO: Sometimes connector is publishing Options? + if (url != null && url.startsWith("quilttconnector://")) { + return true + } + // TODO: handle token expires while the user is using the app + + // Handle quiltt.io links, mailto links and other social media links + if (url != null) { + activity.launchWithCustomTab(url) + return true + } + // Return false to let the WebView handle the URL + return false + } + } + loadUrl(url) + } + }, update = { webView -> + webView.loadUrl(url) + }) +} + +@Preview(showBackground = true) +@Composable +fun QuilttHubActivityPreview() { + QuilttHubContent() +} \ No newline at end of file diff --git a/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/SharedPreferencesHelper.kt b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/SharedPreferencesHelper.kt new file mode 100644 index 0000000..ee55fff --- /dev/null +++ b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/SharedPreferencesHelper.kt @@ -0,0 +1,19 @@ +package app.quiltt.app_quiltthub_webview_example + +import android.content.Context +import android.content.SharedPreferences + +class SharedPreferencesHelper(context: Context) { + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences("MyAppPreferences", Context.MODE_PRIVATE) + + fun saveData(key: String, value: String) { + val editor = sharedPreferences.edit() + editor.putString(key, value) + editor.apply() + } + + fun getData(key: String): String? { + return sharedPreferences.getString(key, null) + } +} \ No newline at end of file diff --git a/app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Color.kt b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Color.kt similarity index 80% rename from app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Color.kt rename to app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Color.kt index d336c01..ea54166 100644 --- a/app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Color.kt +++ b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package app.quiltt.app_jetpack_compose.ui.theme +package app.quiltt.app_quiltthub_webview_example.ui.theme import androidx.compose.ui.graphics.Color diff --git a/app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Theme.kt b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Theme.kt similarity index 97% rename from app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Theme.kt rename to app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Theme.kt index 6a81000..7fda95f 100644 --- a/app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Theme.kt +++ b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package app.quiltt.app_jetpack_compose.ui.theme +package app.quiltt.app_quiltthub_webview_example.ui.theme import android.app.Activity import android.os.Build diff --git a/app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Type.kt b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Type.kt similarity index 94% rename from app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Type.kt rename to app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Type.kt index a99d074..9969be1 100644 --- a/app_jetpack_compose/src/main/java/app/quiltt/app_jetpack_compose/ui/theme/Type.kt +++ b/app_quiltthub_webview_example/src/main/java/app/quiltt/app_quiltthub_webview_example/ui/theme/Type.kt @@ -1,4 +1,4 @@ -package app.quiltt.app_jetpack_compose.ui.theme +package app.quiltt.app_quiltthub_webview_example.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/app_jetpack_compose/src/main/res/drawable/ic_launcher_background.xml b/app_quiltthub_webview_example/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from app_jetpack_compose/src/main/res/drawable/ic_launcher_background.xml rename to app_quiltthub_webview_example/src/main/res/drawable/ic_launcher_background.xml diff --git a/app_jetpack_compose/src/main/res/drawable/ic_launcher_foreground.xml b/app_quiltthub_webview_example/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app_jetpack_compose/src/main/res/drawable/ic_launcher_foreground.xml rename to app_quiltthub_webview_example/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/app_jetpack_compose/src/main/res/drawable/quiltt.xml b/app_quiltthub_webview_example/src/main/res/drawable/quiltt.xml similarity index 100% rename from app_jetpack_compose/src/main/res/drawable/quiltt.xml rename to app_quiltthub_webview_example/src/main/res/drawable/quiltt.xml diff --git a/app_quiltthub_webview_example/src/main/res/drawable/quiltt_app_icon.xml b/app_quiltthub_webview_example/src/main/res/drawable/quiltt_app_icon.xml new file mode 100644 index 0000000..9d9568c --- /dev/null +++ b/app_quiltthub_webview_example/src/main/res/drawable/quiltt_app_icon.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + diff --git a/app_jetpack_compose/src/main/res/mipmap-anydpi/ic_launcher.xml b/app_quiltthub_webview_example/src/main/res/mipmap-anydpi/ic_launcher.xml similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-anydpi/ic_launcher.xml rename to app_quiltthub_webview_example/src/main/res/mipmap-anydpi/ic_launcher.xml diff --git a/app_jetpack_compose/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app_quiltthub_webview_example/src/main/res/mipmap-anydpi/ic_launcher_round.xml similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-anydpi/ic_launcher_round.xml rename to app_quiltthub_webview_example/src/main/res/mipmap-anydpi/ic_launcher_round.xml diff --git a/app_jetpack_compose/src/main/res/mipmap-hdpi/ic_launcher.webp b/app_quiltthub_webview_example/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-hdpi/ic_launcher.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app_quiltthub_webview_example/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-mdpi/ic_launcher.webp b/app_quiltthub_webview_example/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-mdpi/ic_launcher.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app_quiltthub_webview_example/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app_quiltthub_webview_example/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app_quiltthub_webview_example/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app_quiltthub_webview_example/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app_quiltthub_webview_example/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app_quiltthub_webview_example/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/app_jetpack_compose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app_quiltthub_webview_example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from app_jetpack_compose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to app_quiltthub_webview_example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/app_jetpack_compose/src/main/res/values/colors.xml b/app_quiltthub_webview_example/src/main/res/values/colors.xml similarity index 100% rename from app_jetpack_compose/src/main/res/values/colors.xml rename to app_quiltthub_webview_example/src/main/res/values/colors.xml diff --git a/app_jetpack_compose/src/main/res/values/splash.xml b/app_quiltthub_webview_example/src/main/res/values/splash.xml similarity index 100% rename from app_jetpack_compose/src/main/res/values/splash.xml rename to app_quiltthub_webview_example/src/main/res/values/splash.xml diff --git a/app_quiltthub_webview_example/src/main/res/values/strings.xml b/app_quiltthub_webview_example/src/main/res/values/strings.xml new file mode 100644 index 0000000..8288d6e --- /dev/null +++ b/app_quiltthub_webview_example/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Quiltt Hub + QuilttConnectorActivity + QuilttHubActivity + \ No newline at end of file diff --git a/app_jetpack_compose/src/main/res/values/themes.xml b/app_quiltthub_webview_example/src/main/res/values/themes.xml similarity index 100% rename from app_jetpack_compose/src/main/res/values/themes.xml rename to app_quiltthub_webview_example/src/main/res/values/themes.xml diff --git a/app_jetpack_compose/src/test/java/app/quiltt/app_jetpack_compose/ExampleUnitTest.kt b/app_quiltthub_webview_example/src/test/java/app/quiltt/app_quiltthub_webview_example/ExampleUnitTest.kt similarity index 86% rename from app_jetpack_compose/src/test/java/app/quiltt/app_jetpack_compose/ExampleUnitTest.kt rename to app_quiltthub_webview_example/src/test/java/app/quiltt/app_quiltthub_webview_example/ExampleUnitTest.kt index cfa3755..29bcdcb 100644 --- a/app_jetpack_compose/src/test/java/app/quiltt/app_jetpack_compose/ExampleUnitTest.kt +++ b/app_quiltthub_webview_example/src/test/java/app/quiltt/app_quiltthub_webview_example/ExampleUnitTest.kt @@ -1,4 +1,4 @@ -package app.quiltt.app_jetpack_compose +package app.quiltt.app_quiltthub_webview_example import org.junit.Test diff --git a/connector/src/main/AndroidManifest.xml b/connector/src/main/AndroidManifest.xml index a5918e6..74b7379 100644 --- a/connector/src/main/AndroidManifest.xml +++ b/connector/src/main/AndroidManifest.xml @@ -1,4 +1,3 @@ - \ No newline at end of file diff --git a/connector/src/main/java/app/quiltt/connector/QuilttAuthApi.kt b/connector/src/main/java/app/quiltt/connector/QuilttAuthApi.kt new file mode 100644 index 0000000..cd05d25 --- /dev/null +++ b/connector/src/main/java/app/quiltt/connector/QuilttAuthApi.kt @@ -0,0 +1,162 @@ +package app.quiltt.connector + +import org.json.JSONObject +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL + +sealed class UsernamePayload { + data class Email(val email: String) : UsernamePayload() + data class Phone(val phone: String) : UsernamePayload() +} + +data class PasscodePayload(val usernamePayload: UsernamePayload, val passcode: String) + +data class SessionData(val token: String) +data class UnauthorizedData(val message: String, val instruction: String) +data class UnprocessableData(val attribute: Map) + +sealed class PingResponse { + data class SessionResponse(val status: Int, val data: SessionData) : PingResponse() + data class UnprocessableResponse(val status: Int, val data: UnprocessableData) : PingResponse() +} + +sealed class IdentifyResponse { + data class SessionResponse(val status: Int, val data: SessionData) : IdentifyResponse() + data class AcceptedResponse(val status: Int) : IdentifyResponse() + data class UnprocessableResponse(val status: Int, val data: UnprocessableData) : IdentifyResponse() +} + +sealed class AuthenticateResponse { + data class SessionResponse(val status: Int, val data: SessionData) : AuthenticateResponse() + data class UnauthorizedResponse(val status: Int, val data: UnauthorizedData) : AuthenticateResponse() + data class UnprocessableResponse(val status: Int, val data: UnprocessableData) : AuthenticateResponse() +} + +sealed class RevokeResponse { + data class NoContentResponse(val status: Int) : RevokeResponse() + data class UnauthorizedResponse(val status: Int, val data: UnauthorizedData) : RevokeResponse() +} + +class QuilttAuthApi(private val clientId: String?) { + private val endpointAuth = "https://auth.quiltt.app/v1/users/session" + + /** + * Response Statuses: + * - 200: OK -> Session is Valid + * - 401: Unauthorized -> Session is Invalid + */ + fun ping(token: String): PingResponse { + val url = URL(endpointAuth) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "GET" + connection.setRequestProperty("Content-Type", "application/json; utf-8") + connection.setRequestProperty("Authorization", "Bearer $token") + + val statusCode = connection.responseCode + + if (statusCode == 200) { + return PingResponse.SessionResponse(statusCode, SessionData(token)) + } + val inputStream = connection.errorStream + val errorData = errorMap(inputStream) + return PingResponse.UnprocessableResponse(statusCode, errorData) + } + + /** + * Response Statuses: + * - 201: Created -> Profile Created, New Session Returned + * - 202: Accepted -> Profile Found, MFA Code Sent for `authenticate` + * - 422: Unprocessable Entity -> Invalid Payload + */ + fun identify(payload: UsernamePayload): IdentifyResponse { + val url = URL(endpointAuth) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "POST" + connection.setRequestProperty("Content-Type", "application/json; utf-8") + connection.doOutput = true + + val jsonPayload = JSONObject() + jsonPayload.put("clientId", clientId) + when (payload) { + is UsernamePayload.Email -> jsonPayload.put("email", payload.email) + is UsernamePayload.Phone -> jsonPayload.put("phone", payload.phone) + } + + if (connection.responseCode == 201) { + val inputStream = connection.inputStream + val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() }) + return IdentifyResponse.SessionResponse(connection.responseCode, SessionData(jsonObject.getString("token"))) + } + if (connection.responseCode == 202) { + return IdentifyResponse.AcceptedResponse(connection.responseCode) + } + val inputStream = connection.errorStream + val errorData = errorMap(inputStream) + return IdentifyResponse.UnprocessableResponse(connection.responseCode, errorData) + } + + /** + * Response Statuses: + * - 201: Created -> MFA Validated, New Session Returned + * - 401: Unauthorized -> MFA Invalid + * - 422: Unprocessable Entity -> Invalid Payload + */ + fun authenticate(payload: PasscodePayload): AuthenticateResponse { + val url = URL(endpointAuth) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "PUT" + connection.setRequestProperty("Content-Type", "application/json; utf-8") + connection.doOutput = true + + val jsonPayload = JSONObject() + jsonPayload.put("clientId", clientId) + jsonPayload.put("payload", payload) + + if (connection.responseCode == 201) { + val inputStream = connection.inputStream + val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() }) + return AuthenticateResponse.SessionResponse(connection.responseCode, SessionData(jsonObject.getString("token"))) + } + if (connection.responseCode == 401) { + val inputStream = connection.errorStream + val errorData = errorMap(inputStream) as UnauthorizedData + return AuthenticateResponse.UnauthorizedResponse(connection.responseCode, errorData) + } + val inputStream = connection.errorStream + val errorData = errorMap(inputStream) + return AuthenticateResponse.UnprocessableResponse(connection.responseCode, errorData) + } + + /** + * Response Statuses: + * - 204: No Content -> Session Revoked + * - 401: Unauthorized -> Session Not Found + */ + fun revoke(token: String): RevokeResponse { + val url = URL(endpointAuth) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "DELETE" + connection.setRequestProperty("Authorization", "Bearer $token") + connection.responseCode + + if (connection.responseCode == 204) { + return RevokeResponse.NoContentResponse(connection.responseCode) + } + val inputStream = connection.errorStream + val errorData = errorMap(inputStream) as UnauthorizedData + return RevokeResponse.UnauthorizedResponse(connection.responseCode, errorData) + } + + private fun errorMap(inputStream: InputStream): UnprocessableData { + val jsonObject = JSONObject(inputStream.bufferedReader().use { it.readText() }) + val errorMap = mutableMapOf() + val keys = jsonObject.keys() + while (keys.hasNext()) { + val key = keys.next() + val value = jsonObject.getString(key) + errorMap[key] = value + } + return UnprocessableData(errorMap) + } +} diff --git a/connector/src/main/java/app/quiltt/connector/QuilttConnectorConfiguration.kt b/connector/src/main/java/app/quiltt/connector/QuilttConnectorConfiguration.kt index b2b81d2..135e2a9 100644 --- a/connector/src/main/java/app/quiltt/connector/QuilttConnectorConfiguration.kt +++ b/connector/src/main/java/app/quiltt/connector/QuilttConnectorConfiguration.kt @@ -2,13 +2,13 @@ package app.quiltt.connector interface QuilttConnectorConfiguration { val connectorId: String - val oauthRedirectUrl: String + val oauthRedirectUrl: String? val connectionId: String? } data class QuilttConnectorConnectConfiguration( override val connectorId: String, - override val oauthRedirectUrl: String, + override val oauthRedirectUrl: String? = null ) : QuilttConnectorConfiguration { override val connectionId: String? = null // always null for connect, cannot be set } diff --git a/connector/src/main/java/app/quiltt/connector/QuilttConnectorEvent.kt b/connector/src/main/java/app/quiltt/connector/QuilttConnectorEvent.kt index 2d44a98..5da45e7 100644 --- a/connector/src/main/java/app/quiltt/connector/QuilttConnectorEvent.kt +++ b/connector/src/main/java/app/quiltt/connector/QuilttConnectorEvent.kt @@ -3,7 +3,8 @@ package app.quiltt.connector data class ConnectorSDKCallbackMetadata( val connectorId: String, val profileId: String?, - val connectionId: String? + val connectionId: String?, + val token: String? ) typealias ConnectorSDKOnEventCallback = (ConnectorSDKEventType, ConnectorSDKCallbackMetadata) -> Unit diff --git a/connector/src/main/java/app/quiltt/connector/QuilttConnectorWebView.kt b/connector/src/main/java/app/quiltt/connector/QuilttConnectorWebView.kt index 83b9c10..a33a2e5 100644 --- a/connector/src/main/java/app/quiltt/connector/QuilttConnectorWebView.kt +++ b/connector/src/main/java/app/quiltt/connector/QuilttConnectorWebView.kt @@ -76,6 +76,7 @@ class QuilttConnectorWebViewClient(private val params: QuilttConnectorWebViewCli val connectorId = params.config.connectorId val profileId = urlComponents.getQueryParameter("profileId") val connectionId = urlComponents.getQueryParameter("connectionId") + val token = urlComponents.getQueryParameter("token") println("handleQuilttEvent $url") when (url.host) { "Load" -> { @@ -83,19 +84,23 @@ class QuilttConnectorWebViewClient(private val params: QuilttConnectorWebViewCli } "ExitAbort" -> { clearLocalStorage() - params.onExit?.invoke(ConnectorSDKEventType.ExitAbort, ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = null)) - params.onExitAbort?.invoke(ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = null)) + params.onExit?.invoke(ConnectorSDKEventType.ExitAbort, ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = null, token = null)) + params.onExitAbort?.invoke(ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = null, token = null)) } "ExitError" -> { clearLocalStorage() - params.onExit?.invoke(ConnectorSDKEventType.ExitError, ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = null)) - params.onExitError?.invoke(ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = null)) + params.onExit?.invoke(ConnectorSDKEventType.ExitError, ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = null, token = null)) + params.onExitError?.invoke(ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = null, token = null)) } "ExitSuccess" -> { clearLocalStorage() if (connectionId != null) { - params.onExit?.invoke(ConnectorSDKEventType.ExitSuccess, ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = connectionId)) - params.onExitSuccess?.invoke(ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = connectionId)) + params.onExit?.invoke(ConnectorSDKEventType.ExitSuccess, ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = connectionId, token = null)) + params.onExitSuccess?.invoke(ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = null, connectionId = connectionId, token = null)) + } + if (token != null) { + params.onExit?.invoke(ConnectorSDKEventType.ExitSuccess, ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = profileId, connectionId = null, token = token)) + params.onExitSuccess?.invoke(ConnectorSDKCallbackMetadata(connectorId = connectorId, profileId = profileId, connectionId = null, token = token)) } } "Authenticate" -> { diff --git a/settings.gradle.kts b/settings.gradle.kts index e78607a..7fa68c3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,4 +16,4 @@ dependencyResolutionManagement { rootProject.name = "quiltt-android" include(":app") include(":connector") -include(":app_jetpack_compose") +include(":app_quiltthub_webview_example")