diff --git a/.gitignore b/.gitignore index 5f84ec3cf..a8e20f69b 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ nkn.aar **.p12 **.cer /android/app/.cxx/ +/.env + +/lib/upgrade/appcast.json diff --git a/android/.gitignore b/android/.gitignore index bcca131af..1bfdca243 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -8,3 +8,4 @@ GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +/build/ diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 7c033d263..26dcd03ee 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "org.nkn.mobile.app" - compileSdk = 35 + compileSdk = 36 ndkVersion = "27.0.12077973" compileOptions { @@ -27,7 +27,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 24 - targetSdk = flutter.targetSdkVersion + targetSdk = 36 versionCode = flutter.versionCode versionName = flutter.versionName } @@ -72,7 +72,9 @@ dependencies { implementation("androidx.window:window:1.3.0") implementation("androidx.window:window-java:1.3.0") // google - implementation("com.google.firebase:firebase-messaging:24.1.1") + implementation(platform("com.google.firebase:firebase-bom:34.1.0")) + implementation("com.google.firebase:firebase-analytics") + implementation("com.google.firebase:firebase-messaging") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:mockwebserver:4.12.0") implementation("com.squareup.okhttp3:okhttp-tls:4.10.0") diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 000000000..d0e0fbc9b --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1 @@ +-keep class net.sqlcipher.** { *; } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6fb3216ec..b47803a51 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -101,33 +101,27 @@ - - - - - - - - - + + + + + + @@ -138,16 +132,18 @@ - - - - - - - - - - + + + + + + + + + + + + @@ -177,10 +173,6 @@ android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@mipmap/ic_launcher_round" /> - - - - diff --git a/android/app/src/main/kotlin/org/nkn/mobile/app/MainActivity.kt b/android/app/src/main/kotlin/org/nkn/mobile/app/MainActivity.kt index d97704ce2..5b152f2b4 100644 --- a/android/app/src/main/kotlin/org/nkn/mobile/app/MainActivity.kt +++ b/android/app/src/main/kotlin/org/nkn/mobile/app/MainActivity.kt @@ -10,6 +10,7 @@ import io.flutter.plugins.GeneratedPluginRegistrant import org.nkn.mobile.app.channels.impl.Common import org.nkn.mobile.app.channels.impl.nameService.DnsResolver import org.nkn.mobile.app.channels.impl.nameService.EthResolver +import org.nkn.mobile.app.channels.impl.searchService.SearchService import org.nkn.mobile.app.crypto.Crypto import org.nkn.mobile.app.push.APNSPush @@ -74,6 +75,7 @@ class MainActivity : FlutterFragmentActivity() { Crypto.register(flutterEngine) EthResolver.register(flutterEngine) DnsResolver.register(flutterEngine) + SearchService.register(flutterEngine) APNSPush.openClient(assets) } diff --git a/android/app/src/main/kotlin/org/nkn/mobile/app/channels/impl/searchService/Search.kt b/android/app/src/main/kotlin/org/nkn/mobile/app/channels/impl/searchService/Search.kt new file mode 100644 index 000000000..b162b2e4b --- /dev/null +++ b/android/app/src/main/kotlin/org/nkn/mobile/app/channels/impl/searchService/Search.kt @@ -0,0 +1,379 @@ +package org.nkn.mobile.app.channels.impl.searchService + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.nkn.mobile.app.channels.IChannelHandler +import search.Search +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +class SearchService : IChannelHandler, MethodChannel.MethodCallHandler, EventChannel.StreamHandler, ViewModel() { + + companion object { + lateinit var methodChannel: MethodChannel + const val METHOD_CHANNEL_NAME = "org.nkn.mobile/native/search" + + lateinit var eventChannel: EventChannel + const val EVENT_CHANNEL_NAME = "org.nkn.mobile/native/search_event" + private var eventSink: EventChannel.EventSink? = null + + // Store search client instances by ID + private val clients = ConcurrentHashMap() + + fun register(flutterEngine: FlutterEngine) { + SearchService().install(flutterEngine.dartExecutor.binaryMessenger) + } + } + + override fun install(binaryMessenger: BinaryMessenger) { + methodChannel = MethodChannel(binaryMessenger, METHOD_CHANNEL_NAME) + methodChannel.setMethodCallHandler(this) + eventChannel = EventChannel(binaryMessenger, EVENT_CHANNEL_NAME) + eventChannel.setStreamHandler(this) + } + + override fun uninstall() { + methodChannel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) + } + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "newSearchClient" -> newSearchClient(call, result) + "newSearchClientWithAuth" -> newSearchClientWithAuth(call, result) + "query" -> query(call, result) + "submitUserData" -> submitUserData(call, result) + "verify" -> verify(call, result) + "queryByID" -> queryByID(call, result) + "getMyInfo" -> getMyInfo(call, result) + "getPublicKeyHex" -> getPublicKeyHex(call, result) + "getAddress" -> getAddress(call, result) + "isVerified" -> isVerified(call, result) + "disposeClient" -> disposeClient(call, result) + else -> result.notImplemented() + } + } + + // Create a query-only search client + private fun newSearchClient(call: MethodCall, result: MethodChannel.Result) { + val apiBase = call.argument("apiBase") ?: "" + + viewModelScope.launch(Dispatchers.IO) { + try { + val client = Search.newSearchClient(apiBase) + if (client == null) { + resultError(result, "CREATE_CLIENT_FAILED", "Failed to create search client") + return@launch + } + + // Generate unique ID for this client + val clientId = UUID.randomUUID().toString() + clients[clientId] = client + + val response = hashMapOf( + "clientId" to clientId + ) + + resultSuccess(result, response) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Create an authenticated search client + private fun newSearchClientWithAuth(call: MethodCall, result: MethodChannel.Result) { + val apiBase = call.argument("apiBase") ?: "" + val seed = call.argument("seed") + + if (seed == null || seed.size != 32) { + viewModelScope.launch(Dispatchers.IO) { + resultError(result, "INVALID_SEED", "Seed must be exactly 32 bytes") + } + return + } + + viewModelScope.launch(Dispatchers.IO) { + try { + val client = Search.newSearchClientWithAuth(apiBase, seed) + if (client == null) { + resultError(result, "CREATE_AUTH_CLIENT_FAILED", "Failed to create authenticated search client") + return@launch + } + + // Generate unique ID for this client + val clientId = UUID.randomUUID().toString() + clients[clientId] = client + + val response = hashMapOf( + "clientId" to clientId + ) + + resultSuccess(result, response) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Query data by keyword + private fun query(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + val keyword = call.argument("keyword") ?: "" + + val client = clients[clientId] + if (client == null) { + viewModelScope.launch(Dispatchers.IO) { + resultError(result, "CLIENT_NOT_FOUND", "Search client not found") + } + return + } + + viewModelScope.launch(Dispatchers.IO) { + try { + val response = client.query(keyword) + resultSuccess(result, response) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Submit user data + private fun submitUserData(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + var nknAddress = call.argument("nknAddress") ?: "" + val customId = call.argument("customId") ?: "" + val nickname = call.argument("nickname") ?: "" + val phoneNumber = call.argument("phoneNumber") ?: "" + + val client = clients[clientId] + if (client == null) { + viewModelScope.launch(Dispatchers.Default) { + resultError(result, "CLIENT_NOT_FOUND", "Search client not found") + } + return + } + + // Use Dispatchers.Default for CPU-intensive PoW calculation + // Default dispatcher is optimized for CPU-bound work + viewModelScope.launch(Dispatchers.Default) { + try { + // Process nknAddress: if empty, use publicKey + val publicKeyHex = client.publicKeyHex ?: "" + + if (nknAddress.isEmpty()) { + nknAddress = publicKeyHex + } else { + // Validate format if contains dot + if (nknAddress.contains(".")) { + val parts = nknAddress.split(".") + if (parts.size != 2) { + resultError(result, "INVALID_PARAMETER", + "Invalid nknAddress format. Expected: identifier.publickey") + return@launch + } + val providedPubKey = parts[1] + if (providedPubKey.lowercase() != publicKeyHex.lowercase()) { + resultError(result, "INVALID_PARAMETER", + "nknAddress publickey suffix must match your actual publicKey") + return@launch + } + } else { + // If no dot, must equal publicKey + if (nknAddress.lowercase() != publicKeyHex.lowercase()) { + resultError(result, "INVALID_PARAMETER", + "nknAddress must be either \"identifier.publickey\" format or equal to publicKey") + return@launch + } + } + } + + // Validate customId if provided + if (customId.isNotEmpty() && customId.length < 3) { + resultError(result, "INVALID_PARAMETER", + "customId must be at least 3 characters if provided") + return@launch + } + + client.submitUserData(nknAddress, customId, nickname, phoneNumber) + val response = hashMapOf( + "success" to true + ) + resultSuccess(result, response) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Verify the client (optional, for query operations) + private fun verify(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + + val client = clients[clientId] + if (client == null) { + viewModelScope.launch(Dispatchers.Default) { + resultError(result, "CLIENT_NOT_FOUND", "Search client not found") + } + return + } + + // Use Dispatchers.Default for CPU-intensive PoW calculation + viewModelScope.launch(Dispatchers.Default) { + try { + client.verify() + val response = hashMapOf( + "success" to true + ) + resultSuccess(result, response) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Query by ID + private fun queryByID(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + val id = call.argument("id") ?: "" + + val client = clients[clientId] + if (client == null) { + viewModelScope.launch(Dispatchers.IO) { + resultError(result, "CLIENT_NOT_FOUND", "Search client not found") + } + return + } + + viewModelScope.launch(Dispatchers.IO) { + try { + val response = client.queryByID(id) + resultSuccess(result, response) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Get my info by nknAddress + private fun getMyInfo(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + val address = call.argument("address") ?: "" + + val client = clients[clientId] + if (client == null) { + viewModelScope.launch(Dispatchers.IO) { + resultError(result, "CLIENT_NOT_FOUND", "Search client not found") + } + return + } + + viewModelScope.launch(Dispatchers.IO) { + try { + val response = client.getMyInfo(address) + resultSuccess(result, response) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Get public key hex + private fun getPublicKeyHex(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + + val client = clients[clientId] + if (client == null) { + viewModelScope.launch(Dispatchers.IO) { + resultError(result, "CLIENT_NOT_FOUND", "Search client not found") + } + return + } + + viewModelScope.launch(Dispatchers.IO) { + try { + val publicKeyHex = client.publicKeyHex + resultSuccess(result, publicKeyHex) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Get wallet address + private fun getAddress(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + + val client = clients[clientId] + if (client == null) { + viewModelScope.launch(Dispatchers.IO) { + resultError(result, "CLIENT_NOT_FOUND", "Search client not found") + } + return + } + + viewModelScope.launch(Dispatchers.IO) { + try { + val address = client.address + resultSuccess(result, address) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Check if verified + private fun isVerified(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + + val client = clients[clientId] + if (client == null) { + viewModelScope.launch(Dispatchers.IO) { + resultError(result, "CLIENT_NOT_FOUND", "Search client not found") + } + return + } + + viewModelScope.launch(Dispatchers.IO) { + try { + val verified = client.isVerified + resultSuccess(result, verified) + } catch (e: Exception) { + resultError(result, e) + } + } + } + + // Dispose client + private fun disposeClient(call: MethodCall, result: MethodChannel.Result) { + val clientId = call.argument("clientId") ?: "" + + viewModelScope.launch(Dispatchers.IO) { + try { + clients.remove(clientId) + val response = hashMapOf( + "success" to true + ) + resultSuccess(result, response) + } catch (e: Exception) { + resultError(result, e) + } + } + } +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts index aeb91d75b..7f8edd7b9 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -4,8 +4,8 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.1.4") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") + classpath("com.android.tools.build:gradle:8.12.3") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21") } } @@ -39,7 +39,7 @@ subprojects { } val javaVersion = JavaVersion.VERSION_21 - val androidApiVersion = 35 + val androidApiVersion = 36 android.compileSdkVersion(androidApiVersion) android.defaultConfig.targetSdk = androidApiVersion @@ -48,8 +48,10 @@ subprojects { android.compileOptions.targetCompatibility = javaVersion tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java).configureEach { - kotlinOptions { - jvmTarget = javaVersion.toString() + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) + languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_2) } } diff --git a/android/gradle.properties b/android/gradle.properties index e8e664173..24863d218 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true -android.enableR8=true \ No newline at end of file +android.enableJetifier=true \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index efdcc4ace..02767eb1c 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index b51a5bde6..b450fd7f9 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -18,8 +18,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.9.1" apply false - id("org.jetbrains.kotlin.android") version "2.0.0" apply false + id("com.android.application") version "8.12.3" apply false + id("org.jetbrains.kotlin.android") version "2.2.21" apply false } include(":app") diff --git a/assets/icons/smile.svg b/assets/icons/smile.svg new file mode 100644 index 000000000..6eb9964d7 --- /dev/null +++ b/assets/icons/smile.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 000000000..fa0b357c4 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/golib/Makefile b/golib/Makefile index c19b1c22e..de1c09536 100644 --- a/golib/Makefile +++ b/golib/Makefile @@ -5,15 +5,15 @@ GOMOBILE=gomobile GOBIND=$(GOMOBILE) bind BUILDDIR=$(shell pwd)/build IMPORT_PATH=nkn -LDFLAGS='-s -w' -ANDROID_LDFLAGS='-s -w' +LDFLAGS='-s -w -extldflags=-lresolv -checklinkname=0' +ANDROID_LDFLAGS='-s -w -extldflags=-Wl,-z,max-page-size=16384 -checklinkname=0' ANDROID_BUILDDIR=$(BUILDDIR)/android ANDROID_ARTIFACT=$(ANDROID_BUILDDIR)/nkn.aar IOS_BUILDDIR=$(BUILDDIR)/ios IOS_ARTIFACT=$(IOS_BUILDDIR)/Nkn.xcframework -BUILD_PACKAGE=./ ./crypto github.com/nknorg/nkn-sdk-go github.com/nknorg/ncp-go github.com/nknorg/nkn/v2/transaction github.com/nknorg/nkngomobile github.com/nknorg/reedsolomon github.com/nknorg/eth-resolver-go github.com/nknorg/dns-resolver-go +BUILD_PACKAGE=./ ./crypto ./search github.com/nknorg/nkn-sdk-go github.com/nknorg/ncp-go github.com/nknorg/nkn/v2/transaction github.com/nknorg/nkngomobile github.com/nknorg/reedsolomon github.com/nknorg/eth-resolver-go github.com/nknorg/dns-resolver-go ANDROID_BUILD_CMD="$(GOBIND) -ldflags $(ANDROID_LDFLAGS) -target=android -androidapi=21 -o $(ANDROID_ARTIFACT) $(BUILD_PACKAGE)" IOS_BUILD_CMD="$(GOBIND) -ldflags $(LDFLAGS) -target=ios -o $(IOS_ARTIFACT) $(BUILD_PACKAGE)" diff --git a/golib/go.mod b/golib/go.mod index 6a58d253e..e8cf9e540 100644 --- a/golib/go.mod +++ b/golib/go.mod @@ -1,15 +1,17 @@ module nkngolib -go 1.18 +go 1.22.0 + +toolchain go1.22.2 require ( - github.com/nknorg/dns-resolver-go v0.0.0-20220705102626-b041cd8d4a8e + github.com/nknorg/dns-resolver-go v0.0.0-20230404043755-e50d32d9043a github.com/nknorg/eth-resolver-go v0.0.0-20230404061427-d0bd773f899f - github.com/nknorg/ncp-go v1.0.5 // indirect - github.com/nknorg/nkn-sdk-go v1.4.6 - github.com/nknorg/nkn/v2 v2.2.0 + github.com/nknorg/nkn-sdk-go v1.4.9-0.20250718092920-5d1593ad7642 + github.com/nknorg/nkn/v2 v2.2.2-0.20250718093239-1e65fafdf8f0 github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9 github.com/nknorg/reedsolomon v1.9.12-0.20210315025804-a0c1b6031ab4 + golang.org/x/mobile v0.0.0-20240905004112-7c4916698cc9 ) require ( @@ -18,16 +20,16 @@ require ( github.com/ethereum/go-ethereum v1.10.15 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-stack/stack v1.8.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/imdario/mergo v0.3.15 // indirect - github.com/ipfs/go-cid v0.4.1 // indirect + github.com/ipfs/go-cid v0.4.0 // indirect github.com/itchyny/base58-go v0.2.1 // indirect github.com/jbenet/go-is-domain v1.0.5 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect @@ -35,8 +37,26 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.1 // indirect github.com/multiformats/go-varint v0.0.7 // indirect + github.com/nknorg/ncp-go v1.0.7-0.20240928081416-1a805ec168d0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/dtls/v3 v3.0.2 // indirect + github.com/pion/ice/v4 v4.0.1 // indirect + github.com/pion/interceptor v0.1.30 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.14 // indirect + github.com/pion/rtp v1.8.9 // indirect + github.com/pion/sctp v1.8.33 // indirect + github.com/pion/sdp/v3 v3.0.9 // indirect + github.com/pion/srtp/v3 v3.0.3 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/turn/v4 v4.0.0 // indirect + github.com/pion/webrtc/v4 v4.0.0-beta.30 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect @@ -45,15 +65,17 @@ require ( github.com/tklauser/numcpus v0.6.0 // indirect github.com/wealdtech/go-ens/v3 v3.5.4 // indirect github.com/wealdtech/go-multicodec v1.4.0 // indirect + github.com/wlynxg/anet v0.0.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect - golang.org/x/tools v0.6.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + golang.org/x/time v0.6.0 // indirect + golang.org/x/tools v0.24.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect lukechampine.com/blake3 v1.1.7 // indirect ) diff --git a/golib/go.sum b/golib/go.sum index b084a2460..0d57ddf49 100644 --- a/golib/go.sum +++ b/golib/go.sum @@ -166,13 +166,10 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -188,24 +185,27 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/graphql-go v0.0.0-20201113091052-beb923fada29/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -222,12 +222,13 @@ github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZ github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= github.com/holiman/uint256 v1.2.2 h1:TXKcSGc2WaxPD2+bmzAsVthL4+pEN0YwXcL5qED83vk= +github.com/holiman/uint256 v1.2.2/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huin/goupnp v1.0.2 h1:RfGLP+h3mvisuWEyybxNq5Eft3NWhHLPeUN72kpKZoI= github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -243,8 +244,8 @@ github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bS github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= github.com/ipfs/go-cid v0.1.0/go.mod h1:rH5/Xv83Rfy8Rw6xG+id3DYAMUVmem1MowoKwdXmN2o= -github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= -github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-cid v0.4.0 h1:a4pdZq0sx6ZSxbCizebnKiMCx/xI/aBBFlB73IgH4rA= +github.com/ipfs/go-cid v0.4.0/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= github.com/itchyny/base58-go v0.2.1 h1:wtnhAVdOcW3WuHEASmGHMms4juOB8yEpj/KJxlB57+k= github.com/itchyny/base58-go v0.2.1/go.mod h1:BNvrKeAtWNSca1GohNbyhfff9/v0IrZjzWCAGeAvZZE= github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= @@ -255,6 +256,8 @@ github.com/jbenet/go-is-domain v1.0.5/go.mod h1:xbRLRb0S7FgzDBTJlguhDVwLYM/5yNtv github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= @@ -276,8 +279,8 @@ github.com/klauspost/cpuid/v2 v2.0.2/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.11/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -305,8 +308,9 @@ github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HN github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -347,17 +351,17 @@ github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOEL github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= -github.com/nknorg/dns-resolver-go v0.0.0-20220705102626-b041cd8d4a8e h1:GBCXdZ7X3+je1Kz8eq34Y61knTGDkLJqqZEOi87nO7w= -github.com/nknorg/dns-resolver-go v0.0.0-20220705102626-b041cd8d4a8e/go.mod h1:4kmAlcljv8giDwPUs2SoGNB+KKicismVnOrBy8SgEi8= +github.com/nknorg/dns-resolver-go v0.0.0-20230404043755-e50d32d9043a h1:aZW6WMDsTZ39bmVNgJq0z5IsQkF3EWrjW6OXmrl03cE= +github.com/nknorg/dns-resolver-go v0.0.0-20230404043755-e50d32d9043a/go.mod h1:4kmAlcljv8giDwPUs2SoGNB+KKicismVnOrBy8SgEi8= github.com/nknorg/eth-resolver-go v0.0.0-20230404061427-d0bd773f899f h1:EOCImWeST/oFGiNCjX5ciUHmiQaK+idbCNS0eSojrYk= github.com/nknorg/eth-resolver-go v0.0.0-20230404061427-d0bd773f899f/go.mod h1:u5Dhckhgl266RlajmjBqvWHt66br0PG5MMg+dunrD1U= github.com/nknorg/mockconn-go v0.0.0-20230125231524-d664e728352a/go.mod h1:/SvBORYxt9wlm8ZbaEFEri6ooOSDcU3ovU0L2eRRdS4= -github.com/nknorg/ncp-go v1.0.5 h1:alJjq6bi6tRwUAAv932FIfE/R3S7DRR0pgXOgBXNHAk= -github.com/nknorg/ncp-go v1.0.5/go.mod h1:ze88qf5e9/DBXSOaJPL2Caa0IbdZJLzESAFM1S7mGwg= -github.com/nknorg/nkn-sdk-go v1.4.6 h1:GfvOAoC9Lj7WnZrPaO8UC62ieqAeHqNOMrePywsWKQE= -github.com/nknorg/nkn-sdk-go v1.4.6/go.mod h1:d4+iy0NmckVSgTUHUTloN5X5zcfph86126ubc1Rq9Lg= -github.com/nknorg/nkn/v2 v2.2.0 h1:sXOawvVF/T3bBTuWbzBCyrGuxldA3be+f+BDjoWcOEA= -github.com/nknorg/nkn/v2 v2.2.0/go.mod h1:yv3jkg0aOtN9BDHS4yerNSZJtJNBfGvlaD5K6wL6U3E= +github.com/nknorg/ncp-go v1.0.7-0.20240928081416-1a805ec168d0 h1:x19e5JSW9BpO4glxrOYQnfyoRQEUGQKIkg5XQAV0W1Y= +github.com/nknorg/ncp-go v1.0.7-0.20240928081416-1a805ec168d0/go.mod h1:y7WJ8zna/EsQ0Hifp8iLFuDRNQfPmDfALapsDfZoHEM= +github.com/nknorg/nkn-sdk-go v1.4.9-0.20250718092920-5d1593ad7642 h1:9IE50YvWLj8juzBoOt87oG2lKNGFGaq/Ron9P0CfbsQ= +github.com/nknorg/nkn-sdk-go v1.4.9-0.20250718092920-5d1593ad7642/go.mod h1:CyQJd95NT0UxiF/MDZ3843B5xgKwQCuPdU7wZGRL8Jc= +github.com/nknorg/nkn/v2 v2.2.2-0.20250718093239-1e65fafdf8f0 h1:y64x9g1QU6jrvLezQ/D6fB8KeHwPqgUa7GYUAgg8jjI= +github.com/nknorg/nkn/v2 v2.2.2-0.20250718093239-1e65fafdf8f0/go.mod h1:kmV1K5ZNspfubj6KkqfVSw+46uVINUiZ6EWHufg2CwE= github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9 h1:Gr37j7Ttvcn8g7TdC5fs6Y6IJKdmfqCvj03UbsrS77o= github.com/nknorg/nkngomobile v0.0.0-20220615081414-671ad1afdfa9/go.mod h1:zNY9NCyBcJCCDrXhwOjKarkW5cngPs/Z82xVNy/wvEA= github.com/nknorg/reedsolomon v1.9.12-0.20210315025804-a0c1b6031ab4 h1:Ug3GGTsny4ZcPsDeqOqoiBMts/yA8PBX1TJmMOeyc9Q= @@ -382,10 +386,44 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= +github.com/pion/dtls/v3 v3.0.2 h1:425DEeJ/jfuTTghhUDW0GtYZYIwwMtnKKJNMcWccTX0= +github.com/pion/dtls/v3 v3.0.2/go.mod h1:dfIXcFkKoujDQ+jtd8M6RgqKK3DuaUilm3YatAbGp5k= +github.com/pion/ice/v4 v4.0.1 h1:2d3tPoTR90F3TcGYeXUwucGlXI3hds96cwv4kjZmb9s= +github.com/pion/ice/v4 v4.0.1/go.mod h1:2dpakjpd7+74L5j3TAe6gvkbI5UIzOgAnkimm9SuHvA= +github.com/pion/interceptor v0.1.30 h1:au5rlVHsgmxNi+v/mjOPazbW1SHzfx7/hYOEYQnUcxA= +github.com/pion/interceptor v0.1.30/go.mod h1:RQuKT5HTdkP2Fi0cuOS5G5WNymTjzXaGF75J4k7z2nc= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= +github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk= +github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= +github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= +github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= +github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= +github.com/pion/srtp/v3 v3.0.3 h1:tRtEOpmR8NtsB/KndlKXFOj/AIIs6aPrCq4TlAatC4M= +github.com/pion/srtp/v3 v3.0.3/go.mod h1:Bp9ztzPCoE0ETca/R+bTVTO5kBgaQMiQkTmZWwazDTc= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= +github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= +github.com/pion/webrtc/v4 v4.0.0-beta.30 h1:ztchBW2RZiiBflmoCIuViD/axDoNkEzoh0CqRWvf6dc= +github.com/pion/webrtc/v4 v4.0.0-beta.30/go.mod h1:V+nZxyUG8sIUb0uUYQEZzx1PvMPtHlRby4h3xhrjTsg= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -440,6 +478,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -448,8 +487,10 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= @@ -476,6 +517,8 @@ github.com/wealdtech/go-multicodec v1.4.0/go.mod h1:aedGMaTeYkIqi/KCPre1ho5rTb3h github.com/wealdtech/go-string2eth v1.1.0 h1:USJQmysUrBYYmZs7d45pMb90hRSyEwizP7lZaOZLDAw= github.com/wealdtech/go-string2eth v1.1.0/go.mod h1:RUzsLjJtbZaJ/3UKn9kY19a/vCCUHtEWoUW3uiK6yGU= github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/wlynxg/anet v0.0.4 h1:0de1OFQxnNqAu+x2FAKKCVIrnfGKQbs7FQz++tB0+Uw= +github.com/wlynxg/anet v0.0.4/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= @@ -502,8 +545,8 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20220213190939-1e6e3497d506/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -527,15 +570,15 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4= -golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= +golang.org/x/mobile v0.0.0-20240905004112-7c4916698cc9 h1:zWHXwU92JPmIJw5bj6vOZWDZusVxqazOaqhwfUA5l7w= +golang.org/x/mobile v0.0.0-20240905004112-7c4916698cc9/go.mod h1:udWezQGYjqrCxz5nV321pXQTx5oGbZx+khZvFjZNOPM= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -562,8 +605,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -578,7 +621,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -624,10 +668,10 @@ golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -639,14 +683,15 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -674,12 +719,11 @@ golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= @@ -723,12 +767,11 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/golib/gomobile.go b/golib/gomobile.go index 289b9e6aa..398ff70f9 100644 --- a/golib/gomobile.go +++ b/golib/gomobile.go @@ -1,6 +1,8 @@ package nkngolib import ( + "nkngolib/search" + dnsresolver "github.com/nknorg/dns-resolver-go" ethresolver "github.com/nknorg/eth-resolver-go" "github.com/nknorg/nkn-sdk-go" @@ -16,4 +18,5 @@ var ( _ = nkngomobile.NewStringArray _ = reedsolomon.New _ = bind.GenGo + _ = search.NewSearchClient ) diff --git a/golib/search/pow.go b/golib/search/pow.go new file mode 100644 index 000000000..5fe97f4ad --- /dev/null +++ b/golib/search/pow.go @@ -0,0 +1,392 @@ +package search + +import ( + "bytes" + "crypto/ed25519" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "runtime" + "strconv" + "sync" + "time" +) + +// Challenge represents the PoW challenge structure +type Challenge struct { + Challenges []string `json:"challenges"` + Difficulty int `json:"difficulty"` + Count int `json:"count"` + Hint string `json:"hint"` +} + +// ChallengeResponse represents the API response for challenge request +type ChallengeResponse struct { + Success bool `json:"success"` + Data Challenge `json:"data"` + Error string `json:"error,omitempty"` +} + +// Solution represents the solution for a single challenge +type Solution struct { + Challenge string `json:"challenge"` + Signature string `json:"signature"` + Nonce string `json:"nonce"` +} + +// VerifyRequest represents the verification request +type VerifyRequest struct { + PublicKey string `json:"publicKey"` + Solutions []Solution `json:"solutions"` +} + +// VerifyResponse represents the verification response +type VerifyResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Data struct { + PublicKey string `json:"publicKey"` + VerifiedAt int64 `json:"verifiedAt"` + } `json:"data"` + Error string `json:"error,omitempty"` +} + +// PowSolution represents the PoW solution for data submission +type PowSolution struct { + PublicKey string `json:"publicKey"` + Solutions []Solution `json:"solutions"` +} + +// SubmitRequest represents the data submission request +type SubmitRequest struct { + PublicKey string `json:"publicKey"` + PowSolution PowSolution `json:"powSolution"` + NknAddress string `json:"nknAddress"` + CustomId string `json:"customId,omitempty"` + Nickname string `json:"nickname,omitempty"` + PhoneNumber string `json:"phoneNumber,omitempty"` +} + +// SubmitResponse represents the submission response +type SubmitResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Error string `json:"error,omitempty"` +} + +// SearchClient is the search client for NKN search server +type SearchClient struct { + apiBase *url.URL + privateKey []byte + publicKey []byte + walletAddr string + isVerified bool + verifiedUntil time.Time + mu sync.RWMutex + httpClient *http.Client +} + +// init initializes the search package +// Sets GOMAXPROCS to enable parallel execution on multi-core devices +func init() { + // Enable parallel execution by setting GOMAXPROCS to CPU count + numCPU := runtime.NumCPU() + runtime.GOMAXPROCS(numCPU) + log.Printf("Search package initialized: GOMAXPROCS set to %d (CPU cores: %d)", + runtime.GOMAXPROCS(0), numCPU) +} + +// QueryResult represents the query result +type QueryResult struct { + Success bool `json:"success"` + Data string `json:"data"` // JSON formatted result + Error string `json:"error,omitempty"` +} + +// sign signs a message (hex string) +func (c *SearchClient) sign(messageHex string) (string, error) { + // NKN SDK expects message to be a hex string, need to decode first + messageBytes, err := hex.DecodeString(messageHex) + if err != nil { + return "", fmt.Errorf("failed to decode hex message: %w", err) + } + + // Sign using ed25519 + signature := ed25519.Sign(c.privateKey, messageBytes) + + // Return hex encoded signature + return hex.EncodeToString(signature), nil +} + +// GetPublicKeyHex returns the public key in hex format +func (c *SearchClient) GetPublicKeyHex() string { + return hex.EncodeToString(c.publicKey) +} + +// GetAddress returns the wallet address +func (c *SearchClient) GetAddress() string { + return c.walletAddr +} + +// IsVerified checks if the client is verified +func (c *SearchClient) IsVerified() bool { + c.mu.RLock() + defer c.mu.RUnlock() + return c.isVerified && time.Now().Before(c.verifiedUntil) +} + +// solvePoW calculates PoW - finds a nonce that satisfies the difficulty +// Single-threaded optimized version for maximum performance on mobile devices +func solvePoW(signature string, difficulty int) (string, time.Duration) { + startTime := time.Now() + + log.Printf("Starting single-threaded PoW calculation (difficulty: %d)", difficulty) + + // Pre-convert signature to bytes once + sigBytes := []byte(signature) + + // Calculate how many leading zero bits we need + zeroBits := difficulty * 4 // Each hex digit = 4 bits + zeroBytes := zeroBits / 8 + remainingBits := zeroBits % 8 + + // Pre-allocate buffer with enough space + buf := make([]byte, len(sigBytes), len(sigBytes)+20) + copy(buf, sigBytes) + + nonce := uint64(0) + + // Keep track of where signature ends + sigLen := len(sigBytes) + + // Single-threaded tight loop - maximum performance + for { + // Build data: signature + nonce (optimized - reuse buffer) + buf = buf[:sigLen] + buf = strconv.AppendUint(buf, nonce, 10) + + // Calculate hash + hash := sha256.Sum256(buf) + + // Fast check: compare bytes directly instead of hex string + isValid := true + + // Check full zero bytes + for k := 0; k < zeroBytes; k++ { + if hash[k] != 0 { + isValid = false + break + } + } + + // Check remaining bits if needed + if isValid && remainingBits > 0 { + mask := byte(0xFF << (8 - remainingBits)) + if (hash[zeroBytes] & mask) != 0 { + isValid = false + } + } + + if isValid { + duration := time.Since(startTime) + log.Printf("PoW solved: nonce=%d, duration=%v", nonce, duration) + return strconv.FormatUint(nonce, 10), duration + } + + nonce++ + } +} + +// getChallenge gets the PoW challenge +func (c *SearchClient) getChallenge() (*Challenge, error) { + url := fmt.Sprintf("%s/auth/challenge?publicKey=%s", c.apiBase, c.GetPublicKeyHex()) + resp, err := c.httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var challengeResp ChallengeResponse + if err := json.Unmarshal(body, &challengeResp); err != nil { + return nil, err + } + + if !challengeResp.Success { + return nil, fmt.Errorf("failed to get challenge: %s", challengeResp.Error) + } + + return &challengeResp.Data, nil +} + +// getChallengeSubmit gets the PoW challenge for data submission +func (c *SearchClient) getChallengeSubmit() (*Challenge, error) { + url := fmt.Sprintf("%s/auth/challenge-submit?publicKey=%s", c.apiBase, c.GetPublicKeyHex()) + resp, err := c.httpClient.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var challengeResp ChallengeResponse + if err := json.Unmarshal(body, &challengeResp); err != nil { + return nil, err + } + + if !challengeResp.Success { + return nil, fmt.Errorf("failed to get submit challenge: %s", challengeResp.Error) + } + + return &challengeResp.Data, nil +} + +// verify submits PoW solutions for verification +func (c *SearchClient) verify(solutions []Solution) (*VerifyResponse, error) { + url := fmt.Sprintf("%s/auth/verify", c.apiBase) + + reqBody := VerifyRequest{ + PublicKey: c.GetPublicKeyHex(), + Solutions: solutions, + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, err + } + + resp, err := c.httpClient.Post(url, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var verifyResp VerifyResponse + if err := json.Unmarshal(body, &verifyResp); err != nil { + return nil, err + } + + return &verifyResp, nil +} + +// submitData submits user data (requires fresh PoW) +func (c *SearchClient) submitData(nknAddress, customId, nickname, phoneNumber string) (*SubmitResponse, error) { + log.Printf("Starting user data submission...") + + // 1. Get submit challenge + challenge, err := c.getChallengeSubmit() + if err != nil { + return nil, fmt.Errorf("failed to get submit challenge: %w", err) + } + + // 2. Solve challenges + solutions, err := c.solveChallenges(challenge) + if err != nil { + return nil, fmt.Errorf("failed to solve challenges: %w", err) + } + + // 3. Prepare and submit data + url := c.apiBase.JoinPath("data/submit") + + powSolution := PowSolution{ + PublicKey: c.GetPublicKeyHex(), + Solutions: solutions, + } + + reqBody := SubmitRequest{ + PublicKey: c.GetPublicKeyHex(), + PowSolution: powSolution, + NknAddress: nknAddress, + CustomId: customId, + Nickname: nickname, + PhoneNumber: phoneNumber, + } + + jsonData, err := json.Marshal(reqBody) + if err != nil { + return nil, err + } + + log.Printf("Submitting user data to server...") + resp, err := c.httpClient.Post(url.String(), "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Handle rate limit with detailed message + if resp.StatusCode == 429 { + log.Printf("⚠️ Rate limit exceeded. Server allows max 10 submits per minute.") + log.Printf("Please wait a moment before retrying.") + return nil, fmt.Errorf("rate limit exceeded (429): max 10 submits per minute. Please wait and retry") + } + + var submitResp SubmitResponse + if err := json.Unmarshal(body, &submitResp); err != nil { + return nil, err + } + + if submitResp.Success { + log.Printf("✓ User data submitted successfully: %s", submitResp.Message) + } else { + log.Printf("✗ User data submission failed: %s", submitResp.Error) + } + + return &submitResp, nil +} + +// solveChallenges solves all challenges +func (c *SearchClient) solveChallenges(challenge *Challenge) ([]Solution, error) { + solutions := make([]Solution, 0, len(challenge.Challenges)) + totalStart := time.Now() + + log.Printf("Starting to solve %d challenges with difficulty %d", len(challenge.Challenges), challenge.Difficulty) + + for i, ch := range challenge.Challenges { + // Sign the challenge + signature, err := c.sign(ch) + if err != nil { + return nil, fmt.Errorf("failed to sign challenge: %w", err) + } + + // Calculate PoW + log.Printf("Solving challenge %d/%d...", i+1, len(challenge.Challenges)) + nonce, duration := solvePoW(signature, challenge.Difficulty) + log.Printf("Challenge %d/%d solved in %v (nonce: %s)", i+1, len(challenge.Challenges), duration, nonce) + + solutions = append(solutions, Solution{ + Challenge: ch, + Signature: signature, + Nonce: nonce, + }) + } + + totalDuration := time.Since(totalStart) + log.Printf("All challenges solved! Total time: %v (avg: %v per challenge)", + totalDuration, totalDuration/time.Duration(len(challenge.Challenges))) + + return solutions, nil +} diff --git a/golib/search/search.go b/golib/search/search.go new file mode 100644 index 000000000..09002804c --- /dev/null +++ b/golib/search/search.go @@ -0,0 +1,285 @@ +package search + +import ( + "crypto/ed25519" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" + "time" + + "github.com/nknorg/nkn-sdk-go" +) + +// NewSearchClient creates a new search client +// apiBase: API server address, e.g. "https://search.nkn.org/api/v1" +// For query-only usage, you can pass empty strings for privateKeyHex, publicKeyHex, and walletAddr +func NewSearchClient(apiBase string) (*SearchClient, error) { + apiBaseURL, err := url.Parse(apiBase) + if err != nil { + return nil, fmt.Errorf("failed to parse API base URL: %w", err) + } + return &SearchClient{ + apiBase: apiBaseURL, + httpClient: &http.Client{Timeout: 30 * time.Second}, + }, nil +} + +// NewSearchClientWithAuth creates a new search client with authentication +// apiBase: API server address, e.g. "https://search.nkn.org/api/v1" +// seed: NKN seed (hex format, 32 bytes = 64 characters) +func NewSearchClientWithAuth(apiBase string, seed []byte) (*SearchClient, error) { + account, err := nkn.NewAccount(seed) + if err != nil { + return nil, fmt.Errorf("failed to create account: %w", err) + } + + // Decode private key + privateKeyBytes := account.PrivateKey + if len(privateKeyBytes) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("private key must be %d bytes", ed25519.PrivateKeySize) + } + + // Decode public key + publicKeyBytes := account.PublicKey + if len(publicKeyBytes) != ed25519.PublicKeySize { + return nil, fmt.Errorf("public key must be %d bytes", ed25519.PublicKeySize) + } + + apiBaseURL, err := url.Parse(apiBase) + if err != nil { + return nil, fmt.Errorf("failed to parse API base URL: %w", err) + } + + return &SearchClient{ + apiBase: apiBaseURL, + privateKey: privateKeyBytes, + publicKey: publicKeyBytes, + walletAddr: account.WalletAddress(), + httpClient: &http.Client{Timeout: 30 * time.Second}, + }, nil +} + +// Query queries data by keyword +// keyword: search keyword +// Returns JSON formatted query result string, error on failure +func (c *SearchClient) Query(keyword string) (string, error) { + // Build query URL + url := c.apiBase.JoinPath("data/query") + q := url.Query() + q.Set("q", keyword) + url.RawQuery = q.Encode() + + // Create request + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return "", err + } + + // Send request + resp, err := c.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("query request failed: %w", err) + } + defer resp.Body.Close() + + // Read response + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + // Return JSON string + return string(body), nil +} + +// QueryByID queries data by ID +// id: ID +// Returns JSON formatted query result string, error on failure +func (c *SearchClient) QueryByID(id string) (string, error) { + // Build query URL + url := c.apiBase.JoinPath("data/query") + q := url.Query() + q.Set("customId", id) + url.RawQuery = q.Encode() + + // Create request + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return "", err + } + + // Send request + resp, err := c.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("query request failed: %w", err) + } + defer resp.Body.Close() + + // Read response + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + // Return JSON string + return string(body), nil +} + +// GetMyInfo queries my own information by querying with nknAddress +// address: NKN address +// Returns JSON formatted query result string, error on failure +func (c *SearchClient) GetMyInfo(address string) (string, error) { + // Build query URL + url := c.apiBase.JoinPath("data/query") + q := url.Query() + q.Set("nknAddress", address) + url.RawQuery = q.Encode() + + log.Printf("[GetMyInfo] Request URL: %s", url.String()) + + // Create request + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return "", err + } + + // Send request + resp, err := c.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("query request failed: %w", err) + } + defer resp.Body.Close() + + log.Printf("[GetMyInfo] Response status: %d %s", resp.StatusCode, resp.Status) + + // Read response + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + // Check HTTP status code + if resp.StatusCode != http.StatusOK { + log.Printf("[GetMyInfo] Error response body: %s", string(body)) + return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) + } + + log.Printf("[GetMyInfo] Response body: %s", string(body)) + + // Return JSON string + return string(body), nil +} + +// SubmitUserData submits or updates user profile data to the search server +// +// Parameters: +// - nknAddress: NKN client address (optional, format: "identifier.publickey" or just publickey) +// If empty, defaults to publickey. Must be either "identifier.publickey" format or equal to publickey. +// - customId: Custom identifier (optional, min 3 characters if provided, alphanumeric + underscore only) +// - nickname: User nickname (optional, can be empty string) +// - phoneNumber: Phone number (optional, can be empty string) +// +// # Returns nil on success, error on failure +// +// Important notes: +// - Each call performs fresh PoW (Proof of Work) automatically - you'll see timing logs +// - Server rate limit: 10 submits per minute +// - NO need to call Verify() first - SubmitUserData works independently +// - Verify() is only useful for query operations (gives 2-hour query access) +// - If publicKey already exists, will UPDATE the user data (can modify nickname, phoneNumber) +// - nknAddress validation: must be empty, equal to publickey, or in "identifier.publickey" format +// +// Example: +// +// // Option 1: Use default (empty - will use publickey) +// err := client.SubmitUserData("", "", "John Doe", "13800138000") +// +// // Option 2: Use publickey directly +// err := client.SubmitUserData(client.GetPublicKeyHex(), "", "John Doe", "13800138000") +// +// // Option 3: Use custom identifier.publickey format +// err := client.SubmitUserData( +// "alice." + client.GetPublicKeyHex(), // nknAddress - identifier.publickey +// "myid123", // customId - optional +// "John Doe", // nickname - optional +// "13800138000", // phoneNumber - optional +// ) +func (c *SearchClient) SubmitUserData(nknAddress, customId, nickname, phoneNumber string) error { + // Process nknAddress: if empty, use publicKey + finalNknAddress := nknAddress + publicKeyHex := c.GetPublicKeyHex() + + if finalNknAddress == "" { + finalNknAddress = publicKeyHex + } else { + // Validate format if contains dot + if strings.Contains(finalNknAddress, ".") { + parts := strings.Split(finalNknAddress, ".") + if len(parts) != 2 { + return fmt.Errorf("invalid nknAddress format, expected: identifier.publickey") + } + providedPubKey := parts[1] + if strings.ToLower(providedPubKey) != strings.ToLower(publicKeyHex) { + return fmt.Errorf("nknAddress publickey suffix must match your actual publicKey") + } + } else { + // If no dot, must equal publicKey + if strings.ToLower(finalNknAddress) != strings.ToLower(publicKeyHex) { + return fmt.Errorf("nknAddress must be either \"identifier.publickey\" format or equal to publicKey") + } + } + } + + // Validate customId if provided + if customId != "" && len(customId) < 3 { + return fmt.Errorf("customId must be at least 3 characters if provided") + } + + submitResp, err := c.submitData(finalNknAddress, customId, nickname, phoneNumber) + if err != nil { + return fmt.Errorf("failed to submit user data: %w", err) + } + + if !submitResp.Success { + return fmt.Errorf("submit failed: %s", submitResp.Error) + } + + return nil +} + +// Verify verifies the public key (completes PoW challenge) +// Returns nil on success, error on failure +func (c *SearchClient) Verify() error { + // 1. Get challenge + challenge, err := c.getChallenge() + if err != nil { + return fmt.Errorf("failed to get challenge: %w", err) + } + + // 2. Solve challenges + solutions, err := c.solveChallenges(challenge) + if err != nil { + return fmt.Errorf("failed to solve challenges: %w", err) + } + + // 3. Submit verification + verifyResp, err := c.verify(solutions) + if err != nil { + return fmt.Errorf("failed to verify: %w", err) + } + + if !verifyResp.Success { + return fmt.Errorf("verification failed: %s", verifyResp.Error) + } + + // 4. Update status + c.mu.Lock() + c.isVerified = true + c.verifiedUntil = time.Now().Add(2 * time.Hour) // Valid for 2 hours + c.mu.Unlock() + + return nil +} diff --git a/ios/.gitignore b/ios/.gitignore index 151026b91..efa4cb9d3 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -31,3 +31,5 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 +/Flutter/Secrets.xcconfig +/Runner/Config.generated.swift diff --git a/ios/Classes/Push2/APNSPusher.swift b/ios/Classes/Push2/APNSPusher.swift index 3473f8d76..7a3c93522 100644 --- a/ios/Classes/Push2/APNSPusher.swift +++ b/ios/Classes/Push2/APNSPusher.swift @@ -1,14 +1,7 @@ -// -// APNSPusher.swift -// Runner -// -// Created by 蒋治国 on 2021/10/31. -// - import Foundation -let p12FileName = "" -let p12FilePasswordd = "" +let p12FileName = BuildSecrets.apnsP12FileName +let p12FilePasswordd = BuildSecrets.apnsP12Password public class APNSPusher { diff --git a/ios/Classes/impl/SearchService/Search.swift b/ios/Classes/impl/SearchService/Search.swift new file mode 100644 index 000000000..245e8b291 --- /dev/null +++ b/ios/Classes/impl/SearchService/Search.swift @@ -0,0 +1,368 @@ +import Nkn + +class SearchService : ChannelBase, FlutterStreamHandler { + static var instance: SearchService = SearchService() + let searchQueue = DispatchQueue(label: "org.nkn.mobile/native/search/queue", qos: .default, attributes: .concurrent) + + // High priority queue for CPU-intensive PoW calculations + // Use userInitiated QoS to ensure maximum CPU resources + let powQueue = DispatchQueue(label: "org.nkn.mobile/native/search/pow", qos: .userInitiated, attributes: .concurrent) + + private var searchItem: DispatchWorkItem? + + var methodChannel: FlutterMethodChannel? + let METHOD_CHANNEL_NAME = "org.nkn.mobile/native/search" + var eventSink: FlutterEventSink? + + // Store search client instances by ID + private var clients: [String: SearchSearchClient] = [:] + private let clientsLock = NSLock() + + public static func register(controller: FlutterViewController) { + instance.install(binaryMessenger: controller as! FlutterBinaryMessenger) + } + + func install(binaryMessenger: FlutterBinaryMessenger) { + self.methodChannel = FlutterMethodChannel(name: METHOD_CHANNEL_NAME, binaryMessenger: binaryMessenger) + self.methodChannel?.setMethodCallHandler(handle) + } + + func uninstall() { + self.methodChannel?.setMethodCallHandler(nil) + } + + func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + eventSink = events + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + eventSink = nil + return nil + } + + private func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "newSearchClient": + newSearchClient(call, result: result) + case "newSearchClientWithAuth": + newSearchClientWithAuth(call, result: result) + case "query": + query(call, result: result) + case "submitUserData": + submitUserData(call, result: result) + case "verify": + verify(call, result: result) + case "queryByID": + queryByID(call, result: result) + case "getMyInfo": + getMyInfo(call, result: result) + case "getPublicKeyHex": + getPublicKeyHex(call, result: result) + case "getAddress": + getAddress(call, result: result) + case "isVerified": + isVerified(call, result: result) + case "disposeClient": + disposeClient(call, result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + // Create a query-only search client + private func newSearchClient(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let apiBase = args["apiBase"] as? String ?? "" + + var error: NSError? + guard let client = SearchNewSearchClient(apiBase, &error) else { + self.resultError(result: result, error: error, code: "CREATE_CLIENT_FAILED") + return + } + + // Generate unique ID for this client + let clientId = UUID().uuidString + + clientsLock.lock() + clients[clientId] = client + clientsLock.unlock() + + let response: [String: Any] = [ + "clientId": clientId + ] + + self.resultSuccess(result: result, resp: response) + } + + // Create an authenticated search client + private func newSearchClientWithAuth(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let apiBase = args["apiBase"] as? String ?? "" + guard let seedData = args["seed"] as? FlutterStandardTypedData else { + self.resultError(result: result, code: "INVALID_SEED", message: "Seed must be provided") + return + } + + let seed = seedData.data + if seed.count != 32 { + self.resultError(result: result, code: "INVALID_SEED", message: "Seed must be exactly 32 bytes") + return + } + + var error: NSError? + guard let client = SearchNewSearchClientWithAuth(apiBase, seed, &error) else { + self.resultError(result: result, error: error, code: "CREATE_AUTH_CLIENT_FAILED") + return + } + + // Generate unique ID for this client + let clientId = UUID().uuidString + + clientsLock.lock() + clients[clientId] = client + clientsLock.unlock() + + let response: [String: Any] = [ + "clientId": clientId + ] + + self.resultSuccess(result: result, resp: response) + } + + // Query data by keyword + private func query(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + let keyword = args["keyword"] as? String ?? "" + + clientsLock.lock() + guard let client = clients[clientId] else { + clientsLock.unlock() + self.resultError(result: result, code: "CLIENT_NOT_FOUND", message: "Search client not found") + return + } + clientsLock.unlock() + + searchQueue.async { + var error: NSError? + let response = client.query(keyword, error: &error) + + if let error = error { + self.resultError(result: result, error: error, code: "QUERY_FAILED") + return + } + + self.resultSuccess(result: result, resp: response) + } + } + + // Submit user data + private func submitUserData(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + var nknAddress = args["nknAddress"] as? String ?? "" + let customId = args["customId"] as? String ?? "" + let nickname = args["nickname"] as? String ?? "" + let phoneNumber = args["phoneNumber"] as? String ?? "" + + clientsLock.lock() + guard let client = clients[clientId] else { + clientsLock.unlock() + self.resultError(result: result, code: "CLIENT_NOT_FOUND", message: "Search client not found") + return + } + clientsLock.unlock() + + // Use high-priority queue for CPU-intensive PoW calculation + let queueItem = DispatchWorkItem { + // Process nknAddress: if empty, use publicKey + let publicKeyHex = client.getPublicKeyHex() + + if nknAddress.isEmpty { + nknAddress = publicKeyHex + } else { + // Validate format if contains dot + if nknAddress.contains(".") { + let parts = nknAddress.components(separatedBy: ".") + if parts.count != 2 { + self.resultError(result: result, code: "INVALID_PARAMETER", + message: "Invalid nknAddress format. Expected: identifier.publickey") + return + } + let providedPubKey = parts[1] + if providedPubKey.lowercased() != publicKeyHex.lowercased() { + self.resultError(result: result, code: "INVALID_PARAMETER", + message: "nknAddress publickey suffix must match your actual publicKey") + return + } + } else { + // If no dot, must equal publicKey + if nknAddress.lowercased() != publicKeyHex.lowercased() { + self.resultError(result: result, code: "INVALID_PARAMETER", + message: "nknAddress must be either \"identifier.publickey\" format or equal to publicKey") + return + } + } + } + + // Validate customId if provided + if !customId.isEmpty && customId.count < 3 { + self.resultError(result: result, code: "INVALID_PARAMETER", + message: "customId must be at least 3 characters if provided") + return + } + + do { + // PoW calculation happens here - runs on high priority background thread + try client.submitUserData(nknAddress, customId: customId, nickname: nickname, phoneNumber: phoneNumber) + self.resultSuccess(result: result, resp: ["success": true]) + } catch let error as NSError { + self.resultError(result: result, error: error, code: "SUBMIT_FAILED") + } + } + powQueue.async(execute: queueItem) + } + + // Verify the client (optional, for query operations) + private func verify(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + + clientsLock.lock() + guard let client = clients[clientId] else { + clientsLock.unlock() + self.resultError(result: result, code: "CLIENT_NOT_FOUND", message: "Search client not found") + return + } + clientsLock.unlock() + + // Use high-priority queue for CPU-intensive PoW calculation + powQueue.async { + do { + // PoW calculation happens here - runs on high priority background thread + try client.verify() + self.resultSuccess(result: result, resp: ["success": true]) + } catch let error as NSError { + self.resultError(result: result, error: error, code: "VERIFY_FAILED") + } + } + } + + // Query by ID + private func queryByID(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + let id = args["id"] as? String ?? "" + + clientsLock.lock() + guard let client = clients[clientId] else { + clientsLock.unlock() + self.resultError(result: result, code: "CLIENT_NOT_FOUND", message: "Search client not found") + return + } + clientsLock.unlock() + + searchQueue.async { + var error: NSError? + let response = client.query(byID: id, error: &error) + + if let error = error { + self.resultError(result: result, error: error, code: "QUERY_BY_ID_FAILED") + return + } + + self.resultSuccess(result: result, resp: response) + } + } + + // Get my info by nknAddress + private func getMyInfo(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + let address = args["address"] as? String ?? "" + + clientsLock.lock() + guard let client = clients[clientId] else { + clientsLock.unlock() + self.resultError(result: result, code: "CLIENT_NOT_FOUND", message: "Search client not found") + return + } + clientsLock.unlock() + + searchQueue.async { + var error: NSError? + let response = client.getMyInfo(address, error: &error) + + if let error = error { + self.resultError(result: result, error: error, code: "GET_MY_INFO_FAILED") + return + } + + self.resultSuccess(result: result, resp: response) + } + } + + // Get public key hex + private func getPublicKeyHex(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + + clientsLock.lock() + guard let client = clients[clientId] else { + clientsLock.unlock() + self.resultError(result: result, code: "CLIENT_NOT_FOUND", message: "Search client not found") + return + } + clientsLock.unlock() + + let publicKeyHex = client.getPublicKeyHex() + self.resultSuccess(result: result, resp: publicKeyHex) + } + + // Get wallet address + private func getAddress(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + + clientsLock.lock() + guard let client = clients[clientId] else { + clientsLock.unlock() + self.resultError(result: result, code: "CLIENT_NOT_FOUND", message: "Search client not found") + return + } + clientsLock.unlock() + + let address = client.getAddress() + self.resultSuccess(result: result, resp: address) + } + + // Check if verified + private func isVerified(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + + clientsLock.lock() + guard let client = clients[clientId] else { + clientsLock.unlock() + self.resultError(result: result, code: "CLIENT_NOT_FOUND", message: "Search client not found") + return + } + clientsLock.unlock() + + let verified = client.isVerified() + self.resultSuccess(result: result, resp: verified) + } + + // Dispose client + private func disposeClient(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args = call.arguments as? [String: Any] ?? [String: Any]() + let clientId = args["clientId"] as? String ?? "" + + clientsLock.lock() + clients.removeValue(forKey: clientId) + clientsLock.unlock() + + self.resultSuccess(result: result, resp: ["success": true]) + } +} diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c5696400..391a902b2 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index ec97fc6f3..d70aca916 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1,2 +1,3 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" +#include? "Secrets.xcconfig" \ No newline at end of file diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index c4855bfe2..6f461927f 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" +#include? "Secrets.xcconfig" \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index fe5bdacf5..6d741768e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -32,12 +32,15 @@ target 'Runner' do use_modular_headers! #pod 'Sentry', :git => 'https://github.com/getsentry/sentry-cocoa.git', :tag => '8.4.0' - + pod 'Firebase/Analytics' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'Share Extension' do - inherit! :search_paths + # share_handler addition start + target 'ShareExtension' do + inherit! :search_paths + pod "share_handler_ios_models", :path => ".symlinks/plugins/share_handler_ios/ios/Models" end + # share_handler addition end end post_install do |installer| diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 174a5617e..13abb572c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -36,9 +36,47 @@ PODS: - DKPhotoGallery/Resource (0.0.19): - SDWebImage - SwiftyGif + - emoji_picker_flutter (0.0.1): + - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter + - Firebase/Analytics (11.15.0): + - Firebase/Core + - Firebase/Core (11.15.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 11.15.0) + - Firebase/CoreOnly (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseAnalytics (11.15.0): + - FirebaseAnalytics/Default (= 11.15.0) + - FirebaseCore (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/Default (11.15.0): + - FirebaseCore (~> 11.15.0) + - FirebaseInstallations (~> 11.0) + - GoogleAppMeasurement/Default (= 11.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - FirebaseCore (11.15.0): + - FirebaseCoreInternal (~> 11.15.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreInternal (11.15.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseInstallations (11.15.0): + - FirebaseCore (~> 11.15.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) - Flutter (1.0.0) - flutter_image_compress_common (1.0.0): - Flutter @@ -55,12 +93,67 @@ PODS: - Flutter - flutter_sound_core (= 9.28.0) - flutter_sound_core (9.28.0) - - FMDB/SQLCipher (2.7.11): - - SQLCipher (~> 4.0) + - FMDB/Core (2.7.12) + - FMDB/SQLCipher (2.7.12): + - FMDB/Core + - SQLCipher (~> 4.6) + - GoogleAdsOnDeviceConversion (2.1.0): + - GoogleUtilities/Logger (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/Core (11.15.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/Default (11.15.0): + - GoogleAdsOnDeviceConversion (= 2.1.0) + - GoogleAppMeasurement/Core (= 11.15.0) + - GoogleAppMeasurement/IdentitySupport (= 11.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/IdentitySupport (11.15.0): + - GoogleAppMeasurement/Core (= 11.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy - image_cropper (0.0.4): - Flutter - TOCropViewController (~> 2.7.4) - - image_gallery_saver (2.0.2): + - image_gallery_saver_plus (0.0.1): - Flutter - image_picker_ios (0.0.1): - Flutter @@ -83,6 +176,11 @@ PODS: - Mantle/extobjc (= 2.2.0) - Mantle/extobjc (2.2.0) - MTBBarcodeScanner (5.0.11) + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - nkn_sdk_flutter (0.1.15): - Flutter - open_filex (0.0.2): @@ -94,15 +192,14 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter + - PromisesObjC (2.4.0) - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner - - receive_sharing_intent (1.8.1): - - Flutter - - SDWebImage (5.21.0): - - SDWebImage/Core (= 5.21.0) - - SDWebImage/Core (5.21.0) - - SDWebImageWebPCoder (0.14.6): + - SDWebImage (5.21.5): + - SDWebImage/Core (= 5.21.5) + - SDWebImage/Core (5.21.5) + - SDWebImageWebPCoder (0.15.0): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - Sentry/HybridSDK (8.46.0) @@ -110,24 +207,30 @@ PODS: - Flutter - FlutterMacOS - Sentry/HybridSDK (= 8.46.0) + - share_handler_ios (0.0.14): + - Flutter + - share_handler_ios/share_handler_ios_models (= 0.0.14) + - share_handler_ios_models + - share_handler_ios/share_handler_ios_models (0.0.14): + - Flutter + - share_handler_ios_models + - share_handler_ios_models (0.0.9) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.2): - - Flutter - - FMDB/SQLCipher (>= 2.7.5) - - SQLCipher (~> 4.5.0) - sqflite_sqlcipher (0.0.1): - Flutter - FMDB/SQLCipher (~> 2.7.5) - - SQLCipher (= 4.5.4) - - SQLCipher (4.5.4): - - SQLCipher/standard (= 4.5.4) - - SQLCipher/common (4.5.4) - - SQLCipher/standard (4.5.4): + - SQLCipher (= 4.10.0) + - SQLCipher (4.10.0): + - SQLCipher/standard (= 4.10.0) + - SQLCipher/common (4.10.0) + - SQLCipher/standard (4.10.0): - SQLCipher/common + - store_checker (0.0.1): + - Flutter - SwiftyGif (5.4.5) - TOCropViewController (2.7.4) - url_launcher_ios (0.0.1): @@ -148,7 +251,9 @@ DEPENDENCIES: - audio_session (from `.symlinks/plugins/audio_session/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Firebase/Analytics - Flutter (from `Flutter`) - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) @@ -156,7 +261,7 @@ DEPENDENCIES: - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_sound (from `.symlinks/plugins/flutter_sound/ios`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`) - - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) + - image_gallery_saver_plus (from `.symlinks/plugins/image_gallery_saver_plus/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - nkn_sdk_flutter (from `.symlinks/plugins/nkn_sdk_flutter/ios`) @@ -165,12 +270,13 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`) - - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) + - share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`) + - share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite_sqlcipher (from `.symlinks/plugins/sqflite_sqlcipher/ios`) + - store_checker (from `.symlinks/plugins/store_checker/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - vibration (from `.symlinks/plugins/vibration/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) @@ -181,11 +287,21 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery + - Firebase + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreInternal + - FirebaseInstallations - flutter_sound_core - FMDB + - GoogleAdsOnDeviceConversion + - GoogleAppMeasurement + - GoogleUtilities - libwebp - Mantle - MTBBarcodeScanner + - nanopb + - PromisesObjC - SDWebImage - SDWebImageWebPCoder - Sentry @@ -200,6 +316,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + emoji_picker_flutter: + :path: ".symlinks/plugins/emoji_picker_flutter/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: @@ -216,8 +334,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_sound/ios" image_cropper: :path: ".symlinks/plugins/image_cropper/ios" - image_gallery_saver: - :path: ".symlinks/plugins/image_gallery_saver/ios" + image_gallery_saver_plus: + :path: ".symlinks/plugins/image_gallery_saver_plus/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" local_auth_darwin: @@ -234,18 +352,20 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" qr_code_scanner: :path: ".symlinks/plugins/qr_code_scanner/ios" - receive_sharing_intent: - :path: ".symlinks/plugins/receive_sharing_intent/ios" sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" + share_handler_ios: + :path: ".symlinks/plugins/share_handler_ios/ios" + share_handler_ios_models: + :path: ".symlinks/plugins/share_handler_ios/ios/Models" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite: - :path: ".symlinks/plugins/sqflite/ios" sqflite_sqlcipher: :path: ".symlinks/plugins/sqflite_sqlcipher/ios" + store_checker: + :path: ".symlinks/plugins/store_checker/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" vibration: @@ -258,51 +378,63 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - audio_session: 19e9480dbdd4e5f6c4543826b2e8b0e4ab6145fe - connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d - device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 + audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e - flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9 - flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - flutter_sound: 82aba29055d6feba684d08906e0623217b87bcd3 + emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc + file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e + FirebaseAnalytics: 6433dfd311ba78084fc93bdfc145e8cb75740eae + FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e + FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 + FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 + flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4 + flutter_native_splash: 9e672d3818957718ee006a491730c09deeecace9 + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + flutter_sound: b9236a5875299aaa4cef1690afd2f01d52a3f890 flutter_sound_core: 427465f72d07ab8c3edbe8ffdde709ddacd3763c - FMDB: 57486c1117fd8e0e6b947b2f54c3f42bf8e57a4e - image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf - image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb - image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6 + GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64 + GoogleAppMeasurement: 700dce7541804bec33db590a5c496b663fbe2539 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + image_cropper: 5f162dcf988100dc1513f9c6b7eb42cd6fbf9156 + image_gallery_saver_plus: e597bf65a7846979417a3eae0763b71b6dfec6c3 + image_picker_ios: afb77645f1e1060a27edb6793996ff9b42256909 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 + local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb - nkn_sdk_flutter: cb83324eb3ef17419f715707b59bee20a9adfd34 - open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 - package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e - receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1 - SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 - SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + nkn_sdk_flutter: 5f3fb21d48cd76f5d7be35851b9b03b1a18080e2 + open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + qr_code_scanner: d77f94ecc9abf96d9b9b8fc04ef13f611e5a147a + SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838 + SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854 - sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b - share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite: a34731c4ca238cea2ed4869aae5d17559bee4c9f - sqflite_sqlcipher: 2f7e72fbda46fe3255493ba3f21ebe232ff9a243 - SQLCipher: 905b145f65f349f26da9e60a19901ad24adcd381 + sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684 + share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb + share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_sqlcipher: 8e5bdaae6b2c254f8a277a564f07bc4e2a8642a3 + SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc + store_checker: bcaa4c645cbb578f7f087cd6088d42ba26135470 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - vibration: 3797858f8cbf53d841e189ef8bd533d96e4ca93c - video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 - video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 - webview_flutter_wkwebview: a4af96a051138e28e29f60101d094683b9f82188 + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + vibration: 69774ad57825b11c951ee4c46155f455d7a592ce + video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + video_thumbnail: b637e0ad5f588ca9945f6e2c927f73a69a661140 + webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 -PODFILE CHECKSUM: a8cf2dfbe1b071139a60232df9500666d3de11f4 +PODFILE CHECKSUM: ccfab3afb7c1569fe4d39ff1e877dd45b28d84c6 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 06bee2602..2f447dd3f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,17 +3,20 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ + 147F890F2E83CBC000BFD48B /* ShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 147F89052E83CBC000BFD48B /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 147F899F2EA0F29300BFD48B /* Config.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147F899E2EA0F29300BFD48B /* Config.generated.swift */; }; + 147F89C22EAB685B00BFD48B /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147F89C12EAB684F00BFD48B /* Search.swift */; }; + 148FFB1A2E60499C005410D0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 148FFB192E60499C005410D0 /* GoogleService-Info.plist */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 149BC80728FEAEB000D27A6D /* DnsResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149BC80628FEAEB000D27A6D /* DnsResolver.swift */; }; 14A871CE287D6DB00093692A /* EthResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14A871CD287D6DAF0093692A /* EthResolver.swift */; }; - 14CD34D62D4F43B300789E6D /* Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14CD34CC2D4F43B300789E6D /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 26F296F8EDE0173FA7F16398 /* Pods_Share_Extension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91E30255B5E624B5EF52D2D5 /* Pods_Share_Extension.framework */; }; + 2965BED301999E2B8AAC496C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 334D81B7E27D6131B561C943 /* Pods_Runner.framework */; }; + 38534CB55C06EB49E66DECD2 /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6CB8FE12826229F00614A342 /* Pods_ShareExtension.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 405621D90C76768D15AE18AC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AEEB1EA9E73BDE1EEC27FDA /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -43,12 +46,12 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 14CD34D42D4F43B300789E6D /* PBXContainerItemProxy */ = { + 147F890D2E83CBC000BFD48B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; - remoteGlobalIDString = 14CD34CB2D4F43B300789E6D; - remoteInfo = "Share Extension"; + remoteGlobalIDString = 147F89042E83CBC000BFD48B; + remoteInfo = ShareExtension; }; /* End PBXContainerItemProxy section */ @@ -65,11 +68,11 @@ }; E1B3F2B6291BA8040042F7B2 /* Embed App Extensions */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 12; dstPath = ""; dstSubfolderSpec = 13; files = ( - 14CD34D62D4F43B300789E6D /* Share Extension.appex in Embed App Extensions */, + 147F890F2E83CBC000BFD48B /* ShareExtension.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -77,23 +80,30 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 00DDD0F3B83E3CC2A5212805 /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = ""; }; - 0AEEB1EA9E73BDE1EEC27FDA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 02EED84DF132B94A356E3DA4 /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = ""; }; + 0C98E54980A890E1BD057FA6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 142209AC2E7AB14500C679DC /* receive_sharing_intent.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = receive_sharing_intent.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 147F89052E83CBC000BFD48B /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 147F899E2EA0F29300BFD48B /* Config.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.generated.swift; sourceTree = ""; }; + 147F89C12EAB684F00BFD48B /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; + 147F8A132EAF5B4000BFD48B /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + 147F8A252EAF5BE400BFD48B /* libresolv.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.9.tbd; path = usr/lib/libresolv.9.tbd; sourceTree = SDKROOT; }; + 148FFB192E60499C005410D0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 149BC80628FEAEB000D27A6D /* DnsResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DnsResolver.swift; sourceTree = ""; }; 14A871CD287D6DAF0093692A /* EthResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthResolver.swift; sourceTree = ""; }; - 14CD34CC2D4F43B300789E6D /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - 2B0A2868D4D098A65BBC45F1 /* Pods_Sharing_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Sharing_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 2F1390E9C951F8699CC4CC1B /* Pods-Share Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.release.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.release.xcconfig"; sourceTree = ""; }; - 351D750FD58E06401D12F03A /* Pods-Sharing Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sharing Extension.release.xcconfig"; path = "Target Support Files/Pods-Sharing Extension/Pods-Sharing Extension.release.xcconfig"; sourceTree = ""; }; + 2BD846195C7A4EF53B2AAABB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 334D81B7E27D6131B561C943 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3D26C5798BF716A04D05B747 /* Pods-Sharing Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sharing Extension.debug.xcconfig"; path = "Target Support Files/Pods-Sharing Extension/Pods-Sharing Extension.debug.xcconfig"; sourceTree = ""; }; - 5C3F370BC1B3BB2B5BE7210C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 3C6D0BD6B5E0E014F40856BD /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; + 584544DF38FA171ACDCA775B /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; + 6CB8FE12826229F00614A342 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 91E30255B5E624B5EF52D2D5 /* Pods_Share_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Share_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F6144DC7E3F6B8544F0EE4F /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; + 8AF95113A5629FC499DAE55F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -101,9 +111,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A5F39BA325F55CCD6883EEB7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - C210A1A73EDB772675FF4A58 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - C5AB1F67FA58708668EABC72 /* Pods-Sharing Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sharing Extension.profile.xcconfig"; path = "Target Support Files/Pods-Sharing Extension/Pods-Sharing Extension.profile.xcconfig"; sourceTree = ""; }; + 9C257DECA1497057748992AF /* Pods_Share_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Share_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BD68F6F0F500A9479AA449AA /* Pods-Share Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.release.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.release.xcconfig"; sourceTree = ""; }; C61A6E6F266A1A7C00A7F1E1 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; C62BB8042689AA0D00DAAC43 /* NWSSLConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NWSSLConnection.h; sourceTree = ""; }; C62BB8052689AA0D00DAAC43 /* NWSecTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NWSecTools.h; sourceTree = ""; }; @@ -137,36 +146,36 @@ C6FFD762266E09E600410547 /* CommonOc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CommonOc.h; sourceTree = ""; }; C6FFD763266E09E600410547 /* CommonOc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CommonOc.m; sourceTree = ""; }; E1852ED72878179100FC45FD /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; - FF85B414D2042FADD297DB3E /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = ""; }; + EA4800B8A7D823487FC27079 /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 14CD34D72D4F43B300789E6D /* Exceptions for "Share Extension" folder in "Share Extension" target */ = { + 147F89132E83CBC000BFD48B /* Exceptions for "ShareExtension" folder in "ShareExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); - target = 14CD34CB2D4F43B300789E6D /* Share Extension */; + target = 147F89042E83CBC000BFD48B /* ShareExtension */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 14CD34CD2D4F43B300789E6D /* Share Extension */ = { + 147F89062E83CBC000BFD48B /* ShareExtension */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - 14CD34D72D4F43B300789E6D /* Exceptions for "Share Extension" folder in "Share Extension" target */, + 147F89132E83CBC000BFD48B /* Exceptions for "ShareExtension" folder in "ShareExtension" target */, ); - path = "Share Extension"; + path = ShareExtension; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ - 14CD34C92D4F43B300789E6D /* Frameworks */ = { + 147F89022E83CBC000BFD48B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 26F296F8EDE0173FA7F16398 /* Pods_Share_Extension.framework in Frameworks */, + 38534CB55C06EB49E66DECD2 /* Pods_ShareExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -174,16 +183,25 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 405621D90C76768D15AE18AC /* Pods_Runner.framework in Frameworks */, + 2965BED301999E2B8AAC496C /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 147F89C02EAB684700BFD48B /* SearchService */ = { + isa = PBXGroup; + children = ( + 147F89C12EAB684F00BFD48B /* Search.swift */, + ); + path = SearchService; + sourceTree = ""; + }; 149BC80528FD5E9300D27A6D /* impl */ = { isa = PBXGroup; children = ( + 147F89C02EAB684700BFD48B /* SearchService */, 14A871CC287D6D980093692A /* NameService */, ); path = impl; @@ -201,25 +219,28 @@ 3D6E1C9BD273F4DA4DBFC297 /* Pods */ = { isa = PBXGroup; children = ( - 5C3F370BC1B3BB2B5BE7210C /* Pods-Runner.debug.xcconfig */, - A5F39BA325F55CCD6883EEB7 /* Pods-Runner.release.xcconfig */, - C210A1A73EDB772675FF4A58 /* Pods-Runner.profile.xcconfig */, - 3D26C5798BF716A04D05B747 /* Pods-Sharing Extension.debug.xcconfig */, - 351D750FD58E06401D12F03A /* Pods-Sharing Extension.release.xcconfig */, - C5AB1F67FA58708668EABC72 /* Pods-Sharing Extension.profile.xcconfig */, - FF85B414D2042FADD297DB3E /* Pods-Share Extension.debug.xcconfig */, - 2F1390E9C951F8699CC4CC1B /* Pods-Share Extension.release.xcconfig */, - 00DDD0F3B83E3CC2A5212805 /* Pods-Share Extension.profile.xcconfig */, + 0C98E54980A890E1BD057FA6 /* Pods-Runner.debug.xcconfig */, + 8AF95113A5629FC499DAE55F /* Pods-Runner.release.xcconfig */, + 2BD846195C7A4EF53B2AAABB /* Pods-Runner.profile.xcconfig */, + EA4800B8A7D823487FC27079 /* Pods-Share Extension.debug.xcconfig */, + BD68F6F0F500A9479AA449AA /* Pods-Share Extension.release.xcconfig */, + 02EED84DF132B94A356E3DA4 /* Pods-Share Extension.profile.xcconfig */, + 584544DF38FA171ACDCA775B /* Pods-ShareExtension.debug.xcconfig */, + 7F6144DC7E3F6B8544F0EE4F /* Pods-ShareExtension.release.xcconfig */, + 3C6D0BD6B5E0E014F40856BD /* Pods-ShareExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; - 6D8B405AB30FE86D2746B39D /* Frameworks */ = { + 594E66BF7E7D2CAB5FC62094 /* Frameworks */ = { isa = PBXGroup; children = ( - 0AEEB1EA9E73BDE1EEC27FDA /* Pods_Runner.framework */, - 2B0A2868D4D098A65BBC45F1 /* Pods_Sharing_Extension.framework */, - 91E30255B5E624B5EF52D2D5 /* Pods_Share_Extension.framework */, + 147F8A252EAF5BE400BFD48B /* libresolv.9.tbd */, + 147F8A132EAF5B4000BFD48B /* libresolv.tbd */, + 142209AC2E7AB14500C679DC /* receive_sharing_intent.framework */, + 334D81B7E27D6131B561C943 /* Pods_Runner.framework */, + 9C257DECA1497057748992AF /* Pods_Share_Extension.framework */, + 6CB8FE12826229F00614A342 /* Pods_ShareExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -241,10 +262,10 @@ C61A6E6D266A19F000A7F1E1 /* Classes */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - 14CD34CD2D4F43B300789E6D /* Share Extension */, + 147F89062E83CBC000BFD48B /* ShareExtension */, 97C146EF1CF9000F007C117D /* Products */, 3D6E1C9BD273F4DA4DBFC297 /* Pods */, - 6D8B405AB30FE86D2746B39D /* Frameworks */, + 594E66BF7E7D2CAB5FC62094 /* Frameworks */, ); sourceTree = ""; }; @@ -252,7 +273,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 14CD34CC2D4F43B300789E6D /* Share Extension.appex */, + 147F89052E83CBC000BFD48B /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -260,6 +281,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 147F899E2EA0F29300BFD48B /* Config.generated.swift */, + 148FFB192E60499C005410D0 /* GoogleService-Info.plist */, C6CABF45268452450054F007 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -348,46 +371,47 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 14CD34CB2D4F43B300789E6D /* Share Extension */ = { + 147F89042E83CBC000BFD48B /* ShareExtension */ = { isa = PBXNativeTarget; - buildConfigurationList = 14CD34D82D4F43B300789E6D /* Build configuration list for PBXNativeTarget "Share Extension" */; + buildConfigurationList = 147F89142E83CBC000BFD48B /* Build configuration list for PBXNativeTarget "ShareExtension" */; buildPhases = ( - BCFE6FCF12ECC9CC408AED6A /* [CP] Check Pods Manifest.lock */, - 14CD34C82D4F43B300789E6D /* Sources */, - 14CD34C92D4F43B300789E6D /* Frameworks */, - 14CD34CA2D4F43B300789E6D /* Resources */, + F9E650D65F4A45AFD5FC8C65 /* [CP] Check Pods Manifest.lock */, + 147F89012E83CBC000BFD48B /* Sources */, + 147F89022E83CBC000BFD48B /* Frameworks */, + 147F89032E83CBC000BFD48B /* Resources */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( - 14CD34CD2D4F43B300789E6D /* Share Extension */, + 147F89062E83CBC000BFD48B /* ShareExtension */, ); - name = "Share Extension"; - productName = "Share Extension"; - productReference = 14CD34CC2D4F43B300789E6D /* Share Extension.appex */; + name = ShareExtension; + productName = ShareExtension; + productReference = 147F89052E83CBC000BFD48B /* ShareExtension.appex */; productType = "com.apple.product-type.app-extension"; }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 31910CF38E8EA38D5B7FA35B /* [CP] Check Pods Manifest.lock */, + 4D8F8F659CC99985592659DA /* [CP] Check Pods Manifest.lock */, 9705A1C41CF9048500538489 /* Embed Frameworks */, E1B3F2B6291BA8040042F7B2 /* Embed App Extensions */, - 12D1C02525059AFAAE4402B0 /* [CP] Embed Pods Frameworks */, 9740EEB61CF901F6004384FC /* Run Script */, + 147F899D2EA0F0C900BFD48B /* ShellScript */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 60A233074E1A78C397C3A119 /* [CP] Copy Pods Resources */, + 9992624A3087AEB0CE096628 /* [CP] Embed Pods Frameworks */, + 7F5ECBC81637C095A2530E83 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( - 14CD34D52D4F43B300789E6D /* PBXTargetDependency */, + 147F890E2E83CBC000BFD48B /* PBXTargetDependency */, ); name = Runner; productName = Runner; @@ -400,12 +424,12 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1620; + LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { - 14CD34CB2D4F43B300789E6D = { - CreatedOnToolsVersion = 16.2; + 147F89042E83CBC000BFD48B = { + CreatedOnToolsVersion = 26.0.1; }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -427,13 +451,13 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 14CD34CB2D4F43B300789E6D /* Share Extension */, + 147F89042E83CBC000BFD48B /* ShareExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 14CD34CA2D4F43B300789E6D /* Resources */ = { + 147F89032E83CBC000BFD48B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -451,68 +475,69 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, C62BB8282689B1CA00DAAC43 /* nkn.p12 in Resources */, + 148FFB1A2E60499C005410D0 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 12D1C02525059AFAAE4402B0 /* [CP] Embed Pods Frameworks */ = { + 147F899D2EA0F0C900BFD48B /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + ); outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "set -euo pipefail\n\nOUT_FILE=\"$SRCROOT/Runner/Config.generated.swift\"\n\n# 这些值来自 .xcconfig(或 Scheme/CI 环境变量)\n: \"${P12_FILE_NAME:=}\"\n: \"${P12_FILE_PASSWORD:=}\"\n\ncat > \"$OUT_FILE\" < /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 4D8F8F659CC99985592659DA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 60A233074E1A78C397C3A119 /* [CP] Copy Pods Resources */ = { + 7F5ECBC81637C095A2530E83 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -520,10 +545,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; @@ -544,7 +573,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - BCFE6FCF12ECC9CC408AED6A /* [CP] Check Pods Manifest.lock */ = { + 9992624A3087AEB0CE096628 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F9E650D65F4A45AFD5FC8C65 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -559,7 +609,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Share Extension-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -569,7 +619,7 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 14CD34C82D4F43B300789E6D /* Sources */ = { + 147F89012E83CBC000BFD48B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -584,6 +634,7 @@ C6FFD764266E09E600410547 /* CommonOc.m in Sources */, C62BB8192689AA0E00DAAC43 /* NWSSLConnection.m in Sources */, C6824D2E272E4EA900BA7AAF /* APNsPort.swift in Sources */, + 147F899F2EA0F29300BFD48B /* Config.generated.swift in Sources */, C62BB8182689AA0E00DAAC43 /* NWType.m in Sources */, C6824D2B272E4EA900BA7AAF /* PKCS12Adapter.swift in Sources */, E1852ED82878179100FC45FD /* Crypto.swift in Sources */, @@ -601,6 +652,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, C62BB8162689AA0E00DAAC43 /* NWPushFeedback.m in Sources */, C62BB81A2689AA0E00DAAC43 /* NWPusher.m in Sources */, + 147F89C22EAB685B00BFD48B /* Search.swift in Sources */, C6824D29272E4EA900BA7AAF /* Connection.swift in Sources */, 149BC80728FEAEB000D27A6D /* DnsResolver.swift in Sources */, ); @@ -609,10 +661,10 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 14CD34D52D4F43B300789E6D /* PBXTargetDependency */ = { + 147F890E2E83CBC000BFD48B /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 14CD34CB2D4F43B300789E6D /* Share Extension */; - targetProxy = 14CD34D42D4F43B300789E6D /* PBXContainerItemProxy */; + target = 147F89042E83CBC000BFD48B /* ShareExtension */; + targetProxy = 147F890D2E83CBC000BFD48B /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -636,9 +688,9 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 14CD34D92D4F43B300789E6D /* Debug */ = { + 147F89102E83CBC000BFD48B /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FF85B414D2042FADD297DB3E /* Pods-Share Extension.debug.xcconfig */; + baseConfigurationReference = 584544DF38FA171ACDCA775B /* Pods-ShareExtension.debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -648,7 +700,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -656,10 +708,10 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "Share Extension/Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Share Extension"; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -669,20 +721,23 @@ MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.nkn.nmobile.Share-Extension"; + PRODUCT_BUNDLE_IDENTIFIER = org.nkn.nmobile.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; - 14CD34DA2D4F43B300789E6D /* Release */ = { + 147F89112E83CBC000BFD48B /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2F1390E9C951F8699CC4CC1B /* Pods-Share Extension.release.xcconfig */; + baseConfigurationReference = 7F6144DC7E3F6B8544F0EE4F /* Pods-ShareExtension.release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -692,7 +747,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -700,10 +755,10 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "Share Extension/Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Share Extension"; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -712,18 +767,21 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.nkn.nmobile.Share-Extension"; + PRODUCT_BUNDLE_IDENTIFIER = org.nkn.nmobile.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - 14CD34DB2D4F43B300789E6D /* Profile */ = { + 147F89122E83CBC000BFD48B /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 00DDD0F3B83E3CC2A5212805 /* Pods-Share Extension.profile.xcconfig */; + baseConfigurationReference = 3C6D0BD6B5E0E014F40856BD /* Pods-ShareExtension.profile.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -733,7 +791,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements"; + CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -741,10 +799,10 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "Share Extension/Info.plist"; - INFOPLIST_KEY_CFBundleDisplayName = "Share Extension"; + INFOPLIST_FILE = ShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -753,10 +811,13 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "org.nkn.nmobile.Share-Extension"; + PRODUCT_BUNDLE_IDENTIFIER = org.nkn.nmobile.ShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -804,7 +865,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -817,13 +878,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 330; + CURRENT_PROJECT_VERSION = 364; DEVELOPMENT_TEAM = 67P82ZQDAS; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -889,7 +949,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -939,7 +999,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -954,13 +1014,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 330; + CURRENT_PROJECT_VERSION = 364; DEVELOPMENT_TEAM = 67P82ZQDAS; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -983,13 +1042,12 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 330; + CURRENT_PROJECT_VERSION = 364; DEVELOPMENT_TEAM = 67P82ZQDAS; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -1010,12 +1068,12 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 14CD34D82D4F43B300789E6D /* Build configuration list for PBXNativeTarget "Share Extension" */ = { + 147F89142E83CBC000BFD48B /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( - 14CD34D92D4F43B300789E6D /* Debug */, - 14CD34DA2D4F43B300789E6D /* Release */, - 14CD34DB2D4F43B300789E6D /* Profile */, + 147F89102E83CBC000BFD48B /* Debug */, + 147F89112E83CBC000BFD48B /* Release */, + 147F89122E83CBC000BFD48B /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 37be44085..1a6f25533 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> Bool { super.application(application, didFinishLaunchingWithOptions: launchOptions) + FirebaseApp.configure() + if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate } @@ -23,25 +25,14 @@ import receive_sharing_intent Crypto.register(controller: controller) EthResolver.register(controller: controller) DnsResolver.register(controller: controller) - + SearchService.register(controller: controller) + registerNotification(); - - // NotificationCenter.default.addObserver(self, selector:#selector(becomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) - // NotificationCenter.default.addObserver(self, selector:#selector(becomeDeath), name: UIApplication.didEnterBackgroundNotification, object: nil) - + return super.application(application, didFinishLaunchingWithOptions: launchOptions) - // return true; // FIXED: with no share data } override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - let sharingIntent = SwiftReceiveSharingIntentPlugin.instance - if sharingIntent.hasMatchingSchemePrefix(url: url) { - return sharingIntent.application(app, open: url, options: options) - } - - // For example - // return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[.sourceApplication] as? String) - // return false return super.application(app, open: url, options:options) } @@ -67,36 +58,13 @@ import receive_sharing_intent } } -// @objc func becomeActive(noti:Notification) { -// //APNSPushService.shared().connectAPNS() -// } - -// @objc func becomeDeath(noti:Notification) { -// APNSPushService.shared().disConnectAPNS() -// } - override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // deviceToken = 32 bytes let formatDeviceToken = deviceToken.map { String(format: "%02.2hhx", arguments: [$0]) }.joined() print("Application - GetDeviceToken - token = \(formatDeviceToken)") UserDefaults.standard.setValue(formatDeviceToken, forKey: "nkn_device_token") - -// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) { -// APNSPushService.shared().connectAPNS(); -// } } -// override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { -// print("Application - didReceiveRemoteNotification - onReceive - userInfo = \(userInfo)") -// let aps = userInfo["aps"] as? [String: Any] -// let alert = aps?["alert"] as? [String: Any] -// var resultMap: [String: Any] = [String: Any]() -// resultMap["title"] = alert?["title"] -// resultMap["content"] = alert?["body"] -// resultMap["isApplicationForeground"] = application.applicationState == UIApplication.State.active -// Common.eventAdd(name: "onRemoteMessageReceived", map: resultMap) -// } - override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { let userInfo = notification.request.content.userInfo @@ -108,25 +76,10 @@ import receive_sharing_intent resultMap["content"] = alert?["body"] resultMap["isApplicationForeground"] = UIApplication.shared.applicationState == UIApplication.State.active Common.eventAdd(name: "onRemoteMessageReceived", map: resultMap) - // completionHandler([.alert, .badge, .sound]) // show notification on flutter, not here } -// override func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { -// let userInfo = response.notification.request.content.userInfo -// print("Application - userNotificationCenter - onClick - userInfo = \(userInfo)") -// let aps = userInfo["aps"] as? [String: Any] -// let alert = aps?["alert"] as? [String: Any] -// var resultMap: [String: Any] = [String: Any]() -// resultMap["title"] = alert?["title"] -// resultMap["content"] = alert?["body"] -// resultMap["isApplicationForeground"] = UIApplication.shared.applicationState == UIApplication.State.active -// Common.eventAdd(name: "onNotificationClick", map: resultMap) -// completionHandler() -// } - override func applicationWillResignActive(_ application: UIApplication) { window?.addSubview(self.visualEffectView) - } override func applicationDidEnterBackground(_ application: UIApplication) { diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 000000000..2abdc5030 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 980911608673-jsfne43egn1j5595bh58qqiohqu09hut.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.980911608673-jsfne43egn1j5595bh58qqiohqu09hut + API_KEY + AIzaSyAi6yXWU62vUrE65e2xq-zGyzPRvCmKhGo + GCM_SENDER_ID + 980911608673 + PLIST_VERSION + 1 + BUNDLE_ID + org.nkn.nmobile + PROJECT_ID + nmobile + STORAGE_BUCKET + nmobile.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:980911608673:ios:c30a977e5c3c89df5d6b20 + DATABASE_URL + https://nmobile.firebaseio.com + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ed86aab98..f5934c853 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,22 +2,21 @@ - AppGroupId - $(CUSTOM_GROUP_ID) - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleURLSchemes - - ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) - - - - + UIDesignRequiresCompatibility + + AppGroupId + $(CUSTOM_GROUP_ID) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + BGTaskSchedulerPermittedIdentifiers org.nkn.nmobile.backgroundtask @@ -63,6 +62,24 @@ LSSupportsOpeningDocumentsInPlace + CFBundleDocumentTypes + + + CFBundleTypeName + ShareHandler + LSHandlerRank + Alternate + LSItemContentTypes + + public.file-url + public.image + public.text + public.movie + public.url + public.data + + + NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/ios/Share Extension/Info.plist b/ios/Share Extension/Info.plist deleted file mode 100644 index 2ef9ea0a9..000000000 --- a/ios/Share Extension/Info.plist +++ /dev/null @@ -1,46 +0,0 @@ - - - - - AppGroupId - $(CUSTOM_GROUP_ID) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - NSExtension - - NSExtensionAttributes - - PHSupportedMediaTypes - - - Video - - Image - - NSExtensionActivationRule - - - NSExtensionActivationSupportsText - - - NSExtensionActivationSupportsWebURLWithMaxCount - 1 - - NSExtensionActivationSupportsImageWithMaxCount - 100 - - NSExtensionActivationSupportsMovieWithMaxCount - 100 - - - NSExtensionActivationSupportsFileWithMaxCount - 1 - - - NSExtensionMainStoryboard - MainInterface - NSExtensionPointIdentifier - com.apple.share-services - - - \ No newline at end of file diff --git a/ios/Share Extension/ShareViewController.swift b/ios/Share Extension/ShareViewController.swift deleted file mode 100644 index 90f1bc8a7..000000000 --- a/ios/Share Extension/ShareViewController.swift +++ /dev/null @@ -1,38 +0,0 @@ -// If you get no such module 'receive_sharing_intent' error. -// Go to Build Phases of your Runner target and -// move `Embed Foundation Extension` to the top of `Thin Binary`. -import receive_sharing_intent - -class ShareViewController: RSIShareViewController { - - // Use this method to return false if you don't want to redirect to host app automatically. - // Default is true - override func shouldAutoRedirect() -> Bool { - return false - } - -} - -//import UIKit -//import Social -// -//class ShareViewController: SLComposeServiceViewController { -// -// override func isContentValid() -> Bool { -// // Do validation of contentText and/or NSExtensionContext attachments here -// return true -// } -// -// override func didSelectPost() { -// // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. -// -// // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. -// self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) -// } -// -// override func configurationItems() -> [Any]! { -// // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. -// return [] -// } -// -//} diff --git a/ios/Share Extension/Base.lproj/MainInterface.storyboard b/ios/ShareExtension/Base.lproj/MainInterface.storyboard similarity index 100% rename from ios/Share Extension/Base.lproj/MainInterface.storyboard rename to ios/ShareExtension/Base.lproj/MainInterface.storyboard diff --git a/ios/ShareExtension/Info.plist b/ios/ShareExtension/Info.plist new file mode 100644 index 000000000..67c8910d3 --- /dev/null +++ b/ios/ShareExtension/Info.plist @@ -0,0 +1,53 @@ + + + + + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + NSExtension + + NSExtensionAttributes + + + IntentsSupported + + INSendMessageIntent + + + NSExtensionActivationRule + + + + SUBQUERY ( + extensionItems, + $extensionItem, + SUBQUERY ( + $extensionItem.attachments, + $attachment, + ( + ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" + || ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" + ) + ).@count > 0 + ).@count > 0 + + PHSupportedMediaTypes + + Video + Image + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + \ No newline at end of file diff --git a/ios/Share Extension/Share Extension.entitlements b/ios/ShareExtension/ShareExtension.entitlements similarity index 100% rename from ios/Share Extension/Share Extension.entitlements rename to ios/ShareExtension/ShareExtension.entitlements diff --git a/ios/ShareExtension/ShareViewController.swift b/ios/ShareExtension/ShareViewController.swift new file mode 100644 index 000000000..b17cff89c --- /dev/null +++ b/ios/ShareExtension/ShareViewController.swift @@ -0,0 +1,3 @@ +import share_handler_ios_models + +class ShareViewController: ShareHandlerIosViewController {} diff --git a/lib/app.dart b/lib/app.dart index 05e0e85bb..978c69f12 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -16,11 +16,12 @@ import 'package:nmobile/native/common.dart'; import 'package:nmobile/schema/wallet.dart'; import 'package:nmobile/screens/chat/home.dart'; import 'package:nmobile/screens/settings/home.dart'; -import 'package:nmobile/screens/wallet/home.dart'; import 'package:nmobile/services/task.dart'; import 'package:nmobile/utils/asset.dart'; import 'package:nmobile/utils/logger.dart'; -import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:share_handler/share_handler.dart'; + +import 'upgrade/upgrade_checker.dart'; class AppScreen extends StatefulWidget { static const String routeName = '/'; @@ -62,6 +63,7 @@ class _AppScreenState extends State with WidgetsBindingObserver { Completer loginCompleter = Completer(); bool isAuthProgress = false; + SharedMedia? media; @override void initState() { @@ -80,6 +82,8 @@ class _AppScreenState extends State with WidgetsBindingObserver { // await backgroundFetchService.install(); await localNotification.init(); await audioHelper.init(); + + UpgradeChecker.checkAndShowDialog(Settings.appContext); }); application.mounted(); // await @@ -117,25 +121,25 @@ class _AppScreenState extends State with WidgetsBindingObserver { } }); - // For sharing images coming from outside the app while the app is in the memory - _intentDataMediaStreamSubscription = ReceiveSharingIntent.instance.getMediaStream().listen((List? values) async { - if (values == null || values.isEmpty) return; - await loginCompleter.future; - ShareHelper.showWithFiles(this.context, values); - }, onError: (err, stack) { - handleError(err, stack); - }); + initPlatformState(); + // wallet + taskService.addTask(TaskService.KEY_WALLET_BALANCE, 60, (key) => walletCommon.queryAllBalance(), delayMs: 1 * 1000); + } + + Future initPlatformState() async { + final handler = ShareHandlerPlatform.instance; + media = await handler.getInitialSharedMedia(); - // For sharing images coming from outside the app while the app is closed - ReceiveSharingIntent.instance.getInitialMedia().then((List? values) async { - if (values == null || values.isEmpty) return; + handler.sharedMediaStream.listen((SharedMedia media) async { + if (media.attachments?.isNotEmpty != true) return; + if (!mounted) return; + this.media = media; await loginCompleter.future; - ShareHelper.showWithFiles(this.context, values); + ShareHelper.showWithFiles(this.context, media); }); + if (!mounted) return; - - // wallet - taskService.addTask(TaskService.KEY_WALLET_BALANCE, 60, (key) => walletCommon.queryAllBalance(), delayMs: 1 * 1000); + setState(() {}); } @override @@ -216,12 +220,12 @@ class _AppScreenState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - if (Platform.isAndroid) { + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + if (!didPop && Platform.isAndroid) { await Common.backDesktop(); } - return false; }, child: Scaffold( backgroundColor: application.theme.backgroundColor, diff --git a/lib/common/application.dart b/lib/common/application.dart index 45066dac8..6368ddf04 100644 --- a/lib/common/application.dart +++ b/lib/common/application.dart @@ -9,6 +9,7 @@ import 'package:nmobile/utils/logger.dart'; typedef Func = Future Function(); class Application { + static const env = String.fromEnvironment("APP_ENV", defaultValue: "production"); List _initializeFutures = []; List _mountedFutures = []; @@ -33,6 +34,10 @@ class Application { int goBackgroundAt = 0; int goForegroundAt = 0; + + bool get isDev => env == "development"; + bool get isProd => env == "production"; + Application(); void init() { @@ -48,7 +53,20 @@ class Application { goForegroundAt = DateTime.now().millisecondsSinceEpoch; _appLifeSink.add(false); } else { - logger.d("Application - appLifeStream - nothing - states:$states"); + if (states.length >= 2 && states[1] == AppLifecycleState.paused && !inBackGround && !inSystemSelecting) { + logger.i("Application - appLifeStream - in background (missed detection) - states:$states"); + inBackGround = true; + goBackgroundAt = DateTime.now().millisecondsSinceEpoch; + _appLifeSink.add(true); + } + else if (states.length >= 2 && states[1] == AppLifecycleState.resumed && inBackGround) { + logger.i("Application - appLifeStream - in foreground (missed detection) - states:$states"); + inBackGround = false; + goForegroundAt = DateTime.now().millisecondsSinceEpoch; + _appLifeSink.add(false); + } else { + logger.d("Application - appLifeStream - nothing - states:$states"); + } } }); } @@ -77,11 +95,14 @@ class Application { await Future.wait(futures); } - // resumed -> inactive -> paused + // resumed -> inactive -> hidden -> paused (iOS with hidden state) + // resumed -> inactive -> paused (iOS without hidden state / Android) bool _isGoBackground(List states) { if (states.length >= 2) { if (Platform.isIOS) { - return !inSystemSelecting && (states[0] == AppLifecycleState.inactive) && (states[1] == AppLifecycleState.paused); + return !inSystemSelecting && + ((states[0] == AppLifecycleState.inactive && states[1] == AppLifecycleState.paused) || + (states[0] == AppLifecycleState.hidden && states[1] == AppLifecycleState.paused)); } else if (Platform.isAndroid) { return !inSystemSelecting && (states[0] == AppLifecycleState.inactive) && (states[1] == AppLifecycleState.paused); } @@ -89,12 +110,18 @@ class Application { return false; } - // paused -> inactive(just ios) -> resumed + // paused -> hidden -> inactive -> resumed (iOS with hidden state) + // paused -> inactive -> resumed (iOS without hidden state) + // paused -> resumed (Android) bool _isFromBackground(List states) { if (states.length >= 2) { if (Platform.isIOS) { - return inBackGround && (states[0] == AppLifecycleState.paused) && (states[1] == AppLifecycleState.inactive); + return inBackGround && + ((states[0] == AppLifecycleState.paused && states[1] == AppLifecycleState.resumed) || + (states[0] == AppLifecycleState.hidden && states[1] == AppLifecycleState.resumed) || + (states[0] == AppLifecycleState.inactive && states[1] == AppLifecycleState.resumed)); } else if (Platform.isAndroid) { + // Android: paused -> resumed return inBackGround && (states[0] == AppLifecycleState.paused) && (states[1] == AppLifecycleState.resumed); } } diff --git a/lib/common/authentication.dart b/lib/common/authentication.dart index 0e7b66a37..bbda0fa11 100644 --- a/lib/common/authentication.dart +++ b/lib/common/authentication.dart @@ -64,7 +64,13 @@ class Authorization { String? pwd; try { pwd = await walletCommon.getPassword(walletAddress); - if (!authOk || pwd == null || pwd.isEmpty) { + // Default Account / empty password wallet: skip dialog, use empty password to login + if (pwd == '' && Settings.biometricsAuthentication) { + return null; + } else if (pwd == '') { + return ''; + } + if (!authOk || pwd == null) { onInput?.call(true); String? password = await BottomDialog.of(Settings.appContext).showInput( title: Settings.locale((s) => s.verify_wallet_password), diff --git a/lib/common/chat/chat_in.dart b/lib/common/chat/chat_in.dart index 26ca5b5aa..a7c604dd6 100644 --- a/lib/common/chat/chat_in.dart +++ b/lib/common/chat/chat_in.dart @@ -18,6 +18,8 @@ import 'package:nmobile/utils/logger.dart'; import 'package:nmobile/utils/parallel_queue.dart'; import 'package:nmobile/utils/path.dart'; +import '../../storages/message.dart'; + class ChatInCommon with Tag { ChatInCommon(); @@ -167,6 +169,12 @@ class ChatInCommon with Tag { } } } + bool blocked = await contactCommon.isBlocked(received.sender); + if (blocked) { + logger.w("$TAG - _handleMessage - blocked - store as deleted - sender:${received.sender} - targetId:${received.targetId} - type:${received.contentType}"); + received.isDelete = true; + received.deleteAt = DateTime.now().millisecondsSinceEpoch; + } // receive switch (received.contentType) { case MessageContentType.ping: @@ -229,6 +237,9 @@ class ChatInCommon with Tag { case MessageContentType.topicKickOut: await _receiveTopicKickOut(received); break; + case MessageContentType.revoke: + await _receiveRevoke(received); + break; case MessageContentType.privateGroupInvitation: insertOk = await _receivePrivateGroupInvitation(received); break; @@ -942,6 +953,25 @@ class ChatInCommon with Tag { await privateGroupCommon.updatePrivateGroupMembers(received.sender, groupId, version, members); } + Future _receiveRevoke(MessageSchema received) async { + // content is target msgId + String? targetMsgId = received.content?.toString(); + if (targetMsgId == null || targetMsgId.isEmpty) return false; + MessageSchema? target = await messageCommon.query(targetMsgId); + if (target == null) return false; + // Only sender can revoke + String sender = received.sender; + bool isSameSender = target.isOutbound + ? (clientCommon.address == sender) + : (target.sender == sender); + if (!isSameSender) return false; + // Soft delete first + await MessageStorage.instance.updateIsDelete(target.msgId, true); + // Optional: deep delete pieces/content + await messageCommon.messageDelete(target, notify: true); + return true; + } + Future _deletePieces(String msgId) async { final limit = 20; List pieces = []; diff --git a/lib/common/chat/chat_out.dart b/lib/common/chat/chat_out.dart index 63ac72041..0ac09bb8a 100644 --- a/lib/common/chat/chat_out.dart +++ b/lib/common/chat/chat_out.dart @@ -233,7 +233,7 @@ class ChatOutCommon with Tag { if (notification && (contact != null) && !contact.isMe) { deviceInfoCommon.queryDeviceTokenList(contact.address).then((tokens) async { logger.d("$TAG - _sendWithContact - push notification - count:${tokens.length} - target:${contact.address} - tokens:$tokens"); - List results = await RemoteNotification.send(tokens); + List results = await RemoteNotification.send(tokens, targetAddress: contact.address); if (results.isNotEmpty) { message.options = MessageOptions.setPushNotifyId(message.options, results[0]); await messageCommon.updateMessageOptions(message, message.options, notify: false); @@ -320,7 +320,7 @@ class ChatOutCommon with Tag { if (_contact.isMe) continue; deviceInfoCommon.queryDeviceTokenList(_contact.address).then((tokens) { logger.d("$TAG - _sendWithTopic - push notification - count:${tokens.length} - target:${_contact.address} - topic:${topic.topicId} - tokens:$tokens"); - RemoteNotification.send(tokens); // await // no need result + RemoteNotification.send(tokens, targetAddress: _contact.address); // await // no need result }); } }); @@ -388,7 +388,7 @@ class ChatOutCommon with Tag { if (_contact.isMe) continue; deviceInfoCommon.queryDeviceTokenList(_contact.address).then((tokens) { logger.d("$TAG - _sendWithPrivateGroup - push notification - count:${tokens.length} - target:${_contact.address} - groupId:${group.groupId} - tokens:$tokens"); - RemoteNotification.send(tokens); // await // no need result + RemoteNotification.send(tokens, targetAddress: _contact.address); // await // no need result }); } }); @@ -865,6 +865,41 @@ class ChatOutCommon with Tag { return await _sendVisible(message, maxHoldingSeconds: maxHoldingSeconds); } + Future sendRevoke(String msgId) async { + if (msgId.isEmpty) return false; + // Only allow sender to revoke + MessageSchema? origin = await messageCommon.query(msgId); + if (origin == null || !origin.isOutbound) return false; + + String targetId = origin.targetId; + int targetType = origin.targetType; + // Build revoke message schema per target type + MessageSchema revoke = MessageSchema.fromSend( + targetId, + targetType, + MessageContentType.revoke, + msgId, + ); + // no DB persistence for revoke command + revoke.data = MessageData.getRevoke(msgId); + + // Send according to targetType + Uint8List? pid; + if (targetType == SessionType.TOPIC) { + TopicSchema? topic = TopicSchema.create(targetId, type: SessionType.TOPIC); + pid = await _sendWithTopic(topic, revoke, notification: false); + } else if (targetType == SessionType.PRIVATE_GROUP) { + PrivateGroupSchema? group = PrivateGroupSchema.create(targetId, targetId, type: SessionType.PRIVATE_GROUP); + pid = await _sendWithPrivateGroup(group, revoke, notification: false); + } else { + ContactSchema? contact = await chatCommon.contactHandle(revoke); + pid = await _sendWithContact(contact, revoke, notification: false); + } + bool ok = pid?.isNotEmpty == true; + logger.i("$TAG - sendRevoke - type:$targetType - dest:$targetId - msgId:$msgId - ok:$ok"); + return ok; + } + Future saveIpfs(dynamic target, Map data) async { // content String contentPath = data["path"]?.toString() ?? ""; diff --git a/lib/common/client/client.dart b/lib/common/client/client.dart index 5fcfad2d8..f95a182f0 100644 --- a/lib/common/client/client.dart +++ b/lib/common/client/client.dart @@ -188,15 +188,12 @@ class ClientCommon with Tag { } } if (c != null) { - logger.i("$TAG - signIn - try success - tryTimes:$tryTimes - address:${c.address} - wallet:$wallet - password:$password"); success = true; break; } else if (!canTry) { - logger.e("$TAG - signIn - try broken - tryTimes:$tryTimes - address:${c?.address} - wallet:$wallet - password:$password"); await signOut(clearWallet: true, closeDB: true, lock: false); break; } - logger.w("$TAG - signIn - try again - tryTimes:$tryTimes - wallet:$wallet - password:$password"); if ((tryTimes > 0) && isNetworkOk) await RPC.setRpcServers(wallet.address, []); tryTimes++; _statusSink.add(ClientConnectStatus.connecting); // need flush @@ -215,11 +212,11 @@ class ClientCommon with Tag { logger.w("$TAG - _signIn - wait network ok"); await Future.delayed(Duration(milliseconds: 500)); } - // password + // password (allow empty string if stored password is empty) try { - if ((password == null) || password.isEmpty) { + if (password == null) { logger.w("$TAG - _signIn - password is null - wallet:$wallet"); - return {"client": null, "canTry": false}; // , "text": "password empty" + return {"client": null, "canTry": false}; } if (!(await walletCommon.isPasswordRight(wallet.address, password))) { logger.w("$TAG - _signIn - password error - wallet:$wallet"); @@ -340,7 +337,7 @@ class ClientCommon with Tag { await chatInCommon.waitReceiveQueues("_signOut"); // wait db_insert from onMessage await chatInCommon.pause(reset: closeDB); client = null; - if (clearWallet) BlocProvider.of(Settings.appContext).add(DefaultWallet(null)); + if (closeDB) await dbCommon.close(); return true; } catch (e, st) { @@ -470,15 +467,12 @@ class ClientCommon with Tag { bool canTry = result["canTry"]; password = result["password"]?.toString(); if (c != null) { - logger.i("$TAG - reconnect - try success - tryTimes:$tryTimes - address:${c.address} - wallet:$wallet - password:$password"); success = true; break; } else if (!canTry) { - logger.e("$TAG - reconnect - try broken - tryTimes:$tryTimes - address:${c?.address} - wallet:$wallet - password:$password"); await signOut(clearWallet: true, closeDB: true, lock: false); break; } - logger.w("$TAG - reconnect - try again - tryTimes:$tryTimes - wallet:$wallet - password:$password"); if ((tryTimes > 0) && isNetworkOk) await RPC.setRpcServers(wallet.address, []); tryTimes++; _statusSink.add(ClientConnectStatus.connecting); // need first flush diff --git a/lib/common/contact/contact.dart b/lib/common/contact/contact.dart index b9bd02095..21b4b95bd 100644 --- a/lib/common/contact/contact.dart +++ b/lib/common/contact/contact.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:typed_data'; +import 'package:nkn_sdk_flutter/utils/hex.dart'; import 'package:nmobile/common/locator.dart'; import 'package:nmobile/common/name_service/resolver.dart'; import 'package:nmobile/helpers/error.dart'; @@ -11,10 +13,14 @@ import 'package:nmobile/utils/logger.dart'; import 'package:nmobile/utils/path.dart'; import 'package:uuid/uuid.dart'; +import '../search_service/search_service.dart'; + class ContactCommon with Tag { // ignore: close_sinks StreamController _addController = StreamController.broadcast(); + StreamSink get _addSink => _addController.sink; + Stream get addStream => _addController.stream; // ignore: close_sinks @@ -24,12 +30,16 @@ class ContactCommon with Tag { // ignore: close_sinks StreamController _updateController = StreamController.broadcast(); + StreamSink get _updateSink => _updateController.sink; + Stream get updateStream => _updateController.stream; // ignore: close_sinks StreamController _meUpdateController = StreamController.broadcast(); + StreamSink get meUpdateSink => _meUpdateController.sink; + Stream get meUpdateStream => _meUpdateController.stream; ContactCommon(); @@ -58,12 +68,26 @@ class ContactCommon with Tag { if ((address == null) || address.isEmpty) return null; // address String? clientAddress; - try { - Resolver resolver = Resolver(); - clientAddress = await resolver.resolve(address); - } catch (e, st) { - handleError(e, st); + // TODO + // try { + // Resolver resolver = Resolver(); + // clientAddress = await resolver.resolve(address); + // } catch (e, st) { + // handleError(e, st); + // } + + if (clientAddress == null) { + Uint8List? seed = clientCommon.getSeed(); + if (seed == null) { + String? walletAddress = await walletCommon.getDefaultAddress(); + seed = hexDecode(await walletCommon.getSeed(walletAddress)); + } + final service = await SearchService.createWithAuth(seed: seed); + + final response = await service.queryByID(address); + clientAddress = response?.nknAddress; } + bool resolveOk = false; if ((clientAddress != null) && Validate.isNknChatIdentifierOk(clientAddress)) { resolveOk = true; @@ -106,10 +130,11 @@ class ContactCommon with Tag { List contacts = await queryList(type: ContactType.me, orderDesc: false, limit: 1); String myAddress = selfAddress ?? clientCommon.address ?? ""; // TODO: fix multiple me - for(int i = 0; i < contacts.length; i++) { + // Iterate backwards so remove() does not shift indices we haven't visited + for (int i = contacts.length - 1; i >= 0; i--) { if (myAddress.isNotEmpty && contacts[i].address != myAddress) { await setType(contacts[i].address, ContactType.none, notify: false); - contacts.remove(contacts[i]); + contacts.removeAt(i); } } @@ -422,6 +447,31 @@ class ContactCommon with Tag { return data; } + Future isBlocked(String? address) async { + if (address == null || address.isEmpty) return false; + ContactSchema? contact = await query(address, fetchWalletAddress: false); + if (contact == null) return false; + var value = contact.data['blocked']; + if (value is bool) return value; + if (value is num) return value != 0; + String str = value?.toString().toLowerCase() ?? ""; + return str == "1" || str == "true"; + } + + Future?> setBlocked(String? address, bool blocked, {bool notify = false}) async { + if (address == null || address.isEmpty) return null; + Map? data = await ContactStorage.instance.setData(address, { + "blocked": blocked ? 1 : 0, + }); + if (data != null) { + logger.i("$TAG - setBlocked - success - blocked:$blocked - data:$data - address:$address"); + if (notify) queryAndNotify(address); + } else { + logger.w("$TAG - setBlocked - fail - blocked:$blocked - data:$data - address:$address"); + } + return data; + } + Future queryAndNotify(String? address) async { if (address == null || address.isEmpty) return; ContactSchema? updated = await query(address); diff --git a/lib/common/db/db.dart b/lib/common/db/db.dart index 5c6c3cda7..53529c73b 100644 --- a/lib/common/db/db.dart +++ b/lib/common/db/db.dart @@ -78,7 +78,6 @@ class DB { String path = await getDBFilePath(publicKey); bool exists = await databaseExists(path); String password = seed.isEmpty ? "" : hexEncode(Uint8List.fromList(Hash.sha256(seed))); - logger.i("DB - open - exists:$exists - publicKey:$publicKey - seed:$seed - password:$password - path:$path"); if (!Platform.isIOS) { database = await _tryOpenDB(path, password, publicKey: publicKey, upgradeTip: true); @@ -156,7 +155,11 @@ class DB { } } } - if (database != null) _openedSink.add(true); + if (database != null) { + _openedSink.add(true); + } else { + _openedSink.add(false); + } } Future _tryOpenDB(String path, String password, {String publicKey = "", bool upgradeTip = false}) async { @@ -340,6 +343,77 @@ class DB { return join(databasesPath, '${NKN_DATABASE_NAME}_$publicKey.db'); } + /// Backup all databases that haven't been backed up yet + static Future backupAllUnbackedDatabases() async { + try { + final String dbDirPath = await getDatabasesPath(); + final Directory dbDir = Directory(dbDirPath); + + if (!await dbDir.exists()) { + logger.w("DB - backupAll - database directory does not exist: $dbDirPath"); + return; + } + + final List files = dbDir.listSync(); + final String dbPrefix = '${NKN_DATABASE_NAME}_'; + final String dbSuffix = '.db'; + final String backupSuffix = '_bak$dbSuffix'; + + int backedUpCount = 0; + int skippedCount = 0; + + for (final FileSystemEntity entity in files) { + if (entity is! File) continue; + + final String fileName = basename(entity.path); + + // Skip files that don't match the database naming pattern + if (!fileName.startsWith(dbPrefix) || !fileName.endsWith(dbSuffix)) { + continue; + } + + // Skip files that are already backups + if (fileName.contains('_bak')) { + continue; + } + + // Extract publicKey from filename: nkn_{publicKey}.db + final String publicKey = fileName.substring( + dbPrefix.length, + fileName.length - dbSuffix.length, + ); + + if (publicKey.isEmpty) { + continue; + } + + // Check if backup already exists + final String backupPath = join(dbDirPath, '${dbPrefix}${publicKey}${backupSuffix}'); + if (await databaseExists(backupPath)) { + logger.d("DB - backupAll - backup already exists, skipping: $fileName"); + skippedCount++; + continue; + } + + // Create backup + try { + final File sourceFile = File(entity.path); + await sourceFile.copy(backupPath); + logger.i("DB - backupAll - backed up: $fileName -> ${basename(backupPath)}"); + backedUpCount++; + } catch (e, st) { + logger.e("DB - backupAll - failed to backup $fileName: $e"); + handleError(e, st); + } + } + + logger.i("DB - backupAll - completed: $backedUpCount backed up, $skippedCount skipped"); + } catch (e, st) { + logger.e("DB - backupAll - error: $e"); + handleError(e, st); + } + } + Future needUpgrade(String publicKey) async { if (publicKey.isEmpty) return false; int? savedVersion = await SettingsStorage.getSettings("${SettingsStorage.DATABASE_VERSION}:$publicKey"); diff --git a/lib/common/message/session.dart b/lib/common/message/session.dart index d5eb2b963..159c6bd0b 100644 --- a/lib/common/message/session.dart +++ b/lib/common/message/session.dart @@ -196,6 +196,10 @@ class SessionCommon with Tag { return SessionStorage.instance.queryListRecent(offset: offset, limit: limit); } + Future> queryListBySearch(String query) { + return SessionStorage.instance.queryListBySearch(query); + } + Future totalUnReadCount() { return SessionStorage.instance.querySumUnReadCount(); } diff --git a/lib/common/push/badge.dart b/lib/common/push/badge.dart index 01cf624f0..8a4f9ef6e 100644 --- a/lib/common/push/badge.dart +++ b/lib/common/push/badge.dart @@ -13,7 +13,7 @@ class Badge { static Future checkEnable() async { if (isEnable != null) return isEnable!; isEnable = Platform.isIOS; - logger.v("Badge - checkEnable - isEnable:$isEnable"); + logger.d("Badge - checkEnable - isEnable:$isEnable"); return isEnable!; } @@ -21,7 +21,7 @@ class Badge { if (!(await checkEnable())) return; _queue.add(() async { _count = count; - logger.v("Badge - refreshCount - count:$_count"); + logger.d("Badge - refreshCount - count:$_count"); await _updateCount(); }); } @@ -31,7 +31,7 @@ class Badge { if (!(await checkEnable())) return; _queue.add(() async { _count += count; - logger.v("Badge - onCountUp - up:$count - count:$_count"); + logger.d("Badge - onCountUp - up:$count - count:$_count"); await _updateCount(); }); } @@ -41,7 +41,7 @@ class Badge { if (!(await checkEnable())) return; _queue.add(() async { _count -= count; - logger.v("Badge - onCountDown - down:$count - count:$_count"); + logger.d("Badge - onCountDown - down:$count - count:$_count"); await _updateCount(); }); } diff --git a/lib/common/push/remote_notification.dart b/lib/common/push/remote_notification.dart index 9c34403ce..c278dc5fd 100644 --- a/lib/common/push/remote_notification.dart +++ b/lib/common/push/remote_notification.dart @@ -9,8 +9,32 @@ import 'package:nmobile/utils/logger.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uuid/uuid.dart'; +class RecentSentPush { + final String nknAddress; + final String deviceToken; + final int at; + + RecentSentPush({required this.nknAddress, required this.deviceToken, required this.at}); +} + class RemoteNotification { - static Future> send(List tokens, {List? uuids, String? title, String? content}) async { + static const int _maxRecentSent = 30; + static final List recentSentList = []; + + static List get recentSentNotifications => + List.unmodifiable(recentSentList); + + static void _recordSent(String? targetAddress, String deviceToken) { + if (targetAddress == null || targetAddress.isEmpty) return; + recentSentList.insert(0, RecentSentPush( + nknAddress: targetAddress, + deviceToken: deviceToken, + at: DateTime.now().millisecondsSinceEpoch, + )); + while (recentSentList.length > _maxRecentSent) recentSentList.removeLast(); + } + + static Future> send(List tokens, {List? uuids, String? title, String? content, String? targetAddress}) async { if (!Settings.notificationPushEnable) return []; if (tokens.isEmpty) return []; List results = []; @@ -35,6 +59,9 @@ class RemoteNotification { if ((result != null) && result.isNotEmpty) { results.add(result); } + if (targetAddress != null && targetAddress.isNotEmpty) { + _recordSent(targetAddress, deviceToken); + } } if (results.length == tokens.length) { logger.d("RemoteNotification - send - success - ${results.length}/${tokens.length}"); diff --git a/lib/common/search_service/search_service.dart b/lib/common/search_service/search_service.dart new file mode 100644 index 000000000..140afc133 --- /dev/null +++ b/lib/common/search_service/search_service.dart @@ -0,0 +1,132 @@ +import 'dart:typed_data'; + +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +import '../../native/search.dart'; + +class SearchService { + static String _apiBase = dotenv.get('NAME_SERVICE_API_BASE'); + + final SearchClient _client; + + SearchService._(this._client); + + /// Create a query-only search service (no authentication required) + static Future create() async { + final client = await SearchClient.create(apiBase: _apiBase); + return SearchService._(client); + } + + /// Create an authenticated search service + static Future createWithAuth({required Uint8List seed}) async { + final client = await SearchClient.createWithAuth( + apiBase: _apiBase, + seed: seed, + ); + return SearchService._(client); + } + + /// Query by keyword + Future> query(String keyword) async { + return await _client.query(keyword); + } + + /// Submit or update user data (requires authentication) + /// + /// Parameters: + /// - nknAddress: NKN client address (optional, format: "identifier.publickey" or just publickey) + /// If empty, defaults to publickey. Must be either "identifier.publickey" format or equal to publickey. + /// - customId: Custom identifier (optional, min 3 characters if provided) + /// - nickname: User nickname (optional) + /// - phoneNumber: Phone number (optional) + /// + /// If the publicKey already exists, this will UPDATE the user data. + /// + /// Examples: + /// ```dart + /// // Option 1: Use default (empty - will use publickey) + /// await service.submitUserData(nickname: 'Alice'); + /// + /// // Option 2: Use publickey directly + /// final pubKey = await service.getPublicKeyHex(); + /// await service.submitUserData(nknAddress: pubKey, nickname: 'Alice'); + /// + /// // Option 3: Use custom identifier.publickey format + /// await service.submitUserData( + /// nknAddress: 'alice.${await service.getPublicKeyHex()}', + /// customId: 'user123', + /// nickname: 'Alice', + /// ); + /// ``` + Future submitUserData({ + String? nknAddress, + String? customId, + String? nickname, + String? phoneNumber, + }) async { + await _client.submitUserData( + nknAddress: nknAddress, + customId: customId, + nickname: nickname, + phoneNumber: phoneNumber, + ); + } + + /// Query by ID (requires authentication and verification) + Future queryByID(String id) async { + return await _client.queryByID(id); + } + + /// Verify the client + Future verify() async { + await _client.verify(); + } + + /// Check if client is verified + Future isVerified() async { + return await _client.isVerified(); + } + + /// Get public key + Future getPublicKeyHex() async { + return await _client.getPublicKeyHex(); + } + + /// Get wallet address + Future getAddress() async { + return await _client.getAddress(); + } + + /// Get my own information by querying with nknAddress + /// + /// Parameters: + /// - nknAddress: (Optional) The NKN address to query. If not provided, uses the authenticated client's publickey. + /// Can be in "identifier.publickey" format or just the publickey. + /// + /// Returns the user's information if found, or null if not found. + /// + /// Example: + /// ```dart + /// // Query using the authenticated client's publickey (recommended) + /// final myInfo = await service.getMyInfo(); + /// + /// // Or query with a specific nknAddress + /// final pubKey = await service.getPublicKeyHex(); + /// final myInfo = await service.getMyInfo(nknAddress: pubKey); + /// + /// // Or with identifier.publickey format + /// final myInfo = await service.getMyInfo(nknAddress: 'alice.$pubKey'); + /// ``` + Future getMyInfo({String? nknAddress}) async { + // If nknAddress not provided, get the public key from the authenticated client + final address = nknAddress ?? await _client.getPublicKeyHex(); + + // Call the native getMyInfo method + return await _client.getMyInfo(address); + } + + /// Dispose the client and free resources + Future dispose() async { + await _client.dispose(); + } +} diff --git a/lib/common/settings.dart b/lib/common/settings.dart index 1bd9ac759..0e56fb4fd 100644 --- a/lib/common/settings.dart +++ b/lib/common/settings.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:android_id/android_id.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:nmobile/generated/l10n.dart'; import 'package:nmobile/helpers/ipfs.dart'; import 'package:nmobile/storages/settings.dart'; @@ -26,11 +27,11 @@ class Settings { // sentry static bool sentryEnable = true; - static const String sentryDSN = ''; + static late String sentryDSN; // infura - static const String infuraProjectId = ''; - static const String infuraApiKeySecret = ''; + static late String infuraProjectId; + static late String infuraApiKeySecret; // notification static bool notificationPushEnable = true; @@ -149,6 +150,11 @@ class Settings { static const int piecesMaxSize = piecesMaxTotal * piecesPreMaxLen; // <= 160K static init() async { + // init dotenv + sentryDSN = dotenv.get('SENTRY_DSN'); + infuraProjectId = dotenv.get('INFURA_PROJECT_ID'); + infuraApiKeySecret = dotenv.get('INFURA_API_KEY_SECRET'); + // settings var bug = await SettingsStorage.getSettings(SettingsStorage.CLOSE_BUG_UPLOAD_API); sentryEnable = !((bug?.toString() == "true") || (bug == true)); diff --git a/lib/common/wallet/wallet.dart b/lib/common/wallet/wallet.dart index 6391d6e6a..e8057255b 100644 --- a/lib/common/wallet/wallet.dart +++ b/lib/common/wallet/wallet.dart @@ -50,18 +50,17 @@ class WalletCommon with Tag { Future isPasswordRight(String? walletAddress, String? password) async { if (walletAddress == null || walletAddress.isEmpty) return false; - if (password == null || password.isEmpty) return false; + if (password == null) return false; String? storagePassword = await getPassword(walletAddress); - if (storagePassword?.isNotEmpty == true) { + if (storagePassword != null) { return password == storagePassword; - } else { - try { - final keystore = await getKeystore(walletAddress); - Wallet nknWallet = await Wallet.restore(keystore, config: WalletConfig(password: password)); - if (nknWallet.address.isNotEmpty) return true; - } catch (e) { - return false; - } + } + try { + final keystore = await getKeystore(walletAddress); + Wallet nknWallet = await Wallet.restore(keystore, config: WalletConfig(password: password)); + if (nknWallet.address.isNotEmpty) return true; + } catch (e) { + return false; } return false; } diff --git a/lib/components/chat/bubble.dart b/lib/components/chat/bubble.dart index 221f3df59..7fd40e542 100644 --- a/lib/components/chat/bubble.dart +++ b/lib/components/chat/bubble.dart @@ -15,7 +15,6 @@ import 'package:nmobile/components/button/button_icon.dart'; import 'package:nmobile/components/dialog/modal.dart'; import 'package:nmobile/components/text/label.dart'; import 'package:nmobile/components/text/markdown.dart'; -import 'package:nmobile/components/tip/popup_menu.dart' as PopMenu; import 'package:nmobile/helpers/error.dart'; import 'package:nmobile/schema/message.dart'; import 'package:nmobile/screens/common/media.dart'; @@ -30,6 +29,7 @@ class ChatBubble extends BaseStateFulWidget { final bool showTimeAndStatus; final bool hideTopMargin; final bool hideBotMargin; + final bool lastMessageHasReceipt; final Function(String)? onResend; ChatBubble({ @@ -37,6 +37,7 @@ class ChatBubble extends BaseStateFulWidget { this.showTimeAndStatus = true, this.hideTopMargin = false, this.hideBotMargin = false, + this.lastMessageHasReceipt = false, this.onResend, }); @@ -46,6 +47,8 @@ class ChatBubble extends BaseStateFulWidget { class _ChatBubbleState extends BaseStateFulWidgetState with Tag { GlobalKey _contentKey = GlobalKey(); + final MenuController _menuController = MenuController(); + static const double _menuMinWidth = 180; StreamSubscription? _onProgressStreamSubscription; StreamSubscription? _onPlayProgressSubscription; @@ -55,6 +58,7 @@ class _ChatBubbleState extends BaseStateFulWidgetState with Tag { double _fetchProgress = -1; double _playProgress = -1; String? _thumbnailPath; + Offset _menuAlignmentOffset = Offset.zero; @override void onRefreshArguments() { @@ -197,39 +201,15 @@ class _ChatBubbleState extends BaseStateFulWidgetState with Tag { } Widget _widgetStatusTip(bool self) { - // bool isSending = _message.status == MessageStatus.Sending; bool isSendFail = _message.status == MessageStatus.Error; - // bool isSendSuccess = _message.status == MessageStatus.SendSuccess; - // bool isSendReceipt = _message.status == MessageStatus.SendReceipt; - - // bool canProgress = _message.isContentFile && !_message.isTopic; - - // bool showSending = isSending && !canProgress; - // bool showProgress = isSending && canProgress && _uploadProgress < 1; - - // if (showSending) { - // return Padding( - // padding: const EdgeInsets.symmetric(horizontal: 10), - // child: SpinKitRing( - // color: application.theme.fontColor4, - // lineWidth: 1, - // size: 15, - // ), - // ); - // } else if (showProgress) { - // return Container( - // width: 40, - // height: 40, - // padding: EdgeInsets.all(10), - // child: CircularProgressIndicator( - // backgroundColor: application.theme.fontColor4.withAlpha(80), - // color: application.theme.primaryColor.withAlpha(200), - // strokeWidth: 2, - // value: _uploadProgress, - // ), - // ); - // } else - if (isSendFail) { + bool shouldShowResend = isSendFail; + if (!shouldShowResend && widget.lastMessageHasReceipt && _message.isOutbound) { + if (_message.status >= MessageStatus.Success && _message.status < MessageStatus.Receipt) { + shouldShowResend = true; + } + } + + if (shouldShowResend) { return ButtonIcon( icon: Icon( FontAwesomeIcons.circleExclamation, @@ -256,27 +236,6 @@ class _ChatBubbleState extends BaseStateFulWidgetState with Tag { }, ); } - // else if (isSendSuccess) { - // return Container( - // width: 5, - // height: 5, - // margin: EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5), - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(5), - // color: application.theme.strongColor.withAlpha(127), - // ), - // ); - // } else if (isSendReceipt) { - // return Container( - // width: 5, - // height: 5, - // margin: EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5), - // decoration: BoxDecoration( - // borderRadius: BorderRadius.circular(5), - // color: application.theme.successColor.withAlpha(127), - // ), - // ); - // } return SizedBox.shrink(); } @@ -297,6 +256,7 @@ class _ChatBubbleState extends BaseStateFulWidgetState with Tag { } var onTap = _onTapBubble(contentType); + bool includeCopy = contentType == MessageContentType.text || contentType == MessageContentType.textExtension; List childs = [SizedBox.shrink()]; switch (contentType) { @@ -322,21 +282,41 @@ class _ChatBubbleState extends BaseStateFulWidgetState with Tag { break; } - return GestureDetector( - key: _contentKey, - onTap: onTap, - child: Container( - constraints: BoxConstraints(maxWidth: maxWidth), - padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 5), - decoration: decoration, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - ...childs, - _widgetBubbleInfoBottom(), - ], - ), + return MenuAnchor( + controller: _menuController, + style: MenuStyle( + backgroundColor: MaterialStatePropertyAll(application.theme.backgroundColor4), + surfaceTintColor: MaterialStatePropertyAll(Colors.transparent), + elevation: MaterialStatePropertyAll(6), + shape: MaterialStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), + padding: MaterialStatePropertyAll(EdgeInsets.zero), ), + alignmentOffset: _menuAlignmentOffset, + menuChildren: _buildMenuChildren(includeCopy), + builder: (context, anchorController, child) { + return Semantics( + label: 'chat_bubble', + button: true, + child: GestureDetector( + key: _contentKey, + behavior: HitTestBehavior.opaque, + onTap: onTap, + onLongPressStart: (_) => _openMenu(), + child: Container( + constraints: BoxConstraints(maxWidth: maxWidth), + padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 5), + decoration: decoration, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ...childs, + _widgetBubbleInfoBottom(), + ], + ), + ), + ), + ); + }, ); } @@ -345,27 +325,7 @@ class _ChatBubbleState extends BaseStateFulWidgetState with Tag { switch (contentType) { case MessageContentType.text: case MessageContentType.textExtension: - onTap = () { - PopMenu.PopupMenu popupMenu = PopMenu.PopupMenu( - context: context, - items: [ - PopMenu.MenuItem( - userInfo: 0, - title: Settings.locale((s) => s.copy, ctx: context), - textStyle: TextStyle(color: application.theme.fontLightColor, fontSize: 12), - ), - ], - onClickMenu: (PopMenu.MenuItemProvider item) { - var index = (item as PopMenu.MenuItem).userInfo; - switch (index) { - case 0: - Util.copyText(_message.content?.toString() ?? ""); - break; - } - }, - ); - popupMenu.show(widgetKey: _contentKey); - }; + onTap = null; // Open menu via MenuAnchor break; case MessageContentType.image: // image + ipfs_image @@ -446,6 +406,167 @@ class _ChatBubbleState extends BaseStateFulWidgetState with Tag { return onTap; } + List _buildMenuChildren(bool includeCopy) { + List children = []; + Widget buildItem({required String keyName, required String label, required IconData icon, required VoidCallback onPressed, Color? textColor, Color? iconColor}) { + return Semantics( + label: keyName, + button: true, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: _menuMinWidth), + child: MenuItemButton( + key: ValueKey(keyName), + onPressed: onPressed, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text(label, style: TextStyle(color: textColor ?? application.theme.fontLightColor)), + ), + SizedBox(width: 12), + Icon(icon, size: 18, color: iconColor ?? application.theme.fontLightColor), + ], + ), + ), + ), + ); + } + void addDivider() { + children.add(Divider(height: 1, thickness: 0.5, color: application.theme.lineColor)); + } + if (includeCopy) { + children.add(buildItem( + keyName: 'chat_bubble_menu_copy', + label: Settings.locale((s) => s.copy, ctx: context), + icon: Icons.copy_outlined, + onPressed: () { + Util.copyText(_message.content?.toString() ?? ""); + }, + )); + addDivider(); + } + + if (_message.isOutbound) { + children.add(buildItem( + keyName: 'chat_bubble_menu_revoke', + label: Settings.locale((s) => s.revoke, ctx: context), + icon: Icons.undo, + onPressed: () { + ModalDialog.of(Settings.appContext).confirm( + title: Settings.locale((s) => s.tip, ctx: context), + content: Settings.locale((s) => s.confirm_revoke, ctx: context), + agree: Button( + width: double.infinity, + text: Settings.locale((s) => s.revoke, ctx: context), + backgroundColor: application.theme.strongColor, + onPressed: () async { + bool ok = await chatOutCommon.sendRevoke(_message.msgId); + if (ok) await messageCommon.messageDelete(_message, notify: true); + if (Navigator.of(this.context).canPop()) Navigator.pop(this.context); + }, + ), + reject: Button( + width: double.infinity, + text: Settings.locale((s) => s.cancel, ctx: context), + fontColor: application.theme.fontColor2, + backgroundColor: application.theme.backgroundLightColor, + onPressed: () { + if (Navigator.of(this.context).canPop()) Navigator.pop(this.context); + }, + ), + ); + }, + )); + addDivider(); + } + children.add(buildItem( + keyName: 'chat_bubble_menu_delete', + label: Settings.locale((s) => s.delete, ctx: context), + icon: Icons.delete_outline, + textColor: application.theme.fallColor, + iconColor: application.theme.fallColor, + onPressed: () { + ModalDialog.of(Settings.appContext).confirm( + title: Settings.locale((s) => s.delete_message_confirm_title, ctx: context), + agree: Button( + width: double.infinity, + text: Settings.locale((s) => s.delete, ctx: context), + backgroundColor: application.theme.strongColor, + onPressed: () async { + await messageCommon.messageDelete(_message, notify: true); + if (Navigator.of(this.context).canPop()) Navigator.pop(this.context); + }, + ), + reject: Button( + width: double.infinity, + text: Settings.locale((s) => s.cancel, ctx: context), + fontColor: application.theme.fontColor2, + backgroundColor: application.theme.backgroundLightColor, + onPressed: () { + if (Navigator.of(this.context).canPop()) Navigator.pop(this.context); + }, + ), + ); + }, + )); + return children; + } + + void _openMenu() { + if (!mounted) return; + try { + RenderBox? box = _contentKey.currentContext?.findRenderObject() as RenderBox?; + if (box != null) { + final Offset global = box.localToGlobal(Offset.zero); + final double anchorTop = global.dy; + final double anchorHeight = box.size.height; + final double anchorBottom = anchorTop + anchorHeight; + double visibleTop; + double visibleBottom; + final scrollable = Scrollable.of(context); + if (scrollable != null) { + final RenderBox? vpBox = scrollable.context.findRenderObject() as RenderBox?; + final double vpTop = vpBox?.localToGlobal(Offset.zero).dy ?? 0; + final double vpBottom = vpTop + scrollable.position.viewportDimension; + visibleTop = vpTop; + visibleBottom = vpBottom; + } else { + final media = MediaQuery.of(context); + visibleTop = media.padding.top; + visibleBottom = media.size.height - media.viewInsets.bottom; + } + + // Estimate menu height + final int itemsCount = 1 /*delete*/ + (_message.isOutbound ? 1 : 0) + ((_message.contentType == MessageContentType.text || _message.contentType == MessageContentType.textExtension) ? 1 : 0); + final double estimatedMenuHeight = itemsCount * 48.0; + final double gap = 8.0; + final double spaceBelow = visibleBottom - anchorBottom; + final double spaceAbove = anchorTop - visibleTop; + + Offset nextOffset = Offset.zero; + final double needed = estimatedMenuHeight + gap; + if (spaceBelow < needed) { + if (spaceAbove >= needed) { + nextOffset = Offset(0, -needed); + } else { + nextOffset = Offset(0, -max(0.0, spaceAbove - gap)); + } + } + if (nextOffset != _menuAlignmentOffset) { + setState(() { + _menuAlignmentOffset = nextOffset; + }); + } + } + } catch (_) { + // ignore + } + if (!_menuController.isOpen) { + _menuController.open(); + } + } + Widget _widgetBubbleInfoBottom() { Color color = _message.isOutbound ? application.theme.fontLightColor.withAlpha(178) : application.theme.fontColor2.withAlpha(178); diff --git a/lib/components/chat/emoji_picker.dart b/lib/components/chat/emoji_picker.dart new file mode 100644 index 000000000..76b5ab4be --- /dev/null +++ b/lib/components/chat/emoji_picker.dart @@ -0,0 +1,60 @@ +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart' as foundation; +import 'package:flutter/material.dart'; +import 'package:nmobile/common/locator.dart'; +import 'package:nmobile/common/settings.dart'; +import 'package:nmobile/components/layout/expansion_layout.dart'; + +class EmojiPickerBottomMenu extends StatelessWidget { + final String? target; + final bool show; + final Function(List> result)? onPicked; + final TextEditingController controller; + final _scrollController = ScrollController(); + + EmojiPickerBottomMenu({ + this.target, + this.show = false, + this.onPicked, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + double btnSize = Settings.screenWidth() / 6; + double iconSize = btnSize / 2; + + return ExpansionLayout( + isExpanded: show, + child: Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: application.theme.backgroundColor2), + ), + ), + child: EmojiPicker( + textEditingController: controller, + scrollController: _scrollController, + config: Config( + height: 256, + checkPlatformCompatibility: true, + viewOrderConfig: const ViewOrderConfig(), + emojiViewConfig: EmojiViewConfig( + // Issue: https://github.com/flutter/flutter/issues/28894 + emojiSizeMax: 28 * + (foundation.defaultTargetPlatform == + TargetPlatform.iOS + ? 1.2 + : 1.0), + ), + skinToneConfig: const SkinToneConfig(), + categoryViewConfig: const CategoryViewConfig(), + bottomActionBarConfig: const BottomActionBarConfig(), + searchViewConfig: const SearchViewConfig(), + ), + ), + ), + ); + } +} diff --git a/lib/components/chat/message_item.dart b/lib/components/chat/message_item.dart index f1e90de88..96bc6be5b 100644 --- a/lib/components/chat/message_item.dart +++ b/lib/components/chat/message_item.dart @@ -23,6 +23,7 @@ class ChatMessageItem extends BaseStateFulWidget { final MessageSchema message; final MessageSchema? prevMessage; final MessageSchema? nextMessage; + final bool lastMessageHasReceipt; final Function(ContactSchema, MessageSchema)? onAvatarPress; final Function(ContactSchema, MessageSchema)? onAvatarLonePress; final Function(String)? onResend; @@ -31,6 +32,7 @@ class ChatMessageItem extends BaseStateFulWidget { required this.message, this.prevMessage, this.nextMessage, + this.lastMessageHasReceipt = false, this.onAvatarPress, this.onAvatarLonePress, this.onResend, @@ -246,6 +248,7 @@ class _ChatMessageItemState extends BaseStateFulWidgetState { showTimeAndStatus: showTimeAndStatus, hideTopMargin: hideTopMargin, hideBotMargin: hideBotMargin, + lastMessageHasReceipt: this.widget.lastMessageHasReceipt, onResend: this.widget.onResend, ), SizedBox(height: hideBotMargin ? 0 : 4), @@ -366,7 +369,8 @@ class _ChatMessageItemState extends BaseStateFulWidgetState { type: LabelType.bodyRegular, ), onTap: () { - ContactProfileScreen.go(context, schema: this._sender); + final String peerAddress = widget.message.isOutbound ? widget.message.targetId : widget.message.sender; + ContactProfileScreen.go(context, address: peerAddress); }, ), ], diff --git a/lib/components/chat/send_bar.dart b/lib/components/chat/send_bar.dart index a2a927b93..7a4e9d6d6 100644 --- a/lib/components/chat/send_bar.dart +++ b/lib/components/chat/send_bar.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:nmobile/common/locator.dart'; import 'package:nmobile/common/settings.dart'; @@ -11,6 +12,8 @@ import 'package:nmobile/components/text/label.dart'; import 'package:nmobile/theme/theme.dart'; import 'package:nmobile/utils/asset.dart'; +import '../../common/client/client.dart'; + class ChatSendBar extends BaseStateFulWidget { static const String ChangeTypeReplace = "replace"; static const String ChangeTypeAppend = "append"; @@ -20,22 +23,26 @@ class ChatSendBar extends BaseStateFulWidget { final String? targetId; final String? disableTip; final VoidCallback? onMenuPressed; + final VoidCallback? onEmojiPressed; final Function(String)? onSendPress; final Function(bool)? onInputFocus; final Function(bool, bool, int)? onRecordTap; final Function(bool, bool)? onRecordLock; final Stream>? onChangeStream; + final TextEditingController? controller; ChatSendBar({ Key? key, required this.targetId, this.disableTip, this.onMenuPressed, + this.onEmojiPressed, this.onSendPress, this.onInputFocus, this.onRecordTap, this.onRecordLock, this.onChangeStream, + this.controller, }) : super(key: key); @override @@ -46,10 +53,14 @@ class _ChatSendBarState extends BaseStateFulWidgetState { static const double ActionWidth = 66; static const double ActionHeight = 70; + StreamSubscription? _clientStatusChangeSubscription; + int clientConnectStatus = clientCommon.status; + StreamSubscription? _onChangeSubscription; StreamSubscription? _onRecordProgressSubscription; - TextEditingController _inputController = TextEditingController(); + late TextEditingController _inputController; FocusNode _inputFocusNode = FocusNode(); + VoidCallback? _inputControllerListener; String? _draft; bool _canSendText = false; @@ -66,7 +77,31 @@ class _ChatSendBarState extends BaseStateFulWidgetState { @override void initState() { super.initState(); + + _clientStatusChangeSubscription = clientCommon.statusStream.listen((int status) { + if (clientConnectStatus != status) { + setState(() { + clientConnectStatus = status; + }); + } + }); + + _inputController = widget.controller ?? TextEditingController(); // input + _inputControllerListener = () { + String draft = _inputController.text; + if (draft.isNotEmpty) { + memoryCache.setDraft(widget.targetId, draft); + } else { + memoryCache.removeDraft(widget.targetId); + } + if (mounted) { + setState(() { + _canSendText = draft.isNotEmpty; + }); + } + }; + _inputController.addListener(_inputControllerListener!); _onChangeSubscription = widget.onChangeStream?.listen((event) { String? type = event["type"]; if (type == null || type.isEmpty) return; @@ -75,9 +110,6 @@ class _ChatSendBarState extends BaseStateFulWidgetState { } else if (type == ChatSendBar.ChangeTypeAppend) { _inputController.text += event["content"] ?? ""; } - setState(() { - _canSendText = _inputController.text.isNotEmpty; - }); }); _inputFocusNode.addListener(() { widget.onInputFocus?.call(_inputFocusNode.hasFocus); @@ -116,8 +148,12 @@ class _ChatSendBarState extends BaseStateFulWidgetState { @override void dispose() { + _clientStatusChangeSubscription?.cancel(); _onRecordProgressSubscription?.cancel(); _onChangeSubscription?.cancel(); + if (_inputControllerListener != null) { + _inputController.removeListener(_inputControllerListener!); + } super.dispose(); } @@ -335,157 +371,184 @@ class _ChatSendBarState extends BaseStateFulWidgetState { child: Container( height: ActionHeight, color: application.theme.backgroundColor1, - child: Row( - children: [ - Expanded( - child: Stack( - children: [ - Row( - children: [ - SizedBox( - width: ActionWidth, - height: ActionHeight, - child: UnconstrainedBox( - child: Asset.iconSvg( - 'grid', - width: 24, - color: _theme.primaryColor, - ), - ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: _theme.backgroundColor2, - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: FixedTextField( - style: TextStyle(fontSize: 14, height: 1.4), - decoration: InputDecoration( - hintText: Settings.locale((s) => s.type_a_message, ctx: context), - hintStyle: TextStyle(color: _theme.fontColor2), - contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), - border: UnderlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), - borderSide: const BorderSide(width: 0, style: BorderStyle.none), - ), - ), - maxLines: 5, - minLines: 1, - controller: _inputController, - focusNode: _inputFocusNode, - textInputAction: TextInputAction.newline, - onChanged: (val) { - String draft = _inputController.text; - if (draft.isNotEmpty) { - memoryCache.setDraft(widget.targetId, draft); - } else { - memoryCache.removeDraft(widget.targetId); - } - setState(() { - _canSendText = val.isNotEmpty; - }); - }, + child: clientConnectStatus == ClientConnectStatus.connected || clientConnectStatus == ClientConnectStatus.connectPing + ? Row( + children: [ + Expanded( + child: Stack( + children: [ + Row( + children: [ + SizedBox( + width: ActionWidth, + height: ActionHeight, + child: UnconstrainedBox( + child: Asset.iconSvg( + 'grid', + width: 24, + color: _theme.primaryColor, ), ), - ], - ), - ), - ), - ], - ), - _audioRecordVisible - ? Container( - color: application.theme.backgroundColor1, - child: Container( - color: _audioBgColorTween.transform(_audioDragPercent), - child: Row( - children: [ - SizedBox(width: 16), - Icon(FontAwesomeIcons.microphone, size: 24, color: volumeColor), - SizedBox(width: 8), - Container( - child: Label( - recordDurationText, - type: LabelType.bodyRegular, - fontWeight: FontWeight.normal, - color: recordWidgetColor, - ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + color: _theme.backgroundColor2, + borderRadius: BorderRadius.all(Radius.circular(20)), ), - SizedBox(width: 8), - Expanded( - child: Container( - height: ActionHeight, - child: Center( - child: Label( - _audioLockedMode - ? Settings.locale((s) => s.cancel, ctx: context) - : (_audioDragPercent != 0 ? Settings.locale((s) => s.release_to_cancel, ctx: context) : Settings.locale((s) => s.slide_to_cancel)), - type: LabelType.bodyLarge, - textAlign: TextAlign.center, - color: recordWidgetColor, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: FixedTextField( + style: TextStyle(fontSize: 14, height: 1.4), + decoration: InputDecoration( + hintText: Settings.locale((s) => s.type_a_message, ctx: context), + hintStyle: TextStyle(color: _theme.fontColor2), + contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), + suffixIcon: GestureDetector( + onTap: widget.onEmojiPressed, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: EdgeInsets.only(right: 8, top: 4), + child: FaIcon( + FontAwesomeIcons.file, + color: application.theme.primaryColor, + ), + ), + ), + suffixIconConstraints: BoxConstraints(minHeight: 32, minWidth: 32), + border: UnderlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + borderSide: const BorderSide(width: 0, style: BorderStyle.none), + ), + ), + maxLines: 5, + minLines: 1, + controller: _inputController, + focusNode: _inputFocusNode, + textInputAction: TextInputAction.newline, + onChanged: null, ), ), + ], + ), + ), + ), + ], + ), + _audioRecordVisible + ? Container( + color: application.theme.backgroundColor1, + child: Container( + color: _audioBgColorTween.transform(_audioDragPercent), + child: Row( + children: [ + SizedBox(width: 16), + Icon(FontAwesomeIcons.microphone, size: 24, color: volumeColor), + SizedBox(width: 8), + Container( + child: Label( + recordDurationText, + type: LabelType.bodyRegular, + fontWeight: FontWeight.normal, + color: recordWidgetColor, + ), + ), + SizedBox(width: 8), + Expanded( + child: Container( + height: ActionHeight, + child: Center( + child: Label( + _audioLockedMode + ? Settings.locale((s) => s.cancel, ctx: context) + : (_audioDragPercent != 0 + ? Settings.locale((s) => s.release_to_cancel, ctx: context) + : Settings.locale((s) => s.slide_to_cancel)), + type: LabelType.bodyLarge, + textAlign: TextAlign.center, + color: recordWidgetColor, + ), + ), + ), + ), + SizedBox(width: 16), + ], ), ), - SizedBox(width: 16), - ], + ) + : SizedBox.shrink() + ], + ), + ), + _canSendText + ? SizedBox( + width: ActionWidth, + height: ActionHeight, + child: UnconstrainedBox( + child: Asset.iconSvg( + 'send', + width: 24, + color: _canSendText ? _theme.primaryColor : _theme.fontColor2, ), ), ) - : SizedBox.shrink() + : (_audioLockedMode + ? Container( + width: ActionWidth, + height: ActionHeight, + alignment: Alignment.center, + color: application.theme.backgroundColor1, + child: Label( + Settings.locale((s) => s.send, ctx: context), + type: LabelType.bodyLarge, + textAlign: TextAlign.center, + color: _theme.primaryColor, + ), + ) + : Container( + color: application.theme.backgroundColor1, + child: Container( + color: _audioBgColorTween.transform(_audioDragPercent), + child: SizedBox( + width: ActionWidth, + height: ActionHeight, + child: UnconstrainedBox( + child: Asset.iconSvg( + 'microphone', + width: 24, + color: !_canSendText ? _theme.primaryColor : _theme.fontColor2, + ), + ), + ), + ), + )), + // _voiceAndSendWidget(), ], - ), - ), - _canSendText - ? SizedBox( - width: ActionWidth, - height: ActionHeight, - child: UnconstrainedBox( - child: Asset.iconSvg( - 'send', - width: 24, - color: _canSendText ? _theme.primaryColor : _theme.fontColor2, + ) + : Container( + height: ActionHeight, + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Label( + Settings.locale((s) => s.connecting, ctx: context), + type: LabelType.h4, + color: application.theme.fontColor2, + ), + Padding( + padding: const EdgeInsets.only(bottom: 2, left: 4), + child: SpinKitThreeBounce( + color: application.theme.fontColor2, + size: 10, ), ), - ) - : (_audioLockedMode - ? Container( - width: ActionWidth, - height: ActionHeight, - alignment: Alignment.center, - color: application.theme.backgroundColor1, - child: Label( - Settings.locale((s) => s.send, ctx: context), - type: LabelType.bodyLarge, - textAlign: TextAlign.center, - color: _theme.primaryColor, - ), - ) - : Container( - color: application.theme.backgroundColor1, - child: Container( - color: _audioBgColorTween.transform(_audioDragPercent), - child: SizedBox( - width: ActionWidth, - height: ActionHeight, - child: UnconstrainedBox( - child: Asset.iconSvg( - 'microphone', - width: 24, - color: !_canSendText ? _theme.primaryColor : _theme.fontColor2, - ), - ), - ), - ), - )), - // _voiceAndSendWidget(), - ], - ), + ], + ), + ), ), ); } diff --git a/lib/components/dialog/bottom.dart b/lib/components/dialog/bottom.dart index bd56e13d7..0f1d96c9a 100644 --- a/lib/components/dialog/bottom.dart +++ b/lib/components/dialog/bottom.dart @@ -290,26 +290,72 @@ class BottomDialog extends BaseStateFulWidget { String? actionText, bool password = false, double height = 300, + int minLength = 0, int maxLength = 10000, bool enable = true, bool contactSelect = false, bool canTapClose = true, + Future Function(String)? asyncValidator, }) async { TextEditingController _inputController = TextEditingController(); _inputController.text = value ?? ""; + ValueNotifier errorNotifier = ValueNotifier(null); + ValueNotifier loadingNotifier = ValueNotifier(false); - return showWithTitle( + return showWithTitle( title: title, desc: desc, height: height, canTapClose: canTapClose, action: Padding( padding: const EdgeInsets.only(left: 20, right: 20, top: 8, bottom: 34), - child: Button( - text: actionText ?? Settings.locale((s) => s.continue_text, ctx: context), - width: double.infinity, - onPressed: () { - if (Navigator.of(this.context).canPop()) Navigator.pop(this.context, _inputController.text); + child: ValueListenableBuilder( + valueListenable: loadingNotifier, + builder: (context, isLoading, child) { + return Button( + disabled: isLoading, + width: double.infinity, + child: isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(application.theme.fontLightColor), + ), + ) + : Text( + actionText ?? Settings.locale((s) => s.continue_text, ctx: context), + style: TextStyle( + fontSize: application.theme.buttonFontSize, + color: application.theme.fontLightColor, + fontWeight: FontWeight.bold, + ), + ), + onPressed: () async { + // Validate minimum length + if (minLength > 0 && _inputController.text.length < minLength) { + errorNotifier.value = Settings.locale((s) => s.tip_input_min_length(minLength.toString()), ctx: context); + return; + } + + // Async validation + if (asyncValidator != null) { + loadingNotifier.value = true; + try { + final error = await asyncValidator(_inputController.text); + if (error != null) { + errorNotifier.value = error; + return; + } + } finally { + loadingNotifier.value = false; + } + } + + if (Navigator.of(this.context).canPop()) Navigator.pop(this.context, _inputController.text); + }, + ); }, ), ), @@ -323,32 +369,44 @@ class BottomDialog extends BaseStateFulWidget { type: LabelType.h4, textAlign: TextAlign.start, ), - FormText( - controller: _inputController, - hintText: inputHint ?? "", - validator: validator, - password: password, - maxLength: maxLength, - enabled: enable, - suffixIcon: contactSelect - ? GestureDetector( - onTap: () async { - if (clientCommon.isClientOK) { - var contact = await ContactHomeScreen.go(context, selectContact: true); - if ((contact != null) && (contact is ContactSchema)) { - _inputController.text = contact.address; - } - } else { - Toast.show(Settings.locale((s) => s.d_chat_not_login, ctx: context)); - } - }, - child: Container( - width: 20, - alignment: Alignment.centerRight, - child: Icon(FontAwesomeIcons.solidAddressBook), - ), - ) - : SizedBox.shrink(), + ValueListenableBuilder( + valueListenable: errorNotifier, + builder: (context, errorText, child) { + return FormText( + controller: _inputController, + hintText: inputHint ?? "", + validator: validator, + password: password, + maxLength: maxLength, + enabled: enable, + errorText: errorText, + onChanged: (value) { + // Clear error when user types + if (errorNotifier.value != null) { + errorNotifier.value = null; + } + }, + suffixIcon: contactSelect + ? GestureDetector( + onTap: () async { + if (clientCommon.isClientOK) { + var contact = await ContactHomeScreen.go(context, selectContact: true); + if ((contact != null) && (contact is ContactSchema)) { + _inputController.text = contact.address; + } + } else { + Toast.show(Settings.locale((s) => s.d_chat_not_login, ctx: context)); + } + }, + child: Container( + width: 20, + alignment: Alignment.centerRight, + child: Icon(FontAwesomeIcons.solidAddressBook), + ), + ) + : SizedBox.shrink(), + ); + }, ), ], ), diff --git a/lib/components/dialog/modal.dart b/lib/components/dialog/modal.dart index 0e691bc10..5a39efd3a 100644 --- a/lib/components/dialog/modal.dart +++ b/lib/components/dialog/modal.dart @@ -33,6 +33,7 @@ class ModalDialog extends StatelessWidget { bool hasCloseButton = true, double? height, List? actions, + bool barrierDismissible = true, }) { this.title = title; this.titleWidget = titleWidget; @@ -44,12 +45,21 @@ class ModalDialog extends StatelessWidget { this.actions = actions ?? []; return showDialog( context: context, - // barrierDismissible: false, + barrierDismissible: barrierDismissible, builder: (ctx) { - return Container( + Widget dialogWidget = Container( alignment: Alignment.center, child: this, ); + + if (!barrierDismissible) { + dialogWidget = PopScope( + canPop: false, + child: dialogWidget, + ); + } + + return dialogWidget; }, ); } diff --git a/lib/components/layout/nav.dart b/lib/components/layout/nav.dart index ac406f1b5..bb70a16f0 100644 --- a/lib/components/layout/nav.dart +++ b/lib/components/layout/nav.dart @@ -28,6 +28,7 @@ class _NavState extends BaseStateFulWidgetState