Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions src/SDK/Language/Unity.php
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,6 @@ public function getFiles(): array
'destination' => 'Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll.meta',
'template' => 'unity/Assets/Runtime/Core/Plugins/System.Text.Encodings.Web.dll.meta',
],
[
'scope' => 'default',
'destination' => 'Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml',
'template' => 'unity/Assets/Runtime/Core/Plugins/Android/AndroidManifest.xml.twig',
],
[
'scope' => 'copy',
'destination' => 'Assets/Runtime/Core/Plugins/WebGLCookies.jslib',
Expand Down Expand Up @@ -389,6 +384,11 @@ public function getFiles(): array
'destination' => 'Assets/Editor/{{ spec.info.title | caseUcfirst }}SetupWindow.cs',
'template' => 'unity/Assets/Editor/AppwriteSetupWindow.cs.twig',
],
[
'scope' => 'default',
'destination' => 'Assets/Editor/{{ spec.info.title | caseUcfirst }}AndroidManifestProcessor.cs',
'template' => 'unity/Assets/Editor/AppwriteAndroidManifestProcessor.cs.twig',
],
// Samples
[
'scope' => 'default',
Expand Down Expand Up @@ -550,6 +550,7 @@ public function getFiles(): array
'Assets/Editor/{{ spec.info.title | caseUcfirst }}.Editor.asmdef',
'Assets/Editor/{{ spec.info.title | caseUcfirst }}SetupAssistant.cs',
'Assets/Editor/{{ spec.info.title | caseUcfirst }}SetupWindow.cs',
'Assets/Editor/{{ spec.info.title | caseUcfirst }}AndroidManifestProcessor.cs',
];

$files = array_filter($files, fn(array $file): bool => !in_array($file['destination'], $excludeInTest));
Expand Down Expand Up @@ -636,13 +637,6 @@ private function getMetaContents(string $relativePath, bool $isDir): string
return implode("\n", $lines) . "\n";
}

// Plugins/Android/AndroidManifest.xml must import as an Android
// plugin so Unity merges it into the build manifest; a plain
// DefaultImporter would import it as an inert text asset.
if (str_ends_with($relativePath, 'Plugins/Android/AndroidManifest.xml')) {
return implode("\n", $this->pluginImporterMeta($guid, 'Android')) . "\n";
}

$extension = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION));

