diff --git a/composer.lock b/composer.lock index 54b0f3840..d171fc1be 100644 --- a/composer.lock +++ b/composer.lock @@ -1345,7 +1345,7 @@ "sebastian/comparator": "^6.3.2", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.1", - "sebastian/exporter": "^6.3.0", + "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", "sebastian/type": "^5.1.3", @@ -1937,16 +1937,16 @@ }, { "name": "sebastian/exporter", - "version": "6.3.0", + "version": "6.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", - "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", "shasum": "" }, "require": { @@ -1960,7 +1960,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.1-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -2003,15 +2003,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-12-05T09:17:50+00:00" + "time": "2025-09-24T06:12:51+00:00" }, { "name": "sebastian/global-state", @@ -2474,11 +2486,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -2901,6 +2908,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2984,6 +2995,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -3017,7 +3032,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", diff --git a/example.php b/example.php index 852ddfa31..ed516cec9 100644 --- a/example.php +++ b/example.php @@ -3,6 +3,7 @@ include_once 'vendor/autoload.php'; use Appwrite\SDK\Language\GraphQL; +use Appwrite\SDK\Language\KMP; use Appwrite\Spec\Swagger2; use Appwrite\SDK\SDK; use Appwrite\SDK\Language\Web; @@ -250,6 +251,15 @@ function configureSDK($sdk, $overrides = []) { $sdk->generate(__DIR__ . '/examples/android'); } + // KMP + if (!$requestedSdk || $requestedSdk === 'kmp') { + $sdk = new SDK(new KMP(), new Swagger2($spec)); + configureSDK($sdk, [ + 'namespace' => 'io.appwrite', + ]); + $sdk->generate(__DIR__ . '/examples/kmp'); + } + // Kotlin if (!$requestedSdk || $requestedSdk === 'kotlin') { $sdk = new SDK(new Kotlin(), new Swagger2($spec)); diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index 19a22f01a..5e96119b2 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -344,11 +344,11 @@ public function getFiles(): array ]; } - protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T'): string + protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T', bool $withGeneric = true): string { if ($method['type'] === 'webAuth') { return 'Bool'; } - return parent::getReturnType($method, $spec, $namespace, $generic); + return parent::getReturnType($method, $spec, $namespace, $generic, $withGeneric); } } diff --git a/src/SDK/Language/KMP.php b/src/SDK/Language/KMP.php new file mode 100644 index 000000000..8aec7f134 --- /dev/null +++ b/src/SDK/Language/KMP.php @@ -0,0 +1,637 @@ +getName() !== 'propertyType'; + }); + + $filters[] = new TwigFilter('propertyType', function (array $property, array $spec, string $generic = 'T', $contextual = false) { + return $this->getPropertyType($property, $spec, $generic, $contextual); + }); + + $filters[] = new TwigFilter('webAuthServices', function (array $spec) { + return $this->getWebAuthServices($spec); + }); + + $filters[] = new TwigFilter('propertySerializerName', function (array $property) { + return $this->getPropertySerializerName($property); + }); + $filters[] = new TwigFilter('propertyAssignmentKmp', function (array $property, array $spec) { + return $this->getJsonPropertyAssignment($property, $spec); + }); + + return $filters; + } + + /** + * Generate property extraction expression for JsonObject-based deserialization (KMP models) + */ + protected function getJsonPropertyAssignment(array $property, array $spec): string + { + $name = $property['name']; + $escaped = str_replace('$', '\\$', $name); + $key = "jsonObject[\"$escaped\"]"; + + // Enums + if (isset($property['enum']) && !empty($property['enum'])) { + $enumName = $property['enumName'] ?? $property['name']; + $enumClass = $this->toPascalCase($enumName); + if ($property['required']) { + return "$enumClass.values().find { it.value == $key!!.jsonPrimitive.content }!!"; + } + return "$key?.jsonPrimitive?.content?.let { v -> $enumClass.values().find { it.value == v } }"; + } + + // Arrays of primitives + if (($property['type'] ?? '') === 'array') { + $itemType = $property['array']['type'] ?? 'string'; + $mapper = match ($itemType) { + 'integer' => 'it.jsonPrimitive.long', + 'number' => 'it.jsonPrimitive.double', + 'boolean' => 'it.jsonPrimitive.boolean', + default => 'it.jsonPrimitive.content', + }; + if ($property['required']) { + return "$key!!.jsonArray.map { $mapper }"; + } + return "$key?.jsonArray?.map { $mapper }"; + } + + // Objects (kept as JsonElement/Any via @Contextual) + if (($property['type'] ?? '') === 'object') { + return $key; + } + + // Scalars + switch ($property['type'] ?? 'string') { + case 'integer': + return $property['required'] + ? "$key!!.jsonPrimitive.long" + : "$key?.jsonPrimitive?.content?.toLongOrNull()"; + case 'number': + return $property['required'] + ? "$key!!.jsonPrimitive.double" + : "$key?.jsonPrimitive?.content?.toDoubleOrNull()"; + case 'boolean': + return $property['required'] + ? "$key!!.jsonPrimitive.boolean" + : "$key?.jsonPrimitive?.content?.toBooleanStrictOrNull()"; + case 'string': + default: + return $property['required'] + ? "$key!!.jsonPrimitive.content" + : "$key?.jsonPrimitive?.content"; + } + } + protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T', bool $withGeneric = true): string + { + if ($method['type'] === 'webAuth') { + return 'Bool'; + } + return parent::getReturnType($method, $spec, $namespace, $generic, $withGeneric); + } + + protected function getWebAuthServices(array $spec): array + { + $webAuthServices = []; + foreach ($spec['services'] as $service) { + $webAuthMethods = []; + $hasWebAuth = false; + foreach ($service['methods'] as $method) { + if ($method['type'] === 'webAuth') { + $webAuthMethods[] = [ + 'methodName' => $method['name'], + 'parameters' => $method['parameters'], + 'path' => $method['path'], + 'auth' => $method['auth'] + ]; + $hasWebAuth = true; + } + } + if ($hasWebAuth) { + $webAuthServices[] = [ + 'methods' => $webAuthMethods, + 'className' => $service['name'] + ]; + } + } + return $webAuthServices; + } + + + protected function getPropertyType(array $property, array $spec, string $generic = 'T', bool $contextual = false): string + { + $type = parent::getPropertyType($property, $spec, $generic); + if ($contextual && ($type === 'List' || $type === 'List?')) { + $nullable = str_ends_with($type, '?'); + $type = 'List<@Contextual Any>' . ($nullable ? '?' : ''); + } + return $type; + } + + protected function getPropertySerializerName(array $property): string + { + if (isset($property['enumName'])) { + return 'io.appwrite.enums.' . \ucfirst($property['enumName']) . 'Serializer'; + } + if (!empty($property['enumValues'])) { + return 'io.appwrite.enums.' . \ucfirst($property['name']) . 'Serializer'; + } + if (isset($property['items'])) { + $property['array'] = $property['items']; + } + + $name = match ($property['type']) { + self::TYPE_INTEGER => 'Long.serializer()', + self::TYPE_NUMBER => 'Double.serializer()', + self::TYPE_STRING => 'String.serializer()', + self::TYPE_BOOLEAN => 'Boolean.serializer()', + self::TYPE_ARRAY => (!empty(($property['array'] ?? [])['type']) && !\is_array($property['array']['type'])) + ? 'ListSerializer(' . $this->getPropertySerializerName($property['array']) . ')' + : 'ListSerializer(DynamicLookupSerializer)', + self::TYPE_OBJECT => 'DynamicLookupSerializer', + default => $property['type'] . 'Serializer', + }; + + return $name; + } + + + public function getFiles(): array + { + return [ + // Root project config + [ + 'scope' => 'copy', + 'destination' => '.github/workflows/publish.yml', + 'template' => '/kmp/.github/workflows/publish.yml', + ], +// [ +// 'scope' => 'method', +// 'destination' => 'docs/examples/kotlin/{{service.name | caseLower}}/{{method.name | caseDash}}.md', +// 'template' => '/kmp/docs/kotlin/example.md.twig', +// ], +// [ +// 'scope' => 'method', +// 'destination' => 'docs/examples/java/{{service.name | caseLower}}/{{method.name | caseDash}}.md', +// 'template' => '/kmp/docs/java/example.md.twig', +// ], + + // Gradle files + [ + 'scope' => 'copy', + 'destination' => 'gradle/wrapper/gradle-wrapper.jar', + 'template' => '/kmp/gradle/wrapper/gradle-wrapper.jar', + ], + [ + 'scope' => 'copy', + 'destination' => 'gradle/wrapper/gradle-wrapper.properties', + 'template' => '/kmp/gradle/wrapper/gradle-wrapper.properties', + ], + [ + 'scope' => 'copy', + 'destination' => 'gradle/libs.versions.toml', + 'template' => '/kmp/gradle/libs.versions.toml', + ], + + // Root files + [ + 'scope' => 'copy', + 'destination' => '.gitignore', + 'template' => '/kmp/.gitignore', + ], + [ + 'scope' => 'default', + 'destination' => 'build.gradle.kts', + 'template' => '/kmp/build.gradle.kts.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'settings.gradle.kts', + 'template' => '/kmp/settings.gradle.kts', + ], + [ + 'scope' => 'default', + 'destination' => 'CHANGELOG.md', + 'template' => '/kmp/CHANGELOG.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'README.md', + 'template' => '/kmp/README.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'LICENSE.md', + 'template' => '/kmp/LICENSE.md.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'gradlew', + 'template' => '/kmp/gradlew', + ], + [ + 'scope' => 'default', + 'destination' => 'gradlew.bat', + 'template' => '/kmp/gradlew.bat', + ], + [ + 'scope' => 'default', + 'destination' => 'gradle.properties', + 'template' => '/kmp/gradle.properties', + ], + + // Shared module + [ + 'scope' => 'default', + 'destination' => 'shared/build.gradle.kts', + 'template' => '/kmp/shared/build.gradle.kts.twig', + ], + + // Common Main + // Common Main root files + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/BaseClient.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/BaseClient.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/Client.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/Client.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/ID.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/ID.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/Permission.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/Permission.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/Query.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/Query.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/Role.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/Role.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/Service.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/Service.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/WebAuthComponent.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/WebAuthComponent.kt.twig', + ], + + + // Coroutines + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/coroutines/Callback.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/coroutines/Callback.kt.twig', + ], + + // Enums + [ + 'scope' => 'enum', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/enums/{{ enum.name | caseUcfirst }}.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/enums/Enum.kt.twig', + ], + + // Exceptions + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/exceptions/Exception.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/exceptions/Exception.kt.twig', + ], + + // Extensions + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/extensions/CollectionExtensions.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/extensions/CollectionExtensions.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/extensions/JsonExtensions.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/extensions/JsonExtensions.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/extensions/TypeExtensions.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/extensions/TypeExtensions.kt.twig', + ], + + // Models + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/models/InputFile.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/models/InputFile.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/models/RealtimeModels.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/models/RealtimeModels.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/models/UploadProgress.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/models/UploadProgress.kt.twig', + ], + + // Serializers + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/serializers/DynamicLookupSerializer.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/serializers/DynamicLookupSerializer.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/serializers/StringCollectionSerializer.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/serializers/StringCollectionSerializer.kt.twig', + ], + + // Services + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/services/Realtime.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/services/Realtime.kt.twig', + ], + [ + 'scope' => 'service', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/services/{{service.name | caseUcfirst}}.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/services/Service.kt.twig', + ], + + // Web Interface + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/webInterface/ParsedUrl.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/webInterface/ParsedUrl.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/webInterface/UrlParser.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/webInterface/UrlParser.kt.twig', + ], + + + // Android Main + // Android Main root files + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/Client.android.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/Client.android.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/HttpClientConfig.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/HttpClientConfig.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/KeepAliveService.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/KeepAliveService.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/WebAuthComponent.android.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/WebAuthComponent.android.kt.twig', + ], + + // Cookies + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/SerializableCookie.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/SerializableCookie.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/stores/DataStoreManager.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/DataStoreManager.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/stores/DataStoreCookieStorage.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/cookies/stores/DataStoreCookieStorage.kt.twig', + ], + + // Extensions + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/extensions/OAuth2Extensions.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/extensions/IOExtensions.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/extensions/IOExtensions.kt.twig', + ], + + // Models + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/models/InputFile.android.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/models/InputFile.android.kt.twig', + ], + + // Views + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/views/CallbackActivity.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/views/CallbackActivity.kt.twig', + ], + + // Web Interface + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/webInterface/AndroidParsedUrl.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/webInterface/AndroidParsedUrl.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/androidMain/kotlin/{{ sdk.namespace | caseSlash }}/webInterface/UrlParser.android.kt', + 'template' => '/kmp/shared/src/androidMain/kotlin/io/package/webInterface/UrlParser.android.kt.twig', + ], + + // iOS Main + // iOS Main root files + [ + 'scope' => 'default', + 'destination' => 'shared/src/iosMain/kotlin/{{ sdk.namespace | caseSlash }}/Client.ios.kt', + 'template' => '/kmp/shared/src/iosMain/kotlin/io/package/Client.ios.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/iosMain/kotlin/{{ sdk.namespace | caseSlash }}/HttpClientConfig.ios.kt', + 'template' => '/kmp/shared/src/iosMain/kotlin/io/package/HttpClientConfig.ios.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/iosMain/kotlin/{{ sdk.namespace | caseSlash }}/WebAuthComponent.ios.kt', + 'template' => '/kmp/shared/src/iosMain/kotlin/io/package/WebAuthComponent.ios.kt.twig', + ], + + // Cookies + [ + 'scope' => 'default', + 'destination' => 'shared/src/iosMain/kotlin/{{ sdk.namespace | caseSlash }}/cookies/IosCookieStorage.kt', + 'template' => '/kmp/shared/src/iosMain/kotlin/io/package/cookies/IosCookieStorage.kt.twig', + ], + + // Extensions + [ + 'scope' => 'default', + 'destination' => 'shared/src/iosMain/kotlin/{{ sdk.namespace | caseSlash }}/extensions/OAuth2Extensions.kt', + 'template' => '/kmp/shared/src/iosMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig', + ], + + // Models + [ + 'scope' => 'default', + 'destination' => 'shared/src/iosMain/kotlin/{{ sdk.namespace | caseSlash }}/models/InputFile.ios.kt', + 'template' => '/kmp/shared/src/iosMain/kotlin/io/package/models/InputFile.ios.kt.twig', + ], + + // Web Interface + [ + 'scope' => 'default', + 'destination' => 'shared/src/iosMain/kotlin/{{ sdk.namespace | caseSlash }}/webInterface/IosParsedUrl.kt', + 'template' => '/kmp/shared/src/iosMain/kotlin/io/package/webInterface/IosParsedUrl.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/iosMain/kotlin/{{ sdk.namespace | caseSlash }}/webInterface/UrlParser.ios.kt', + 'template' => '/kmp/shared/src/iosMain/kotlin/io/package/webInterface/UrlParser.ios.kt.twig', + ], + + // Android App + // Android App root files + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/AndroidManifest.xml', + 'template' => '/kmp/androidApp/src/main/AndroidManifest.xml.twig', + ], + +// Java files + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/java/{{ sdk.namespace | caseSlash }}/android/MainActivity.kt', + 'template' => '/kmp/androidApp/src/main/java/io/package/android/MainActivity.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/java/{{ sdk.namespace | caseSlash }}/android/services/MessagingService.kt', + 'template' => '/kmp/androidApp/src/main/java/io/package/android/services/MessagingService.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/java/{{ sdk.namespace | caseSlash }}/android/ui/accounts/AccountsFragment.kt', + 'template' => '/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/java/{{ sdk.namespace | caseSlash }}/android/ui/accounts/AccountsViewModel.kt', + 'template' => '/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsViewModel.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/java/{{ sdk.namespace | caseSlash }}/android/utils/Client.kt', + 'template' => '/kmp/androidApp/src/main/java/io/package/android/utils/Client.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/java/{{ sdk.namespace | caseSlash }}/android/utils/Event.kt', + 'template' => '/kmp/androidApp/src/main/java/io/package/android/utils/Event.kt.twig', + ], + +// Resource files + [ + 'scope' => 'copy', + 'destination' => 'androidApp/src/main/res/drawable/ic_launcher_background.xml', + 'template' => '/kmp/androidApp/src/main/res/drawable/ic_launcher_background.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'androidApp/src/main/res/drawable/ic_launcher_foreground.xml', + 'template' => '/kmp/androidApp/src/main/res/drawable/ic_launcher_foreground.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'androidApp/src/main/res/layout/activity_main.xml', + 'template' => '/kmp/androidApp/src/main/res/layout/activity_main.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'androidApp/src/main/res/layout/fragment_account.xml', + 'template' => '/kmp/androidApp/src/main/res/layout/fragment_account.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml', + 'template' => '/kmp/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml', + 'template' => '/kmp/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'androidApp/src/main/res/values/colors.xml', + 'template' => '/kmp/androidApp/src/main/res/values/colors.xml', + ], + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/res/values/strings.xml', + 'template' => '/kmp/androidApp/src/main/res/values/strings.xml.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'androidApp/src/main/res/values/themes.xml', + 'template' => '/kmp/androidApp/src/main/res/values/themes.xml.twig', + ], + + + // Models, Services, and other common components + [ + 'scope' => 'service', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/services/{{service.name | caseUcfirst}}.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/services/Service.kt.twig', + ], + [ + 'scope' => 'definition', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/models/{{ definition.name | caseUcfirst }}.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/models/Model.kt.twig', + ], + [ + 'scope' => 'enum', + 'destination' => 'shared/src/commonMain/kotlin/{{ sdk.namespace | caseSlash }}/enums/{{ enum.name | caseUcfirst }}.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/enums/Enum.kt.twig', + ], + ]; + } +} diff --git a/src/SDK/Language/Kotlin.php b/src/SDK/Language/Kotlin.php index c072e9d3a..29c982420 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -475,8 +475,8 @@ public function getFiles(): array public function getFilters(): array { return [ - new TwigFilter('returnType', function (array $method, array $spec, string $namespace, string $generic = 'T') { - return $this->getReturnType($method, $spec, $namespace, $generic); + new TwigFilter('returnType', function (array $method, array $spec, string $namespace, string $generic = 'T', bool $withGeneric = true) { + return $this->getReturnType($method, $spec, $namespace, $generic, $withGeneric); }), new TwigFilter('modelType', function (array $property, array $spec, string $generic = 'T') { return $this->getModelType($property, $spec, $generic); @@ -499,7 +499,7 @@ public function getFilters(): array ]; } - protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T'): string + protected function getReturnType(array $method, array $spec, string $namespace, string $generic = 'T', bool $withGeneric = true): string { if ($method['type'] === 'webAuth') { return 'String'; @@ -518,7 +518,7 @@ protected function getReturnType(array $method, array $spec, string $namespace, $ret = $this->toPascalCase($method['responseModel']); - if ($this->hasGenericType($method['responseModel'], $spec)) { + if ($this->hasGenericType($method['responseModel'], $spec) && $withGeneric) { $ret .= '<' . $generic . '>'; } diff --git a/templates/kmp/.github/workflows/autoclose.yml b/templates/kmp/.github/workflows/autoclose.yml new file mode 100644 index 000000000..3e2b3cbce --- /dev/null +++ b/templates/kmp/.github/workflows/autoclose.yml @@ -0,0 +1,11 @@ +name: Auto-close External Pull Requests + +on: + pull_request_target: + types: [opened, reopened] + +jobs: + auto_close: + uses: appwrite/.github/.github/workflows/autoclose.yml@main + secrets: + GH_AUTO_CLOSE_PR_TOKEN: ${{ secrets.GH_AUTO_CLOSE_PR_TOKEN }} diff --git a/templates/kmp/.github/workflows/publish.yml b/templates/kmp/.github/workflows/publish.yml new file mode 100644 index 000000000..de6a6b8d3 --- /dev/null +++ b/templates/kmp/.github/workflows/publish.yml @@ -0,0 +1,52 @@ +name: Publish to Maven Central + +on: + release: + types: [released] + +jobs: + publish: + name: Release build and publish + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'temurin' + + - name: Prepare environment + env: + GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} + SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} + run: | + git fetch --unshallow + sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'" + chmod +x ./gradlew + + - name: Build Release Artifacts + run: ./gradlew assemble + + - name: Generate Documentation + run: ./gradlew dokkaHtml + + - name: Publish to Maven Central + run: | + if ${{ contains(github.event.release.tag_name, '-rc') }}; then + echo "Publishing Snapshot Version ${{ github.event.release.tag_name}}" + ./gradlew publishAllPublicationsToSonatypeRepository + else + echo "Publishing Release Version ${{ github.event.release.tag_name}}" + ./gradlew publishAllPublicationsToSonatypeRepository closeAndReleaseSonatypeStagingRepository + fi + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SDK_VERSION: ${{ github.event.release.tag_name }} diff --git a/templates/kmp/.gitignore b/templates/kmp/.gitignore new file mode 100644 index 000000000..f7a9fde78 --- /dev/null +++ b/templates/kmp/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +.idea +.DS_Store +build +captures +.externalNativeBuild +.cxx +local.properties +xcuserdata diff --git a/templates/kmp/CHANGELOG.md.twig b/templates/kmp/CHANGELOG.md.twig new file mode 100644 index 000000000..e87fcf8f2 --- /dev/null +++ b/templates/kmp/CHANGELOG.md.twig @@ -0,0 +1 @@ +{{sdk.changelog}} diff --git a/templates/kmp/LICENSE.md.twig b/templates/kmp/LICENSE.md.twig new file mode 100644 index 000000000..ce6435c38 --- /dev/null +++ b/templates/kmp/LICENSE.md.twig @@ -0,0 +1 @@ +{{sdk.licenseContent | raw}} diff --git a/templates/kmp/README.md.twig b/templates/kmp/README.md.twig new file mode 100644 index 000000000..8040a6ebb --- /dev/null +++ b/templates/kmp/README.md.twig @@ -0,0 +1,79 @@ +# {{ spec.title }} {{sdk.name}} SDK + +![Maven Central](https://img.shields.io/maven-central/v/{{ sdk.namespace | caseDot }}/{{ sdk.gitRepoName | caseDash }}.svg?color=green&style=flat-square) +![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?style=flat-square) +[![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) +{% if sdk.twitterHandle %} +[![Twitter Account](https://img.shields.io/twitter/follow/{{ sdk.twitterHandle }}?color=00acee&label=twitter&style=flat-square)](https://twitter.com/{{ sdk.twitterHandle }}) +{% endif %} +{% if sdk.discordChannel %} +[![Discord](https://img.shields.io/discord/{{ sdk.discordChannel }}?label=discord&style=flat-square)]({{ sdk.discordUrl }}) +{% endif %} +{% if sdk.warning %} + +{{ sdk.warning|raw }} +{% endif %} + +{{ sdk.description }} + +{% if sdk.logo %} +![{{ spec.title }}]({{ sdk.logo }}) +{% endif %} + +## Installation + +### Gradle + +Appwrite's KMP SDK is hosted on Maven Central. In order to fetch the Appwrite SDK, add this to your root level `build.gradle(.kts)` file: + +### Gradle Setup +Add the following to your root level `settings.gradle.kts`: + +``` +dependencyResolutionManagement { + repositories { + mavenLocal() + } +} +``` + +In your shared module's `build.gradle.kts`: + +``` +kotlin { + sourceSets { + commonMain { + dependencies { + implementation("{{ sdk.namespace | caseDot }}:{{ sdk.gitRepoName | caseDash }}") + } + } + } +} +``` + +### Maven +Add this to your project's `pom.xml` file: + +```xml + + + {{ sdk.namespace | caseDot }} + {{ sdk.gitRepoName | caseDash }} + {{sdk.version}} + + +``` + +{% if sdk.gettingStarted %} + +{{ sdk.gettingStarted|raw }} +{% endif %} + +## Contribution + +This library is auto-generated by Appwrite custom [SDK Generator](https://github.com/appwrite/sdk-generator). To learn more about how you can help us improve this SDK, please check the [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md) before sending a pull-request. + +## License + +Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. diff --git a/templates/kmp/androidApp/build.gradle.kts.twig b/templates/kmp/androidApp/build.gradle.kts.twig new file mode 100644 index 000000000..e4264b7e7 --- /dev/null +++ b/templates/kmp/androidApp/build.gradle.kts.twig @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.kotlinAndroid) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlinx.serialization) +} + +android { + namespace = "{{ sdk.namespace | caseDot }}.android" + compileSdk = 35 + defaultConfig { + applicationId = "{{ sdk.namespace | caseDot }}.android" + minSdk = 21 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + } + buildFeatures { + compose = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(projects.shared) + implementation(libs.compose.ui) + implementation(libs.compose.ui.tooling.preview) + implementation(libs.compose.material3) + implementation(libs.androidx.activity.compose) + debugImplementation(libs.compose.ui.tooling) +} diff --git a/templates/kmp/androidApp/src/main/AndroidManifest.xml.twig b/templates/kmp/androidApp/src/main/AndroidManifest.xml.twig new file mode 100644 index 000000000..e2bc849c4 --- /dev/null +++ b/templates/kmp/androidApp/src/main/AndroidManifest.xml.twig @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/kmp/androidApp/src/main/java/io/package/android/MainActivity.kt.twig b/templates/kmp/androidApp/src/main/java/io/package/android/MainActivity.kt.twig new file mode 100644 index 000000000..1dc2a057c --- /dev/null +++ b/templates/kmp/androidApp/src/main/java/io/package/android/MainActivity.kt.twig @@ -0,0 +1,23 @@ +package {{ sdk.namespace | caseDot }}.android + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.fragment.app.add +import androidx.fragment.app.commit +import {{ sdk.namespace | caseDot }}.android.ui.accounts.AccountsFragment +import {{ sdk.namespace | caseDot }}.android.utils.Client + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + Client.create(applicationContext) + + if (savedInstanceState == null) { + supportFragmentManager.commit { + setReorderingAllowed(true) + add(R.id.fragment_container_view) + } + } + } +} diff --git a/templates/kmp/androidApp/src/main/java/io/package/android/services/MessagingService.kt.twig b/templates/kmp/androidApp/src/main/java/io/package/android/services/MessagingService.kt.twig new file mode 100644 index 000000000..c4248f96a --- /dev/null +++ b/templates/kmp/androidApp/src/main/java/io/package/android/services/MessagingService.kt.twig @@ -0,0 +1,37 @@ +package {{ sdk.namespace | caseDot }}.android.services + +import com.google.firebase.messaging.FirebaseMessagingService +import {{ sdk.namespace | caseDot }}.ID +import {{ sdk.namespace | caseDot }}.services.Account +import kotlinx.coroutines.runBlocking + +class MessagingService : FirebaseMessagingService() { + + companion object { + var account: Account? = null + } + + override fun onNewToken(token: String) { + super.onNewToken(token) + + val prefs = getSharedPreferences("example", MODE_PRIVATE) + + prefs.edit().putString("fcmToken", token).apply() + + if (account == null) { + return + } + + val targetId = prefs.getString("targetId", null) + + runBlocking { + if (targetId == null) { + val target = account!!.createPushTarget(ID.unique(), token) + + prefs.edit().putString("targetId", target.id).apply() + } else { + account!!.updatePushTarget(targetId, token) + } + } + } +} diff --git a/templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig b/templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig new file mode 100644 index 000000000..ab08a813d --- /dev/null +++ b/templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsFragment.kt.twig @@ -0,0 +1,85 @@ +package {{ sdk.namespace | caseDot }}.android.ui.accounts + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import {{ sdk.namespace | caseDot }}.android.R +import {{ sdk.namespace | caseDot }}.android.databinding.FragmentAccountBinding + + +class AccountsFragment : Fragment() { + + private lateinit var binding: FragmentAccountBinding + private val viewModel: AccountsViewModel by viewModels() + + override fun onCreateView( + inflater: LayoutInflater , + container: ViewGroup? , + savedInstanceState: Bundle? + ): View { + binding = DataBindingUtil.inflate( + inflater, + R.layout.fragment_account, + container, + false + ) + binding.lifecycleOwner = viewLifecycleOwner + + binding.login.setOnClickListener{ + viewModel.onLogin( + binding.email.text.toString(), + binding.password.text.toString(), + context + ?.getSharedPreferences("example", Context.MODE_PRIVATE) + ?.getString("fcmToken", null) ?: "" + ) + } + binding.signup.setOnClickListener{ + viewModel.onSignup( + binding.email.text.toString(), + binding.password.text.toString(), + binding.name.text.toString() + ) + } + binding.getUser.setOnClickListener{ + viewModel.getUser() + } + binding.oAuth.setOnClickListener{ + viewModel.oAuthLogin(activity as ComponentActivity) + } + binding.logout.setOnClickListener{ + viewModel.logout() + } + + viewModel.error.observe(viewLifecycleOwner) { event -> + event?.getContentIfNotHandled()?.let { + Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show() + } + } + + viewModel.response.observe(viewLifecycleOwner) { event -> + event?.getContentIfNotHandled()?.let { + binding.responseTV.setText(it) + } + } + + viewModel.target.observe(viewLifecycleOwner) { event -> + event?.getContentIfNotHandled()?.let { + context + ?.getSharedPreferences("example", Context.MODE_PRIVATE) + ?.edit() + ?.putString("targetId", it.id) + ?.apply() + } + } + + return binding.root + } +} diff --git a/templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsViewModel.kt.twig b/templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsViewModel.kt.twig new file mode 100644 index 000000000..b841b6636 --- /dev/null +++ b/templates/kmp/androidApp/src/main/java/io/package/android/ui/accounts/AccountsViewModel.kt.twig @@ -0,0 +1,116 @@ +package {{ sdk.namespace | caseDot }}.android.ui.accounts + +import androidx.activity.ComponentActivity +import androidx.lifecycle.* +import {{ sdk.namespace | caseDot }}.ID +import {{ sdk.namespace | caseDot }}.android.services.MessagingService +import {{ sdk.namespace | caseDot }}.android.utils.Client.client +import {{ sdk.namespace | caseDot }}.android.utils.Event +import {{ sdk.namespace | caseDot }}.enums.OAuthProvider +import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception +import {{ sdk.namespace | caseDot }}.extensions.toJson +import {{ sdk.namespace | caseDot }}.models.Target +import {{ sdk.namespace | caseDot }}.services.Account +import kotlinx.coroutines.launch + +class AccountsViewModel : ViewModel() { + + private val _error = MutableLiveData>().apply { value = null } + val error: LiveData> = _error + + private val _response = MutableLiveData>().apply { value = null } + val response: LiveData> = _response + + private val _target = MutableLiveData>().apply { value = null } + val target: LiveData> = _target + + private val account by lazy { + val account = Account(client) + + MessagingService.account = account + + account + } + + fun onLogin( + email: String, + password: String, + token: String?, + ) { + viewModelScope.launch { + try { + val session = account.createEmailPasswordSession( + email, + password + ) + + if (token != null) { + val target = account.createPushTarget(ID.unique(), token) + + _target.postValue(Event(target)) + } + + _response.postValue(Event(session.toJson())) + } catch (e: AppwriteException) { + _error.postValue(Event(e)) + } + } + + } + + fun onSignup(email: String, password: String, name: String) { + viewModelScope.launch { + try { + val user = account.create( + ID.unique(), + email, + password, + name + ) + _response.postValue(Event(user.toJson())) + } catch (e: AppwriteException) { + _error.postValue(Event(e)) + } + } + + } + + fun oAuthLogin(activity: ComponentActivity) { + viewModelScope.launch { + try { + account.createOAuth2Session( + activity, + OAuthProvider.FACEBOOK, + "appwrite-callback-6070749e6acd4://demo.appwrite.io/auth/oauth2/success", + "appwrite-callback-6070749e6acd4://demo.appwrite.io/auth/oauth2/failure" + ) + } catch (e: Exception) { + _error.postValue(Event(e)) + } catch (e: AppwriteException) { + _error.postValue(Event(e)) + } + } + } + + fun getUser() { + viewModelScope.launch { + try { + val user = account.get() + _response.postValue(Event(user.toJson())) + } catch (e: AppwriteException) { + _error.postValue(Event(e)) + } + } + } + + fun logout() { + viewModelScope.launch { + try { + val result = account.deleteSession("current") + _response.postValue(Event(result.toJson())) + } catch (e: AppwriteException) { + _error.postValue(Event(e)) + } + } + } +} diff --git a/templates/kmp/androidApp/src/main/java/io/package/android/utils/Client.kt.twig b/templates/kmp/androidApp/src/main/java/io/package/android/utils/Client.kt.twig new file mode 100644 index 000000000..15accc843 --- /dev/null +++ b/templates/kmp/androidApp/src/main/java/io/package/android/utils/Client.kt.twig @@ -0,0 +1,15 @@ +package {{ sdk.namespace | caseDot }}.android.utils + +import android.content.Context +import {{ sdk.namespace | caseDot }}.Client + +object Client { + lateinit var client : Client + + fun create(context: Context) { + client = Client(context) + .setEndpoint("http://192.168.4.24/v1") + .setProject("65a8e2b4632c04b1f5da") + .setSelfSigned(true) + } +} diff --git a/templates/kmp/androidApp/src/main/java/io/package/android/utils/Event.kt.twig b/templates/kmp/androidApp/src/main/java/io/package/android/utils/Event.kt.twig new file mode 100644 index 000000000..a43a58886 --- /dev/null +++ b/templates/kmp/androidApp/src/main/java/io/package/android/utils/Event.kt.twig @@ -0,0 +1,27 @@ +package {{ sdk.namespace | caseDot }}.android.utils + +/** + * Used as a wrapper for data that is exposed via a LiveData that represents an event. + */ +open class Event(private val content: T) { + + var hasBeenHandled = false + private set // Allow external read but not write + + /** + * Returns the content and prevents its use again. + */ + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + /** + * Returns the content, even if it's already been handled. + */ + fun peekContent(): T = content +} diff --git a/templates/kmp/androidApp/src/main/res/drawable/ic_launcher_background.xml b/templates/kmp/androidApp/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/templates/kmp/androidApp/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/kmp/androidApp/src/main/res/drawable/ic_launcher_foreground.xml b/templates/kmp/androidApp/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..7706ab9e6 --- /dev/null +++ b/templates/kmp/androidApp/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/templates/kmp/androidApp/src/main/res/layout/activity_main.xml b/templates/kmp/androidApp/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..00cef8106 --- /dev/null +++ b/templates/kmp/androidApp/src/main/res/layout/activity_main.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/templates/kmp/androidApp/src/main/res/layout/fragment_account.xml b/templates/kmp/androidApp/src/main/res/layout/fragment_account.xml new file mode 100644 index 000000000..4173be134 --- /dev/null +++ b/templates/kmp/androidApp/src/main/res/layout/fragment_account.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + +