diff --git a/example.php b/example.php index f9845b9a9..60d8ab982 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; @@ -465,6 +466,31 @@ function getSSLPage($url) { ; $sdk->generate(__DIR__ . '/examples/android'); + // KMP + + $sdk = new SDK(new KMP(), new Swagger2($spec)); + + $sdk + ->setName('KMP') + ->setNamespace('io appwrite') + ->setDescription('Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Flutter SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to https://appwrite.io/docs') + ->setShortDescription('Appwrite KMP SDK') + ->setURL('https://example.com') + ->setGitUserName('appwrite') + ->setGitRepoName('sdk-for-kmp') + ->setLogo('https://appwrite.io/v1/images/console.png') + ->setLicenseContent('test test test') + ->setWarning('**This SDK is compatible with Appwrite server version 0.7.x. For older versions, please check previous releases.**') + ->setChangelog('**CHANGELOG**') + ->setVersion('0.0.0-SNAPSHOT') + ->setTwitter('appwrite_io') + ->setDiscord('564160730845151244', 'https://appwrite.io/discord') + ->setDefaultHeaders([ + 'x-appwrite-response-format' => '1.6.0', + ]) + ; + $sdk->generate(__DIR__ . '/examples/kmp'); + // Kotlin $sdk = new SDK(new Kotlin(), new Swagger2($spec)); diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index 8f1fdb1f9..22b8af970 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -339,11 +339,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..784367c73 --- /dev/null +++ b/src/SDK/Language/KMP.php @@ -0,0 +1,624 @@ +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); + }); + + return $filters; + } + + 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?')) { + $type = 'List<@Contextual Any>'; + } + 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/StringCollectionSeriailizer.kt', + 'template' => '/kmp/shared/src/commonMain/kotlin/io/package/serializers/StringCollectionSeriailizer.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', + ], + + + // JVM Main + // JVM Main root files + [ + 'scope' => 'default', + 'destination' => 'shared/src/jvmMain/kotlin/{{ sdk.namespace | caseSlash }}/AllCertsTrustManager.kt', + 'template' => '/kmp/shared/src/jvmMain/kotlin/io/package/AllCertsTrustManager.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/jvmMain/kotlin/{{ sdk.namespace | caseSlash }}/Client.jvm.kt', + 'template' => '/kmp/shared/src/jvmMain/kotlin/io/package/Client.jvm.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/jvmMain/kotlin/{{ sdk.namespace | caseSlash }}/HttpClient.kt', + 'template' => '/kmp/shared/src/jvmMain/kotlin/io/package/HttpClient.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/jvmMain/kotlin/{{ sdk.namespace | caseSlash }}/WebAuthComponent.jvm.kt', + 'template' => '/kmp/shared/src/jvmMain/kotlin/io/package/WebAuthComponent.jvm.kt.twig', + ], + + // Extensions + [ + 'scope' => 'default', + 'destination' => 'shared/src/jvmMain/kotlin/{{ sdk.namespace | caseSlash }}/extensions/OAuth2Extensions.kt', + 'template' => '/kmp/shared/src/jvmMain/kotlin/io/package/extensions/OAuth2Extensions.kt.twig', + ], + + // Models + [ + 'scope' => 'default', + 'destination' => 'shared/src/jvmMain/kotlin/{{ sdk.namespace | caseSlash }}/models/InputFile.jvm.kt', + 'template' => '/kmp/shared/src/jvmMain/kotlin/io/package/models/InputFile.jvm.kt.twig', + ], + + // Web Interface + [ + 'scope' => 'default', + 'destination' => 'shared/src/jvmMain/kotlin/{{ sdk.namespace | caseSlash }}/webInterface/JvmParsedUrl.kt', + 'template' => '/kmp/shared/src/jvmMain/kotlin/io/package/webInterface/JvmParsedUrl.kt.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'shared/src/jvmMain/kotlin/{{ sdk.namespace | caseSlash }}/webInterface/UrlParser.jvm.kt', + 'template' => '/kmp/shared/src/jvmMain/kotlin/io/package/webInterface/UrlParser.jvm.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' => 'copy', + 'destination' => 'androidApp/src/main/res/values/strings.xml', + 'template' => '/kmp/androidApp/src/main/res/values/strings.xml', + ], + [ + 'scope' => 'copy', + 'destination' => 'androidApp/src/main/res/values/themes.xml', + 'template' => '/kmp/androidApp/src/main/res/values/themes.xml', + ], + + + // 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 764949528..7cf53f8ff 100644 --- a/src/SDK/Language/Kotlin.php +++ b/src/SDK/Language/Kotlin.php @@ -426,8 +426,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); @@ -447,7 +447,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'; @@ -466,7 +466,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..ab3d61fe8 --- /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("io.github.camka14.appwrite:sdk-for-kmp:0.1.0") + } + } + } +} +``` + +### 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..01cebe28b --- /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 @@ + + + + + + + + + + + + + + + + +