$lines = match ($extension) {
Expand Down Expand Up @@ -680,7 +674,7 @@ private function getMetaContents(string $relativePath, bool $isDir): string
* Build PluginImporter meta lines enabling a single target platform
* (and the editor stub), with every other platform disabled.
*
* @param string $platform Unity platform key (e.g. 'WebGL', 'Android').
* @param string $platform Unity platform key (e.g. 'WebGL').
* @return array<string>
*/
private function pluginImporterMeta(string $guid, string $platform): array
Expand Down
107 changes: 107 additions & 0 deletions templates/unity/Assets/Editor/AppwriteAndroidManifestProcessor.cs.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#if UNITY_ANDROID
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using UnityEditor;
using UnityEditor.Android;
using UnityEngine;

namespace {{ spec.info.title | caseUcfirst }}.Editor
{
public class {{ spec.info.title | caseUcfirst }}AndroidManifestProcessor : IPostGenerateGradleAndroidProject
{
private const string AndroidNamespace = "http://schemas.android.com/apk/res/android";
private const string CallbackSchemePrefix = "{{ spec.info.title | caseLower }}-callback-";

public int callbackOrder => 0;

public void OnPostGenerateGradleAndroidProject(string path)
{
var schemes = FindCallbackSchemes();
if (schemes.Count == 0)
{
Debug.Log("{{ spec.info.title | caseUcfirst }}: No {{ spec.info.title | caseUcfirst }}Config with a project ID was found, so no OAuth callback was registered. To use OAuth on Android, set the project ID on a {{ spec.info.title | caseUcfirst }}Config asset, or add an intent filter for the {{ spec.info.title | caseLower }}-callback-<PROJECT_ID> scheme to a custom main manifest.");
return;
}

var manifestPath = Path.Combine(path, "src", "main", "AndroidManifest.xml");
var manifest = new XmlDocument();
manifest.Load(manifestPath);

var activity = FindUnityActivity(manifest);
if (activity == null)
{
Debug.LogWarning($"{{ spec.info.title | caseUcfirst }}: The Unity activity was not found in {manifestPath}, so no OAuth callback was registered.");
return;
}

var registered = new HashSet<string>(manifest.GetElementsByTagName("data")
.OfType<XmlElement>()
.Select(data => data.GetAttribute("scheme", AndroidNamespace)));

foreach (var scheme in schemes.Where(scheme => !registered.Contains(scheme)))
{
activity.AppendChild(CreateIntentFilter(manifest, scheme));
}

manifest.Save(manifestPath);
}

private static HashSet<string> FindCallbackSchemes()
{
var schemes = new HashSet<string>();
foreach (var guid in AssetDatabase.FindAssets("t:{{ spec.info.title | caseUcfirst }}Config"))
{
var config = AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(guid));
if (config == null)
{
continue;
}

var projectId = new SerializedObject(config).FindProperty("projectId")?.stringValue;
if (!string.IsNullOrEmpty(projectId))
{
schemes.Add(CallbackSchemePrefix + projectId);
}
}

return schemes;
}

private static XmlElement FindUnityActivity(XmlDocument manifest)
{
return manifest.GetElementsByTagName("activity")
.OfType<XmlElement>()
.FirstOrDefault(activity => HasAndroidName(activity, "meta-data", "unityplayer.UnityActivity")
|| HasAndroidName(activity, "category", "android.intent.category.LAUNCHER"));
}

private static bool HasAndroidName(XmlElement parent, string tag, string name)
{
return parent.GetElementsByTagName(tag)
.OfType<XmlElement>()
.Any(element => element.GetAttribute("name", AndroidNamespace) == name);
}

private static XmlElement CreateIntentFilter(XmlDocument manifest, string scheme)
{
var filter = manifest.CreateElement("intent-filter");
filter.AppendChild(CreateElement(manifest, "action", "name", "android.intent.action.VIEW"));
filter.AppendChild(CreateElement(manifest, "category", "name", "android.intent.category.DEFAULT"));
filter.AppendChild(CreateElement(manifest, "category", "name", "android.intent.category.BROWSABLE"));
filter.AppendChild(CreateElement(manifest, "data", "scheme", scheme));
return filter;
}

private static XmlElement CreateElement(XmlDocument manifest, string tag, string attribute, string value)
{
var element = manifest.CreateElement(tag);
var node = manifest.CreateAttribute("android", attribute, AndroidNamespace);
node.Value = value;
element.Attributes.Append(node);
return element;
}
}
}
#endif
6 changes: 4 additions & 2 deletions templates/unity/Assets/Editor/AppwriteSetupWindow.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,10 @@ namespace {{ spec.info.title | caseUcfirst }}.Editor
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.BeginHorizontal();
var buttonStyle = new GUIStyle(GUI.skin.button) { padding = new RectOffset(15, 15, 8, 8), margin = new RectOffset(5, 5, 5, 5), fontSize = 11 };
if (GUILayout.Button(new GUIContent(" 📖 Documentation", "Open {{ spec.info.title | caseUcfirst }} documentation"), buttonStyle)) { Application.OpenURL("{{ sdk.url }}"); }
if (GUILayout.Button(new GUIContent(" 💬 Discord Community", "Join our Discord community"), buttonStyle)) { Application.OpenURL("https://discord.gg/dJ9xrMr9hq"); }
if (GUILayout.Button(new GUIContent(" 📖 Documentation", "Open the {{ spec.info.title | caseUcfirst }} SDK for Unity documentation"), buttonStyle)) { Application.OpenURL("https://github.com/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}#readme"); }
{% if sdk.discordUrl %}
if (GUILayout.Button(new GUIContent(" 💬 Discord Community", "Join our Discord community"), buttonStyle)) { Application.OpenURL("{{ sdk.discordUrl }}"); }
{% endif %}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
Expand Down

