diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..2b6eacf
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,343 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+permissions:
+ contents: read
+
+jobs:
+ build-test:
+ name: Build & Test (C# compilation check)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Create Unity stub project for compilation
+ run: |
+ mkdir -p .ci-build
+
+ # Create a .csproj that references all Runtime .cs files
+ # with Unity Engine stubs so pure C# logic compiles
+ cat > .ci-build/Allow2.SDK.CI.csproj << 'CSPROJ'
+
+
+ net8.0
+ disable
+ false
+
+ CS0649;CS0414;CS0169;CS0067
+
+
+
+
+
+
+ CSPROJ
+
+ # Create minimal Unity Engine stubs so the code compiles
+ cat > .ci-build/UnityStubs.cs << 'STUBS'
+ // Minimal Unity Engine stubs for CI compilation checks.
+ // These provide just enough type surface for the SDK's pure C#
+ // to compile outside of the Unity Editor.
+
+ using System;
+ using System.Collections;
+ using System.Text;
+
+ namespace UnityEngine
+ {
+ public class Object { }
+
+ public class MonoBehaviour : Behaviour
+ {
+ public Coroutine StartCoroutine(IEnumerator routine) => new Coroutine();
+ public void StopCoroutine(Coroutine routine) { }
+ }
+
+ public class Behaviour : Component
+ {
+ public bool enabled { get; set; }
+ }
+
+ public class Component : Object
+ {
+ public GameObject gameObject => null;
+ public Transform transform => null;
+ }
+
+ public class GameObject : Object
+ {
+ public string name { get; set; }
+ public T AddComponent() where T : Component => default;
+ public T GetComponent() => default;
+ public static void DontDestroyOnLoad(Object target) { }
+ public GameObject(string name) { }
+ }
+
+ public class Transform : Component { }
+
+ public class ScriptableObject : Object { }
+
+ public class Coroutine { }
+
+ public class WaitForSeconds
+ {
+ public WaitForSeconds(float seconds) { }
+ }
+
+ public class WaitForSecondsRealtime : CustomYieldInstruction
+ {
+ public WaitForSecondsRealtime(float seconds) { }
+ public override bool keepWaiting => false;
+ }
+
+ public abstract class CustomYieldInstruction : IEnumerator
+ {
+ public abstract bool keepWaiting { get; }
+ public object Current => null;
+ public bool MoveNext() => keepWaiting;
+ public void Reset() { }
+ }
+
+ public static class Debug
+ {
+ public static void Log(object message) { }
+ public static void LogWarning(object message) { }
+ public static void LogError(object message) { }
+ }
+
+ public static class PlayerPrefs
+ {
+ public static string GetString(string key, string defaultValue = "") => defaultValue;
+ public static void SetString(string key, string value) { }
+ public static int GetInt(string key, int defaultValue = 0) => defaultValue;
+ public static void SetInt(string key, int value) { }
+ public static void DeleteKey(string key) { }
+ public static void Save() { }
+ public static bool HasKey(string key) => false;
+ }
+
+ public static class JsonUtility
+ {
+ public static string ToJson(object obj) => "{}";
+ public static T FromJson(string json) => default;
+ }
+
+ public static class Application
+ {
+ public static RuntimePlatform platform => RuntimePlatform.LinuxPlayer;
+ public static string productName => "CI";
+ public static string version => "0.0.0";
+ public static string unityVersion => "2021.3.0f1";
+ }
+
+ public enum RuntimePlatform
+ {
+ LinuxPlayer, WindowsPlayer, OSXPlayer, Android, IPhonePlayer, WebGLPlayer
+ }
+
+ [AttributeUsage(AttributeTargets.Field)]
+ public class SerializeFieldAttribute : Attribute { }
+
+ [AttributeUsage(AttributeTargets.Field)]
+ public class HeaderAttribute : Attribute
+ {
+ public HeaderAttribute(string header) { }
+ }
+
+ [AttributeUsage(AttributeTargets.Field)]
+ public class TooltipAttribute : Attribute
+ {
+ public TooltipAttribute(string tooltip) { }
+ }
+
+ [AttributeUsage(AttributeTargets.Field)]
+ public class SpaceAttribute : Attribute
+ {
+ public SpaceAttribute() { }
+ public SpaceAttribute(float height) { }
+ }
+
+ [AttributeUsage(AttributeTargets.Field)]
+ public class TextAreaAttribute : Attribute
+ {
+ public TextAreaAttribute() { }
+ public TextAreaAttribute(int minLines, int maxLines) { }
+ }
+
+ [AttributeUsage(AttributeTargets.Field)]
+ public class RangeAttribute : Attribute
+ {
+ public RangeAttribute(float min, float max) { }
+ }
+ }
+
+ namespace UnityEngine.Events
+ {
+ public class UnityEvent
+ {
+ public void Invoke() { }
+ public void AddListener(Action call) { }
+ public void RemoveListener(Action call) { }
+ }
+
+ public class UnityEvent : UnityEvent
+ {
+ public void Invoke(T0 arg0) { }
+ public void AddListener(Action call) { }
+ public void RemoveListener(Action call) { }
+ }
+
+ public class UnityEvent : UnityEvent
+ {
+ public void Invoke(T0 arg0, T1 arg1) { }
+ public void AddListener(Action call) { }
+ public void RemoveListener(Action call) { }
+ }
+
+ public class UnityEvent : UnityEvent
+ {
+ public void Invoke(T0 arg0, T1 arg1, T2 arg2) { }
+ public void AddListener(Action call) { }
+ public void RemoveListener(Action call) { }
+ }
+ }
+
+ namespace UnityEngine.Networking
+ {
+ public class UnityWebRequest : IDisposable
+ {
+ public string url { get; set; }
+ public string method { get; set; }
+ public long responseCode { get; }
+ public Result result { get; }
+ public DownloadHandler downloadHandler { get; set; }
+ public UploadHandler uploadHandler { get; set; }
+
+ public UnityWebRequest(string url, string method) { }
+
+ public static UnityWebRequest Get(string uri) => new UnityWebRequest(uri, "GET");
+ public static string EscapeURL(string s) => Uri.EscapeDataString(s);
+
+ public void SetRequestHeader(string name, string value) { }
+ public UnityWebRequestAsyncOperation SendWebRequest() => new UnityWebRequestAsyncOperation();
+ public void Dispose() { }
+
+ public enum Result { InProgress, Success, ConnectionError, ProtocolError, DataProcessingError }
+ }
+
+ public class UnityWebRequestAsyncOperation : AsyncOperation { }
+
+ public class AsyncOperation : YieldInstruction
+ {
+ public bool isDone { get; }
+ }
+
+ public class YieldInstruction { }
+
+ public class DownloadHandler : IDisposable
+ {
+ public virtual string text => "";
+ public virtual byte[] data => Array.Empty();
+ public void Dispose() { }
+ }
+
+ public class DownloadHandlerBuffer : DownloadHandler
+ {
+ public DownloadHandlerBuffer() { }
+ }
+
+ public class UploadHandler : IDisposable
+ {
+ public string contentType { get; set; }
+ public void Dispose() { }
+ }
+
+ public class UploadHandlerRaw : UploadHandler
+ {
+ public UploadHandlerRaw(byte[] data) { }
+ }
+ }
+ STUBS
+
+ - name: Restore .NET packages
+ run: dotnet restore .ci-build/Allow2.SDK.CI.csproj
+
+ - name: Build SDK
+ run: dotnet build .ci-build/Allow2.SDK.CI.csproj --configuration Release --no-restore
+
+ - name: Run tests (if present)
+ run: |
+ if compgen -G "com.allow2.sdk/Tests/**/*.cs" > /dev/null 2>&1; then
+ echo "Test files found — running tests"
+ dotnet test .ci-build/Allow2.SDK.CI.csproj --no-build --configuration Release
+ else
+ echo "No test files found in com.allow2.sdk/Tests/ — skipping"
+ fi
+
+ validate-package:
+ name: Validate package.json
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Validate package.json structure
+ run: |
+ PKG="com.allow2.sdk/package.json"
+
+ if [ ! -f "$PKG" ]; then
+ echo "::error::package.json not found at $PKG"
+ exit 1
+ fi
+
+ # Validate it's valid JSON
+ if ! python3 -c "import json; json.load(open('$PKG'))"; then
+ echo "::error::package.json is not valid JSON"
+ exit 1
+ fi
+
+ # Check required fields
+ for field in name version displayName description unity author license; do
+ if ! python3 -c "
+ import json, sys
+ pkg = json.load(open('$PKG'))
+ if '$field' not in pkg or not pkg['$field']:
+ print(f'::error::Missing required field: $field')
+ sys.exit(1)
+ "; then
+ exit 1
+ fi
+ done
+
+ # Verify package name matches expected
+ NAME=$(python3 -c "import json; print(json.load(open('$PKG'))['name'])")
+ if [ "$NAME" != "com.allow2.sdk" ]; then
+ echo "::error::Package name should be 'com.allow2.sdk', got '$NAME'"
+ exit 1
+ fi
+
+ VERSION=$(python3 -c "import json; print(json.load(open('$PKG'))['version'])")
+ echo "Package: $NAME v$VERSION"
+ echo "package-version=$VERSION" >> "$GITHUB_OUTPUT"
+
+ - name: Check version matches tag (on tag push only)
+ if: startsWith(github.ref, 'refs/tags/v')
+ run: |
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
+ PKG_VERSION=$(python3 -c "import json; print(json.load(open('com.allow2.sdk/package.json'))['version'])")
+
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
+ echo "::error::Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)"
+ exit 1
+ fi
+
+ echo "Version match confirmed: v$PKG_VERSION"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..b1ff815
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,299 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: write
+
+jobs:
+ validate:
+ name: Validate release
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.check.outputs.version }}
+ prerelease: ${{ steps.check.outputs.prerelease }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Validate tag matches package.json
+ id: check
+ run: |
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
+ PKG_VERSION=$(python3 -c "import json; print(json.load(open('com.allow2.sdk/package.json'))['version'])")
+
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
+ echo "::error::Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)"
+ exit 1
+ fi
+
+ echo "version=$PKG_VERSION" >> "$GITHUB_OUTPUT"
+
+ # Detect pre-release (alpha, beta, rc, preview)
+ if echo "$PKG_VERSION" | grep -qE '[-](alpha|beta|rc|preview)'; then
+ echo "prerelease=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "prerelease=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ echo "Releasing v$PKG_VERSION (prerelease=${{ steps.check.outputs.prerelease || 'false' }})"
+
+ build-check:
+ name: Compilation check
+ needs: validate
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Build SDK
+ run: |
+ mkdir -p .ci-build
+
+ cat > .ci-build/Allow2.SDK.CI.csproj << 'CSPROJ'
+
+
+ net8.0
+ disable
+ CS0649;CS0414;CS0169;CS0067
+
+
+
+
+
+
+ CSPROJ
+
+ # Reuse stubs from CI workflow (same file)
+ cat > .ci-build/UnityStubs.cs << 'STUBS'
+ using System;
+ using System.Collections;
+
+ namespace UnityEngine
+ {
+ public class Object { }
+ public class MonoBehaviour : Behaviour
+ {
+ public Coroutine StartCoroutine(IEnumerator routine) => new Coroutine();
+ public void StopCoroutine(Coroutine routine) { }
+ }
+ public class Behaviour : Component { public bool enabled { get; set; } }
+ public class Component : Object
+ {
+ public GameObject gameObject => null;
+ public Transform transform => null;
+ }
+ public class GameObject : Object
+ {
+ public string name { get; set; }
+ public T AddComponent() where T : Component => default;
+ public T GetComponent() => default;
+ public static void DontDestroyOnLoad(Object target) { }
+ public GameObject(string name) { }
+ }
+ public class Transform : Component { }
+ public class ScriptableObject : Object { }
+ public class Coroutine { }
+ public class WaitForSeconds { public WaitForSeconds(float s) { } }
+ public class WaitForSecondsRealtime : CustomYieldInstruction
+ {
+ public WaitForSecondsRealtime(float s) { }
+ public override bool keepWaiting => false;
+ }
+ public abstract class CustomYieldInstruction : IEnumerator
+ {
+ public abstract bool keepWaiting { get; }
+ public object Current => null;
+ public bool MoveNext() => keepWaiting;
+ public void Reset() { }
+ }
+ public static class Debug
+ {
+ public static void Log(object m) { }
+ public static void LogWarning(object m) { }
+ public static void LogError(object m) { }
+ }
+ public static class PlayerPrefs
+ {
+ public static string GetString(string k, string d = "") => d;
+ public static void SetString(string k, string v) { }
+ public static int GetInt(string k, int d = 0) => d;
+ public static void SetInt(string k, int v) { }
+ public static void DeleteKey(string k) { }
+ public static void Save() { }
+ public static bool HasKey(string k) => false;
+ }
+ public static class JsonUtility
+ {
+ public static string ToJson(object o) => "{}";
+ public static T FromJson(string j) => default;
+ }
+ public static class Application
+ {
+ public static RuntimePlatform platform => RuntimePlatform.LinuxPlayer;
+ public static string productName => "CI";
+ public static string version => "0.0.0";
+ public static string unityVersion => "2021.3.0f1";
+ }
+ public enum RuntimePlatform { LinuxPlayer, WindowsPlayer, OSXPlayer, Android, IPhonePlayer, WebGLPlayer }
+ [AttributeUsage(AttributeTargets.Field)] public class SerializeFieldAttribute : Attribute { }
+ [AttributeUsage(AttributeTargets.Field)] public class HeaderAttribute : Attribute { public HeaderAttribute(string h) { } }
+ [AttributeUsage(AttributeTargets.Field)] public class TooltipAttribute : Attribute { public TooltipAttribute(string t) { } }
+ [AttributeUsage(AttributeTargets.Field)] public class SpaceAttribute : Attribute { public SpaceAttribute() { } public SpaceAttribute(float h) { } }
+ [AttributeUsage(AttributeTargets.Field)] public class TextAreaAttribute : Attribute { public TextAreaAttribute() { } public TextAreaAttribute(int a, int b) { } }
+ [AttributeUsage(AttributeTargets.Field)] public class RangeAttribute : Attribute { public RangeAttribute(float a, float b) { } }
+ }
+ namespace UnityEngine.Events
+ {
+ public class UnityEvent { public void Invoke() { } public void AddListener(Action c) { } public void RemoveListener(Action c) { } }
+ public class UnityEvent : UnityEvent { public void Invoke(T0 a) { } public void AddListener(Action c) { } public void RemoveListener(Action c) { } }
+ public class UnityEvent : UnityEvent { public void Invoke(T0 a, T1 b) { } public void AddListener(Action c) { } public void RemoveListener(Action c) { } }
+ public class UnityEvent : UnityEvent { public void Invoke(T0 a, T1 b, T2 c) { } public void AddListener(Action c) { } public void RemoveListener(Action c) { } }
+ }
+ namespace UnityEngine.Networking
+ {
+ public class UnityWebRequest : IDisposable
+ {
+ public string url { get; set; }
+ public string method { get; set; }
+ public long responseCode { get; }
+ public Result result { get; }
+ public DownloadHandler downloadHandler { get; set; }
+ public UploadHandler uploadHandler { get; set; }
+ public UnityWebRequest(string u, string m) { }
+ public static UnityWebRequest Get(string u) => new UnityWebRequest(u, "GET");
+ public static string EscapeURL(string s) => Uri.EscapeDataString(s);
+ public void SetRequestHeader(string n, string v) { }
+ public UnityWebRequestAsyncOperation SendWebRequest() => new UnityWebRequestAsyncOperation();
+ public void Dispose() { }
+ public enum Result { InProgress, Success, ConnectionError, ProtocolError, DataProcessingError }
+ }
+ public class UnityWebRequestAsyncOperation : AsyncOperation { }
+ public class AsyncOperation : YieldInstruction { public bool isDone { get; } }
+ public class YieldInstruction { }
+ public class DownloadHandler : IDisposable { public virtual string text => ""; public virtual byte[] data => Array.Empty(); public void Dispose() { } }
+ public class DownloadHandlerBuffer : DownloadHandler { public DownloadHandlerBuffer() { } }
+ public class UploadHandler : IDisposable { public string contentType { get; set; } public void Dispose() { } }
+ public class UploadHandlerRaw : UploadHandler { public UploadHandlerRaw(byte[] d) { } }
+ }
+ STUBS
+
+ dotnet build .ci-build/Allow2.SDK.CI.csproj --configuration Release
+
+ create-release:
+ name: Create GitHub Release
+ needs: [validate, build-check]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Generate changelog
+ id: changelog
+ run: |
+ VERSION="${{ needs.validate.outputs.version }}"
+
+ # Find the previous tag
+ PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]' | head -2 | tail -1)
+
+ if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "v$VERSION" ]; then
+ # First release or only one tag — use all commits
+ COMMITS=$(git log --pretty=format:"- %s (%h)" --no-merges HEAD)
+ else
+ COMMITS=$(git log --pretty=format:"- %s (%h)" --no-merges "$PREV_TAG"..HEAD)
+ fi
+
+ if [ -z "$COMMITS" ]; then
+ COMMITS="- Initial release"
+ fi
+
+ # Write changelog to file (handles multiline safely)
+ cat > /tmp/changelog.md << CHANGELOG_EOF
+ ## What's Changed
+
+ $COMMITS
+
+ ## Installation
+
+ ### Unity Package Manager (Git URL)
+
+ Add to your \`Packages/manifest.json\`:
+
+ \`\`\`json
+ {
+ "dependencies": {
+ "com.allow2.sdk": "https://github.com/Allow2/allow2unity.git?path=com.allow2.sdk#v${VERSION}"
+ }
+ }
+ \`\`\`
+
+ Or in the Unity Editor:
+ 1. Open **Window > Package Manager**
+ 2. Click **+** > **Add package from git URL**
+ 3. Enter: \`https://github.com/Allow2/allow2unity.git?path=com.allow2.sdk#v${VERSION}\`
+
+ ### OpenUPM
+
+ \`\`\`bash
+ openupm add com.allow2.sdk
+ \`\`\`
+
+ ## Requirements
+
+ - Unity 2021.3 or later
+ - No additional dependencies
+ CHANGELOG_EOF
+
+ - name: Create release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ VERSION="${{ needs.validate.outputs.version }}"
+ PRERELEASE="${{ needs.validate.outputs.prerelease }}"
+
+ PRERELEASE_FLAG=""
+ if [ "$PRERELEASE" = "true" ]; then
+ PRERELEASE_FLAG="--prerelease"
+ fi
+
+ gh release create "v${VERSION}" \
+ --title "v${VERSION}" \
+ --notes-file /tmp/changelog.md \
+ $PRERELEASE_FLAG
+
+ notify-openupm:
+ name: Notify OpenUPM
+ needs: [validate, create-release]
+ runs-on: ubuntu-latest
+ # This job is optional — it will not fail the release if OpenUPM notification fails
+ continue-on-error: true
+ steps:
+ - name: Trigger OpenUPM update
+ run: |
+ VERSION="${{ needs.validate.outputs.version }}"
+
+ # OpenUPM automatically detects new tags for registered packages.
+ # This step creates a lightweight notification; if the package is
+ # not yet registered on OpenUPM, this will simply be a no-op.
+ #
+ # To register: https://openupm.com/packages/add/
+ # Package name: com.allow2.sdk
+ # Repository: https://github.com/Allow2/allow2unity
+
+ echo "Release v${VERSION} published. OpenUPM will auto-detect the new tag."
+ echo ""
+ echo "If the package is not yet on OpenUPM, register it at:"
+ echo " https://openupm.com/packages/add/"
+ echo ""
+ echo "Package: com.allow2.sdk"
+ echo "Git URL: https://github.com/Allow2/allow2unity.git"
+ echo "Min Unity: 2021.3"
diff --git a/.github/workflows/store-publish.yml b/.github/workflows/store-publish.yml
new file mode 100644
index 0000000..9b8ca45
--- /dev/null
+++ b/.github/workflows/store-publish.yml
@@ -0,0 +1,197 @@
+name: Asset Store Publish
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: write
+
+jobs:
+ build-unitypackage:
+ name: Build .unitypackage
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ filename: ${{ steps.version.outputs.filename }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Extract version
+ id: version
+ run: |
+ TAG_VERSION="${GITHUB_REF#refs/tags/v}"
+ PKG_VERSION=$(python3 -c "import json; print(json.load(open('com.allow2.sdk/package.json'))['version'])")
+
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
+ echo "::error::Tag version ($TAG_VERSION) does not match package.json version ($PKG_VERSION)"
+ exit 1
+ fi
+
+ echo "version=$PKG_VERSION" >> "$GITHUB_OUTPUT"
+ echo "filename=Allow2SDK-${PKG_VERSION}.unitypackage" >> "$GITHUB_OUTPUT"
+ echo "Building .unitypackage for v$PKG_VERSION"
+
+ - name: Build .unitypackage
+ run: |
+ chmod +x packaging/asset-store/export-unitypackage.sh
+ ./packaging/asset-store/export-unitypackage.sh --version "${{ steps.version.outputs.version }}"
+
+ - name: Verify package
+ run: |
+ FILENAME="${{ steps.version.outputs.filename }}"
+
+ if [ ! -f "$FILENAME" ]; then
+ echo "::error::Expected output file $FILENAME not found"
+ exit 1
+ fi
+
+ # Verify it is a valid gzipped tar
+ if ! file "$FILENAME" | grep -q "gzip"; then
+ echo "::error::$FILENAME is not a valid gzip archive"
+ exit 1
+ fi
+
+ # List contents to verify structure
+ echo "Package contents (first 30 entries):"
+ tar -tzf "$FILENAME" | head -30
+
+ # Verify Assets/Allow2 paths exist
+ if ! tar -tzf "$FILENAME" | grep -q "pathname"; then
+ echo "::error::Package does not contain pathname entries — invalid .unitypackage structure"
+ exit 1
+ fi
+
+ echo "Package verified: $(du -h "$FILENAME" | cut -f1)"
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: unitypackage
+ path: ${{ steps.version.outputs.filename }}
+ retention-days: 90
+
+ attach-to-release:
+ name: Attach to GitHub Release
+ needs: build-unitypackage
+ runs-on: ubuntu-latest
+ steps:
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: unitypackage
+
+ - name: Wait for release to exist
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ run: |
+ VERSION="${{ needs.build-unitypackage.outputs.version }}"
+ TAG="v${VERSION}"
+
+ # The release.yml workflow may still be creating the release.
+ # Poll for up to 5 minutes (30 attempts, 10s apart).
+ for i in $(seq 1 30); do
+ if gh release view "$TAG" > /dev/null 2>&1; then
+ echo "Release $TAG found."
+ exit 0
+ fi
+ echo "Waiting for release $TAG to be created... (attempt $i/30)"
+ sleep 10
+ done
+
+ echo "::error::Release $TAG was not found after 5 minutes"
+ exit 1
+
+ - name: Upload .unitypackage to release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ run: |
+ VERSION="${{ needs.build-unitypackage.outputs.version }}"
+ FILENAME="${{ needs.build-unitypackage.outputs.filename }}"
+ TAG="v${VERSION}"
+
+ gh release upload "$TAG" "$FILENAME" --clobber
+
+ echo "Attached $FILENAME to release $TAG"
+
+ submit-asset-store:
+ name: Submit to Asset Store
+ needs: [build-unitypackage, attach-to-release]
+ runs-on: ubuntu-latest
+ if: vars.ENABLE_ASSET_STORE == 'true'
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: unitypackage
+
+ - name: Submit to Unity Asset Store
+ env:
+ UNITY_ASSET_STORE_TOKEN: ${{ secrets.UNITY_ASSET_STORE_TOKEN }}
+ run: |
+ VERSION="${{ needs.build-unitypackage.outputs.version }}"
+ FILENAME="${{ needs.build-unitypackage.outputs.filename }}"
+
+ echo "============================================================"
+ echo " Asset Store Submission — v${VERSION}"
+ echo "============================================================"
+ echo ""
+
+ if [ -z "${UNITY_ASSET_STORE_TOKEN:-}" ]; then
+ echo "::warning::UNITY_ASSET_STORE_TOKEN secret is not set."
+ echo ""
+ echo "Automated Asset Store submission is not yet available."
+ echo "Unity does not currently provide a public API for automated"
+ echo "package submission. Follow the manual steps below."
+ echo ""
+ echo "MANUAL SUBMISSION STEPS:"
+ echo "========================"
+ echo ""
+ echo "1. Open Unity Editor (2021.3+)"
+ echo ""
+ echo "2. Install Asset Store Tools:"
+ echo " Window > Package Manager > + > Add package by name"
+ echo " Enter: com.unity.asset-store-tools"
+ echo ""
+ echo "3. Open Asset Store Tools:"
+ echo " Window > Asset Store Tools > Package Upload"
+ echo ""
+ echo "4. Log in with your Unity Publisher account"
+ echo ""
+ echo "5. Select your package draft or create a new one:"
+ echo " - Title: Allow2 Parental Freedom SDK"
+ echo " - Category: Tools/Integration"
+ echo " - Price: Free"
+ echo ""
+ echo "6. Upload method — choose ONE:"
+ echo ""
+ echo " a) Pre-built .unitypackage (recommended for CI):"
+ echo " - Download $FILENAME from the GitHub Release"
+ echo " - Use 'Upload from .unitypackage' option"
+ echo ""
+ echo " b) From project folder:"
+ echo " - Import the package into a Unity project"
+ echo " - Select Assets/Allow2 as the upload root"
+ echo ""
+ echo "7. Fill in metadata from:"
+ echo " packaging/asset-store/asset-store-metadata.json"
+ echo ""
+ echo "8. Upload key images (see metadata for required sizes)"
+ echo ""
+ echo "9. Submit for review"
+ echo ""
+ echo "Review typically takes 5-10 business days."
+ exit 0
+ fi
+
+ # If Unity provides an API in the future, automated submission
+ # would go here. For now, this is a placeholder.
+ echo "Asset Store token is set but automated submission API"
+ echo "is not yet implemented. Follow manual steps above."
diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml
new file mode 100644
index 0000000..ce98d23
--- /dev/null
+++ b/.github/workflows/version-check.yml
@@ -0,0 +1,77 @@
+name: Version Check
+
+on:
+ pull_request:
+ branches: [master]
+
+permissions:
+ contents: read
+ pull-requests: read
+
+jobs:
+ check-version-bump:
+ name: Verify version bump if Runtime changed
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check if Runtime files changed
+ id: changes
+ run: |
+ # Get list of changed files in this PR
+ CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}"...HEAD 2>/dev/null || \
+ git diff --name-only HEAD~1)
+
+ echo "Changed files:"
+ echo "$CHANGED"
+
+ # Check if any Runtime/ C# files changed
+ RUNTIME_CHANGED=$(echo "$CHANGED" | grep -c '^com\.allow2\.sdk/Runtime/.*\.cs$' || true)
+ echo "runtime-changed=$RUNTIME_CHANGED" >> "$GITHUB_OUTPUT"
+
+ # Check if package.json changed
+ PKG_CHANGED=$(echo "$CHANGED" | grep -c '^com\.allow2\.sdk/package\.json$' || true)
+ echo "package-changed=$PKG_CHANGED" >> "$GITHUB_OUTPUT"
+
+ echo "Runtime .cs files changed: $RUNTIME_CHANGED"
+ echo "package.json changed: $PKG_CHANGED"
+
+ - name: Verify version was bumped
+ if: steps.changes.outputs.runtime-changed != '0'
+ run: |
+ PKG_CHANGED="${{ steps.changes.outputs.package-changed }}"
+
+ if [ "$PKG_CHANGED" = "0" ]; then
+ echo "::warning::Runtime C# files were modified but package.json version was not updated."
+ echo ""
+ echo "If this PR includes user-facing changes, please bump the version in"
+ echo "com.allow2.sdk/package.json before merging."
+ echo ""
+ echo "Current version: $(python3 -c "import json; print(json.load(open('com.allow2.sdk/package.json'))['version'])")"
+ echo ""
+ echo "Semver guide:"
+ echo " - Bug fixes: patch (e.g., 2.0.1)"
+ echo " - New features: minor (e.g., 2.1.0)"
+ echo " - Breaking changes: major (e.g., 3.0.0)"
+ echo " - Pre-release: append (e.g., 2.0.0-alpha.2)"
+ exit 1
+ fi
+
+ # Verify the version actually changed (not just formatting)
+ BASE_VERSION=$(git show "origin/${{ github.base_ref }}:com.allow2.sdk/package.json" 2>/dev/null | \
+ python3 -c "import json, sys; print(json.load(sys.stdin)['version'])" 2>/dev/null || echo "unknown")
+ HEAD_VERSION=$(python3 -c "import json; print(json.load(open('com.allow2.sdk/package.json'))['version'])")
+
+ if [ "$BASE_VERSION" = "$HEAD_VERSION" ]; then
+ echo "::warning::package.json was modified but the version field is unchanged ($HEAD_VERSION)."
+ echo "Please bump the version number."
+ exit 1
+ fi
+
+ echo "Version bump confirmed: $BASE_VERSION -> $HEAD_VERSION"
+
+ - name: Skip (no Runtime changes)
+ if: steps.changes.outputs.runtime-changed == '0'
+ run: echo "No Runtime C# files changed — version bump not required."
diff --git a/.gitignore b/.gitignore
index bf793ed..22ad11b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
# build
[Oo]bj/
[Bb]in/
+*.unitypackage
packages/
TestResults/
@@ -39,3 +40,73 @@ Thumbs.db
# dotCover
*.dotCover
+
+# meta
+**/*.meta
+
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e69de29
diff --git a/.idea/Allow2.iml b/.idea/Allow2.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/.idea/Allow2.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..28a804d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..e01cbc3
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Allow2/Allow2.cs b/Allow2/Allow2.cs
index 7274ae0..64630bd 100644
--- a/Allow2/Allow2.cs
+++ b/Allow2/Allow2.cs
@@ -1,35 +1,755 @@
-using System;
-using System.Net.Http;
-using System.Threading.Tasks;
+//
+// Allow2Unity
+// Allow2.cs
+//
+// Created by Andrew Longhorn in Jan 2019.
+// Copyright © 2019 Allow2 Pty Ltd. All rights reserved.
+//
+// LICENSE:
+// See LICENSE file in root directory
+//
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.Networking;
+using Allow2_SimpleJSON;
+using System.Text;
namespace Allow2
{
- public class Connection
+
+ ///
+ /// Access functionality of the Allow2 platform easily.
+ ///
+ public static class Allow2
{
- private static readonly HttpClient client = new HttpClient();
+ private static readonly string uuid;
+ private static string _deviceToken = "Not Set"; // ie: "iug893-kjg-fiug23" - not persisted: always set this on start
+ public static EnvType env = EnvType.Production;
+
+ //
+ // relevant persistence items
+ //
+ static int userId; // ie: 27634
+ static string pairToken; // ie: "98hbieg87-ilulieugil-dilufkucy"
+ static string _timezone; // ie: "Australia/Brisbane"
+ static Dictionary _children = new Dictionary();
+ static int _childId = 0;
+
+ //HashSet checkers = new HashSet(); // contains uuids for running autocheckers (abort if your uuid is missing)
+ static string checkerUuid = null; // uuid for the current checker
+ static IEnumerator checker = null; // the current autochecker
+ static IEnumerator qrCall = null;
+ static DateTime qrDebounce = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
+ static string pairingUuid = null;
+
+ public static int childId; // ie: 34
+
+ public static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
+
+ public static string ApiUrl
+ {
+ get
+ {
+ switch (env)
+ {
+ //case EnvType.Sandbox:
+ //return "https://sandbox-api.allow2.com"
+ case EnvType.Staging:
+ return "https://staging-api.allow2.com";
+ default:
+ return "https://api.allow2.com";
+ }
+ }
+ }
+
+ public static Dictionary Children
+ {
+ get
+ {
+ return _children;
+ }
+ }
+
+ public static string ServiceUrl
+ {
+ get
+ {
+ switch (env)
+ {
+ //case EnvType.Sandbox:
+ //return "https://sandbox-service.allow2.com"
+ case EnvType.Staging:
+ return "https://staging-service.allow2.com";
+ default:
+ return "https://service.allow2.com";
+ }
+ }
+ }
+
+ public static resultClosure checkResultHandler;
+
+ public static bool IsPaired
+ {
+ get
+ {
+ return (userId > 0) && (pairToken != null);
+ }
+ }
+
+ public static string Timezone
+ {
+ get
+ {
+ return _timezone;
+ }
+ set
+ {
+ _timezone = value;
+ PlayerPrefs.SetString("timezone", _timezone);
+ }
+ }
+
+ ///
+ /// Gets or sets the device token.
+ /// The device token is mandatory, this needs to be set before making any calls to the sdk/api.
+ /// Generate your device token for free at https://developer.allow2.com
+ /// Use it to manage your app/game/device, promote it on the Allow2 platform and track downloads and usage.
+ ///
+ /// The device token.
+ public static string DeviceToken
+ {
+ get
+ {
+ return _deviceToken;
+ }
+ set
+ {
+ _deviceToken = value;
+ PlayerPrefs.SetString("deviceToken", _deviceToken);
+ //if (!IsPaired)
+ //{
+ // todo: start a regular timer to keep trying to call home on startup until we confirm we are NOT paired.
+ CheckForBrokenPairing();
+ //}
+ }
+ }
+
+ private static void Persist()
+ {
+ // todo: write to protected namespace storage?
+ PlayerPrefs.SetInt("userId", userId);
+ PlayerPrefs.SetString("pairToken", pairToken);
+ }
+
+ // no persistence here
+ private static Dictionary resultCache = new Dictionary();
+
+ ///
+ /// A result closure provides the result from a call to the Allow2 platform.
+ ///
+ public delegate void resultClosure(string err, Allow2CheckResult result);
+
+ ///
+ /// An image closure provides the image returned by the Allow2 platform.
+ ///
+ public delegate void imageClosure(string err, Texture2D image);
+
+ static Allow2()
+ {
+ uuid = SystemInfo.deviceUniqueIdentifier;
+ if (uuid == SystemInfo.unsupportedIdentifier)
+ {
+ // cannot use on this platform, kludge is to generate and store one, use auditing on the server side to detect disconnection in any case
+ uuid = PlayerPrefs.GetString("uuid");
+ if (uuid == null)
+ {
+ uuid = System.Guid.NewGuid().ToString();
+ PlayerPrefs.SetString("uuid", uuid);
+ }
+ }
+ userId = PlayerPrefs.GetInt("userId");
+ pairToken = PlayerPrefs.GetString("pairToken");
+ _timezone = PlayerPrefs.GetString("timezone");
+ }
+
+ static IEnumerator CheckForBrokenPairing()
+ {
+ // here we just ask the question, was our unique ID paired and somehow it lost it's pairing?
+ WWWForm form = new WWWForm();
+ form.AddField("uuid", uuid);
+ form.AddField("deviceToken", _deviceToken);
+
+ using (UnityWebRequest www = UnityWebRequest.Post(ApiUrl + "/api/isDevicePaired", form))
+ {
+ yield return www.SendWebRequest();
+
+ // we actually only need to check for a 200 response to know the server is informed and it's been checked.
+ if (www.isNetworkError || www.isHttpError)
+ {
+ Debug.Log(www.error);
+ }
+ else
+ {
+ Debug.Log(www.downloadHandler.text);
+ var response = Allow2_SimpleJSON.JSON.Parse(www.downloadHandler.text);
+ // extract
+ }
+ }
+ }
+
+ ///
+ /// Pair your game/app/device to a parents Allow2 account.
+ ///
+ /// Provide a (any) MonoBehaviour for the sdk to use to call the platform.
+ /// The email address of the Allow2 account being paired.
+ /// The associated password for the Allow2 account being paired.
+ /// The name the user would like to use to identify this app/game/device.
+ /// Provides the image of the QR Code.
+ public static void Pair(MonoBehaviour behaviour,
+ string user, // ie: "fred@gmail.com",
+ string pass, // ie: "my super secret password",
+ string deviceName, // ie: "Fred's iPhone"
+ resultClosure callback)
+ {
+ behaviour.StartCoroutine(_Pair(user, pass, deviceName, callback));
+ }
+
+ static IEnumerator _Pair(string user, // ie: "fred@gmail.com",
+ string pass, // ie: "my super secret password",
+ string deviceName, // ie: "Fred's iPhone"
+ resultClosure callback
+ )
+ {
+ if (IsPaired)
+ {
+ callback(Allow2Error.AlreadyPaired, null);
+ yield break;
+ }
+ WWWForm form = new WWWForm();
+ form.AddField("user", user);
+ form.AddField("pass", pass);
+ form.AddField("deviceToken", _deviceToken);
+ form.AddField("name", deviceName);
+ form.AddField("uuid", uuid);
+
+ Debug.Log(ApiUrl + "/api/pairDevice");
+
+ using (UnityWebRequest www = UnityWebRequest.Post(ApiUrl + "/api/pairDevice", form))
+ {
+ yield return www.SendWebRequest();
+
+ var response = JSONNode.Parse(www.downloadHandler.text);
+
+ if (response == null)
+ {
+ if (www.isNetworkError || www.isHttpError)
+ {
+ Debug.Log(www.error.ToString());
+ callback(www.error, null);
+ }
+ yield break;
+ }
+
+ //{
+ // "status":"success",
+ // "pairId":21105,
+ // "token":"8314c722-36fe-4256-81f4-8cce6e4da32d"
+ // "name":"Unity Test"
+ // "userId":6
+ // "children": [
+ // {"id":68,"name":"Cody"},
+ // {"id":69,"name":"Mikayla"},
+ // {"id":21423,"name":"Mary"}
+ // ]
+ //}
+
+ Debug.Log(www.downloadHandler.text);
+ var json = Allow2_SimpleJSON.JSON.Parse(www.downloadHandler.text);
+
+ if (json["status"] != "success")
+ {
+ callback(Allow2Error.InvalidResponse, null);
+ yield break;
+ }
+
+ userId = json["userId"];
+ pairToken = json["token"];
+ _children = ParseChildren(json["children"]);
+
+ Persist();
+
+ // return
+ callback(null, null);
+ }
+ }
+
+ const int QRDebounceDelay = 500;
+
+ ///
+ /// Gets a new QR Code texture to show to the user to enable them to pair your game/app/device with Allow2.
+ /// Call this to get a new QR Code any time the user changes the name of the device.
+ /// Note this is debounced automatically, so just keep calling it immediately (even if the user is still typing).
+ ///
+ /// Provide a (any) MonoBehaviour for the sdk to use to call the platform.
+ /// The name the user would like to use to identify this app/game/device.
+ /// Callback.
+ public static void GetQR(MonoBehaviour behaviour, string deviceName, imageClosure callback) {
+ DateTime now = DateTime.Now;
+ Debug.Log(qrDebounce.CompareTo(now));
+ if ((qrCall != null) && (qrDebounce.CompareTo(now) > 0)) {
+ Debug.Log("debounce " + qrDebounce.ToShortTimeString() + " < " + now.ToShortTimeString());
+ IEnumerator oldCall = qrCall;
+ qrCall = null;
+ behaviour.StopCoroutine(oldCall);
+ }
+ qrDebounce = now.AddMilliseconds(QRDebounceDelay);
+ qrCall = _GetQR(deviceName, callback);
+ behaviour.StartCoroutine(qrCall);
+ }
+
+ static IEnumerator _GetQR(string deviceName, imageClosure callback)
+ {
+ yield return new WaitForSeconds(QRDebounceDelay/1000);
+ string qrURL = ApiUrl + "/genqr/" +
+ UnityWebRequest.EscapeURL(_deviceToken) + "/" +
+ UnityWebRequest.EscapeURL(uuid) + "/" +
+ UnityWebRequest.EscapeURL(deviceName);
+ UnityWebRequest www = UnityWebRequestTexture.GetTexture(qrURL);
+ yield return www.SendWebRequest();
+
+ if (www.isNetworkError || www.isHttpError)
+ {
+ Debug.Log("QR LOAD ERROR: " + www.error);
+ Texture errorImage = Resources.Load("Allow2/QRError") as Texture2D;
+ callback(www.error, null);
+ yield break;
+ }
+ Texture2D qrCode = DownloadHandlerTexture.GetContent(www);
+ callback(null, qrCode);
+ }
+
+ //public static IEnumerator Check(int[] activities,
+ // resultClosure callback,
+ // bool log = false
+ // )
+ //{
+ // return Check(_child, activities,
+ // resultClosure callback,
+ // bool log = false
+ // )
+ //}
+
+ private static Dictionary ParseChildren(JSONNode json)
+ {
+ Dictionary children = new Dictionary();
+ foreach (JSONNode child in json)
+ {
+ children[child["id"]] = child["name"];
+ }
+ return children;
+ }
+
+ ///
+ /// Check if the specified child can use the current activities and optionally log usage.
+ /// Note that if you specify log as true, usage will be recorded even if the child is technically not allowed to use one of the
+ /// . This is to allow you the ability to be flexible in allowing usage, but should be used sparingly.
+ /// If you are, for instance, just checking if something CAN be done at this time, then make sure you supply false for the log parameter.
+ ///
+ /// Provide a (any) MonoBehaviour for the sdk to use to call the platform.
+ /// Id of the child for which you wish to check (and possibly log) activities.
+ /// The activity ids to check.
+ /// Provides the result of the check.
+ /// If set to true, then log the usage of these activities as well.
+ public static void Check(MonoBehaviour behaviour,
+ int childId, // childId == 0 ? Get Updated Child List and confirm Pairing
+ int[] activities,
+ resultClosure callback,
+ bool log = false
+ )
+ {
+ behaviour.StartCoroutine(_Check(null, childId, activities, callback, log));
+ }
+
+ ///
+ /// Check if the specified child can use the current activities and optionally log usage.
+ /// You should ALWAYS log usage when the child is using the activities, otherwise their usage will not be debited from their quota.
+ /// Note that if you specify log as true, usage will be recorded even if the child is technically not allowed to use one of the
+ /// . This is to allow you the ability to be flexible in allowing usage, but should be used sparingly.
+ /// If you are, for instance, just checking if something CAN be done at this time, then make sure you supply false for the log parameter.
+ ///
+ /// Provide a (any) MonoBehaviour for the sdk to use to call the platform.
+ /// The activity ids to check.
+ /// Provides the result of the check.
+ /// If set to true, then log the usage of these activities as well.
+ public static void Check(MonoBehaviour behaviour,
+ int[] activities,
+ resultClosure callback,
+ bool log = false
+ )
+ {
+ behaviour.StartCoroutine(_Check(null, childId, activities, callback, log));
+ }
+
+ static IEnumerator _Check(string myUuid,
+ int childId, // childId == 0 ? Get Updated Child List and confirm Pairing
+ int[] activities,
+ resultClosure callback,
+ bool log) // if set, then this is an autochecker and we drop it if we replace it. If null, it's adhoc, always return a value
+ {
+ if (!IsPaired)
+ {
+ callback(Allow2Error.NotPaired, null);
+ yield break;
+ }
+
+ Debug.Log(userId);
+ Debug.Log(pairToken);
+ Debug.Log(_deviceToken);
+
+ JSONNode body = new JSONObject();
+ body.Add("userId", userId);
+ body.Add("pairToken", pairToken);
+ body.Add("deviceToken", _deviceToken);
+ body.Add("tz", _timezone);
+ JSONArray activityJson = new JSONArray();
+ foreach (int activity in activities)
+ {
+ JSONNode activityParams = new JSONObject();
+ activityParams.Add("id", activity);
+ activityParams.Add("log", log);
+ activityJson.Add(activityParams);
+ }
+ body.Add("activities", activityJson);
+ body.Add("log", log);
+ if (childId > 0)
+ {
+ body.Add("childId", childId);
+ }
+ string bodyStr = body.ToString();
+
+ // check the cache first
+ if (resultCache.ContainsKey(bodyStr))
+ {
+ Allow2CheckResult checkResult = resultCache[bodyStr];
+
+ if (checkResult.Expires.CompareTo(new DateTime()) < 0)
+ {
+ // not expired yet, use cached value
+ callback(null, checkResult);
+ yield break;
+ }
+
+ // clear cached value and ask the server again
+ resultCache.Remove(bodyStr);
+ }
+
+ byte[] bytes = Encoding.UTF8.GetBytes(bodyStr);
+ using (UnityWebRequest www = new UnityWebRequest(ServiceUrl + "/serviceapi/check"))
+ {
+ www.method = UnityWebRequest.kHttpVerbPOST;
+ www.uploadHandler = new UploadHandlerRaw(bytes);
+ www.downloadHandler = new DownloadHandlerBuffer();
+ www.uploadHandler.contentType = "application/json";
+ www.chunkedTransfer = false;
+ yield return www.SendWebRequest();
+
+ if ((myUuid != null) && (checkerUuid != myUuid)) {
+ Debug.Log("drop response for check: " + myUuid);
+ yield break; // this check is aborted, just drop the response and don't return;
+ }
+
+ Debug.Log(www.downloadHandler.text);
+ var json = Allow2_SimpleJSON.JSON.Parse(www.downloadHandler.text);
+
+ if ((www.responseCode == 401) ||
+ ((json["status"] == "error") &&
+ ((json["message"] == "Invalid user.") ||
+ (json["message"] == "invalid pairToken"))))
+ {
+ // special case, no longer controlled
+ Debug.Log("No Longer Paired");
+ userId = 0;
+ pairToken = null;
+ Persist();
+ //childId = 0;
+ //_children = []
+ //_dayTypes = []
+ var failOpen = new Allow2CheckResult();
+ failOpen.Add("subscription", new Allow2_SimpleJSON.JSONArray());
+ failOpen.Add("allowed", true);
+ failOpen.Add("activities", new Allow2_SimpleJSON.JSONArray());
+ failOpen.Add("dayTypes", new Allow2_SimpleJSON.JSONArray());
+ failOpen.Add("allDayTypes", new Allow2_SimpleJSON.JSONArray());
+ failOpen.Add("children", new Allow2_SimpleJSON.JSONArray());
+ callback(null, failOpen);
+ yield break;
+ }
+
+ if (www.isNetworkError || www.isHttpError)
+ {
+ Debug.Log(www.error);
+ callback(www.error, null);
+ yield break;
+ }
+
+ if (json["allowed"] == null)
+ {
+ callback(Allow2Error.InvalidResponse, null);
+ yield break;
+ }
+
+ var response = new Allow2CheckResult();
+ response.Add("activities", json["activities"]);
+ response.Add("subscription", json["subscription"]);
+ response.Add("dayTypes", json["dayTypes"]);
+ response.Add("children", json["children"]);
+ var _dayTypes = json["allDayTypes"];
+ response.Add("allDayTypes", _dayTypes);
+ var oldChildIds = _children.Keys;
+ var children = json["children"];
+ _children = ParseChildren(children);
+ response.Add("children", children);
+
+ if (oldChildIds != _children.Keys)
+ {
+ Persist(); // only persist if the children change, this won't happen often.
+ }
+
+ // cache the response
+ resultCache[bodyStr] = response;
+ callback(null, response);
+ }
+ }
- public Connection()
+ private static IEnumerator CheckLoop(string myUuid,
+ int childId,
+ int[] activities,
+ resultClosure callback,
+ bool log = false)
{
+ while (checkerUuid == myUuid) {
+ Debug.Log("check uuid: " + myUuid);
+ yield return _Check(myUuid, childId, activities, delegate (string err, Allow2CheckResult result) {
+ if (!IsPaired && (checkerUuid != myUuid)) {
+ checkerUuid = null; // stop the checker, we have been unpaired
+ }
+ callback(err, result);
+ }, log);
+ yield return new WaitForSeconds(3);
+ }
}
- public async Task test()
+ ///
+ /// Start checking (and optionally logging) the ability for the child to use the given activities.
+ /// This starts a process that regularly checks (and optionally logs) usage until stopped using Allow2.StopChecking.
+ /// You can call this repeatedly and change the child id at any time, but there will only ever be one process and
+ /// it will continue to use the last provided child id.
+ /// Note, that if the child is unable or disallowed to use any of the , they will still be continually checked/logged until Allow2.StopChecking() is called.
+ /// This is to allow you to selectively allow the child to finish an activity, but will put them in negative credit (which will come off future usage).
+ /// You should ALWAYS log usage when the child is using the activities, otherwise their usage will not be debited from their quota.
+ ///
+ /// Provide a (any) MonoBehaviour for the sdk to use to call the platform.
+ /// The child for which the activites are being checked (and optionally logged).
+ /// The activity ids to check.
+ /// Provides the result of the check.
+ /// If set to true, then log the usage of these activities as well.
+ public static void StartChecking(MonoBehaviour behaviour,
+ int childId,
+ int[] activities,
+ resultClosure callback,
+ bool log)
{
+ // change the parameters
+ bool changed = (_childId != childId);
+ _childId = childId;
+ if (changed || (checkerUuid == null)) {
+ //switch checker
+ checkerUuid = System.Guid.NewGuid().ToString(); // this will abort the current checker and kill it.
+ /*checker = */ behaviour.StartCoroutine(CheckLoop(checkerUuid, childId, activities, callback, log));
+ }
+ }
+
+ ///
+ /// Stop checking (and logging) usage.
+ /// If there is no current checking/logging process started with Allow2.StartChecking(), this call has no effect.
+ ///
+ public static void StopChecking()
+ {
+ checkerUuid = null; // this will kill the running checker
+ }
+
+ ///
+ /// Submit a request on behalf of the current child.
+ ///
+ /// Results of the request.
+ /// Provide a (any) MonoBehaviour for the sdk to use to call the platform.
+ /// The Id of the child making the request.
+ /// (optional)The Id of the day type they are requesting.
+ /// (optional) An Array of ids for Bans they are asking to be lifted.
+ /// (optional) Message to send with the request.
+ /// callback that will return the response success or error.
+ public static void Request(MonoBehaviour behaviour,
+ int childId,
+ int dayTypeId,
+ int[] lift,
+ string message,
+ resultClosure callback)
+ {
+ behaviour.StartCoroutine(_Request(childId, dayTypeId, lift, message, callback));
+ }
+
+ ///
+ /// Submit a request on behalf of the current child.
+ ///
+ /// Results of the request.
+ /// Provide a (any) MonoBehaviour for the sdk to use to call the platform.
+ /// (optional)The Id of the day type they are requesting.
+ /// (optional) An Array of ids for Bans they are asking to be lifted.
+ /// (optional) Message to send with the request.
+ /// callback that will return the response success or error.
+ public static void Request(MonoBehaviour behaviour,
+ int dayTypeId,
+ int[] lift,
+ string message,
+ resultClosure callback)
+ {
+ behaviour.StartCoroutine(_Request(childId, dayTypeId, lift, message, callback));
+ }
+
+ static IEnumerator _Request(int childId,
+ int dayTypeId,
+ int[] lift,
+ string message,
+ resultClosure callback)
+ {
+ if (!IsPaired)
+ {
+ callback(Allow2Error.NotPaired, null);
+ yield break;
+ }
+ if (childId < 1)
+ {
+ callback(Allow2Error.MissingChildId, null);
+ yield break;
+ }
+
+ WWWForm body = new WWWForm();
+ body.AddField("userId", userId);
+ body.AddField("pairToken", pairToken);
+ body.AddField("deviceToken", _deviceToken);
+ body.AddField("childId", childId);
+ //form.AddField("lift", lift.asJson);
+ //if (dayTypeId != nil) {
+ // body["dayType"] = JSON(dayTypeId!)
+ // body["changeDayType"] = true
+ //}
+ string bodyStr = body.ToString();
+
+ byte[] bytes = Encoding.UTF8.GetBytes(bodyStr);
+ using (UnityWebRequest www = new UnityWebRequest(ApiUrl + "/api/checkPairing"))
+ {
+ www.method = UnityWebRequest.kHttpVerbPOST;
+ www.uploadHandler = new UploadHandlerRaw(bytes);
+ www.downloadHandler = new DownloadHandlerBuffer();
+ www.uploadHandler.contentType = "application/json";
+ www.chunkedTransfer = false;
+ yield return www.SendWebRequest();
+
+ // anything other than a 200 response is a "try again" as far as we are concerned
+ if (www.responseCode != 200)
+ {
+ callback(Allow2Error.NoConnection, null); // let the caller know we are having problems
+ yield break;
+ }
+
+ }
+ }
+
+ ///
+ /// Use this routine to notify Allow2 you are starting a pairing session for a QR Code pairing.
+ /// Call this when you are about to display a QR code to the user to allow them to pair with ALlow2.
+ /// Get the appropriate QR Code using Allow2.GetQR().
+ ///
+ ///
+ /// Provide a (any) MonoBehaviour for the sdk to use to call the platform.
+ /// Callback that will return response success or error
+ public static void StartPairing(MonoBehaviour behaviour, resultClosure callback)
+ {
+ //switch checker
+ pairingUuid = System.Guid.NewGuid().ToString(); // this will abort the current poll and kill it.
+ behaviour.StartCoroutine(PairingLoop(pairingUuid, callback));
+ }
+
+ ///
+ /// Tell Allow2 the QR Code for pairing is no longer being displayed.
+ /// Call this when you stop showing the QR code and therefore the user can no longer scan it.
+ ///
+ public static void StopPairing()
+ {
+ pairingUuid = null; // this will kill the running checker
+ }
+
+ private static IEnumerator PairingLoop(string myUuid, resultClosure callback)
+ {
+ while (pairingUuid == myUuid)
+ {
+ Debug.Log("poll uuid: " + myUuid);
+ yield return _PollPairing(myUuid, delegate (string err, Allow2CheckResult result) {
+ if (IsPaired)
+ {
+ pairingUuid = null; // stop the checker, we have been paired
+ }
+ callback(err, result);
+ });
+ yield return new WaitForSeconds(3);
+ }
+ }
+
+ static IEnumerator _PollPairing(string myUuid, resultClosure callback)
+ {
+ JSONNode body = new JSONObject();
+ body.Add("uuid", uuid);
+ body.Add("deviceToken", _deviceToken);
+ string bodyStr = body.ToString();
+
+ byte[] bytes = Encoding.UTF8.GetBytes(bodyStr);
+ using (UnityWebRequest www = new UnityWebRequest(ApiUrl + "/api/checkPairing"))
+ {
+ www.method = UnityWebRequest.kHttpVerbPOST;
+ www.uploadHandler = new UploadHandlerRaw(bytes);
+ www.downloadHandler = new DownloadHandlerBuffer();
+ www.uploadHandler.contentType = "application/json";
+ www.chunkedTransfer = false;
+ yield return www.SendWebRequest();
- //var values = new Dictionary
- // {
- // { "thing1", "hello" },
- // { "thing2", "world" }
- // };
+ // anything other than a 200 response is a "try again" as far as we are concerned
+ if (www.responseCode != 200)
+ {
+ callback(Allow2Error.NoConnection, null); // let the caller know we are having problems
+ yield break;
+ }
- //var content = new FormUrlEncodedContent(values);
+ Debug.Log(www.downloadHandler.text);
+ var json = Allow2_SimpleJSON.JSON.Parse(www.downloadHandler.text);
- //var response = await client.PostAsync("http://api.allow2.com/", content);
+ string status = json["status"];
- //var responseString = await response.Content.ReadAsStringAsync();
+ if (status != "success")
+ {
+ callback(json["message"] ?? "Unknown Error", null);
+ yield break;
+ }
- var responseString = await client.GetStringAsync("http://api.allow2.com/");
+ pairToken = json["pairToken"];
+ userId = json["userId"];
+ childId = json["childId"];
+ _children = childId > 0 ? new Dictionary() : ParseChildren(json["children"]);
- return responseString;
+ callback(null, null);
+ }
}
}
-}
+}
\ No newline at end of file
diff --git a/Allow2/Allow2.csproj b/Allow2/Allow2.csproj
deleted file mode 100644
index 7a6acc4..0000000
--- a/Allow2/Allow2.csproj
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- {FFE961F4-4FDE-4736-A89D-721EB5C11A97}
- {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- Library
- Allow2
- Allow2
- v4.5
- Profile111
- Allow2
- 1.0.0
- andrew
- Allow2
-
-
- true
- full
- false
- bin\Debug
- DEBUG;
- prompt
- 4
-
-
- true
- bin\Release
- prompt
- 4
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Allow2/Allow2Ban.cs b/Allow2/Allow2Ban.cs
new file mode 100644
index 0000000..2a5c7f4
--- /dev/null
+++ b/Allow2/Allow2Ban.cs
@@ -0,0 +1,34 @@
+//
+// Allow2Unity
+// Allow2Ban.cs
+//
+// Created by Andrew Longhorn in May 2019.
+// Copyright © 2019 Allow2 Pty Ltd. All rights reserved.
+//
+// LICENSE:
+// See LICENSE file in root directory
+//
+
+using System;
+using Allow2_SimpleJSON;
+
+namespace Allow2
+{
+ public class Allow2Ban
+ {
+ public int Id { get; private set; }
+ public string Title { get; private set; }
+ public DateTime AppliedAt { get; private set; }
+ public int Duration { get; private set; }
+ public bool Selected { get; private set; }
+
+ public Allow2Ban(string name, JSONNode val)
+ {
+ Id = val["id"].AsInt;
+ Title = name;
+ AppliedAt = Allow2.Epoch.AddSeconds(val["appliedAt"].AsInt).ToUniversalTime();
+ Duration = val["durationMinutes"].AsInt;
+ Selected = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Allow2/Allow2CheckResult.cs b/Allow2/Allow2CheckResult.cs
new file mode 100644
index 0000000..b1c3834
--- /dev/null
+++ b/Allow2/Allow2CheckResult.cs
@@ -0,0 +1,259 @@
+//
+// Allow2Unity
+// Allow2CheckResult.cs
+//
+// Created by Andrew Longhorn in Jan 2019.
+// Copyright © 2019 Allow2 Pty Ltd. All rights reserved.
+//
+// LICENSE:
+// See LICENSE file in root directory
+//
+
+using System;
+using System.Collections.Generic;
+using Allow2_SimpleJSON;
+
+namespace Allow2
+{
+ // Allow2Response JSON structure
+ //{
+ // "allowed":false,
+ // "activities":{
+ // "1":{
+ // "id":1,
+ // "name":"Internet",
+ // "timed":true,
+ // "units":"minutes",
+ // "banned":false,
+ // "remaining":0,
+ // "cached":false,
+ // "expires":1557067560,
+ // "timeBlock":{
+ // "allowed":true,
+ // "remaining":495
+ // }
+ // },
+ // "2":{
+ // "id":2,
+ // "name":"Computer",
+ // "timed":true,
+ // "units":"minutes",
+ // "banned":false,
+ // "remaining":0,
+ // "cached":false,
+ // "expires":1557067560,
+ // "timeBlock":{
+ // "allowed":true,
+ // "remaining":375
+ // }
+ // }
+ // },
+ // "dayTypes":{
+ // "today":{"id":57,"name":"Weekend"},
+ // "tomorrow":{"id":61,"name":"School Day"}
+ // },
+ // "allDayTypes":[
+ // {"id":57,"name":"Weekend"},
+ // {"id":59,"name":"Weekday"},
+ // {"id":61,"name":"School Day"},
+ // {"id":64,"name":"Sick Day"},
+ // {"id":66,"name":"Holiday"},
+ // {"id":16997,"name":"No Limit"}
+ // ],
+ // "children":[
+ // {"id":681,"name":"Bob","pin":"1234"},
+ // {"id":639,"name":"Mary","pin":"4567"},
+ // {"id":21423,"name":"Milly","pin":"5566"}
+ // ],
+ // "subscription":{
+ // "active":false,
+ // "type":1,
+ // "maxChildren":6,
+ // "childCount":3,
+ // "deviceCount":12,
+ // "serviceCount":0,
+ // "financial":false
+ // }
+ //}
+
+ public class Allow2CheckResult : JSONObject // shortcut implementation for now
+ {
+ //public Allow2CheckResult()
+ //{
+ //}
+
+ ///
+ /// Convenience method.
+ ///
+ /// The activities.
+ public JSONNode Activities {
+ get {
+ return this["activities"];
+ }
+ }
+
+ ///
+ /// Convenience method.
+ ///
+ /// The subscription.
+ public JSONNode Subscription
+ {
+ get
+ {
+ return this["subscription"];
+ }
+ }
+
+ ///
+ /// When does the validity of this result expire? (cache expiry).
+ ///
+ /// The expiry Date/Time
+ public DateTime Expires {
+ get {
+ int unixTimeStamp = ((Activities != null) && (Activities[0] != null)) ?
+ this["activities"]["0"]["expires"].AsInt : 0;
+ return Allow2.Epoch.AddSeconds(unixTimeStamp).ToUniversalTime(); // nineteen70.AddSeconds(unixTimeStamp).ToLocalTime();
+ }
+ }
+
+ ///
+ /// Returns if the user is financial or within free usage tier, otherwise returns a message indicating they need a subscription
+ ///
+ /// The need subscription.
+ public string NeedSubscription {
+ get {
+ if (!Subscription["financial"].AsBool) {
+ int childCount = Subscription["childCount"].AsInt;
+ int maxChildren = Subscription["maxChildren"].AsInt;
+ //int serviceCount = subscription["serviceCount"].AsInt;
+ //int deviceCount = subscription["deviceCount"].AsInt;
+ int type = Subscription["type"].AsInt;
+
+ if ((maxChildren > 0 ) && (childCount > maxChildren) && (type == 1)) {
+ return "Subscription Upgrade Required.";
+ }
+
+ return "Subscription Required.";
+ }
+ return null;
+ }
+ }
+
+ ///
+ /// A simple top level result, the child is currently allowed or not based on the activity time and quotas.
+ ///
+ /// true if is allowed; otherwise, false.
+ public bool IsAllowed {
+ get {
+ return this["allowed"].AsBool;
+ }
+ }
+
+ ///
+ /// A Summary explanation of the current reasons they may not be allowed at this time.
+ ///
+ /// The explanation.
+ public string Explanation {
+ get {
+ List reasons = new List();
+ string subscriptionString = NeedSubscription;
+ if (subscriptionString != null) {
+ reasons.Add(subscriptionString);
+ }
+ foreach (JSONNode activity in Activities) {
+ if (activity["banned"].IsBoolean && activity["banned"].AsBool)
+ {
+ reasons.Add("You are currently banned from " + activity["name"].ToString());
+ }
+ else
+ {
+ JSONNode timeblock = activity["timeblock"];
+ if ((timeblock == null) || !timeblock["allowed"].IsBoolean || !timeblock["allowed"].AsBool)
+ {
+ reasons.Add("You cannot use " + activity["name"].ToString() + " at this time.");
+ }
+ else
+ {
+ // todo: reasons.append("You have \(activity["remaining"]) to use \(activity["name"]).")
+ }
+ }
+ }
+ return String.Join("/n", reasons.ToArray());
+ }
+ }
+
+ ///
+ /// A list of the current bans in place for this child.
+ ///
+ /// The current bans.
+ public Allow2Ban[] CurrentBans {
+ get {
+ List bans = new List();
+ foreach (JSONNode activity in Activities) {
+
+ if (activity["banned"].AsBool) {
+ //int id = activity.dictionary?["id"]?.uInt64Value,
+ string name = activity["name"].ToString();
+ if ((activity["bans"] != null) && (activity["bans"]["bans"] != null) && (activity["bans"]["bans"].IsArray))
+ {
+ JSONArray items = activity["bans"]["bans"].AsArray;
+ foreach (JSONNode item in items)
+ {
+ bans.Add(new Allow2Ban(name, item));
+ }
+ }
+ else
+ {
+ // todo: reasons.append("You have \(activity["remaining"]) to use \(activity["name"]).")
+ }
+ }
+ }
+ return bans.ToArray();
+ }
+ }
+
+ ///
+ /// The type of day it is today.
+ ///
+ /// The day type.
+ public Allow2Day Today
+ {
+ get
+ {
+ if (this["dayTypes"] == null) { return null; }
+ return Allow2Day.DayOrNull(this["dayTypes"]["today"]);
+ }
+ }
+
+ ///
+ /// The type of day it will be tomorrow.
+ ///
+ /// The day type.
+ public Allow2Day Tomorrow
+ {
+ get
+ {
+ if (this["dayTypes"] == null) { return null; }
+ return Allow2Day.DayOrNull(this["dayTypes"]["tomorrow"]);
+ }
+ }
+
+ ///
+ /// All Day Types the parent has set on their account that the child should be aware of or can choose from for a request.
+ ///
+ /// All day types.
+ public Allow2Day[] AllDayTypes
+ {
+ get
+ {
+ List dayTypes = new List();
+ foreach (JSONNode dayType in this["allDayTypes"])
+ {
+ dayTypes.Add(new Allow2Day(dayType));
+ }
+ return dayTypes.ToArray();
+ }
+ }
+ }
+
+}
diff --git a/Allow2/Allow2Child.cs b/Allow2/Allow2Child.cs
new file mode 100644
index 0000000..015ca8f
--- /dev/null
+++ b/Allow2/Allow2Child.cs
@@ -0,0 +1,26 @@
+//
+// Allow2Unity
+// Allow2Child.cs
+//
+// Created by Andrew Longhorn in May 2019.
+// Copyright © 2019 Allow2 Pty Ltd. All rights reserved.
+//
+// LICENSE:
+// See LICENSE file in root directory
+//
+
+namespace Allow2
+{
+ public class Allow2Child
+ {
+ public int Id { get; private set; }
+ public string Name { get; private set; }
+ public string Pin { get; private set; }
+
+ Allow2Child(int id, string name, string pin) {
+ Id = id;
+ Name = name;
+ Pin = pin;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Allow2/Allow2Day.cs b/Allow2/Allow2Day.cs
new file mode 100644
index 0000000..e562243
--- /dev/null
+++ b/Allow2/Allow2Day.cs
@@ -0,0 +1,42 @@
+//
+// Allow2Unity
+// Allow2Day.cs
+//
+// Created by Andrew Longhorn in May 2019.
+// Copyright © 2019 Allow2 Pty Ltd. All rights reserved.
+//
+// LICENSE:
+// See LICENSE file in root directory
+//
+
+using Allow2_SimpleJSON;
+
+namespace Allow2
+{
+ public class Allow2Day // shortcut implementation for now
+ {
+ public int Id { get; private set; }
+ public string Name { get; private set; }
+
+ public Allow2Day(int id, string name)
+ {
+ Id = id;
+ Name = name;
+ }
+
+ public Allow2Day(JSONNode json)
+ {
+ Id = json["id"].AsInt;
+ Name = json["name"];
+ }
+
+ public static Allow2Day DayOrNull(JSONNode json)
+ {
+ if (json == null) {
+ return null;
+ }
+
+ return new Allow2Day(json["id"].AsInt, json["name"]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Allow2/EnumeratedTypes.cs b/Allow2/EnumeratedTypes.cs
new file mode 100644
index 0000000..97742a3
--- /dev/null
+++ b/Allow2/EnumeratedTypes.cs
@@ -0,0 +1,49 @@
+//
+// Allow2Unity
+// EnumeratedTypes.cs
+//
+// Created by Andrew Longhorn in Jan 2019.
+// Copyright © 2019 Allow2 Pty Ltd. All rights reserved.
+//
+// LICENSE:
+// See LICENSE file in root directory
+//
+
+namespace Allow2
+{
+ ///
+ /// Environment: Use Production ONLY (staging is for internal testing).
+ ///
+ public enum EnvType {
+ Production,
+ // Sandbox,
+ Staging
+ }
+
+ ///
+ /// Activities: These are the current activities for Allow2.
+ ///
+ public enum Activity: int {
+ Internet = 1,
+ Computer = 2,
+ Gaming = 3,
+ Message = 4,
+ JunkFood = 5,
+ Lollies = 6,
+ Electricity = 7,
+ ScreenTime = 8,
+ Social = 9,
+ PhoneTime = 10
+ }
+
+ public static class Allow2Error
+ {
+ public const string NotPaired = "NotPaired";
+ public const string AlreadyPaired = "AlreadyPaired";
+ public const string MissingChildId = "MissingChildId";
+ public const string NotAuthorised = "NotAuthorised";
+ public const string InvalidResponse = "InvalidResponse";
+ public const string NoConnection = "NoConnection";
+ }
+
+}
diff --git a/Allow2/Examples/CheckButton.cs b/Allow2/Examples/CheckButton.cs
new file mode 100644
index 0000000..e1da1d7
--- /dev/null
+++ b/Allow2/Examples/CheckButton.cs
@@ -0,0 +1,91 @@
+//
+// Copyright (C) 2019 Allow2
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+// the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and limitations under the License.
+//
+namespace Allow2.Allow2Examples
+{
+
+ using UnityEngine;
+
+ public class CheckButton : MonoBehaviour
+ {
+
+ public int childId = 68;
+ public int[] activities = {
+ (int)Activity.Internet,
+ (int)Activity.Computer
+ };
+
+ ///
+ /// Check once and log activity for the given child and activities.
+ ///
+ public void Check()
+ {
+ Debug.Log("Check");
+ Allow2.Check(
+ this,
+ childId,
+ activities,
+ delegate (string err, Allow2CheckResult result)
+ {
+ Debug.Log("Check Error" + err);
+ Debug.Log("Paired: " + Allow2.IsPaired);
+ if (result != null)
+ {
+ Debug.Log("Allowed: " + result.IsAllowed);
+ if (!result.IsAllowed)
+ {
+ Debug.Log(result.Explanation);
+ }
+ }
+ },
+ true);
+ }
+
+ ///
+ /// Start a continuous check (and logging usage) for the given child and activities.
+ ///
+ public void StartChecking()
+ {
+ Debug.Log("Start Checking");
+ Allow2.StartChecking(
+ this,
+ childId,
+ activities,
+ delegate (string err, Allow2CheckResult result)
+ {
+ Debug.Log("Check Error" + err);
+ Debug.Log("Paired: " + Allow2.IsPaired);
+ if (result != null)
+ {
+ Debug.Log("Allowed: " + result.IsAllowed);
+ if (!result.IsAllowed)
+ {
+ Debug.Log(result.Explanation);
+ }
+ }
+ },
+ true);
+ }
+
+ ///
+ /// Stop checking and logging.
+ ///
+ public void StopChecking()
+ {
+ Debug.Log("Stop Checking");
+ Allow2.StopChecking();
+ }
+ }
+}
diff --git a/Allow2/Examples/DeviceNameInput.cs b/Allow2/Examples/DeviceNameInput.cs
new file mode 100644
index 0000000..95500bc
--- /dev/null
+++ b/Allow2/Examples/DeviceNameInput.cs
@@ -0,0 +1,50 @@
+//
+// Copyright (C) 2019 Allow2
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+// the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and limitations under the License.
+//
+namespace Allow2.Allow2Examples
+{
+
+ using UnityEngine;
+ using UnityEngine.UI;
+
+ public class DeviceNameInput : MonoBehaviour
+ {
+
+ public InputField inputField;
+ public RawImage qrImage;
+
+ ///
+ /// On creating the field, we pre-populate it with the system device name.
+ /// This will auto-trigger the "InputValueChanged" and update the QR Code for pairing.
+ ///
+ void Awake()
+ {
+ inputField.text = SystemInfo.deviceName;
+ }
+
+ ///
+ /// When the input value changes, generate a new QR Code, so the user can interactively edit the name and the pairing process uses that name.
+ ///
+ /// Input.
+ public void InputValueChanged(string input)
+ {
+ Allow2.GetQR(this, input, delegate (string err, Texture2D qrCode)
+ {
+ Debug.Log("Input Value qrcode error: " + (err ?? "No Error") + " : " + (qrCode != null ? qrCode.width.ToString() + "," + qrCode.height.ToString() : "no"));
+ qrImage.GetComponent().texture = qrCode;
+ });
+ }
+ }
+}
diff --git a/Allow2/Examples/PairButton.cs b/Allow2/Examples/PairButton.cs
new file mode 100644
index 0000000..49f8771
--- /dev/null
+++ b/Allow2/Examples/PairButton.cs
@@ -0,0 +1,52 @@
+//
+// Copyright (C) 2019 Allow2
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+// the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and limitations under the License.
+//
+namespace Allow2.Allow2Examples
+{
+
+ using UnityEngine;
+ using UnityEngine.UI;
+
+ public class PairButton : MonoBehaviour
+ {
+
+ public InputField UsernameField;
+ public InputField PasswordField;
+ public InputField DeviceNameField;
+
+ ///
+ /// Manually pair with Allow2 by providing the username and password entered by the user in your pairing interface.
+ ///
+ public void Pair()
+ {
+ Debug.Log("Start Pairing");
+ Allow2.Pair(
+ this,
+ UsernameField.text,
+ PasswordField.text,
+ DeviceNameField.text,
+ delegate (string err, Allow2CheckResult result)
+ {
+ Debug.Log("Stop Pairing");
+ Debug.Log("Pairing Error" + err);
+ if (result)
+ {
+ Debug.Log("Pairing Result" + result.ToString());
+ }
+ }
+ );
+ }
+ }
+}
diff --git a/Allow2/Examples/RequestButton.cs b/Allow2/Examples/RequestButton.cs
new file mode 100644
index 0000000..7f3bd7a
--- /dev/null
+++ b/Allow2/Examples/RequestButton.cs
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2019 Allow2
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+// the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and limitations under the License.
+//
+namespace Allow2.Allow2Examples
+{
+
+ using UnityEngine;
+
+ public class RequestButton : MonoBehaviour
+ {
+
+ public int dayTypeId = 23;
+ public int[] bansToLift = {};
+
+ ///
+ /// Send a request on behalf of the child.
+ ///
+ public void Request()
+ {
+ Debug.Log("Request");
+ Allow2.childId = 68;
+ Allow2.Request(
+ this,
+ dayTypeId,
+ bansToLift,
+ "test",
+ delegate (string err, Allow2CheckResult result)
+ {
+ Debug.Log("Request Error" + err);
+ if (result != null)
+ {
+ Debug.Log(result.Explanation);
+ }
+ });
+ }
+ }
+}
diff --git a/Allow2/Examples/SceneClass.cs b/Allow2/Examples/SceneClass.cs
new file mode 100644
index 0000000..a247859
--- /dev/null
+++ b/Allow2/Examples/SceneClass.cs
@@ -0,0 +1,68 @@
+//
+// Copyright (C) 2019 Allow2
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+// the License.
+//
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and limitations under the License.
+//
+namespace Allow2.Allow2Examples
+{
+
+ using UnityEngine;
+ using UnityEngine.UI;
+
+ public class SceneClass : MonoBehaviour
+ {
+
+ public RawImage qrImage;
+
+ void Awake()
+ {
+ // staging is only really for Allow2 internal development, omit this line in your code
+ Allow2.env = EnvType.Staging;
+
+ // set the deviceToken
+ // this needs top be done before ANY calls to the Allow2 platform
+ // create your own deviceToken at https://developer.allow2.com for free
+ // use it to manage your app/game/device and track metrics
+ // it's also important to use your own deviceToken and app definition in order to make use of
+ // the additional marketing channel opportunities that Allow2 provides for free
+ Allow2.DeviceToken = "B0hNax6VCFi9vphu";
+
+ // We can also now check if the device/app/game is already paired and what children are in the account
+ Debug.Log("isPaired: " + Allow2.IsPaired);
+ Debug.Log("Children: " + Allow2.Children);
+
+ // Not Required if the textfield sets the name on display, it will also trigger an update of the QR Code
+ // and in the pairing interface, we need a QR code to make the process simple for our users
+ //Allow2.GetQR(this, SystemInfo.deviceName, delegate (string err, Texture2D qrCode)
+ //{
+ // Debug.Log("qrcode error: " + (err ?? "No Error") + " : " + (qrCode ? "yes" : "no"));
+ // Debug.Log(qrImage.GetComponent());
+ // qrImage.GetComponent().texture = qrCode;
+ //});
+
+ // usually start the pairing background process here,
+ // unless you have opted to not allow pairing with QR Code (but this is highly recommended for a better user experience)
+ Allow2.StartPairing(this, delegate (string err, Allow2CheckResult result) {
+ // this may be called several times with errors
+ if (err != null) {
+ Debug.Log(err);
+ return;
+ }
+ // once paired, the pairing process will automatically stop itself
+ // you should close off the pairing interface here and display success to the end user
+ Debug.Log("isPaired: " + Allow2.IsPaired);
+ Debug.Log("Children: " + Allow2.Children);
+ });
+ }
+ }
+}
diff --git a/Allow2/Properties/AssemblyInfo.cs b/Allow2/Properties/AssemblyInfo.cs
deleted file mode 100644
index b8e169d..0000000
--- a/Allow2/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-// Information about this assembly is defined by the following attributes.
-// Change them to the values specific to your project.
-
-[assembly: AssemblyTitle("Allow2")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("")]
-[assembly: AssemblyCopyright("${AuthorCopyright}")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
-// The form "{Major}.{Minor}.*" will automatically update the build and revision,
-// and "{Major}.{Minor}.{Build}.*" will update just the revision.
-
-[assembly: AssemblyVersion("1.0.0.0")]
-
-// The following attributes are used to specify the signing key for the assembly,
-// if desired. See the Mono documentation for more information about signing.
-
-//[assembly: AssemblyDelaySign(false)]
-//[assembly: AssemblyKeyFile("")]
diff --git a/Allow2/SimpleJSON.cs b/Allow2/SimpleJSON.cs
new file mode 100644
index 0000000..a3926b5
--- /dev/null
+++ b/Allow2/SimpleJSON.cs
@@ -0,0 +1,1367 @@
+/* * * * *
+ * A simple JSON Parser / builder
+ * ------------------------------
+ *
+ * It mainly has been written as a simple JSON parser. It can build a JSON string
+ * from the node-tree, or generate a node tree from any valid JSON string.
+ *
+ * If you want to use compression when saving to file / stream / B64 you have to include
+ * SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) in your project and
+ * define "USE_SharpZipLib" at the top of the file
+ *
+ * Written by Bunny83
+ * 2012-06-09
+ *
+ * [2012-06-09 First Version]
+ * - provides strongly typed node classes and lists / dictionaries
+ * - provides easy access to class members / array items / data values
+ * - the parser now properly identifies types. So generating JSON with this framework should work.
+ * - only double quotes (") are used for quoting strings.
+ * - provides "casting" properties to easily convert to / from those types:
+ * int / float / double / bool
+ * - provides a common interface for each node so no explicit casting is required.
+ * - the parser tries to avoid errors, but if malformed JSON is parsed the result is more or less undefined
+ * - It can serialize/deserialize a node tree into/from an experimental compact binary format. It might
+ * be handy if you want to store things in a file and don't want it to be easily modifiable
+ *
+ *
+ * [2012-12-17 Update]
+ * - Added internal JSONLazyCreator class which simplifies the construction of a JSON tree
+ * Now you can simple reference any item that doesn't exist yet and it will return a JSONLazyCreator
+ * The class determines the required type by it's further use, creates the type and removes itself.
+ * - Added binary serialization / deserialization.
+ * - Added support for BZip2 zipped binary format. Requires the SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ )
+ * The usage of the SharpZipLib library can be disabled by removing or commenting out the USE_SharpZipLib define at the top
+ * - The serializer uses different types when it comes to store the values. Since my data values
+ * are all of type string, the serializer will "try" which format fits best. The order is: int, float, double, bool, string.
+ * It's not the most efficient way but for a moderate amount of data it should work on all platforms.
+ *
+ * [2017-03-08 Update]
+ * - Optimised parsing by using a StringBuilder for token. This prevents performance issues when large
+ * string data fields are contained in the json data.
+ * - Finally refactored the badly named JSONClass into JSONObject.
+ * - Replaced the old JSONData class by distict typed classes ( JSONString, JSONNumber, JSONBool, JSONNull ) this
+ * allows to propertly convert the node tree back to json without type information loss. The actual value
+ * parsing now happens at parsing time and not when you actually access one of the casting properties.
+ *
+ * [2017-04-11 Update]
+ * - Fixed parsing bug where empty string values have been ignored.
+ * - Optimised "ToString" by using a StringBuilder internally. This should heavily improve performance for large files
+ * - Changed the overload of "ToString(string aIndent)" to "ToString(int aIndent)"
+ *
+ * [2017-11-29 Update]
+ * - Removed the IEnumerator implementations on JSONArray & JSONObject and replaced it with a common
+ * struct Enumerator in JSONNode that should avoid garbage generation. The enumerator always works
+ * on KeyValuePair, even for JSONArray.
+ * - Added two wrapper Enumerators that allows for easy key or value enumeration. A JSONNode now has
+ * a "Keys" and a "Values" enumerable property. Those are also struct enumerators / enumerables
+ * - A KeyValuePair can now be implicitly converted into a JSONNode. This allows
+ * a foreach loop over a JSONNode to directly access the values only. Since KeyValuePair as well as
+ * all the Enumerators are structs, no garbage is allocated.
+ * - To add Linq support another "LinqEnumerator" is available through the "Linq" property. This
+ * enumerator does implement the generic IEnumerable interface so most Linq extensions can be used
+ * on this enumerable object. This one does allocate memory as it's a wrapper class.
+ * - The Escape method now escapes all control characters (# < 32) in strings as uncode characters
+ * (\uXXXX) and if the static bool JSONNode.forceASCII is set to true it will also escape all
+ * characters # > 127. This might be useful if you require an ASCII output. Though keep in mind
+ * when your strings contain many non-ascii characters the strings become much longer (x6) and are
+ * no longer human readable.
+ * - The node types JSONObject and JSONArray now have an "Inline" boolean switch which will default to
+ * false. It can be used to serialize this element inline even you serialize with an indented format
+ * This is useful for arrays containing numbers so it doesn't place every number on a new line
+ * - Extracted the binary serialization code into a seperate extension file. All classes are now declared
+ * as "partial" so an extension file can even add a new virtual or abstract method / interface to
+ * JSONNode and override it in the concrete type classes. It's of course a hacky approach which is
+ * generally not recommended, but i wanted to keep everything tightly packed.
+ * - Added a static CreateOrGet method to the JSONNull class. Since this class is immutable it could
+ * be reused without major problems. If you have a lot null fields in your data it will help reduce
+ * the memory / garbage overhead. I also added a static setting (reuseSameInstance) to JSONNull
+ * (default is true) which will change the behaviour of "CreateOrGet". If you set this to false
+ * CreateOrGet will not reuse the cached instance but instead create a new JSONNull instance each time.
+ * I made the JSONNull constructor private so if you need to create an instance manually use
+ * JSONNull.CreateOrGet()
+ *
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2012-2017 Markus Göbel (Bunny83)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * * * * */
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Allow2_SimpleJSON
+{
+ public enum JSONNodeType
+ {
+ Array = 1,
+ Object = 2,
+ String = 3,
+ Number = 4,
+ NullValue = 5,
+ Boolean = 6,
+ None = 7,
+ Custom = 0xFF,
+ }
+ public enum JSONTextMode
+ {
+ Compact,
+ Indent
+ }
+
+ public abstract partial class JSONNode
+ {
+ #region Enumerators
+ public struct Enumerator
+ {
+ private enum Type { None, Array, Object }
+ private Type type;
+ private Dictionary.Enumerator m_Object;
+ private List.Enumerator m_Array;
+ public bool IsValid { get { return type != Type.None; } }
+ public Enumerator(List.Enumerator aArrayEnum)
+ {
+ type = Type.Array;
+ m_Object = default(Dictionary.Enumerator);
+ m_Array = aArrayEnum;
+ }
+ public Enumerator(Dictionary.Enumerator aDictEnum)
+ {
+ type = Type.Object;
+ m_Object = aDictEnum;
+ m_Array = default(List.Enumerator);
+ }
+ public KeyValuePair Current
+ {
+ get
+ {
+ if (type == Type.Array)
+ return new KeyValuePair(string.Empty, m_Array.Current);
+ else if (type == Type.Object)
+ return m_Object.Current;
+ return new KeyValuePair(string.Empty, null);
+ }
+ }
+ public bool MoveNext()
+ {
+ if (type == Type.Array)
+ return m_Array.MoveNext();
+ else if (type == Type.Object)
+ return m_Object.MoveNext();
+ return false;
+ }
+ }
+ public struct ValueEnumerator
+ {
+ private Enumerator m_Enumerator;
+ public ValueEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { }
+ public ValueEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { }
+ public ValueEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; }
+ public JSONNode Current { get { return m_Enumerator.Current.Value; } }
+ public bool MoveNext() { return m_Enumerator.MoveNext(); }
+ public ValueEnumerator GetEnumerator() { return this; }
+ }
+ public struct KeyEnumerator
+ {
+ private Enumerator m_Enumerator;
+ public KeyEnumerator(List.Enumerator aArrayEnum) : this(new Enumerator(aArrayEnum)) { }
+ public KeyEnumerator(Dictionary.Enumerator aDictEnum) : this(new Enumerator(aDictEnum)) { }
+ public KeyEnumerator(Enumerator aEnumerator) { m_Enumerator = aEnumerator; }
+ public JSONNode Current { get { return m_Enumerator.Current.Key; } }
+ public bool MoveNext() { return m_Enumerator.MoveNext(); }
+ public KeyEnumerator GetEnumerator() { return this; }
+ }
+
+ public class LinqEnumerator : IEnumerator>, IEnumerable>
+ {
+ private JSONNode m_Node;
+ private Enumerator m_Enumerator;
+ internal LinqEnumerator(JSONNode aNode)
+ {
+ m_Node = aNode;
+ if (m_Node != null)
+ m_Enumerator = m_Node.GetEnumerator();
+ }
+ public KeyValuePair Current { get { return m_Enumerator.Current; } }
+ object IEnumerator.Current { get { return m_Enumerator.Current; } }
+ public bool MoveNext() { return m_Enumerator.MoveNext(); }
+
+ public void Dispose()
+ {
+ m_Node = null;
+ m_Enumerator = new Enumerator();
+ }
+
+ public IEnumerator> GetEnumerator()
+ {
+ return new LinqEnumerator(m_Node);
+ }
+
+ public void Reset()
+ {
+ if (m_Node != null)
+ m_Enumerator = m_Node.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return new LinqEnumerator(m_Node);
+ }
+ }
+
+ #endregion Enumerators
+
+ #region common interface
+
+ public static bool forceASCII = false; // Use Unicode by default
+
+ public abstract JSONNodeType Tag { get; }
+
+ public virtual JSONNode this[int aIndex] { get { return null; } set { } }
+
+ public virtual JSONNode this[string aKey] { get { return null; } set { } }
+
+ public virtual string Value { get { return ""; } set { } }
+
+ public virtual int Count { get { return 0; } }
+
+ public virtual bool IsNumber { get { return false; } }
+ public virtual bool IsString { get { return false; } }
+ public virtual bool IsBoolean { get { return false; } }
+ public virtual bool IsNull { get { return false; } }
+ public virtual bool IsArray { get { return false; } }
+ public virtual bool IsObject { get { return false; } }
+
+ public virtual bool Inline { get { return false; } set { } }
+
+ public virtual void Add(string aKey, JSONNode aItem)
+ {
+ }
+ public virtual void Add(JSONNode aItem)
+ {
+ Add("", aItem);
+ }
+
+ public virtual JSONNode Remove(string aKey)
+ {
+ return null;
+ }
+
+ public virtual JSONNode Remove(int aIndex)
+ {
+ return null;
+ }
+
+ public virtual JSONNode Remove(JSONNode aNode)
+ {
+ return aNode;
+ }
+
+ public virtual IEnumerable Children
+ {
+ get
+ {
+ yield break;
+ }
+ }
+
+ public IEnumerable DeepChildren
+ {
+ get
+ {
+ foreach (var C in Children)
+ foreach (var D in C.DeepChildren)
+ yield return D;
+ }
+ }
+
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder();
+ WriteToStringBuilder(sb, 0, 0, JSONTextMode.Compact);
+ return sb.ToString();
+ }
+
+ public virtual string ToString(int aIndent)
+ {
+ StringBuilder sb = new StringBuilder();
+ WriteToStringBuilder(sb, 0, aIndent, JSONTextMode.Indent);
+ return sb.ToString();
+ }
+ internal abstract void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode);
+
+ public abstract Enumerator GetEnumerator();
+ public IEnumerable> Linq { get { return new LinqEnumerator(this); } }
+ public KeyEnumerator Keys { get { return new KeyEnumerator(GetEnumerator()); } }
+ public ValueEnumerator Values { get { return new ValueEnumerator(GetEnumerator()); } }
+
+ #endregion common interface
+
+ #region typecasting properties
+
+
+ public virtual double AsDouble
+ {
+ get
+ {
+ double v = 0.0;
+ if (double.TryParse(Value, out v))
+ return v;
+ return 0.0;
+ }
+ set
+ {
+ Value = value.ToString();
+ }
+ }
+
+ public virtual int AsInt
+ {
+ get { return (int)AsDouble; }
+ set { AsDouble = value; }
+ }
+
+ public virtual float AsFloat
+ {
+ get { return (float)AsDouble; }
+ set { AsDouble = value; }
+ }
+
+ public virtual bool AsBool
+ {
+ get
+ {
+ bool v = false;
+ if (bool.TryParse(Value, out v))
+ return v;
+ return !string.IsNullOrEmpty(Value);
+ }
+ set
+ {
+ Value = (value) ? "true" : "false";
+ }
+ }
+
+ public virtual JSONArray AsArray
+ {
+ get
+ {
+ return this as JSONArray;
+ }
+ }
+
+ public virtual JSONObject AsObject
+ {
+ get
+ {
+ return this as JSONObject;
+ }
+ }
+
+
+ #endregion typecasting properties
+
+ #region operators
+
+ public static implicit operator JSONNode(string s)
+ {
+ return new JSONString(s);
+ }
+ public static implicit operator string(JSONNode d)
+ {
+ return (d == null) ? null : d.Value;
+ }
+
+ public static implicit operator JSONNode(double n)
+ {
+ return new JSONNumber(n);
+ }
+ public static implicit operator double(JSONNode d)
+ {
+ return (d == null) ? 0 : d.AsDouble;
+ }
+
+ public static implicit operator JSONNode(float n)
+ {
+ return new JSONNumber(n);
+ }
+ public static implicit operator float(JSONNode d)
+ {
+ return (d == null) ? 0 : d.AsFloat;
+ }
+
+ public static implicit operator JSONNode(int n)
+ {
+ return new JSONNumber(n);
+ }
+ public static implicit operator int(JSONNode d)
+ {
+ return (d == null) ? 0 : d.AsInt;
+ }
+
+ public static implicit operator JSONNode(bool b)
+ {
+ return new JSONBool(b);
+ }
+ public static implicit operator bool(JSONNode d)
+ {
+ return (d == null) ? false : d.AsBool;
+ }
+
+ public static implicit operator JSONNode(KeyValuePair aKeyValue)
+ {
+ return aKeyValue.Value;
+ }
+
+ public static bool operator ==(JSONNode a, object b)
+ {
+ if (ReferenceEquals(a, b))
+ return true;
+ bool aIsNull = a is JSONNull || ReferenceEquals(a, null) || a is JSONLazyCreator;
+ bool bIsNull = b is JSONNull || ReferenceEquals(b, null) || b is JSONLazyCreator;
+ if (aIsNull && bIsNull)
+ return true;
+ return !aIsNull && a.Equals(b);
+ }
+
+ public static bool operator !=(JSONNode a, object b)
+ {
+ return !(a == b);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return ReferenceEquals(this, obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ #endregion operators
+
+ [ThreadStatic]
+ private static StringBuilder m_EscapeBuilder;
+ internal static StringBuilder EscapeBuilder
+ {
+ get
+ {
+ if (m_EscapeBuilder == null)
+ m_EscapeBuilder = new StringBuilder();
+ return m_EscapeBuilder;
+ }
+ }
+ internal static string Escape(string aText)
+ {
+ var sb = EscapeBuilder;
+ sb.Length = 0;
+ if (sb.Capacity < aText.Length + aText.Length / 10)
+ sb.Capacity = aText.Length + aText.Length / 10;
+ foreach (char c in aText)
+ {
+ switch (c)
+ {
+ case '\\':
+ sb.Append("\\\\");
+ break;
+ case '\"':
+ sb.Append("\\\"");
+ break;
+ case '\n':
+ sb.Append("\\n");
+ break;
+ case '\r':
+ sb.Append("\\r");
+ break;
+ case '\t':
+ sb.Append("\\t");
+ break;
+ case '\b':
+ sb.Append("\\b");
+ break;
+ case '\f':
+ sb.Append("\\f");
+ break;
+ default:
+ if (c < ' ' || (forceASCII && c > 127))
+ {
+ ushort val = c;
+ sb.Append("\\u").Append(val.ToString("X4"));
+ }
+ else
+ sb.Append(c);
+ break;
+ }
+ }
+ string result = sb.ToString();
+ sb.Length = 0;
+ return result;
+ }
+
+ static void ParseElement(JSONNode ctx, string token, string tokenName, bool quoted)
+ {
+ if (quoted)
+ {
+ ctx.Add(tokenName, token);
+ return;
+ }
+ string tmp = token.ToLower();
+ if (tmp == "false" || tmp == "true")
+ ctx.Add(tokenName, tmp == "true");
+ else if (tmp == "null")
+ ctx.Add(tokenName, null);
+ else
+ {
+ double val;
+ if (double.TryParse(token, out val))
+ ctx.Add(tokenName, val);
+ else
+ ctx.Add(tokenName, token);
+ }
+ }
+
+ public static JSONNode Parse(string aJSON)
+ {
+ Stack stack = new Stack();
+ JSONNode ctx = null;
+ int i = 0;
+ StringBuilder Token = new StringBuilder();
+ string TokenName = "";
+ bool QuoteMode = false;
+ bool TokenIsQuoted = false;
+ while (i < aJSON.Length)
+ {
+ switch (aJSON[i])
+ {
+ case '{':
+ if (QuoteMode)
+ {
+ Token.Append(aJSON[i]);
+ break;
+ }
+ stack.Push(new JSONObject());
+ if (ctx != null)
+ {
+ ctx.Add(TokenName, stack.Peek());
+ }
+ TokenName = "";
+ Token.Length = 0;
+ ctx = stack.Peek();
+ break;
+
+ case '[':
+ if (QuoteMode)
+ {
+ Token.Append(aJSON[i]);
+ break;
+ }
+
+ stack.Push(new JSONArray());
+ if (ctx != null)
+ {
+ ctx.Add(TokenName, stack.Peek());
+ }
+ TokenName = "";
+ Token.Length = 0;
+ ctx = stack.Peek();
+ break;
+
+ case '}':
+ case ']':
+ if (QuoteMode)
+ {
+
+ Token.Append(aJSON[i]);
+ break;
+ }
+ if (stack.Count == 0)
+ throw new Exception("JSON Parse: Too many closing brackets");
+
+ stack.Pop();
+ if (Token.Length > 0 || TokenIsQuoted)
+ {
+ ParseElement(ctx, Token.ToString(), TokenName, TokenIsQuoted);
+ TokenIsQuoted = false;
+ }
+ TokenName = "";
+ Token.Length = 0;
+ if (stack.Count > 0)
+ ctx = stack.Peek();
+ break;
+
+ case ':':
+ if (QuoteMode)
+ {
+ Token.Append(aJSON[i]);
+ break;
+ }
+ TokenName = Token.ToString();
+ Token.Length = 0;
+ TokenIsQuoted = false;
+ break;
+
+ case '"':
+ QuoteMode ^= true;
+ TokenIsQuoted |= QuoteMode;
+ break;
+
+ case ',':
+ if (QuoteMode)
+ {
+ Token.Append(aJSON[i]);
+ break;
+ }
+ if (Token.Length > 0 || TokenIsQuoted)
+ {
+ ParseElement(ctx, Token.ToString(), TokenName, TokenIsQuoted);
+ TokenIsQuoted = false;
+ }
+ TokenName = "";
+ Token.Length = 0;
+ TokenIsQuoted = false;
+ break;
+
+ case '\r':
+ case '\n':
+ break;
+
+ case ' ':
+ case '\t':
+ if (QuoteMode)
+ Token.Append(aJSON[i]);
+ break;
+
+ case '\\':
+ ++i;
+ if (QuoteMode)
+ {
+ char C = aJSON[i];
+ switch (C)
+ {
+ case 't':
+ Token.Append('\t');
+ break;
+ case 'r':
+ Token.Append('\r');
+ break;
+ case 'n':
+ Token.Append('\n');
+ break;
+ case 'b':
+ Token.Append('\b');
+ break;
+ case 'f':
+ Token.Append('\f');
+ break;
+ case 'u':
+ {
+ string s = aJSON.Substring(i + 1, 4);
+ Token.Append((char)int.Parse(
+ s,
+ System.Globalization.NumberStyles.AllowHexSpecifier));
+ i += 4;
+ break;
+ }
+ default:
+ Token.Append(C);
+ break;
+ }
+ }
+ break;
+
+ default:
+ Token.Append(aJSON[i]);
+ break;
+ }
+ ++i;
+ }
+ if (QuoteMode)
+ {
+ throw new Exception("JSON Parse: Quotation marks seems to be messed up.");
+ }
+ return ctx;
+ }
+
+ }
+ // End of JSONNode
+
+ public partial class JSONArray : JSONNode
+ {
+ private List m_List = new List();
+ private bool inline = false;
+ public override bool Inline
+ {
+ get { return inline; }
+ set { inline = value; }
+ }
+
+ public override JSONNodeType Tag { get { return JSONNodeType.Array; } }
+ public override bool IsArray { get { return true; } }
+ public override Enumerator GetEnumerator() { return new Enumerator(m_List.GetEnumerator()); }
+
+ public override JSONNode this[int aIndex]
+ {
+ get
+ {
+ if (aIndex < 0 || aIndex >= m_List.Count)
+ return new JSONLazyCreator(this);
+ return m_List[aIndex];
+ }
+ set
+ {
+ if (value == null)
+ value = JSONNull.CreateOrGet();
+ if (aIndex < 0 || aIndex >= m_List.Count)
+ m_List.Add(value);
+ else
+ m_List[aIndex] = value;
+ }
+ }
+
+ public override JSONNode this[string aKey]
+ {
+ get { return new JSONLazyCreator(this); }
+ set
+ {
+ if (value == null)
+ value = JSONNull.CreateOrGet();
+ m_List.Add(value);
+ }
+ }
+
+ public override int Count
+ {
+ get { return m_List.Count; }
+ }
+
+ public override void Add(string aKey, JSONNode aItem)
+ {
+ if (aItem == null)
+ aItem = JSONNull.CreateOrGet();
+ m_List.Add(aItem);
+ }
+
+ public override JSONNode Remove(int aIndex)
+ {
+ if (aIndex < 0 || aIndex >= m_List.Count)
+ return null;
+ JSONNode tmp = m_List[aIndex];
+ m_List.RemoveAt(aIndex);
+ return tmp;
+ }
+
+ public override JSONNode Remove(JSONNode aNode)
+ {
+ m_List.Remove(aNode);
+ return aNode;
+ }
+
+ public override IEnumerable Children
+ {
+ get
+ {
+ foreach (JSONNode N in m_List)
+ yield return N;
+ }
+ }
+
+
+ internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
+ {
+ aSB.Append('[');
+ int count = m_List.Count;
+ if (inline)
+ aMode = JSONTextMode.Compact;
+ for (int i = 0; i < count; i++)
+ {
+ if (i > 0)
+ aSB.Append(',');
+ if (aMode == JSONTextMode.Indent)
+ aSB.AppendLine();
+
+ if (aMode == JSONTextMode.Indent)
+ aSB.Append(' ', aIndent + aIndentInc);
+ m_List[i].WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode);
+ }
+ if (aMode == JSONTextMode.Indent)
+ aSB.AppendLine().Append(' ', aIndent);
+ aSB.Append(']');
+ }
+ }
+ // End of JSONArray
+
+ public partial class JSONObject : JSONNode
+ {
+ private Dictionary m_Dict = new Dictionary();
+
+ private bool inline = false;
+ public override bool Inline
+ {
+ get { return inline; }
+ set { inline = value; }
+ }
+
+ public override JSONNodeType Tag { get { return JSONNodeType.Object; } }
+ public override bool IsObject { get { return true; } }
+
+ public override Enumerator GetEnumerator() { return new Enumerator(m_Dict.GetEnumerator()); }
+
+
+ public override JSONNode this[string aKey]
+ {
+ get
+ {
+ if (m_Dict.ContainsKey(aKey))
+ return m_Dict[aKey];
+ else
+ return new JSONLazyCreator(this, aKey);
+ }
+ set
+ {
+ if (value == null)
+ value = JSONNull.CreateOrGet();
+ if (m_Dict.ContainsKey(aKey))
+ m_Dict[aKey] = value;
+ else
+ m_Dict.Add(aKey, value);
+ }
+ }
+
+ public override JSONNode this[int aIndex]
+ {
+ get
+ {
+ if (aIndex < 0 || aIndex >= m_Dict.Count)
+ return null;
+ return m_Dict.ElementAt(aIndex).Value;
+ }
+ set
+ {
+ if (value == null)
+ value = JSONNull.CreateOrGet();
+ if (aIndex < 0 || aIndex >= m_Dict.Count)
+ return;
+ string key = m_Dict.ElementAt(aIndex).Key;
+ m_Dict[key] = value;
+ }
+ }
+
+ public override int Count
+ {
+ get { return m_Dict.Count; }
+ }
+
+ public override void Add(string aKey, JSONNode aItem)
+ {
+ if (aItem == null)
+ aItem = JSONNull.CreateOrGet();
+
+ if (!string.IsNullOrEmpty(aKey))
+ {
+ if (m_Dict.ContainsKey(aKey))
+ m_Dict[aKey] = aItem;
+ else
+ m_Dict.Add(aKey, aItem);
+ }
+ else
+ m_Dict.Add(Guid.NewGuid().ToString(), aItem);
+ }
+
+ public override JSONNode Remove(string aKey)
+ {
+ if (!m_Dict.ContainsKey(aKey))
+ return null;
+ JSONNode tmp = m_Dict[aKey];
+ m_Dict.Remove(aKey);
+ return tmp;
+ }
+
+ public override JSONNode Remove(int aIndex)
+ {
+ if (aIndex < 0 || aIndex >= m_Dict.Count)
+ return null;
+ var item = m_Dict.ElementAt(aIndex);
+ m_Dict.Remove(item.Key);
+ return item.Value;
+ }
+
+ public override JSONNode Remove(JSONNode aNode)
+ {
+ try
+ {
+ var item = m_Dict.Where(k => k.Value == aNode).First();
+ m_Dict.Remove(item.Key);
+ return aNode;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public override IEnumerable Children
+ {
+ get
+ {
+ foreach (KeyValuePair N in m_Dict)
+ yield return N.Value;
+ }
+ }
+
+ internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
+ {
+ aSB.Append('{');
+ bool first = true;
+ if (inline)
+ aMode = JSONTextMode.Compact;
+ foreach (var k in m_Dict)
+ {
+ if (!first)
+ aSB.Append(',');
+ first = false;
+ if (aMode == JSONTextMode.Indent)
+ aSB.AppendLine();
+ if (aMode == JSONTextMode.Indent)
+ aSB.Append(' ', aIndent + aIndentInc);
+ aSB.Append('\"').Append(Escape(k.Key)).Append('\"');
+ if (aMode == JSONTextMode.Compact)
+ aSB.Append(':');
+ else
+ aSB.Append(" : ");
+ k.Value.WriteToStringBuilder(aSB, aIndent + aIndentInc, aIndentInc, aMode);
+ }
+ if (aMode == JSONTextMode.Indent)
+ aSB.AppendLine().Append(' ', aIndent);
+ aSB.Append('}');
+ }
+
+ }
+ // End of JSONObject
+
+ public partial class JSONString : JSONNode
+ {
+ private string m_Data;
+
+ public override JSONNodeType Tag { get { return JSONNodeType.String; } }
+ public override bool IsString { get { return true; } }
+
+ public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+
+ public override string Value
+ {
+ get { return m_Data; }
+ set
+ {
+ m_Data = value;
+ }
+ }
+
+ public JSONString(string aData)
+ {
+ m_Data = aData;
+ }
+
+ internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
+ {
+ aSB.Append('\"').Append(Escape(m_Data)).Append('\"');
+ }
+ public override bool Equals(object obj)
+ {
+ if (base.Equals(obj))
+ return true;
+ string s = obj as string;
+ if (s != null)
+ return m_Data == s;
+ JSONString s2 = obj as JSONString;
+ if (s2 != null)
+ return m_Data == s2.m_Data;
+ return false;
+ }
+ public override int GetHashCode()
+ {
+ return m_Data.GetHashCode();
+ }
+ }
+ // End of JSONString
+
+ public partial class JSONNumber : JSONNode
+ {
+ private double m_Data;
+
+ public override JSONNodeType Tag { get { return JSONNodeType.Number; } }
+ public override bool IsNumber { get { return true; } }
+ public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+ public override string Value
+ {
+ get { return m_Data.ToString(); }
+ set
+ {
+ double v;
+ if (double.TryParse(value, out v))
+ m_Data = v;
+ }
+ }
+
+ public override double AsDouble
+ {
+ get { return m_Data; }
+ set { m_Data = value; }
+ }
+
+ public JSONNumber(double aData)
+ {
+ m_Data = aData;
+ }
+
+ public JSONNumber(string aData)
+ {
+ Value = aData;
+ }
+
+ internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
+ {
+ aSB.Append(m_Data);
+ }
+ private static bool IsNumeric(object value)
+ {
+ return value is int || value is uint
+ || value is float || value is double
+ || value is decimal
+ || value is long || value is ulong
+ || value is short || value is ushort
+ || value is sbyte || value is byte;
+ }
+ public override bool Equals(object obj)
+ {
+ if (obj == null)
+ return false;
+ if (base.Equals(obj))
+ return true;
+ JSONNumber s2 = obj as JSONNumber;
+ if (s2 != null)
+ return m_Data == s2.m_Data;
+ if (IsNumeric(obj))
+ return Convert.ToDouble(obj) == m_Data;
+ return false;
+ }
+ public override int GetHashCode()
+ {
+ return m_Data.GetHashCode();
+ }
+ }
+ // End of JSONNumber
+
+ public partial class JSONBool : JSONNode
+ {
+ private bool m_Data;
+
+ public override JSONNodeType Tag { get { return JSONNodeType.Boolean; } }
+ public override bool IsBoolean { get { return true; } }
+ public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+ public override string Value
+ {
+ get { return m_Data.ToString(); }
+ set
+ {
+ bool v;
+ if (bool.TryParse(value, out v))
+ m_Data = v;
+ }
+ }
+ public override bool AsBool
+ {
+ get { return m_Data; }
+ set { m_Data = value; }
+ }
+
+ public JSONBool(bool aData)
+ {
+ m_Data = aData;
+ }
+
+ public JSONBool(string aData)
+ {
+ Value = aData;
+ }
+
+ internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
+ {
+ aSB.Append((m_Data) ? "true" : "false");
+ }
+ public override bool Equals(object obj)
+ {
+ if (obj == null)
+ return false;
+ if (obj is bool)
+ return m_Data == (bool)obj;
+ return false;
+ }
+ public override int GetHashCode()
+ {
+ return m_Data.GetHashCode();
+ }
+ }
+ // End of JSONBool
+
+ public partial class JSONNull : JSONNode
+ {
+ static JSONNull m_StaticInstance = new JSONNull();
+ public static bool reuseSameInstance = true;
+ public static JSONNull CreateOrGet()
+ {
+ if (reuseSameInstance)
+ return m_StaticInstance;
+ return new JSONNull();
+ }
+ private JSONNull() { }
+
+ public override JSONNodeType Tag { get { return JSONNodeType.NullValue; } }
+ public override bool IsNull { get { return true; } }
+ public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+ public override string Value
+ {
+ get { return "null"; }
+ set { }
+ }
+ public override bool AsBool
+ {
+ get { return false; }
+ set { }
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (object.ReferenceEquals(this, obj))
+ return true;
+ return (obj is JSONNull);
+ }
+ public override int GetHashCode()
+ {
+ return 0;
+ }
+
+ internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
+ {
+ aSB.Append("null");
+ }
+ }
+ // End of JSONNull
+
+ internal partial class JSONLazyCreator : JSONNode
+ {
+ private JSONNode m_Node = null;
+ private string m_Key = null;
+ public override JSONNodeType Tag { get { return JSONNodeType.None; } }
+ public override Enumerator GetEnumerator() { return new Enumerator(); }
+
+ public JSONLazyCreator(JSONNode aNode)
+ {
+ m_Node = aNode;
+ m_Key = null;
+ }
+
+ public JSONLazyCreator(JSONNode aNode, string aKey)
+ {
+ m_Node = aNode;
+ m_Key = aKey;
+ }
+
+ private void Set(JSONNode aVal)
+ {
+ if (m_Key == null)
+ {
+ m_Node.Add(aVal);
+ }
+ else
+ {
+ m_Node.Add(m_Key, aVal);
+ }
+ m_Node = null; // Be GC friendly.
+ }
+
+ public override JSONNode this[int aIndex]
+ {
+ get
+ {
+ return new JSONLazyCreator(this);
+ }
+ set
+ {
+ var tmp = new JSONArray();
+ tmp.Add(value);
+ Set(tmp);
+ }
+ }
+
+ public override JSONNode this[string aKey]
+ {
+ get
+ {
+ return new JSONLazyCreator(this, aKey);
+ }
+ set
+ {
+ var tmp = new JSONObject();
+ tmp.Add(aKey, value);
+ Set(tmp);
+ }
+ }
+
+ public override void Add(JSONNode aItem)
+ {
+ var tmp = new JSONArray();
+ tmp.Add(aItem);
+ Set(tmp);
+ }
+
+ public override void Add(string aKey, JSONNode aItem)
+ {
+ var tmp = new JSONObject();
+ tmp.Add(aKey, aItem);
+ Set(tmp);
+ }
+
+ public static bool operator ==(JSONLazyCreator a, object b)
+ {
+ if (b == null)
+ return true;
+ return System.Object.ReferenceEquals(a, b);
+ }
+
+ public static bool operator !=(JSONLazyCreator a, object b)
+ {
+ return !(a == b);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (obj == null)
+ return true;
+ return System.Object.ReferenceEquals(this, obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return 0;
+ }
+
+ public override int AsInt
+ {
+ get
+ {
+ JSONNumber tmp = new JSONNumber(0);
+ Set(tmp);
+ return 0;
+ }
+ set
+ {
+ JSONNumber tmp = new JSONNumber(value);
+ Set(tmp);
+ }
+ }
+
+ public override float AsFloat
+ {
+ get
+ {
+ JSONNumber tmp = new JSONNumber(0.0f);
+ Set(tmp);
+ return 0.0f;
+ }
+ set
+ {
+ JSONNumber tmp = new JSONNumber(value);
+ Set(tmp);
+ }
+ }
+
+ public override double AsDouble
+ {
+ get
+ {
+ JSONNumber tmp = new JSONNumber(0.0);
+ Set(tmp);
+ return 0.0;
+ }
+ set
+ {
+ JSONNumber tmp = new JSONNumber(value);
+ Set(tmp);
+ }
+ }
+
+ public override bool AsBool
+ {
+ get
+ {
+ JSONBool tmp = new JSONBool(false);
+ Set(tmp);
+ return false;
+ }
+ set
+ {
+ JSONBool tmp = new JSONBool(value);
+ Set(tmp);
+ }
+ }
+
+ public override JSONArray AsArray
+ {
+ get
+ {
+ JSONArray tmp = new JSONArray();
+ Set(tmp);
+ return tmp;
+ }
+ }
+
+ public override JSONObject AsObject
+ {
+ get
+ {
+ JSONObject tmp = new JSONObject();
+ Set(tmp);
+ return tmp;
+ }
+ }
+ internal override void WriteToStringBuilder(StringBuilder aSB, int aIndent, int aIndentInc, JSONTextMode aMode)
+ {
+ aSB.Append("null");
+ }
+ }
+ // End of JSONLazyCreator
+
+ public static class JSON
+ {
+ public static JSONNode Parse(string aJSON)
+ {
+ return JSONNode.Parse(aJSON);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Allow2/packages.config b/Allow2/packages.config
deleted file mode 100644
index e98b858..0000000
--- a/Allow2/packages.config
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/Documentation b/Documentation
new file mode 160000
index 0000000..3066f7e
--- /dev/null
+++ b/Documentation
@@ -0,0 +1 @@
+Subproject commit 3066f7efcd8c000b7c282d115d595ced731bb172
diff --git a/Images/Allow2Logo500.png b/Images/Allow2Logo500.png
new file mode 100644
index 0000000..5e947d3
Binary files /dev/null and b/Images/Allow2Logo500.png differ
diff --git a/Images/Allow2Logo_200x258.png b/Images/Allow2Logo_200x258.png
new file mode 100644
index 0000000..e38967c
Binary files /dev/null and b/Images/Allow2Logo_200x258.png differ
diff --git a/Images/Allow2Logo_860x389.png b/Images/Allow2Logo_860x389.png
new file mode 100644
index 0000000..d7c3647
Binary files /dev/null and b/Images/Allow2Logo_860x389.png differ
diff --git a/Images/Allow2_200x124.png b/Images/Allow2_200x124.png
new file mode 100644
index 0000000..07dbef1
Binary files /dev/null and b/Images/Allow2_200x124.png differ
diff --git a/Images/Allow2_516x389.png b/Images/Allow2_516x389.png
new file mode 100644
index 0000000..c7a5ddc
Binary files /dev/null and b/Images/Allow2_516x389.png differ
diff --git a/README.md b/README.md
index a87fa01..9564674 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,17 @@
-# Allow2Unity
+# Allow2 - Free and Powerful Parental Freedom for your apps and devices
-Allow2 makes it easy to add parental controls to your apps.
+*Who wants **more downloads**?*
-This is an early release of the new SDK build in progress
+Integrating **Allow2** in a few lines of code will remove fear and guilt so parents can purchase your apps!
+
+**Allow2** is a unique "Parental Freedom" platform.
+
+Simply integrate **Allow2** in about as many steps as integrating something like google analytics or Facebook, and you unlock a whole new demographic!
+
+It provides parents with a unified centralised mechanism to help teach responsibility and raise their children.
+
+Essentially, for parents and users that don't care, they will download your app or game anyway. But how can you address a brand new market you never had access to? Simply download and integrate **Allow2** for free!
+
+**[Visit the Wiki](https://github.com/Allow2/Allow2Unity/wiki)** for [Getting Started Guides](https://github.com/Allow2/Allow2Unity/wiki), [API Documentation](https://github.com/Allow2/Allow2Unity/wiki) and [Samples](https://github.com/Allow2/Allow2Unity/wiki).
+
+If you need help understanding it, integrating it or just love it, get in touch or discuss it online.
diff --git a/com.allow2.sdk/Runtime/Allow2.Runtime.asmdef b/com.allow2.sdk/Runtime/Allow2.Runtime.asmdef
new file mode 100644
index 0000000..efd5250
--- /dev/null
+++ b/com.allow2.sdk/Runtime/Allow2.Runtime.asmdef
@@ -0,0 +1,14 @@
+{
+ "name": "Allow2.Runtime",
+ "rootNamespace": "Allow2",
+ "references": [],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/com.allow2.sdk/Runtime/Bridge/Allow2Coroutines.cs b/com.allow2.sdk/Runtime/Bridge/Allow2Coroutines.cs
new file mode 100644
index 0000000..3b5cd8c
--- /dev/null
+++ b/com.allow2.sdk/Runtime/Bridge/Allow2Coroutines.cs
@@ -0,0 +1,178 @@
+// Allow2 Unity SDK v2
+// Copyright (c) 2026 Allow2 Pty Ltd. All rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Allow2
+{
+ ///
+ /// Coroutine wrappers for async operations.
+ /// Provides WebGL-compatible alternatives to async/await.
+ ///
+ /// All coroutines run on the Allow2Manager MonoBehaviour.
+ ///
+ public class Allow2Coroutines
+ {
+ private readonly MonoBehaviour _runner;
+ private readonly Allow2Api _api;
+
+ public Allow2Coroutines(MonoBehaviour runner, Allow2Api api)
+ {
+ _runner = runner;
+ _api = api;
+ }
+
+ ///
+ /// Start a coroutine on the runner MonoBehaviour.
+ ///
+ public Coroutine Run(IEnumerator routine)
+ {
+ if (_runner == null || !_runner.gameObject.activeInHierarchy)
+ {
+ return null;
+ }
+ return _runner.StartCoroutine(routine);
+ }
+
+ ///
+ /// Stop a coroutine.
+ ///
+ public void Cancel(Coroutine coroutine)
+ {
+ if (_runner != null && coroutine != null)
+ {
+ _runner.StopCoroutine(coroutine);
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // API coroutine wrappers
+ // ----------------------------------------------------------------
+
+ ///
+ /// Run a permission check against the API.
+ ///
+ public Coroutine RunCheck(int userId, int pairId, string pairToken,
+ int childId, Dictionary activities, string tz,
+ Action callback)
+ {
+ return Run(_api.Check(userId, pairId, pairToken, childId, activities, tz, true, callback));
+ }
+
+ ///
+ /// Initialize PIN pairing.
+ ///
+ public Coroutine RunInitPairing(string uuid, string deviceName, string platform,
+ Action callback)
+ {
+ return Run(_api.InitPINPairing(uuid, deviceName, platform, callback));
+ }
+
+ ///
+ /// Poll pairing status.
+ ///
+ public Coroutine RunCheckPairingStatus(string sessionId, Action callback)
+ {
+ return Run(_api.CheckPairingStatus(sessionId, callback));
+ }
+
+ ///
+ /// Poll for updates.
+ ///
+ public Coroutine RunGetUpdates(int userId, int pairId, string pairToken,
+ long timestampMillis, Action callback)
+ {
+ return Run(_api.GetUpdates(userId, pairId, pairToken, timestampMillis, callback));
+ }
+
+ ///
+ /// Create a request (more time, etc.).
+ ///
+ public Coroutine RunCreateRequest(int userId, int pairId, string pairToken,
+ int childId, int duration, int activityId, string message,
+ Action callback)
+ {
+ return Run(_api.CreateRequest(userId, pairId, pairToken, childId,
+ duration, activityId, message, callback));
+ }
+
+ ///
+ /// Poll request status.
+ ///
+ public Coroutine RunGetRequestStatus(string requestId, string statusSecret,
+ Action callback)
+ {
+ return Run(_api.GetRequestStatus(requestId, statusSecret, callback));
+ }
+
+ ///
+ /// Submit feedback.
+ ///
+ public Coroutine RunSubmitFeedback(int userId, int pairId, string pairToken,
+ int childId, string category, string message,
+ Dictionary deviceContext, Action callback)
+ {
+ return Run(_api.SubmitFeedback(userId, pairId, pairToken, childId,
+ category, message, deviceContext, callback));
+ }
+
+ ///
+ /// Load feedback.
+ ///
+ public Coroutine RunLoadFeedback(int userId, int pairId, string pairToken,
+ Action callback)
+ {
+ return Run(_api.LoadFeedback(userId, pairId, pairToken, callback));
+ }
+
+ ///
+ /// Reply to feedback.
+ ///
+ public Coroutine RunFeedbackReply(int userId, int pairId, string pairToken,
+ string discussionId, string message, Action callback)
+ {
+ return Run(_api.FeedbackReply(userId, pairId, pairToken,
+ discussionId, message, callback));
+ }
+
+ // ----------------------------------------------------------------
+ // Utility coroutines
+ // ----------------------------------------------------------------
+
+ ///
+ /// Wait for a number of seconds, then invoke the callback.
+ ///
+ public Coroutine Delay(float seconds, Action callback)
+ {
+ return Run(DelayCoroutine(seconds, callback));
+ }
+
+ private IEnumerator DelayCoroutine(float seconds, Action callback)
+ {
+ yield return new WaitForSecondsRealtime(seconds);
+ if (callback != null) callback();
+ }
+
+ ///
+ /// Repeat an action at a fixed interval.
+ /// Stops when the action returns true.
+ ///
+ public Coroutine RepeatUntil(float intervalSeconds, Func action)
+ {
+ return Run(RepeatCoroutine(intervalSeconds, action));
+ }
+
+ private IEnumerator RepeatCoroutine(float intervalSeconds, Func action)
+ {
+ while (true)
+ {
+ bool done = action();
+ if (done) yield break;
+ yield return new WaitForSecondsRealtime(intervalSeconds);
+ }
+ }
+ }
+}
diff --git a/com.allow2.sdk/Runtime/Bridge/Allow2Manager.cs b/com.allow2.sdk/Runtime/Bridge/Allow2Manager.cs
new file mode 100644
index 0000000..116dba3
--- /dev/null
+++ b/com.allow2.sdk/Runtime/Bridge/Allow2Manager.cs
@@ -0,0 +1,802 @@
+// Allow2 Unity SDK v2
+// Copyright (c) 2026 Allow2 Pty Ltd. All rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Allow2
+{
+ ///
+ /// MonoBehaviour singleton that bridges the pure C# Allow2Daemon
+ /// with Unity's coroutine system and lifecycle.
+ ///
+ /// This is the main Unity entry point. Add it to a GameObject or
+ /// call Allow2Manager.Instance to auto-create one.
+ ///
+ /// Responsibilities:
+ /// - DontDestroyOnLoad persistence
+ /// - Coroutine-based API calls (check loop, pairing polls, etc.)
+ /// - Unity lifecycle hooks (OnApplicationPause, OnApplicationQuit)
+ /// - Inspector-bindable UnityEvents
+ ///
+ public class Allow2Manager : MonoBehaviour
+ {
+ // ----------------------------------------------------------------
+ // Singleton
+ // ----------------------------------------------------------------
+
+ private static Allow2Manager _instance;
+ private static bool _applicationQuitting;
+
+ public static Allow2Manager Instance
+ {
+ get
+ {
+ if (_applicationQuitting) return null;
+ if (_instance == null)
+ {
+ _instance = FindObjectOfType();
+ if (_instance == null)
+ {
+ GameObject go = new GameObject("Allow2Manager");
+ _instance = go.AddComponent();
+ }
+ }
+ return _instance;
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // Inspector fields
+ // ----------------------------------------------------------------
+
+ [Header("Allow2 Configuration")]
+ [Tooltip("Version ID from developer.allow2.com")]
+ public int Vid;
+
+ [Tooltip("Device token from developer.allow2.com")]
+ public string DeviceToken;
+
+ [Tooltip("Activities this game monitors")]
+ public Allow2Activity[] Activities;
+
+ [Tooltip("API base URL (leave empty for production)")]
+ public string ApiUrl;
+
+ [Header("Behaviour")]
+ [Tooltip("Seconds between permission checks")]
+ public int CheckIntervalSeconds = 60;
+
+ [Tooltip("Auto-pause game on soft-lock (Time.timeScale = 0)")]
+ public bool AutoPauseOnLock = true;
+
+ [Header("Unity Events (Inspector Binding)")]
+ public Allow2PairingRequiredEvent OnPairingRequiredEvent;
+ public Allow2PairedEvent OnPairedEvent;
+ public Allow2ChildSelectRequiredEvent OnChildSelectRequiredEvent;
+ public Allow2ChildSelectedEvent OnChildSelectedEvent;
+ public Allow2SoftLockEvent OnSoftLockEvent;
+ public Allow2HardLockEvent OnHardLockEvent;
+ public Allow2UnlockEvent OnUnlockEvent;
+ public Allow2WarningEvent OnWarningEvent;
+ public Allow2CheckResultEvent OnCheckResultEvent;
+ public Allow2StateChangedEvent OnStateChangedEvent;
+ public Allow2UnpairedEvent OnUnpairedEvent;
+ public Allow2ParentModeEvent OnParentModeEvent;
+ public Allow2SessionTimeoutEvent OnSessionTimeoutEvent;
+ public Allow2ErrorEvent OnErrorEvent;
+
+ // ----------------------------------------------------------------
+ // Internal state
+ // ----------------------------------------------------------------
+
+ private Allow2Daemon _daemon;
+ private Allow2Coroutines _coroutines;
+ private ICredentialStore _credentialStore;
+ private bool _configured;
+ private bool _started;
+
+ // Coroutine handles
+ private Coroutine _checkLoopCoroutine;
+ private Coroutine _pairingPollCoroutine;
+ private Coroutine _updatePollCoroutine;
+ private Coroutine _requestPollCoroutine;
+
+ // ----------------------------------------------------------------
+ // Public API
+ // ----------------------------------------------------------------
+
+ ///
+ /// The underlying daemon for advanced usage.
+ ///
+ public Allow2Daemon Daemon { get { return _daemon; } }
+
+ ///
+ /// Current SDK state.
+ ///
+ public Allow2State State
+ {
+ get { return _daemon != null ? _daemon.State : Allow2State.Unpaired; }
+ }
+
+ ///
+ /// Configure the SDK with the given config.
+ /// Call this before StartDaemon() if not using Inspector fields.
+ ///
+ public void Configure(Allow2Config config)
+ {
+ Configure(config, null);
+ }
+
+ ///
+ /// Configure with a custom credential store.
+ ///
+ public void Configure(Allow2Config config, ICredentialStore credentialStore)
+ {
+ if (credentialStore == null)
+ {
+ credentialStore = new PlayerPrefsStore();
+ }
+ _credentialStore = credentialStore;
+
+ _daemon = new Allow2Daemon(config, credentialStore);
+ _coroutines = new Allow2Coroutines(this, _daemon.Api);
+
+ WireDaemonEvents();
+ _configured = true;
+ }
+
+ ///
+ /// Start the daemon. Loads credentials and begins enforcement if paired.
+ ///
+ public void StartDaemon()
+ {
+ if (!_configured)
+ {
+ // Auto-configure from Inspector fields
+ AutoConfigureFromInspector();
+ }
+
+ if (_daemon == null)
+ {
+ Debug.LogError("[Allow2] Cannot start: not configured. Call Configure() first.");
+ return;
+ }
+
+ _daemon.Start();
+ _started = true;
+
+ // Start the check loop if already enforcing
+ if (_daemon.State == Allow2State.Enforcing)
+ {
+ StartCheckLoop();
+ }
+
+ // Start the pairing flow if unpaired
+ if (_daemon.State == Allow2State.Pairing)
+ {
+ StartPairingFlow();
+ }
+ }
+
+ ///
+ /// Stop the daemon and all coroutines.
+ ///
+ public void StopDaemon()
+ {
+ _started = false;
+ StopAllLoops();
+ if (_daemon != null)
+ {
+ _daemon.Stop();
+ }
+ }
+
+ ///
+ /// Open the Allow2 app (triggers pairing if unpaired).
+ ///
+ public void OpenApp()
+ {
+ if (_daemon != null)
+ {
+ _daemon.OpenApp();
+ }
+ }
+
+ ///
+ /// Select a child by ID with PIN verification.
+ ///
+ public bool SelectChild(int childId, string pin)
+ {
+ if (_daemon == null) return false;
+ bool success = _daemon.SelectChild(childId, pin);
+ if (success)
+ {
+ StartCheckLoop();
+ }
+ return success;
+ }
+
+ ///
+ /// Select a child without PIN (honour system).
+ ///
+ public bool SelectChild(int childId)
+ {
+ return SelectChild(childId, null);
+ }
+
+ ///
+ /// Enter parent mode with PIN.
+ ///
+ public bool EnterParentMode(string pin)
+ {
+ if (_daemon == null) return false;
+ bool success = _daemon.EnterParentMode(pin);
+ if (success)
+ {
+ StopCheckLoop();
+ }
+ return success;
+ }
+
+ ///
+ /// End the current child/parent session.
+ ///
+ public void EndSession()
+ {
+ StopCheckLoop();
+ if (_daemon != null)
+ {
+ _daemon.EndSession();
+ }
+ }
+
+ ///
+ /// Request more time for an activity.
+ ///
+ public void RequestMoreTime(int activityId, int durationMinutes, string message)
+ {
+ if (_daemon == null || _daemon.Credentials == null) return;
+ Allow2Credentials creds = _daemon.Credentials;
+
+ _coroutines.RunCreateRequest(
+ creds.UserId, creds.PairId, creds.PairToken,
+ _daemon.ChildId, durationMinutes, activityId, message,
+ delegate(Allow2ApiResponse response)
+ {
+ _daemon.Request.HandleCreateResponse(response);
+ if (_daemon.Request.IsPolling)
+ {
+ StartRequestPolling();
+ }
+ }
+ );
+ }
+
+ ///
+ /// Submit feedback.
+ ///
+ public void SubmitFeedback(string category, string message)
+ {
+ if (_daemon == null || _daemon.Credentials == null) return;
+
+ string error = _daemon.Feedback.ValidateSubmission(category, message);
+ if (error != null)
+ {
+ Debug.LogWarning("[Allow2] Feedback validation: " + error);
+ return;
+ }
+
+ Allow2Credentials creds = _daemon.Credentials;
+ Dictionary deviceContext = new Dictionary();
+ deviceContext["deviceName"] = _daemon.Config.DeviceName;
+ deviceContext["platform"] = Application.platform.ToString();
+ deviceContext["sdkVersion"] = "2.0.0-alpha.1";
+ deviceContext["productName"] = Application.productName;
+
+ _coroutines.RunSubmitFeedback(
+ creds.UserId, creds.PairId, creds.PairToken,
+ _daemon.ChildId, category, message, deviceContext,
+ delegate(Allow2ApiResponse response)
+ {
+ _daemon.Feedback.HandleSubmitResponse(response, category);
+ }
+ );
+ }
+
+ // ----------------------------------------------------------------
+ // Unity Lifecycle
+ // ----------------------------------------------------------------
+
+ private void Awake()
+ {
+ if (_instance != null && _instance != this)
+ {
+ Destroy(gameObject);
+ return;
+ }
+
+ _instance = this;
+ DontDestroyOnLoad(gameObject);
+
+ if (OnPairingRequiredEvent == null) OnPairingRequiredEvent = new Allow2PairingRequiredEvent();
+ if (OnPairedEvent == null) OnPairedEvent = new Allow2PairedEvent();
+ if (OnChildSelectRequiredEvent == null) OnChildSelectRequiredEvent = new Allow2ChildSelectRequiredEvent();
+ if (OnChildSelectedEvent == null) OnChildSelectedEvent = new Allow2ChildSelectedEvent();
+ if (OnSoftLockEvent == null) OnSoftLockEvent = new Allow2SoftLockEvent();
+ if (OnHardLockEvent == null) OnHardLockEvent = new Allow2HardLockEvent();
+ if (OnUnlockEvent == null) OnUnlockEvent = new Allow2UnlockEvent();
+ if (OnWarningEvent == null) OnWarningEvent = new Allow2WarningEvent();
+ if (OnCheckResultEvent == null) OnCheckResultEvent = new Allow2CheckResultEvent();
+ if (OnStateChangedEvent == null) OnStateChangedEvent = new Allow2StateChangedEvent();
+ if (OnUnpairedEvent == null) OnUnpairedEvent = new Allow2UnpairedEvent();
+ if (OnParentModeEvent == null) OnParentModeEvent = new Allow2ParentModeEvent();
+ if (OnSessionTimeoutEvent == null) OnSessionTimeoutEvent = new Allow2SessionTimeoutEvent();
+ if (OnErrorEvent == null) OnErrorEvent = new Allow2ErrorEvent();
+ }
+
+ private void Update()
+ {
+ if (_daemon == null) return;
+
+ // Tick the soft-lock timer
+ if (_daemon.Checker != null && _daemon.Checker.IsRunning)
+ {
+ _daemon.Checker.UpdateSoftLockTimer(Time.unscaledDeltaTime);
+ }
+
+ // Tick the session timer
+ if (_daemon.ChildShield != null)
+ {
+ _daemon.ChildShield.UpdateSessionTimer(Time.unscaledDeltaTime);
+ }
+ }
+
+ private void OnApplicationPause(bool paused)
+ {
+ if (_daemon == null) return;
+
+ if (paused)
+ {
+ // App backgrounded -- stop polling to save battery
+ StopCheckLoop();
+ StopUpdateLoop();
+ }
+ else
+ {
+ // App resumed -- restart loops if enforcing
+ if (_started && _daemon.State == Allow2State.Enforcing)
+ {
+ StartCheckLoop();
+ StartUpdateLoop();
+ }
+ }
+ }
+
+ private void OnApplicationFocus(bool hasFocus)
+ {
+ // Record activity for session timer
+ if (hasFocus && _daemon != null && _daemon.ChildShield != null)
+ {
+ _daemon.ChildShield.RecordActivity();
+ }
+ }
+
+ private void OnApplicationQuit()
+ {
+ _applicationQuitting = true;
+ StopDaemon();
+ }
+
+ private void OnDestroy()
+ {
+ if (_instance == this)
+ {
+ _instance = null;
+ }
+ StopAllLoops();
+ }
+
+ // ----------------------------------------------------------------
+ // Coroutine loops
+ // ----------------------------------------------------------------
+
+ private void StartCheckLoop()
+ {
+ StopCheckLoop();
+ if (_daemon == null || _daemon.Credentials == null) return;
+ _daemon.Checker.Start();
+ _checkLoopCoroutine = StartCoroutine(CheckLoopCoroutine());
+ }
+
+ private void StopCheckLoop()
+ {
+ if (_checkLoopCoroutine != null)
+ {
+ StopCoroutine(_checkLoopCoroutine);
+ _checkLoopCoroutine = null;
+ }
+ if (_daemon != null && _daemon.Checker != null)
+ {
+ _daemon.Checker.Stop();
+ }
+ }
+
+ private IEnumerator CheckLoopCoroutine()
+ {
+ while (_daemon != null && _daemon.Checker != null && _daemon.Checker.IsRunning)
+ {
+ Allow2Credentials creds = _daemon.Credentials;
+ if (creds == null || !creds.IsValid)
+ {
+ yield break;
+ }
+
+ bool done = false;
+ _coroutines.RunCheck(
+ creds.UserId, creds.PairId, creds.PairToken,
+ _daemon.ChildId, _daemon.Checker.GetActivityMap(),
+ _daemon.Timezone,
+ delegate(Allow2ApiResponse response)
+ {
+ if (response.IsSuccess)
+ {
+ _daemon.Checker.ProcessResult(response);
+ // Cache for offline
+ if (response.Body != null)
+ {
+ _daemon.Offline.CacheResult(response.Body);
+ }
+ }
+ else
+ {
+ _daemon.Checker.HandleError(response);
+ }
+ done = true;
+ }
+ );
+
+ // Wait for the request to complete
+ while (!done)
+ {
+ yield return null;
+ }
+
+ // Wait for check interval
+ yield return new WaitForSecondsRealtime(_daemon.Config.CheckIntervalSeconds);
+ }
+ }
+
+ private void StartPairingFlow()
+ {
+ if (_daemon == null || _daemon.Pairing == null) return;
+
+ string uuid = _daemon.Pairing.GetOrCreateUuid();
+ string deviceName = _daemon.Config.DeviceName;
+ if (string.IsNullOrEmpty(deviceName))
+ {
+ deviceName = SystemInfo.deviceName;
+ }
+
+ _coroutines.RunInitPairing(uuid, deviceName, Application.platform.ToString(),
+ delegate(Allow2ApiResponse response)
+ {
+ _daemon.Pairing.HandleInitResponse(response);
+
+ if (!string.IsNullOrEmpty(_daemon.Pairing.SessionId))
+ {
+ StartPairingPoll();
+ }
+ else
+ {
+ // Retry init after 5 seconds
+ StartCoroutine(RetryPairingInit());
+ }
+ }
+ );
+ }
+
+ private IEnumerator RetryPairingInit()
+ {
+ while (_daemon != null && _daemon.State == Allow2State.Pairing
+ && string.IsNullOrEmpty(_daemon.Pairing.SessionId)
+ && !_daemon.Pairing.IsPaired)
+ {
+ yield return new WaitForSecondsRealtime(5f);
+
+ if (_daemon == null || _daemon.State != Allow2State.Pairing) yield break;
+
+ string uuid = _daemon.Pairing.GetOrCreateUuid();
+ string deviceName = _daemon.Config.DeviceName;
+ if (string.IsNullOrEmpty(deviceName))
+ {
+ deviceName = SystemInfo.deviceName;
+ }
+
+ bool done = false;
+ _coroutines.RunInitPairing(uuid, deviceName, Application.platform.ToString(),
+ delegate(Allow2ApiResponse response)
+ {
+ _daemon.Pairing.HandleInitResponse(response);
+ done = true;
+ }
+ );
+
+ while (!done) yield return null;
+
+ if (!string.IsNullOrEmpty(_daemon.Pairing.SessionId))
+ {
+ StartPairingPoll();
+ yield break;
+ }
+ }
+ }
+
+ private void StartPairingPoll()
+ {
+ StopPairingPoll();
+ _pairingPollCoroutine = StartCoroutine(PairingPollCoroutine());
+ }
+
+ private void StopPairingPoll()
+ {
+ if (_pairingPollCoroutine != null)
+ {
+ StopCoroutine(_pairingPollCoroutine);
+ _pairingPollCoroutine = null;
+ }
+ }
+
+ private IEnumerator PairingPollCoroutine()
+ {
+ while (_daemon != null && _daemon.State == Allow2State.Pairing
+ && !_daemon.Pairing.IsPaired)
+ {
+ yield return new WaitForSecondsRealtime(5f);
+
+ if (_daemon == null || _daemon.Pairing.IsPaired) yield break;
+
+ string sessionId = _daemon.Pairing.SessionId;
+ if (string.IsNullOrEmpty(sessionId)) continue;
+
+ bool done = false;
+ _coroutines.RunCheckPairingStatus(sessionId,
+ delegate(Allow2ApiResponse response)
+ {
+ _daemon.Pairing.HandlePollResponse(response);
+ done = true;
+ }
+ );
+
+ while (!done) yield return null;
+
+ if (_daemon.Pairing.IsPaired)
+ {
+ // Pairing succeeded -- start enforcement
+ if (_daemon.State == Allow2State.Enforcing)
+ {
+ StartCheckLoop();
+ StartUpdateLoop();
+ }
+ yield break;
+ }
+ }
+ }
+
+ private void StartUpdateLoop()
+ {
+ StopUpdateLoop();
+ if (_daemon == null || _daemon.Credentials == null) return;
+ _daemon.Updates.Start();
+ _updatePollCoroutine = StartCoroutine(UpdatePollCoroutine());
+ }
+
+ private void StopUpdateLoop()
+ {
+ if (_updatePollCoroutine != null)
+ {
+ StopCoroutine(_updatePollCoroutine);
+ _updatePollCoroutine = null;
+ }
+ if (_daemon != null && _daemon.Updates != null)
+ {
+ _daemon.Updates.Stop();
+ }
+ }
+
+ private IEnumerator UpdatePollCoroutine()
+ {
+ while (_daemon != null && _daemon.Updates != null && _daemon.Updates.IsRunning)
+ {
+ Allow2Credentials creds = _daemon.Credentials;
+ if (creds == null || !creds.IsValid)
+ {
+ yield break;
+ }
+
+ bool done = false;
+ _coroutines.RunGetUpdates(
+ creds.UserId, creds.PairId, creds.PairToken,
+ _daemon.Updates.LastTimestamp,
+ delegate(Allow2ApiResponse response)
+ {
+ _daemon.Updates.HandleResponse(response);
+ done = true;
+ }
+ );
+
+ while (!done) yield return null;
+
+ yield return new WaitForSecondsRealtime(30f);
+ }
+ }
+
+ private void StartRequestPolling()
+ {
+ StopRequestPolling();
+ _requestPollCoroutine = StartCoroutine(RequestPollCoroutine());
+ }
+
+ private void StopRequestPolling()
+ {
+ if (_requestPollCoroutine != null)
+ {
+ StopCoroutine(_requestPollCoroutine);
+ _requestPollCoroutine = null;
+ }
+ }
+
+ private IEnumerator RequestPollCoroutine()
+ {
+ while (_daemon != null && _daemon.Request != null && _daemon.Request.IsPolling)
+ {
+ yield return new WaitForSecondsRealtime(5f);
+
+ if (_daemon == null || !_daemon.Request.IsPolling) yield break;
+
+ string requestId = _daemon.Request.RequestId;
+ string statusSecret = _daemon.Request.StatusSecret;
+ if (string.IsNullOrEmpty(requestId)) yield break;
+
+ bool done = false;
+ _coroutines.RunGetRequestStatus(requestId, statusSecret,
+ delegate(Allow2ApiResponse response)
+ {
+ _daemon.Request.HandlePollResponse(response);
+ done = true;
+ }
+ );
+
+ while (!done) yield return null;
+ }
+ }
+
+ private void StopAllLoops()
+ {
+ StopCheckLoop();
+ StopPairingPoll();
+ StopUpdateLoop();
+ StopRequestPolling();
+ }
+
+ // ----------------------------------------------------------------
+ // Auto-configure from Inspector
+ // ----------------------------------------------------------------
+
+ private void AutoConfigureFromInspector()
+ {
+ if (Vid <= 0 || string.IsNullOrEmpty(DeviceToken))
+ {
+ Debug.LogWarning("[Allow2] Vid and DeviceToken must be set. Configure via Inspector or call Configure().");
+ return;
+ }
+
+ Allow2Config config = new Allow2Config();
+ config.Vid = Vid;
+ config.DeviceToken = DeviceToken;
+ config.DeviceName = SystemInfo.deviceName;
+ config.Activities = Activities;
+ config.ApiUrl = ApiUrl;
+ config.CheckIntervalSeconds = CheckIntervalSeconds > 0 ? CheckIntervalSeconds : 60;
+
+ Configure(config);
+ }
+
+ // ----------------------------------------------------------------
+ // Wire daemon events to UnityEvents
+ // ----------------------------------------------------------------
+
+ private void WireDaemonEvents()
+ {
+ _daemon.OnPairingRequired += delegate(string pin, string qrUrl)
+ {
+ if (OnPairingRequiredEvent != null) OnPairingRequiredEvent.Invoke(pin, qrUrl);
+ // Start the pairing poll loop
+ if (_daemon.State == Allow2State.Pairing)
+ {
+ StartPairingFlow();
+ }
+ };
+
+ _daemon.OnPaired += delegate(Allow2Credentials creds)
+ {
+ if (OnPairedEvent != null) OnPairedEvent.Invoke();
+ };
+
+ _daemon.OnChildSelectRequired += delegate(Allow2Child[] children)
+ {
+ if (OnChildSelectRequiredEvent != null) OnChildSelectRequiredEvent.Invoke();
+ };
+
+ _daemon.OnChildSelected += delegate(int childId, string name)
+ {
+ if (OnChildSelectedEvent != null) OnChildSelectedEvent.Invoke(childId, name);
+ };
+
+ _daemon.OnSoftLock += delegate(string reason)
+ {
+ if (AutoPauseOnLock)
+ {
+ Time.timeScale = 0f;
+ }
+ if (OnSoftLockEvent != null) OnSoftLockEvent.Invoke(reason);
+ };
+
+ _daemon.OnHardLock += delegate(string reason)
+ {
+ if (OnHardLockEvent != null) OnHardLockEvent.Invoke(reason);
+ };
+
+ _daemon.OnUnlock += delegate(string reason)
+ {
+ if (AutoPauseOnLock)
+ {
+ Time.timeScale = 1f;
+ }
+ if (OnUnlockEvent != null) OnUnlockEvent.Invoke(reason);
+ };
+
+ _daemon.OnWarning += delegate(Allow2WarningEventArgs args)
+ {
+ if (OnWarningEvent != null)
+ {
+ OnWarningEvent.Invoke(args.Level.ToString(), args.ActivityId, args.RemainingSeconds);
+ }
+ };
+
+ _daemon.OnCheckResult += delegate(Allow2CheckResult result)
+ {
+ if (OnCheckResultEvent != null) OnCheckResultEvent.Invoke();
+ };
+
+ _daemon.OnStateChanged += delegate(Allow2State newState)
+ {
+ if (OnStateChangedEvent != null) OnStateChangedEvent.Invoke((int)newState);
+ };
+
+ _daemon.OnUnpaired += delegate()
+ {
+ StopAllLoops();
+ if (OnUnpairedEvent != null) OnUnpairedEvent.Invoke();
+ };
+
+ _daemon.OnParentMode += delegate()
+ {
+ if (OnParentModeEvent != null) OnParentModeEvent.Invoke();
+ };
+
+ _daemon.OnSessionTimeout += delegate()
+ {
+ StopCheckLoop();
+ if (OnSessionTimeoutEvent != null) OnSessionTimeoutEvent.Invoke();
+ };
+
+ _daemon.OnPairingError += delegate(string error)
+ {
+ if (OnErrorEvent != null) OnErrorEvent.Invoke(error);
+ };
+ }
+ }
+}
diff --git a/com.allow2.sdk/Runtime/Core/Allow2Api.cs b/com.allow2.sdk/Runtime/Core/Allow2Api.cs
new file mode 100644
index 0000000..7cca88f
--- /dev/null
+++ b/com.allow2.sdk/Runtime/Core/Allow2Api.cs
@@ -0,0 +1,659 @@
+// Allow2 Unity SDK v2
+// Copyright (c) 2026 Allow2 Pty Ltd. All rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace Allow2
+{
+ ///
+ /// UnityWebRequest-based API client for the Allow2 REST API.
+ /// All methods return data via callbacks (coroutine-compatible).
+ ///
+ public class Allow2Api
+ {
+ public string BaseUrl { get; private set; }
+ public int Vid { get; private set; }
+ public string Token { get; private set; }
+ public int TimeoutSeconds { get; set; }
+
+ public Allow2Api(string baseUrl, int vid, string token)
+ {
+ BaseUrl = string.IsNullOrEmpty(baseUrl) ? "https://api.allow2.com" : baseUrl;
+ Vid = vid;
+ Token = token;
+ TimeoutSeconds = 15;
+ }
+
+ // ----------------------------------------------------------------
+ // Pairing
+ // ----------------------------------------------------------------
+
+ ///
+ /// Initiate PIN-code pairing. Returns session info via callback.
+ ///
+ public IEnumerator InitPINPairing(string uuid, string deviceName, string platform,
+ Action callback)
+ {
+ Dictionary body = new Dictionary();
+ body["uuid"] = uuid;
+ body["name"] = deviceName;
+ body["deviceToken"] = Token;
+ body["vid"] = Vid;
+ body["platform"] = string.IsNullOrEmpty(platform) ? "unity" : platform;
+
+ yield return PostJson("/api/pair/pin/init", body, callback);
+ }
+
+ ///
+ /// Poll pairing status.
+ ///
+ public IEnumerator CheckPairingStatus(string sessionId, Action callback)
+ {
+ string url = BaseUrl + "/api/pair/status/" + sessionId;
+ yield return GetJson(url, null, callback);
+ }
+
+ // ----------------------------------------------------------------
+ // Check
+ // ----------------------------------------------------------------
+
+ ///
+ /// Check permissions for a child + activities.
+ ///
+ public IEnumerator Check(int userId, int pairId, string pairToken,
+ int childId, Dictionary activities, string tz, bool log,
+ Action callback)
+ {
+ Dictionary body = new Dictionary();
+ body["userId"] = userId;
+ body["pairId"] = pairId;
+ body["pairToken"] = pairToken;
+ body["deviceToken"] = Token;
+ body["tz"] = tz;
+ body["childId"] = childId;
+ body["log"] = log;
+
+ // Activities as object keyed by id
+ Dictionary actObj = new Dictionary();
+ foreach (KeyValuePair kvp in activities)
+ {
+ actObj[kvp.Key.ToString()] = kvp.Value;
+ }
+ body["activities"] = actObj;
+
+ yield return PostJson("/serviceapi/check", body, callback);
+ }
+
+ // ----------------------------------------------------------------
+ // Updates
+ // ----------------------------------------------------------------
+
+ ///
+ /// Poll for updates (extensions, day type changes, etc.).
+ ///
+ public IEnumerator GetUpdates(int userId, int pairId, string pairToken,
+ long timestampMillis, Action callback)
+ {
+ StringBuilder sb = new StringBuilder(BaseUrl);
+ sb.Append("/api/getUpdates?userId=").Append(userId);
+ sb.Append("&pairId=").Append(pairId);
+ sb.Append("&pairToken=").Append(UnityWebRequest.EscapeURL(pairToken));
+ sb.Append("&deviceToken=").Append(UnityWebRequest.EscapeURL(Token));
+ if (timestampMillis > 0)
+ {
+ sb.Append("×tampMillis=").Append(timestampMillis);
+ }
+
+ yield return GetJson(sb.ToString(), null, callback);
+ }
+
+ // ----------------------------------------------------------------
+ // Requests
+ // ----------------------------------------------------------------
+
+ ///
+ /// Create a "Request More Time" request.
+ ///
+ public IEnumerator CreateRequest(int userId, int pairId, string pairToken,
+ int childId, int duration, int activityId, string message,
+ Action callback)
+ {
+ Dictionary body = new Dictionary();
+ body["userId"] = userId;
+ body["pairId"] = pairId;
+ body["pairToken"] = pairToken;
+ body["childId"] = childId;
+ body["duration"] = duration;
+ body["activity"] = activityId;
+ if (!string.IsNullOrEmpty(message))
+ {
+ body["message"] = message;
+ }
+
+ yield return PostJson("/api/request/createRequest", body, callback);
+ }
+
+ ///
+ /// Poll request approval status.
+ ///
+ public IEnumerator GetRequestStatus(string requestId, string statusSecret,
+ Action callback)
+ {
+ string url = BaseUrl + "/api/request/" + requestId + "/status";
+ Dictionary headers = new Dictionary();
+ headers["X-Status-Secret"] = statusSecret;
+
+ yield return GetJson(url, headers, callback);
+ }
+
+ // ----------------------------------------------------------------
+ // Feedback
+ // ----------------------------------------------------------------
+
+ ///
+ /// Submit feedback to the Allow2 server.
+ ///
+ public IEnumerator SubmitFeedback(int userId, int pairId, string pairToken,
+ int childId, string category, string message,
+ Dictionary deviceContext, Action callback)
+ {
+ Dictionary body = new Dictionary();
+ body["userId"] = userId;
+ body["pairId"] = pairId;
+ body["pairToken"] = pairToken;
+ body["childId"] = childId;
+ body["vid"] = Vid;
+ body["category"] = category;
+ body["message"] = message;
+ if (deviceContext != null)
+ {
+ body["deviceContext"] = deviceContext;
+ }
+
+ yield return PostJson("/api/feedback/submit", body, callback);
+ }
+
+ ///
+ /// Load feedback discussions for this device.
+ ///
+ public IEnumerator LoadFeedback(int userId, int pairId, string pairToken,
+ Action callback)
+ {
+ Dictionary body = new Dictionary();
+ body["userId"] = userId;
+ body["pairId"] = pairId;
+ body["pairToken"] = pairToken;
+
+ yield return PostJson("/api/feedback/load", body, callback);
+ }
+
+ ///
+ /// Reply to an existing feedback discussion.
+ ///
+ public IEnumerator FeedbackReply(int userId, int pairId, string pairToken,
+ string discussionId, string message, Action callback)
+ {
+ Dictionary body = new Dictionary();
+ body["userId"] = userId;
+ body["pairId"] = pairId;
+ body["pairToken"] = pairToken;
+ body["discussionId"] = discussionId;
+ body["message"] = message;
+
+ yield return PostJson("/api/feedback/reply", body, callback);
+ }
+
+ // ----------------------------------------------------------------
+ // Internal HTTP helpers
+ // ----------------------------------------------------------------
+
+ private IEnumerator PostJson(string path, Dictionary body,
+ Action callback)
+ {
+ string url = path.StartsWith("http") ? path : BaseUrl + path;
+ string jsonBody = MiniJson.Serialize(body);
+ byte[] bodyBytes = Encoding.UTF8.GetBytes(jsonBody);
+
+ using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
+ {
+ request.uploadHandler = new UploadHandlerRaw(bodyBytes);
+ request.downloadHandler = new DownloadHandlerBuffer();
+ request.SetRequestHeader("Content-Type", "application/json");
+ request.timeout = TimeoutSeconds;
+
+ yield return request.SendWebRequest();
+
+ Allow2ApiResponse response = ParseResponse(request);
+ if (callback != null)
+ {
+ callback(response);
+ }
+ }
+ }
+
+ private IEnumerator GetJson(string url, Dictionary headers,
+ Action callback)
+ {
+ using (UnityWebRequest request = UnityWebRequest.Get(url))
+ {
+ request.SetRequestHeader("Content-Type", "application/json");
+ request.timeout = TimeoutSeconds;
+
+ if (headers != null)
+ {
+ foreach (KeyValuePair kvp in headers)
+ {
+ request.SetRequestHeader(kvp.Key, kvp.Value);
+ }
+ }
+
+ yield return request.SendWebRequest();
+
+ Allow2ApiResponse response = ParseResponse(request);
+ if (callback != null)
+ {
+ callback(response);
+ }
+ }
+ }
+
+ private Allow2ApiResponse ParseResponse(UnityWebRequest request)
+ {
+ Allow2ApiResponse response = new Allow2ApiResponse();
+ response.StatusCode = (int)request.responseCode;
+
+#if UNITY_2020_1_OR_NEWER
+ bool isError = request.result != UnityWebRequest.Result.Success;
+#else
+ bool isError = request.isNetworkError || request.isHttpError;
+#endif
+
+ if (isError && request.responseCode == 0)
+ {
+ // Network error (no response at all)
+ response.IsNetworkError = true;
+ response.ErrorMessage = request.error;
+ return response;
+ }
+
+ string text = request.downloadHandler != null ? request.downloadHandler.text : "";
+ if (!string.IsNullOrEmpty(text))
+ {
+ try
+ {
+ response.Body = MiniJson.Deserialize(text) as Dictionary;
+ }
+ catch (Exception)
+ {
+ response.Body = null;
+ }
+ }
+
+ if (isError)
+ {
+ response.IsHttpError = true;
+ response.ErrorMessage = request.error;
+ if (response.Body != null && response.Body.ContainsKey("message"))
+ {
+ response.ErrorMessage = response.Body["message"].ToString();
+ }
+ }
+
+ return response;
+ }
+ }
+
+ ///
+ /// API response wrapper.
+ ///
+ public class Allow2ApiResponse
+ {
+ public int StatusCode;
+ public bool IsNetworkError;
+ public bool IsHttpError;
+ public string ErrorMessage;
+ public Dictionary Body;
+
+ public bool IsSuccess
+ {
+ get { return !IsNetworkError && !IsHttpError && StatusCode >= 200 && StatusCode < 300; }
+ }
+
+ ///
+ /// Get a string value from the response body.
+ ///
+ public string GetString(string key)
+ {
+ if (Body != null && Body.ContainsKey(key))
+ {
+ object val = Body[key];
+ return val != null ? val.ToString() : null;
+ }
+ return null;
+ }
+
+ ///
+ /// Get an int value from the response body.
+ ///
+ public int GetInt(string key, int defaultValue = 0)
+ {
+ if (Body != null && Body.ContainsKey(key))
+ {
+ object val = Body[key];
+ if (val is long) return (int)(long)val;
+ if (val is double) return (int)(double)val;
+ if (val is int) return (int)val;
+ int parsed;
+ if (val != null && int.TryParse(val.ToString(), out parsed))
+ {
+ return parsed;
+ }
+ }
+ return defaultValue;
+ }
+
+ ///
+ /// Get a long value from the response body.
+ ///
+ public long GetLong(string key, long defaultValue = 0)
+ {
+ if (Body != null && Body.ContainsKey(key))
+ {
+ object val = Body[key];
+ if (val is long) return (long)val;
+ if (val is double) return (long)(double)val;
+ long parsed;
+ if (val != null && long.TryParse(val.ToString(), out parsed))
+ {
+ return parsed;
+ }
+ }
+ return defaultValue;
+ }
+
+ ///
+ /// Get a bool value from the response body.
+ ///
+ public bool GetBool(string key, bool defaultValue = false)
+ {
+ if (Body != null && Body.ContainsKey(key))
+ {
+ object val = Body[key];
+ if (val is bool) return (bool)val;
+ }
+ return defaultValue;
+ }
+ }
+
+ // ----------------------------------------------------------------
+ // Minimal JSON serializer/deserializer
+ // Unity's JsonUtility doesn't handle Dictionary.
+ // This is a lightweight alternative.
+ // ----------------------------------------------------------------
+
+ internal static class MiniJson
+ {
+ public static string Serialize(object obj)
+ {
+ if (obj == null) return "null";
+
+ if (obj is string)
+ {
+ return "\"" + EscapeString((string)obj) + "\"";
+ }
+
+ if (obj is bool)
+ {
+ return (bool)obj ? "true" : "false";
+ }
+
+ if (obj is int || obj is long || obj is float || obj is double)
+ {
+ return obj.ToString();
+ }
+
+ if (obj is Dictionary)
+ {
+ Dictionary dict = (Dictionary)obj;
+ StringBuilder sb = new StringBuilder("{");
+ bool first = true;
+ foreach (KeyValuePair kvp in dict)
+ {
+ if (!first) sb.Append(",");
+ first = false;
+ sb.Append("\"").Append(EscapeString(kvp.Key)).Append("\":");
+ sb.Append(Serialize(kvp.Value));
+ }
+ sb.Append("}");
+ return sb.ToString();
+ }
+
+ if (obj is Dictionary)
+ {
+ Dictionary dict = (Dictionary)obj;
+ StringBuilder sb = new StringBuilder("{");
+ bool first = true;
+ foreach (KeyValuePair kvp in dict)
+ {
+ if (!first) sb.Append(",");
+ first = false;
+ sb.Append("\"").Append(EscapeString(kvp.Key)).Append("\":");
+ sb.Append("\"").Append(EscapeString(kvp.Value)).Append("\"");
+ }
+ sb.Append("}");
+ return sb.ToString();
+ }
+
+ if (obj is IList