diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/Ipc.kt b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/Ipc.kt new file mode 100644 index 00000000..8e030f8e --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/Ipc.kt @@ -0,0 +1,33 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("unused") + +package com.eid_wallet.app + +import android.webkit.* + +class Ipc(val webViewClient: RustWebViewClient) { + @JavascriptInterface + fun postMessage(message: String?) { + message?.let {m -> + // we're not using WebView::getUrl() here because it needs to be executed on the main thread + // and it would slow down the Ipc + // so instead we track the current URL on the webview client + this.ipc(webViewClient.currentUrl, m) + } + } + + companion object { + init { + System.loadLibrary("eid_wallet_lib") + } + } + + private external fun ipc(url: String, message: String) + + +} diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/Logger.kt b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/Logger.kt new file mode 100644 index 00000000..0494c0e0 --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/Logger.kt @@ -0,0 +1,89 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package com.eid_wallet.app + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/Logger.java + +import android.text.TextUtils +import android.util.Log + +class Logger { + companion object { + private const val LOG_TAG_CORE = "Tauri" + + fun tags(vararg subtags: String): String { + return if (subtags.isNotEmpty()) { + LOG_TAG_CORE + "/" + TextUtils.join("/", subtags) + } else LOG_TAG_CORE + } + + fun verbose(message: String) { + verbose(LOG_TAG_CORE, message) + } + + private fun verbose(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.v(tag, message) + } + + fun debug(message: String) { + debug(LOG_TAG_CORE, message) + } + + fun debug(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.d(tag, message) + } + + fun info(message: String) { + info(LOG_TAG_CORE, message) + } + + fun info(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.i(tag, message) + } + + fun warn(message: String) { + warn(LOG_TAG_CORE, message) + } + + fun warn(tag: String, message: String) { + if (!shouldLog()) { + return + } + Log.w(tag, message) + } + + fun error(message: String) { + error(LOG_TAG_CORE, message, null) + } + + fun error(message: String, e: Throwable?) { + error(LOG_TAG_CORE, message, e) + } + + fun error(tag: String, message: String, e: Throwable?) { + if (!shouldLog()) { + return + } + Log.e(tag, message, e) + } + + private fun shouldLog(): Boolean { + return BuildConfig.DEBUG + } + } +} diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/PermissionHelper.kt b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/PermissionHelper.kt new file mode 100644 index 00000000..824b2b25 --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/PermissionHelper.kt @@ -0,0 +1,117 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.eid_wallet.app + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/PermissionHelper.java + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.ActivityCompat +import java.util.ArrayList + +object PermissionHelper { + /** + * Checks if a list of given permissions are all granted by the user + * + * @param permissions Permissions to check. + * @return True if all permissions are granted, false if at least one is not. + */ + fun hasPermissions(context: Context?, permissions: Array): Boolean { + for (perm in permissions) { + if (ActivityCompat.checkSelfPermission( + context!!, + perm + ) != PackageManager.PERMISSION_GRANTED + ) { + return false + } + } + return true + } + + /** + * Check whether the given permission has been defined in the AndroidManifest.xml + * + * @param permission A permission to check. + * @return True if the permission has been defined in the Manifest, false if not. + */ + fun hasDefinedPermission(context: Context, permission: String): Boolean { + var hasPermission = false + val requestedPermissions = getManifestPermissions(context) + if (!requestedPermissions.isNullOrEmpty()) { + val requestedPermissionsList = listOf(*requestedPermissions) + val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) + if (requestedPermissionsArrayList.contains(permission)) { + hasPermission = true + } + } + return hasPermission + } + + /** + * Check whether all of the given permissions have been defined in the AndroidManifest.xml + * @param context the app context + * @param permissions a list of permissions + * @return true only if all permissions are defined in the AndroidManifest.xml + */ + fun hasDefinedPermissions(context: Context, permissions: Array): Boolean { + for (permission in permissions) { + if (!hasDefinedPermission(context, permission)) { + return false + } + } + return true + } + + /** + * Get the permissions defined in AndroidManifest.xml + * + * @return The permissions defined in AndroidManifest.xml + */ + private fun getManifestPermissions(context: Context): Array? { + var requestedPermissions: Array? = null + try { + val pm = context.packageManager + val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())) + } else { + @Suppress("DEPRECATION") + pm.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) + } + if (packageInfo != null) { + requestedPermissions = packageInfo.requestedPermissions + } + } catch (_: Exception) { + } + return requestedPermissions + } + + /** + * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml + * + * @param neededPermissions The permissions needed. + * @return The permissions not present in AndroidManifest.xml + */ + fun getUndefinedPermissions(context: Context, neededPermissions: Array): Array { + val undefinedPermissions = ArrayList() + val requestedPermissions = getManifestPermissions(context) + if (!requestedPermissions.isNullOrEmpty()) { + val requestedPermissionsList = listOf(*requestedPermissions) + val requestedPermissionsArrayList = ArrayList(requestedPermissionsList) + for (permission in neededPermissions) { + if (!requestedPermissionsArrayList.contains(permission)) { + undefinedPermissions.add(permission) + } + } + var undefinedPermissionArray = arrayOfNulls(undefinedPermissions.size) + undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray) + return undefinedPermissionArray + } + return neededPermissions + } +} diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebChromeClient.kt b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebChromeClient.kt new file mode 100644 index 00000000..ab675a11 --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebChromeClient.kt @@ -0,0 +1,495 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("ObsoleteSdkInt", "RedundantOverride", "QueryPermissionsNeeded", "SimpleDateFormat") + +package com.eid_wallet.app + +// taken from https://github.com/ionic-team/capacitor/blob/6658bca41e78239347e458175b14ca8bd5c1d6e8/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java + +import android.Manifest +import android.app.Activity +import android.app.AlertDialog +import android.content.ActivityNotFoundException +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.view.View +import android.webkit.* +import android.widget.EditText +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.FileProvider +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* + +class RustWebChromeClient(appActivity: WryActivity) : WebChromeClient() { + private interface PermissionListener { + fun onPermissionSelect(isGranted: Boolean?) + } + + private interface ActivityResultListener { + fun onActivityResult(result: ActivityResult?) + } + + private val activity: WryActivity + private var permissionLauncher: ActivityResultLauncher> + private var activityLauncher: ActivityResultLauncher + private var permissionListener: PermissionListener? = null + private var activityListener: ActivityResultListener? = null + + init { + activity = appActivity + val permissionCallback = + ActivityResultCallback { isGranted: Map -> + if (permissionListener != null) { + var granted = true + for ((_, value) in isGranted) { + if (!value) granted = false + } + permissionListener!!.onPermissionSelect(granted) + } + } + permissionLauncher = + activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions(), permissionCallback) + activityLauncher = activity.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (activityListener != null) { + activityListener!!.onActivityResult(result) + } + } + } + + /** + * Render web content in `view`. + * + * Both this method and [.onHideCustomView] are required for + * rendering web content in full screen. + * + * @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView + ) */ + override fun onShowCustomView(view: View, callback: CustomViewCallback) { + callback.onCustomViewHidden() + super.onShowCustomView(view, callback) + } + + /** + * Render web content in the original Web View again. + * + * Do not remove this method--@see #onShowCustomView(View, CustomViewCallback). + */ + override fun onHideCustomView() { + super.onHideCustomView() + } + + override fun onPermissionRequest(request: PermissionRequest) { + val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + val permissionList: MutableList = ArrayList() + if (listOf(*request.resources).contains("android.webkit.resource.VIDEO_CAPTURE")) { + permissionList.add(Manifest.permission.CAMERA) + } + if (listOf(*request.resources).contains("android.webkit.resource.AUDIO_CAPTURE")) { + permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) + permissionList.add(Manifest.permission.RECORD_AUDIO) + } + if (permissionList.isNotEmpty() && isRequestPermissionRequired) { + val permissions = permissionList.toTypedArray() + permissionListener = object : PermissionListener { + override fun onPermissionSelect(isGranted: Boolean?) { + if (isGranted == true) { + request.grant(request.resources) + } else { + request.deny() + } + } + } + permissionLauncher.launch(permissions) + } else { + request.grant(request.resources) + } + } + + /** + * Show the browser alert modal + * @param view + * @param url + * @param message + * @param result + * @return + */ + override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean { + if (activity.isFinishing) { + return true + } + val builder = AlertDialog.Builder(view.context) + builder + .setMessage(message) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + result.confirm() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + val dialog = builder.create() + dialog.show() + return true + } + + /** + * Show the browser confirm modal + * @param view + * @param url + * @param message + * @param result + * @return + */ + override fun onJsConfirm(view: WebView, url: String, message: String, result: JsResult): Boolean { + if (activity.isFinishing) { + return true + } + val builder = AlertDialog.Builder(view.context) + builder + .setMessage(message) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + result.confirm() + } + .setNegativeButton( + "Cancel" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + result.cancel() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + val dialog = builder.create() + dialog.show() + return true + } + + /** + * Show the browser prompt modal + * @param view + * @param url + * @param message + * @param defaultValue + * @param result + * @return + */ + override fun onJsPrompt( + view: WebView, + url: String, + message: String, + defaultValue: String, + result: JsPromptResult + ): Boolean { + if (activity.isFinishing) { + return true + } + val builder = AlertDialog.Builder(view.context) + val input = EditText(view.context) + builder + .setMessage(message) + .setView(input) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + val inputText1 = input.text.toString().trim { it <= ' ' } + result.confirm(inputText1) + } + .setNegativeButton( + "Cancel" + ) { dialog: DialogInterface, _: Int -> + dialog.dismiss() + result.cancel() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + val dialog = builder.create() + dialog.show() + return true + } + + /** + * Handle the browser geolocation permission prompt + * @param origin + * @param callback + */ + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) { + super.onGeolocationPermissionsShowPrompt(origin, callback) + Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin") + val geoPermissions = + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) + if (!PermissionHelper.hasPermissions(activity, geoPermissions)) { + permissionListener = object : PermissionListener { + override fun onPermissionSelect(isGranted: Boolean?) { + if (isGranted == true) { + callback.invoke(origin, true, false) + } else { + val coarsePermission = + arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + PermissionHelper.hasPermissions(activity, coarsePermission) + ) { + callback.invoke(origin, true, false) + } else { + callback.invoke(origin, false, false) + } + } + } + } + permissionLauncher.launch(geoPermissions) + } else { + // permission is already granted + callback.invoke(origin, true, false) + Logger.debug("onGeolocationPermissionsShowPrompt: has required permission") + } + } + + override fun onShowFileChooser( + webView: WebView, + filePathCallback: ValueCallback?>, + fileChooserParams: FileChooserParams + ): Boolean { + val acceptTypes = listOf(*fileChooserParams.acceptTypes) + val captureEnabled = fileChooserParams.isCaptureEnabled + val capturePhoto = captureEnabled && acceptTypes.contains("image/*") + val captureVideo = captureEnabled && acceptTypes.contains("video/*") + if (capturePhoto || captureVideo) { + if (isMediaCaptureSupported) { + showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) + } else { + permissionListener = object : PermissionListener { + override fun onPermissionSelect(isGranted: Boolean?) { + if (isGranted == true) { + showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) + } else { + Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted") + filePathCallback.onReceiveValue(null) + } + } + } + val camPermission = arrayOf(Manifest.permission.CAMERA) + permissionLauncher.launch(camPermission) + } + } else { + showFilePicker(filePathCallback, fileChooserParams) + } + return true + } + + private val isMediaCaptureSupported: Boolean + get() { + val permissions = arrayOf(Manifest.permission.CAMERA) + return PermissionHelper.hasPermissions(activity, permissions) || + !PermissionHelper.hasDefinedPermission(activity, Manifest.permission.CAMERA) + } + + private fun showMediaCaptureOrFilePicker( + filePathCallback: ValueCallback?>, + fileChooserParams: FileChooserParams, + isVideo: Boolean + ) { + val isVideoCaptureSupported = true + val shown = if (isVideo && isVideoCaptureSupported) { + showVideoCapturePicker(filePathCallback) + } else { + showImageCapturePicker(filePathCallback) + } + if (!shown) { + Logger.warn( + Logger.tags("FileChooser"), + "Media capture intent could not be launched. Falling back to default file picker." + ) + showFilePicker(filePathCallback, fileChooserParams) + } + } + + private fun showImageCapturePicker(filePathCallback: ValueCallback?>): Boolean { + val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + if (takePictureIntent.resolveActivity(activity.packageManager) == null) { + return false + } + val imageFileUri: Uri = try { + createImageFileUri() + } catch (ex: Exception) { + Logger.error("Unable to create temporary media capture file: " + ex.message) + return false + } + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri) + activityListener = object : ActivityResultListener { + override fun onActivityResult(result: ActivityResult?) { + var res: Array? = null + if (result?.resultCode == Activity.RESULT_OK) { + res = arrayOf(imageFileUri) + } + filePathCallback.onReceiveValue(res) + } + } + activityLauncher.launch(takePictureIntent) + return true + } + + private fun showVideoCapturePicker(filePathCallback: ValueCallback?>): Boolean { + val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) + if (takeVideoIntent.resolveActivity(activity.packageManager) == null) { + return false + } + activityListener = object : ActivityResultListener { + override fun onActivityResult(result: ActivityResult?) { + var res: Array? = null + if (result?.resultCode == Activity.RESULT_OK) { + res = arrayOf(result.data!!.data) + } + filePathCallback.onReceiveValue(res) + } + } + activityLauncher.launch(takeVideoIntent) + return true + } + + private fun showFilePicker( + filePathCallback: ValueCallback?>, + fileChooserParams: FileChooserParams + ) { + val intent = fileChooserParams.createIntent() + if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) { + val validTypes = getValidTypes(fileChooserParams.acceptTypes) + intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes) + if (intent.type!!.startsWith(".")) { + intent.type = validTypes[0] + } + } + try { + activityListener = object : ActivityResultListener { + override fun onActivityResult(result: ActivityResult?) { + val res: Array? + val resultIntent = result?.data + if (result?.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) { + val numFiles = resultIntent.clipData!!.itemCount + res = arrayOfNulls(numFiles) + for (i in 0 until numFiles) { + res[i] = resultIntent.clipData!!.getItemAt(i).uri + } + } else { + res = FileChooserParams.parseResult( + result?.resultCode ?: 0, + resultIntent + ) + } + filePathCallback.onReceiveValue(res) + } + } + activityLauncher.launch(intent) + } catch (e: ActivityNotFoundException) { + filePathCallback.onReceiveValue(null) + } + } + + private fun getValidTypes(currentTypes: Array): Array { + val validTypes: MutableList = ArrayList() + val mtm = MimeTypeMap.getSingleton() + for (mime in currentTypes) { + if (mime.startsWith(".")) { + val extension = mime.substring(1) + val extensionMime = mtm.getMimeTypeFromExtension(extension) + if (extensionMime != null && !validTypes.contains(extensionMime)) { + validTypes.add(extensionMime) + } + } else if (!validTypes.contains(mime)) { + validTypes.add(mime) + } + } + val validObj: Array = validTypes.toTypedArray() + return Arrays.copyOf( + validObj, validObj.size, + Array::class.java + ) + } + + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + val tag: String = Logger.tags("Console") + if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) { + val msg = String.format( + "File: %s - Line %d - Msg: %s", + consoleMessage.sourceId(), + consoleMessage.lineNumber(), + consoleMessage.message() + ) + val level = consoleMessage.messageLevel().name + if ("ERROR".equals(level, ignoreCase = true)) { + Logger.error(tag, msg, null) + } else if ("WARNING".equals(level, ignoreCase = true)) { + Logger.warn(tag, msg) + } else if ("TIP".equals(level, ignoreCase = true)) { + Logger.debug(tag, msg) + } else { + Logger.info(tag, msg) + } + } + return true + } + + private fun isValidMsg(msg: String): Boolean { + return !(msg.contains("%cresult %c") || + msg.contains("%cnative %c") || + msg.equals("[object Object]", ignoreCase = true) || + msg.equals("console.groupEnd", ignoreCase = true)) + } + + @Throws(IOException::class) + private fun createImageFileUri(): Uri { + val photoFile = createImageFile(activity) + return FileProvider.getUriForFile( + activity, + activity.packageName.toString() + ".fileprovider", + photoFile + ) + } + + @Throws(IOException::class) + private fun createImageFile(activity: Activity): File { + // Create an image file name + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + val imageFileName = "JPEG_" + timeStamp + "_" + val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + return File.createTempFile(imageFileName, ".jpg", storageDir) + } + + override fun onReceivedTitle( + view: WebView, + title: String + ) { + handleReceivedTitle(view, title) + } + + private external fun handleReceivedTitle(webview: WebView, title: String) +} diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebView.kt b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebView.kt new file mode 100644 index 00000000..d51e077a --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebView.kt @@ -0,0 +1,101 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +@file:Suppress("unused", "SetJavaScriptEnabled") + +package com.eid_wallet.app + +import android.annotation.SuppressLint +import android.webkit.* +import android.content.Context +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewFeature +import kotlin.collections.Map + +@SuppressLint("RestrictedApi") +class RustWebView(context: Context, val initScripts: Array, val id: String): WebView(context) { + val isDocumentStartScriptEnabled: Boolean + + init { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.setGeolocationEnabled(true) + settings.databaseEnabled = true + settings.mediaPlaybackRequiresUserGesture = false + settings.javaScriptCanOpenWindowsAutomatically = true + + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + isDocumentStartScriptEnabled = true + for (script in initScripts) { + WebViewCompat.addDocumentStartJavaScript(this, script, setOf("*")); + } + } else { + isDocumentStartScriptEnabled = false + } + + + } + + fun loadUrlMainThread(url: String) { + post { + loadUrl(url) + } + } + + fun loadUrlMainThread(url: String, additionalHttpHeaders: Map) { + post { + loadUrl(url, additionalHttpHeaders) + } + } + + override fun loadUrl(url: String) { + if (!shouldOverride(url)) { + super.loadUrl(url); + } + } + + override fun loadUrl(url: String, additionalHttpHeaders: Map) { + if (!shouldOverride(url)) { + super.loadUrl(url, additionalHttpHeaders); + } + } + + fun loadHTMLMainThread(html: String) { + post { + super.loadData(html, "text/html", null) + } + } + + fun evalScript(id: Int, script: String) { + post { + super.evaluateJavascript(script) { result -> + onEval(id, result) + } + } + } + + fun clearAllBrowsingData() { + try { + super.getContext().deleteDatabase("webviewCache.db") + super.getContext().deleteDatabase("webview.db") + super.clearCache(true) + super.clearHistory() + super.clearFormData() + } catch (ex: Exception) { + Logger.error("Unable to create temporary media capture file: " + ex.message) + } + } + + fun getCookies(url: String): String { + val cookieManager = CookieManager.getInstance() + return cookieManager.getCookie(url) + } + + private external fun shouldOverride(url: String): Boolean + private external fun onEval(id: Int, result: String) + + +} diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebViewClient.kt b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebViewClient.kt new file mode 100644 index 00000000..5d262951 --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/RustWebViewClient.kt @@ -0,0 +1,107 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.eid_wallet.app + +import android.net.Uri +import android.webkit.* +import android.content.Context +import android.graphics.Bitmap +import android.os.Handler +import android.os.Looper +import androidx.webkit.WebViewAssetLoader + +class RustWebViewClient(context: Context): WebViewClient() { + private val interceptedState = mutableMapOf() + var currentUrl: String = "about:blank" + private var lastInterceptedUrl: Uri? = null + private var pendingUrlRedirect: String? = null + + private val assetLoader = WebViewAssetLoader.Builder() + .setDomain(assetLoaderDomain()) + .addPathHandler("/", WebViewAssetLoader.AssetsPathHandler(context)) + .build() + + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest + ): WebResourceResponse? { + pendingUrlRedirect?.let { + Handler(Looper.getMainLooper()).post { + view.loadUrl(it) + } + pendingUrlRedirect = null + return null + } + + lastInterceptedUrl = request.url + return if (withAssetLoader()) { + assetLoader.shouldInterceptRequest(request.url) + } else { + val rustWebview = view as RustWebView; + val response = handleRequest(rustWebview.id, request, rustWebview.isDocumentStartScriptEnabled) + interceptedState[request.url.toString()] = response != null + return response + } + } + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + return shouldOverride(request.url.toString()) + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + currentUrl = url + if (interceptedState[url] == false) { + val webView = view as RustWebView + for (script in webView.initScripts) { + view.evaluateJavascript(script, null) + } + } + return onPageLoading(url) + } + + override fun onPageFinished(view: WebView, url: String) { + onPageLoaded(url) + } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest, + error: WebResourceError + ) { + // we get a net::ERR_CONNECTION_REFUSED when an external URL redirects to a custom protocol + // e.g. oauth flow, because shouldInterceptRequest is not called on redirects + // so we must force retry here with loadUrl() to get a chance of the custom protocol to kick in + if (error.errorCode == ERROR_CONNECT && request.isForMainFrame && request.url != lastInterceptedUrl) { + // prevent the default error page from showing + view.stopLoading() + // without this initial loadUrl the app is stuck + view.loadUrl(request.url.toString()) + // ensure the URL is actually loaded - for some reason there's a race condition and we need to call loadUrl() again later + pendingUrlRedirect = request.url.toString() + } else { + super.onReceivedError(view, request, error) + } + } + + companion object { + init { + System.loadLibrary("eid_wallet_lib") + } + } + + private external fun assetLoaderDomain(): String + private external fun withAssetLoader(): Boolean + private external fun handleRequest(webviewId: String, request: WebResourceRequest, isDocumentStartScriptEnabled: Boolean): WebResourceResponse? + private external fun shouldOverride(url: String): Boolean + private external fun onPageLoading(url: String) + private external fun onPageLoaded(url: String) + + +} diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/TauriActivity.kt b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/TauriActivity.kt new file mode 100644 index 00000000..e067eb0c --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/TauriActivity.kt @@ -0,0 +1,30 @@ +// Copyright 2019-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +package com.eid_wallet.app + +import android.os.Bundle +import android.content.Intent +import app.tauri.plugin.PluginManager + +abstract class TauriActivity : WryActivity() { + var pluginManager: PluginManager = PluginManager(this) + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + pluginManager.onNewIntent(intent) + } + + override fun onResume() { + super.onResume() + pluginManager.onResume() + } + + override fun onPause() { + super.onPause() + pluginManager.onPause() + } +} diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/WryActivity.kt b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/WryActivity.kt new file mode 100644 index 00000000..027bd951 --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/WryActivity.kt @@ -0,0 +1,136 @@ +/* THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! */ + +// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +package com.eid_wallet.app + +import com.eid_wallet.app.RustWebView +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +import android.webkit.WebView +import android.view.KeyEvent +import androidx.appcompat.app.AppCompatActivity + +abstract class WryActivity : AppCompatActivity() { + private lateinit var mWebView: RustWebView + + open fun onWebViewCreate(webView: WebView) { } + + fun setWebView(webView: RustWebView) { + mWebView = webView + onWebViewCreate(webView) + } + + val version: String + @SuppressLint("WebViewApiAvailability", "ObsoleteSdkInt") + get() { + // Check getCurrentWebViewPackage() directly if above Android 8 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return WebView.getCurrentWebViewPackage()?.versionName ?: "" + } + + // Otherwise manually check WebView versions + var webViewPackage = "com.google.android.webview" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + webViewPackage = "com.android.chrome" + } + try { + @Suppress("DEPRECATION") + val info = packageManager.getPackageInfo(webViewPackage, 0) + return info.versionName.toString() + } catch (ex: Exception) { + Logger.warn("Unable to get package info for '$webViewPackage'$ex") + } + + try { + @Suppress("DEPRECATION") + val info = packageManager.getPackageInfo("com.android.webview", 0) + return info.versionName.toString() + } catch (ex: Exception) { + Logger.warn("Unable to get package info for 'com.android.webview'$ex") + } + + // Could not detect any webview, return empty string + return "" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + create(this) + } + + override fun onStart() { + super.onStart() + start() + } + + override fun onResume() { + super.onResume() + resume() + } + + override fun onPause() { + super.onPause() + pause() + } + + override fun onStop() { + super.onStop() + stop() + } + + override fun onWindowFocusChanged(hasFocus: Boolean) { + super.onWindowFocusChanged(hasFocus) + focus(hasFocus) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + save() + } + + override fun onDestroy() { + super.onDestroy() + destroy() + onActivityDestroy() + } + + override fun onLowMemory() { + super.onLowMemory() + memory() + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) { + mWebView.goBack() + return true + } + return super.onKeyDown(keyCode, event) + } + + fun getAppClass(name: String): Class<*> { + return Class.forName(name) + } + + companion object { + init { + System.loadLibrary("eid_wallet_lib") + } + } + + private external fun create(activity: WryActivity) + private external fun start() + private external fun resume() + private external fun pause() + private external fun stop() + private external fun save() + private external fun destroy() + private external fun onActivityDestroy() + private external fun memory() + private external fun focus(focus: Boolean) + + +} diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/proguard-wry.pro b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/proguard-wry.pro new file mode 100644 index 00000000..3c1c28b7 --- /dev/null +++ b/infrastructure/eid-wallet/src-tauri/gen/android/app/src/main/java/com/eid_wallet/app/generated/proguard-wry.pro @@ -0,0 +1,35 @@ +# THIS FILE IS AUTO-GENERATED. DO NOT MODIFY!! + +# Copyright 2020-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +-keep class com.eid_wallet.app.* { + native ; +} + +-keep class com.eid_wallet.app.WryActivity { + public (...); + + void setWebView(com.eid_wallet.app.RustWebView); + java.lang.Class getAppClass(...); + java.lang.String getVersion(); +} + +-keep class com.eid_wallet.app.Ipc { + public (...); + + @android.webkit.JavascriptInterface public ; +} + +-keep class com.eid_wallet.app.RustWebView { + public (...); + + void loadUrlMainThread(...); + void loadHTMLMainThread(...); + void evalScript(...); +} + +-keep class com.eid_wallet.app.RustWebChromeClient,com.eid_wallet.app.RustWebViewClient { + public (...); +} diff --git a/platforms/pictique/src/lib/fragments/BottomNav/BottomNav.svelte b/platforms/pictique/src/lib/fragments/BottomNav/BottomNav.svelte index 4552b572..4710c139 100644 --- a/platforms/pictique/src/lib/fragments/BottomNav/BottomNav.svelte +++ b/platforms/pictique/src/lib/fragments/BottomNav/BottomNav.svelte @@ -2,7 +2,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/state'; import { Camera, CommentsTwo, Home, Search } from '$lib/icons'; - import { isNavigatingThroughNav, ownerId } from '$lib/store/store.svelte'; + import { isNavigatingThroughNav } from '$lib/store/store.svelte'; import { uploadedImages } from '$lib/store/store.svelte'; import { revokeImageUrls } from '$lib/utils'; import type { HTMLAttributes } from 'svelte/elements'; diff --git a/platforms/pictique/src/lib/fragments/Post/Post.svelte b/platforms/pictique/src/lib/fragments/Post/Post.svelte index 36544ce6..c924f81d 100644 --- a/platforms/pictique/src/lib/fragments/Post/Post.svelte +++ b/platforms/pictique/src/lib/fragments/Post/Post.svelte @@ -12,6 +12,7 @@ } from '@hugeicons/core-free-icons'; import { HugeiconsIcon } from '@hugeicons/svelte'; import type { HTMLAttributes } from 'svelte/elements'; + import ActionMenu from '../ActionMenu/ActionMenu.svelte'; interface IPostProps extends HTMLAttributes { avatar: string; @@ -29,6 +30,7 @@ comment: () => void; }; time: string; + options?: Array<{ name: string; handler: () => void }>; } function pairAndJoinChunks(chunks: string[]): string[] { @@ -65,10 +67,11 @@ count, callback, time, + options, ...restProps }: IPostProps = $props(); - let imgUris = $derived(uris); + let imgUris = $derived(pairAndJoinChunks(uris)); let galleryRef: HTMLDivElement | undefined = $state(); let currentIndex = $state(0); @@ -109,12 +112,7 @@ >

{username}

- + {/if} {#if imgUris.length > 0} diff --git a/platforms/pictique/src/lib/fragments/Profile/Profile.svelte b/platforms/pictique/src/lib/fragments/Profile/Profile.svelte index 5ea63c1c..11e1bce6 100644 --- a/platforms/pictique/src/lib/fragments/Profile/Profile.svelte +++ b/platforms/pictique/src/lib/fragments/Profile/Profile.svelte @@ -26,8 +26,8 @@ class="h-20 w-20 rounded-full object-cover" />
-

{profileData.username}

-

{profileData.userBio}

+

{profileData?.handle}

+

{profileData?.description}

{#if variant === 'other'}
diff --git a/platforms/pictique/src/lib/fragments/SettingsNavigationButton/SettingsNavigationButton.svelte b/platforms/pictique/src/lib/fragments/SettingsNavigationButton/SettingsNavigationButton.svelte index fda243ca..05617d8b 100644 --- a/platforms/pictique/src/lib/fragments/SettingsNavigationButton/SettingsNavigationButton.svelte +++ b/platforms/pictique/src/lib/fragments/SettingsNavigationButton/SettingsNavigationButton.svelte @@ -38,7 +38,7 @@ diff --git a/platforms/pictique/src/lib/fragments/SideBar/SideBar.svelte b/platforms/pictique/src/lib/fragments/SideBar/SideBar.svelte index af7f0d6d..fe52a611 100644 --- a/platforms/pictique/src/lib/fragments/SideBar/SideBar.svelte +++ b/platforms/pictique/src/lib/fragments/SideBar/SideBar.svelte @@ -159,7 +159,7 @@ profile diff --git a/platforms/pictique/src/lib/stores/auth.ts b/platforms/pictique/src/lib/stores/auth.ts index 9b68757b..18ac776f 100644 --- a/platforms/pictique/src/lib/stores/auth.ts +++ b/platforms/pictique/src/lib/stores/auth.ts @@ -1,5 +1,5 @@ import { writable } from 'svelte/store'; -import { apiClient, setAuthToken, removeAuthToken } from '$lib/utils/axios'; +import { apiClient, setAuthToken, removeAuthToken, removeAuthId } from '$lib/utils/axios'; export const isAuthenticated = writable(false); @@ -19,6 +19,7 @@ export const login = (token: string) => { export const logout = () => { removeAuthToken(); + removeAuthId(); delete apiClient.defaults.headers.common['Authorization']; isAuthenticated.set(false); }; diff --git a/platforms/pictique/src/routes/(protected)/+layout.svelte b/platforms/pictique/src/routes/(protected)/+layout.svelte index 357a3eb0..39165c1f 100644 --- a/platforms/pictique/src/routes/(protected)/+layout.svelte +++ b/platforms/pictique/src/routes/(protected)/+layout.svelte @@ -163,7 +163,7 @@ alert('menu') }} + options = {[{name: "Report",handler: () => alert("asd")}]} /> {/each} diff --git a/platforms/pictique/src/routes/(protected)/settings/logout/+page.svelte b/platforms/pictique/src/routes/(protected)/settings/logout/+page.svelte index 59f7e83d..d19b7753 100644 --- a/platforms/pictique/src/routes/(protected)/settings/logout/+page.svelte +++ b/platforms/pictique/src/routes/(protected)/settings/logout/+page.svelte @@ -2,10 +2,11 @@ import { goto } from '$app/navigation'; import { Button } from '$lib/ui'; import { SettingsNavigationButton } from '$lib/fragments'; - import { removeAuthToken } from '$lib/utils'; + import { removeAuthId, removeAuthToken } from '$lib/utils'; async function handleLogout() { removeAuthToken(); + removeAuthId(); window.location.href = '/auth'; } @@ -24,4 +25,4 @@
-
+ \ No newline at end of file