This file was deleted.

15 changes: 15 additions & 0 deletions templates/unity/README.md.twig
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ You can also add the package to `Packages/manifest.json` directly:
1. Download the source code archive of a release from [GitHub](https://github.com/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}/releases) and extract it.
2. In **Window > Package Manager**, click the **+** button and select **Add package from disk** (**Install package from disk** in Unity 6).
3. Select the `package.json` file in the extracted folder.

## OAuth on Android

OAuth sign-in returns to your app through the `{{ spec.info.title | caseLower }}-callback-<PROJECT_ID>` URL scheme. When you build for Android, the package registers this scheme on the Unity activity for the project ID set on each `{{ spec.info.title | caseUcfirst }}Config` asset in your project, so you do not need to edit any manifest.

If you create the client in code without a `{{ spec.info.title | caseUcfirst }}Config` asset, enable **Custom Main Manifest** in **Project Settings > Player > Android > Publishing Settings** and add this intent filter to the Unity activity in `Assets/Plugins/Android/AndroidManifest.xml`, replacing `<PROJECT_ID>` with your project ID:

```xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="{{ spec.info.title | caseLower }}-callback-<PROJECT_ID>" />
</intent-filter>
```
{% if sdk.gettingStarted %}

{{ sdk.gettingStarted|trim|raw }}
Expand Down
38 changes: 38 additions & 0 deletions tests/generation/GenerationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,44 @@ public function testUnityConfigReadsUntrackedAssetsAsKnowingTheReleasedFlags():
$this->assertStringContainsString("var known = knownservices == 0 ? {$mask} : knownservices;", $config);
}

/**
* A package installed from a git URL is read-only, so a manifest shipped in
* it keeps a placeholder OAuth scheme that the app can never fill in. The
* scheme the runtime waits for is registered at build time instead.
*/
public function testUnityRegistersTheOAuthCallbackWithoutAPackageManifest(): void
{
$files = $this->generate('unity', 'client');

$manifests = \array_filter(\array_keys($files), fn(string $path): bool => \str_contains(\strtolower($path), 'androidmanifest.xml'));
$this->assertSame([], \array_values($manifests));

foreach ($files as $path => $contents) {
if (!\str_starts_with($path, 'docs~/')) {
$this->assertStringNotContainsString('<your_project_id>', $contents, "{$path} ships a project ID placeholder");
}
}

$this->assertStringContainsString('$"appwrite-callback-{projectid}"', $files['Runtime/Core/WebAuthComponent.cs']);
$processor = $files['Editor/AppwriteAndroidManifestProcessor.cs'] ?? '';
$this->assertStringContainsString(': ipostgenerategradleandroidproject', $processor);
$this->assertStringContainsString('callbackschemeprefix = "appwrite-callback-"', $processor);
$this->assertStringContainsString('"android.intent.category.browsable"', $processor);
}

/**
* The setup window links to this SDK's documentation and the configured
* community, not the generator's homepage URL or a fixed invite.
*/
public function testUnitySetupWindowLinksToTheSdkDocumentation(): void
{
$window = $this->generate('unity', 'client')['Editor/AppwriteSetupWindow.cs'];

$this->assertStringContainsString('application.openurl("https://github.com/', $window);
$this->assertStringContainsString('#readme")', $window);
$this->assertStringNotContainsString('discord.gg', $window);
}

/**
* A plugin left out of the WebGL player is missing when that player compiles,
* so its namespace may only be imported where WebGL players skip the code.
Expand Down
Loading