diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ee9a439 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: defold +patreon: Defold +custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NBNBHTUW4GS4C'] diff --git a/.github/workflows/trigger-site-rebuild.yml b/.github/workflows/trigger-site-rebuild.yml new file mode 100644 index 0000000..ec697cc --- /dev/null +++ b/.github/workflows/trigger-site-rebuild.yml @@ -0,0 +1,19 @@ +name: Trigger site rebuild + +on: [push] + +jobs: + site-rebuild: + runs-on: ubuntu-latest + + steps: [ + { + name: 'Repository dispatch', + uses: defold/repository-dispatch@1.2.1, + with: { + repo: 'defold/defold.github.io', + token: '${{ secrets.SERVICES_GITHUB_TOKEN }}', + user: 'services@defold.se', + action: 'extension-siwa' + } + }] diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..a32d29f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.internal +/build +.externalToolBuilders +.DS_Store +Thumbs.db +.lock-wscript +*.pyc +.project +.cproject +builtins \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2126b2e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Defold + +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. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..0852cbe --- /dev/null +++ b/docs/index.md @@ -0,0 +1,66 @@ +--- +title: Sign in with Apple extension for Defold +brief: This manual covers how to setup and use Sign in with Apple in Defold. +--- + +# Sign in with Apple extension for Defold + +This extension provides functions to use [Sign in with Apple](https://developer.apple.com/sign-in-with-apple/) to allow users to set up an account and sign in to your game with their Apple ID. + +## Installation + +To use this library in your Defold project, add the following URL to your `game.project` dependencies: + +https://github.com/defold/extension-siwa/archive/master.zip + +We recommend using a link to a zip file of a [specific release](https://github.com/defold/extension-siwa/releases). + + +## Setting up your app for Sign in with Apple + +To get started you need to enable your app’s App ID with the Sign in with Apple capability. [Follow the official Apple developer instructions](https://help.apple.com/developer-account/?lang=en#/devde676e696) to get started. + + +## Usage + +### Check Sign in with Apple support + +```Lua +if siwa.is_supported() then + print("Sign in with Apple is supported") +end) +``` + +### Trigger Sign in with Apple + +```Lua +siwa.authenticate(id, function(self, data) + print(data.identity_token) + print(data.user_id) + print(data.first_name, data.family_name) + print(data.email) + if data.user_status == siwa.STATUS_LIKELY_REAL then + print("Likely a real person") + end +end) +``` + +### Check credential state + +```Lua +siwa.get_credential_state(id, function(self, data) + if data.credential_state == siwa.STATE_AUTHORIZED then + print("User has still authorized the application", data.user_id) + elseif data.credential_state == siwa.STATE_REVOKED then + print("User has revoked authorization for the application", data.user_id) + end +end) +``` + + +## Source code + +The source code is available on [GitHub](https://github.com/defold/extension-siwa) + + +## API reference diff --git a/game.project b/game.project new file mode 100755 index 0000000..fe0d1d3 --- /dev/null +++ b/game.project @@ -0,0 +1,20 @@ +[project] +title = extension-siwa +dependencies = https://github.com/andsve/dirtylarry/archive/master.zip + +[script] +shared_state = 1 + +[library] +include_dirs = siwa + +[bootstrap] +main_collection = /main/main.collectionc + +[display] +width = 640 +height = 1136 + +[ios] +bundle_identifier = com.defold.extension.siwa + diff --git a/input/game.input_binding b/input/game.input_binding new file mode 100755 index 0000000..8ed1d4e --- /dev/null +++ b/input/game.input_binding @@ -0,0 +1,4 @@ +mouse_trigger { + input: MOUSE_BUTTON_1 + action: "touch" +} diff --git a/main/main.collection b/main/main.collection new file mode 100755 index 0000000..6c66d44 --- /dev/null +++ b/main/main.collection @@ -0,0 +1,37 @@ +name: "default" +scale_along_z: 0 +embedded_instances { + id: "main_go" + data: "components {\n" + " id: \"main\"\n" + " component: \"/main/main.gui\"\n" + " position {\n" + " x: 0.0\n" + " y: 0.0\n" + " z: 0.0\n" + " }\n" + " rotation {\n" + " x: 0.0\n" + " y: 0.0\n" + " z: 0.0\n" + " w: 1.0\n" + " }\n" + "}\n" + "" + position { + x: 0.0 + y: 0.0 + z: 0.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale3 { + x: 1.0 + y: 1.0 + z: 1.0 + } +} diff --git a/main/main.gui b/main/main.gui new file mode 100755 index 0000000..6aa1014 --- /dev/null +++ b/main/main.gui @@ -0,0 +1,551 @@ +script: "/main/main.gui_script" +fonts { + name: "system_font" + font: "/builtins/fonts/system_font.font" +} +background_color { + x: 0.0 + y: 0.0 + z: 0.0 + w: 0.0 +} +nodes { + position { + x: 114.0 + y: 418.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.5 + y: 0.5 + z: 1.0 + w: 1.0 + } + size { + x: 200.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEMPLATE + id: "login" + layer: "" + inherit_alpha: true + alpha: 1.0 + template: "/dirtylarry/button.gui" + template_node_child: false +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 300.0 + y: 88.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "button/button_normal" + id: "login/larrybutton" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "login" + layer: "" + inherit_alpha: true + slice9 { + x: 32.0 + y: 32.0 + z: 32.0 + w: 32.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: true + size_mode: SIZE_MODE_MANUAL +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 200.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Login" + font: "larryfont" + id: "login/larrylabel" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "login/larrybutton" + layer: "" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 1.0 + overridden_fields: 8 + template_node_child: true + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 114.0 + y: 333.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.5 + y: 0.5 + z: 1.0 + w: 1.0 + } + size { + x: 200.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEMPLATE + id: "check" + layer: "" + inherit_alpha: true + alpha: 1.0 + template: "/dirtylarry/button.gui" + template_node_child: false +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 300.0 + y: 88.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "button/button_normal" + id: "check/larrybutton" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "check" + layer: "" + inherit_alpha: true + slice9 { + x: 32.0 + y: 32.0 + z: 32.0 + w: 32.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: true + size_mode: SIZE_MODE_MANUAL +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 200.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Check" + font: "larryfont" + id: "check/larrylabel" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "check/larrybutton" + layer: "" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 1.0 + overridden_fields: 8 + template_node_child: true + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 114.0 + y: 255.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 0.5 + y: 0.5 + z: 1.0 + w: 1.0 + } + size { + x: 200.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEMPLATE + id: "check_fail" + layer: "" + inherit_alpha: true + alpha: 1.0 + template: "/dirtylarry/button.gui" + template_node_child: false +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 300.0 + y: 88.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_BOX + blend_mode: BLEND_MODE_ALPHA + texture: "button/button_normal" + id: "check_fail/larrybutton" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + adjust_mode: ADJUST_MODE_FIT + parent: "check_fail" + layer: "" + inherit_alpha: true + slice9 { + x: 32.0 + y: 32.0 + z: 32.0 + w: 32.0 + } + clipping_mode: CLIPPING_MODE_NONE + clipping_visible: true + clipping_inverted: false + alpha: 1.0 + template_node_child: true + size_mode: SIZE_MODE_MANUAL +} +nodes { + position { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 200.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Check\n" + "Fail" + font: "larryfont" + id: "check_fail/larrylabel" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_CENTER + outline { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: false + parent: "check_fail/larrybutton" + layer: "" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 1.0 + overridden_fields: 8 + template_node_child: true + text_leading: 1.0 + text_tracking: 0.0 +} +nodes { + position { + x: 20.0 + y: 899.0 + z: 0.0 + w: 1.0 + } + rotation { + x: 0.0 + y: 0.0 + z: 0.0 + w: 1.0 + } + scale { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + size { + x: 600.0 + y: 100.0 + z: 0.0 + w: 1.0 + } + color { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + type: TYPE_TEXT + blend_mode: BLEND_MODE_ALPHA + text: "Hello World, I\'m SIWA" + font: "system_font" + id: "test_text" + xanchor: XANCHOR_NONE + yanchor: YANCHOR_NONE + pivot: PIVOT_NW + outline { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + shadow { + x: 1.0 + y: 1.0 + z: 1.0 + w: 1.0 + } + adjust_mode: ADJUST_MODE_FIT + line_break: true + layer: "" + inherit_alpha: true + alpha: 1.0 + outline_alpha: 1.0 + shadow_alpha: 1.0 + template_node_child: false + text_leading: 1.0 + text_tracking: 0.0 +} +material: "/builtins/materials/gui.material" +adjust_reference: ADJUST_REFERENCE_PARENT +max_nodes: 512 diff --git a/main/main.gui_script b/main/main.gui_script new file mode 100755 index 0000000..6480b6b --- /dev/null +++ b/main/main.gui_script @@ -0,0 +1,64 @@ +local dl = require("dirtylarry.dirtylarry") + +local lines = {} +local function log(msg, ...) + msg = msg:format(...) + print(msg) + + table.insert(lines, 1, msg) + table.remove(lines, 10) + gui.set_text(gui.get_node("test_text"), table.concat(lines, "\n")) +end + +local function get_credential_state(self, id) + log("get_credential_state %s", id) + siwa.get_credential_state(id, function(self, data) + for k,v in pairs(data) do + log("%s: %s", k, tostring(v)) + end + end) +end + +local function authenticate(self) + log("authenticate") + siwa.authenticate(function(self, data) + self.user_id = data.user_id + for k,v in pairs(data) do + log("%s: %s", k, tostring(v)) + end + end) +end + +local function is_siwa_supported() + return siwa and siwa.is_supported() +end + +function init(self) + msg.post(".", "acquire_input_focus") + + if is_siwa_supported() then + log("SIWA supported!") + else + log("SIWA not supported...") + end +end + +function on_input(self, action_id, action) + if is_siwa_supported() then + dl:button("check", action_id, action, function() + if self.user_id then + get_credential_state(self, self.user_id) + else + log("No user id. Login first") + end + end) + + dl:button("check_fail", action_id, action, function() + check_credentials_status(self, "foobar") + end) + + dl:button("login", action_id, action, function() + authenticate(self) + end) + end +end diff --git a/siwa/api/siwa.script_api b/siwa/api/siwa.script_api new file mode 100644 index 0000000..7f10acd --- /dev/null +++ b/siwa/api/siwa.script_api @@ -0,0 +1,115 @@ +- name: siwa + type: table + desc: Functions and constants for interacting Sign in with Apple. + [icon:ios] + members: + +#***************************************************************************************************** + + - name: is_supported + type: function + desc: Check if Sign in with Apple is available (iOS 13+). + + +#***************************************************************************************************** + + - name: get_credential_state + type: function + desc: Get the credential state of a user. + + parameters: + - name: user_id + type: string + desc: User id to get credential state for. + - name: callback + type: function + desc: Credential state callback function. + parameters: + - name: self + type: object + desc: The current object. + + - name: state + type: table + desc: The credential state (user_id, credential_state) + + examples: + - desc: |- + ```lua + siwa.get_credential_state(id, function(self, data) + if data.credential_state == siwa.STATE_AUTHORIZED then + print("User has still authorized the application", data.user_id) + elseif data.credential_state == siwa.STATE_REVOKED then + print("User has revoked authorization for the application", data.user_id) + end + end) + ``` + +#***************************************************************************************************** + + - name: authenticate + type: function + desc: Show the Sign in with Apple UI + + parameters: + - name: callback + type: function + desc: Authentication callback function. + parameters: + - name: self + type: object + desc: The current object. + + - name: state + type: table + desc: The authentication result data (user_id, identity_token, email, first_name, family_name, status, result) + + examples: + - desc: |- + ```lua + siwa.authenticate(function(self, data) + print(data.identity_token) + print(data.user_id) + print(data.first_name, data.family_name) + print(data.email) + if data.user_status == siwa.STATUS_LIKELY_REAL then + print("Likely a real person") + end + end) + ``` + +#***************************************************************************************************** + + - name: STATE_NOT_FOUND + type: number + desc: The user can’t be found. + + + - name: STATE_UNKNOWN + type: number + desc: Unknown credential state. + + + - name: STATE_AUTHORIZED + type: number + desc: The user is authorized. + + + - name: STATE_REVOKED + type: number + desc: Authorization for the given user has been revoked. + + + - name: STATUS_UNKNOWN + type: number + desc: The system hasn’t determined whether the user might be a real person. + + + - name: STATUS_UNSUPPORTED + type: number + desc: The system can’t determine this user’s status as a real person. + + + - name: STATUS_LIKELY_REAL + type: number + desc: The user appears to be a real person. diff --git a/siwa/ext.manifest b/siwa/ext.manifest new file mode 100755 index 0000000..a84eef5 --- /dev/null +++ b/siwa/ext.manifest @@ -0,0 +1,30 @@ +name: siwa + +platforms: + x86-osx: + context: + frameworks: [] + flags: ["-std=c++11", "-stdlib=libc++"] + linkFlags: ["-stdlib=libc++"] + libs: ["c++"] + + x86_64-osx: + context: + frameworks: [] + flags: ["-std=c++11", "-stdlib=libc++"] + libs: ["c++"] + + x86_64-ios: + context: + frameworks: ['AuthenticationServices'] + defines: ['SUPPORTS_SIWA'] + + armv7-ios: + context: + frameworks: ['AuthenticationServices'] + defines: ['SUPPORTS_SIWA'] + + arm64-ios: + context: + frameworks: ['AuthenticationServices'] + defines: ['SUPPORTS_SIWA'] \ No newline at end of file diff --git a/siwa/src/siwa.cpp b/siwa/src/siwa.cpp new file mode 100755 index 0000000..882da87 --- /dev/null +++ b/siwa/src/siwa.cpp @@ -0,0 +1,292 @@ +#if defined(DM_PLATFORM_IOS) + +#include "siwa.h" +#include + +#define MODULE_NAME "siwa" + +SiwaData g_SiwaData; +SiwaCallbackData g_SiwaCallbackData; + +char* Siwa_GetUserId() +{ + return g_SiwaData.m_userID; +} + +static void Siwa_ResetCallbackData() +{ + free(g_SiwaCallbackData.m_userID); + g_SiwaCallbackData.m_userID = 0; + free(g_SiwaCallbackData.m_identityToken); + g_SiwaCallbackData.m_identityToken = 0; + free(g_SiwaCallbackData.m_userID); + g_SiwaCallbackData.m_userID = 0; + free(g_SiwaCallbackData.m_email); + g_SiwaCallbackData.m_email = 0; + free(g_SiwaCallbackData.m_firstName); + g_SiwaCallbackData.m_firstName = 0; + free(g_SiwaCallbackData.m_familyName); + g_SiwaCallbackData.m_familyName = 0; + free(g_SiwaCallbackData.m_identityToken); + g_SiwaCallbackData.m_identityToken = 0; + free(g_SiwaCallbackData.m_message); + g_SiwaCallbackData.m_message = 0; + + g_SiwaCallbackData.m_userStatus = STATUS_UNSUPPORTED; + g_SiwaCallbackData.m_state = STATE_UNKNOWN; + g_SiwaCallbackData.m_cmd = CMD_NONE; +} + +void Siwa_QueueCredentialCallback(const char* userID, const SiwaCredentialState state) +{ + if(g_SiwaCallbackData.m_cmd != CMD_NONE) { + dmLogError("Can't queue credential callback, already have a callback queued!"); + return; + } + + g_SiwaCallbackData.m_cmd = CMD_CREDENTIAL; + g_SiwaCallbackData.m_userID = strdup(userID); + g_SiwaCallbackData.m_state = state; +} + + +void Siwa_QueueAuthSuccessCallback(const char* identityToken, const char* userID, const char* email, const char* firstName, const char* familyName, const SiwaUserDetectionStatus userStatus) +{ + if(g_SiwaCallbackData.m_cmd != CMD_NONE) { + dmLogError("Can't queue auth success callback, already have a callback queued!"); + return; + } + + g_SiwaCallbackData.m_cmd = CMD_AUTH_SUCCESS; + g_SiwaCallbackData.m_identityToken = strdup(identityToken); + g_SiwaCallbackData.m_userID = strdup(userID); + g_SiwaCallbackData.m_email = strdup(email != 0 ? email: ""); + g_SiwaCallbackData.m_firstName = strdup(firstName != 0 ? firstName: ""); + g_SiwaCallbackData.m_familyName = strdup(familyName != 0 ? familyName : ""); + g_SiwaCallbackData.m_userStatus = userStatus; + g_SiwaCallbackData.m_message = strdup(""); +} + +void Siwa_QueueAuthFailureCallback(const char* message) +{ + if(g_SiwaCallbackData.m_cmd != CMD_NONE) { + dmLogError("Can't queue auth error callback, already have a callback queued!"); + return; + } + + g_SiwaCallbackData.m_cmd = CMD_AUTH_FAILED; + g_SiwaCallbackData.m_message = strdup(message); +} + + +static void Siwa_TriggerCallback() +{ + lua_State* L = dmScript::GetCallbackLuaContext(g_SiwaData.m_callback); + DM_LUA_STACK_CHECK(L, 0); + + if (dmScript::SetupCallback(g_SiwaData.m_callback)) + { + lua_createtable(L, 0, 3); + + if (g_SiwaCallbackData.m_cmd == CMD_CREDENTIAL) + { + lua_pushstring(L, "result"); + lua_pushstring(L, "SUCCESS"); + lua_settable(L, -3); + + lua_pushstring(L, "user_id"); + lua_pushstring(L, g_SiwaCallbackData.m_userID); + lua_settable(L, -3); + + lua_pushstring(L, "credential_state"); + lua_pushnumber(L, g_SiwaCallbackData.m_state); + lua_settable(L, -3); + } + else if (g_SiwaCallbackData.m_cmd == CMD_AUTH_SUCCESS) + { + lua_pushstring(L, "result"); + lua_pushstring(L, "SUCCESS"); + lua_settable(L, -3); + + lua_pushstring(L, "identity_token"); + lua_pushstring(L, g_SiwaCallbackData.m_identityToken); + lua_settable(L, -3); + + lua_pushstring(L, "user_id"); + lua_pushstring(L, g_SiwaCallbackData.m_userID); + lua_settable(L, -3); + + lua_pushstring(L, "email"); + lua_pushstring(L, g_SiwaCallbackData.m_email); + lua_settable(L, -3); + + lua_pushstring(L, "first_name"); + lua_pushstring(L, g_SiwaCallbackData.m_firstName); + lua_settable(L, -3); + + lua_pushstring(L, "family_name"); + lua_pushstring(L, g_SiwaCallbackData.m_familyName); + lua_settable(L, -3); + + lua_pushstring(L, "user_status"); + lua_pushnumber(L, g_SiwaCallbackData.m_userStatus); + lua_settable(L, -3); + } + else if (g_SiwaCallbackData.m_cmd == CMD_AUTH_FAILED) + { + lua_pushstring(L, "result"); + lua_pushstring(L, "ERROR"); + lua_settable(L, -3); + + lua_pushstring(L, "message"); + lua_pushstring(L, g_SiwaCallbackData.m_message); + lua_settable(L, -3); + } + + if (lua_pcall(L, 2, 0, 0) != 0) + { + dmLogError("Error running siwa callback: %s", lua_tostring(L, -1)); + lua_pop(L, 1); + } + dmScript::TeardownCallback(g_SiwaData.m_callback); + } +} + + +static void Siwa_SetupCallback(lua_State* L, int index) +{ + if (g_SiwaData.m_callback) { + dmScript::DestroyCallback(g_SiwaData.m_callback); + } + g_SiwaData.m_callback = dmScript::CreateCallback(L, index); +} + +static void Siwa_CleanupCallback() { + if (g_SiwaData.m_callback) { + dmScript::DestroyCallback(g_SiwaData.m_callback); + g_SiwaData.m_callback = 0; + } +} + +static int Siwa_GetCredentialState(lua_State* L){ + DM_LUA_STACK_CHECK(L, 1); + + if (!Siwa_PlatformIsSupported()) { + dmLogWarning("Sign in with Apple is not available"); + lua_pushboolean(L, 0); + return 1; + } + + if(g_SiwaData.m_callback != 0) + { + dmLogError("Callback already in progress"); + lua_pushboolean(L, 0); + return 1; + } + + luaL_checktype(L, 1, LUA_TSTRING); + if (g_SiwaData.m_userID) free(g_SiwaData.m_userID); + g_SiwaData.m_userID = strdup(lua_tostring(L, 1)); + + luaL_checktype(L, 2, LUA_TFUNCTION); + Siwa_SetupCallback(L, 2); + Siwa_PlatformGetCredentialState(); + + lua_pushboolean(L, 1); + return 1; +} + +static int Siwa_AuthenticateWithApple(lua_State* L) { + DM_LUA_STACK_CHECK(L, 1); + + if (!Siwa_PlatformIsSupported()) { + dmLogWarning("Sign in with Apple is not available"); + lua_pushboolean(L, 0); + return 1; + } + + if(g_SiwaData.m_callback != 0) + { + dmLogError("Callback already in progress"); + lua_pushboolean(L, 0); + return 1; + } + + luaL_checktype(L, 1, LUA_TFUNCTION); + Siwa_SetupCallback(L, 1); + Siwa_PlatformAuthenticateWithApple(); + + lua_pushboolean(L, 1); + return 1; +} + +static int Siwa_IsSupported(lua_State* L) { + DM_LUA_STACK_CHECK(L, 1); + lua_pushboolean(L, Siwa_PlatformIsSupported()); + return 1; +} + +static dmExtension::Result SiwaAppInitialize(dmExtension::AppParams* params) +{ + Siwa_ResetCallbackData(); + return dmExtension::RESULT_OK; +} + +static dmExtension::Result SiwaAppFinalize(dmExtension::AppParams* params) +{ + return dmExtension::RESULT_OK; +} + +const luaL_reg lua_register[] = +{ + {"is_supported", Siwa_IsSupported}, + {"get_credential_state", Siwa_GetCredentialState}, + {"authenticate", Siwa_AuthenticateWithApple}, + {0, 0} +}; + +static dmExtension::Result SiwaInitialize(dmExtension::Params* params) +{ + lua_State* L = params->m_L; + int top = lua_gettop(L); + luaL_register(L, MODULE_NAME, lua_register); + + #define SETCONSTANT(name) \ + lua_pushnumber(L, (lua_Number) name); \ + lua_setfield(L, -2, #name);\ + + SETCONSTANT(STATE_NOT_FOUND) + SETCONSTANT(STATE_UNKNOWN) + SETCONSTANT(STATE_AUTHORIZED) + SETCONSTANT(STATE_REVOKED) + + SETCONSTANT(STATUS_UNKNOWN) + SETCONSTANT(STATUS_UNSUPPORTED) + SETCONSTANT(STATUS_LIKELY_REAL) + + #undef SETCONSTANT + + lua_pop(L, 1); + assert(top == lua_gettop(L)); + return dmExtension::RESULT_OK; +} + +static dmExtension::Result SiwaUpdate(dmExtension::Params* params) +{ + if(g_SiwaCallbackData.m_cmd != CMD_NONE) + { + Siwa_TriggerCallback(); + Siwa_ResetCallbackData(); + Siwa_CleanupCallback(); + } + return dmExtension::RESULT_OK; +} + +static dmExtension::Result SiwaFinalize(dmExtension::Params* params) +{ + return dmExtension::RESULT_OK; +} + +DM_DECLARE_EXTENSION(siwa, MODULE_NAME, SiwaAppInitialize, SiwaAppFinalize, SiwaInitialize, SiwaUpdate, 0, SiwaFinalize); + +#endif diff --git a/siwa/src/siwa.h b/siwa/src/siwa.h new file mode 100755 index 0000000..767c029 --- /dev/null +++ b/siwa/src/siwa.h @@ -0,0 +1,86 @@ +#pragma once +#if defined(DM_PLATFORM_IOS) + +#include + +enum SiwaCallbackCmd +{ + CMD_NONE = 0, + CMD_CREDENTIAL = 1, + CMD_AUTH_SUCCESS = 2, + CMD_AUTH_FAILED = 3 +}; + +// https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidprovidercredentialstate/asauthorizationappleidprovidercredentialauthorized?language=objc +enum SiwaCredentialState +{ + STATE_UNKNOWN = 0, + STATE_AUTHORIZED = 1, + STATE_REVOKED = 2, + STATE_NOT_FOUND = 3 +}; + +// https://developer.apple.com/documentation/authenticationservices/asuserdetectionstatus?language=objc +enum SiwaUserDetectionStatus +{ + STATUS_UNSUPPORTED = 0, + STATUS_LIKELY_REAL = 1, + STATUS_UNKNOWN = 2 +}; + +struct SiwaCallbackData +{ + SiwaCallbackData() + { + memset(this, 0, sizeof(*this)); + }; + + SiwaCallbackCmd m_cmd; + SiwaCredentialState m_state; + SiwaUserDetectionStatus m_userStatus; + + char* m_identityToken; + char* m_userID; + char* m_email; + char* m_firstName; + char* m_familyName; + + char* m_message; +}; + +struct SiwaData +{ + SiwaData() + { + memset(this, 0, sizeof(*this)); + }; + + // the user ID used for checking credential state + char* m_userID; + + dmScript::LuaCallbackInfo* m_callback; +}; + +char* Siwa_GetUserId(); + +// Queue the credential check callback to be triggered next update call in the main thread. +void Siwa_QueueCredentialCallback(const char* userID, const SiwaCredentialState state); +// Queue the sign in authorization callback to be triggered next update call in the main thread, when authorization succeeds. +void Siwa_QueueAuthSuccessCallback(const char* identityToken, const char* userID, const char* email, const char* firstName, const char* familyName, const SiwaUserDetectionStatus userStatus); +// Queue the sign in authorization callback to be triggered next update call in the main thread, when authorization fails. +void Siwa_QueueAuthFailureCallback(const char* message); + +// Trigged by a call from lua to check if sign in with apple is supported on this device. +bool Siwa_PlatformIsSupported(); + +// Triggered by a call from lua to start the sign in with apple flow +// expects the callback to be a reference number to the lua registry +// expects the context to be reference number to the lua registry +void Siwa_PlatformAuthenticateWithApple(); + +// Triggered by a call from lua to check if a provided apple id grants this app permission to use that id. +// expects the callback to be a reference number to the lua registry +// expects the context to be reference number to the lua registry +void Siwa_PlatformGetCredentialState(); + +#endif diff --git a/siwa/src/siwa_ios.mm b/siwa/src/siwa_ios.mm new file mode 100755 index 0000000..781b0c6 --- /dev/null +++ b/siwa/src/siwa_ios.mm @@ -0,0 +1,184 @@ +#if defined(DM_PLATFORM_IOS) + +#include "siwa.h" + +#include + +#include + +#include +#include + + +// The sign in with Apple flow expects us to have a delegate to which it can both pass data from the sign in flow +// but also how to figure out in which UI context it should display the native login UI. +// This class is that delegate. +// It also owns the provider that is both used for the sign in flow, as well as for credential state checking. +API_AVAILABLE(ios(13.0)) +@interface SiwaManager : NSObject +@property (nonatomic, strong) ASAuthorizationAppleIDProvider *m_idProvider; +@end + +@implementation SiwaManager + +- (instancetype) init +{ + self = [super init]; + if (self) + { + self.m_idProvider = [[ASAuthorizationAppleIDProvider alloc] init]; + } + return self; +} + +// Check if the user id provided to use from lua still grants our app permission +// to use it for sign in. User's can revoke app's permissions to use the id for sign in +// at any time, so we need to be able to monitor this. +// The possible results are revoked(0), authorized(1), and unknown(2). +// In practice, we have recieved unknown when revoking permission to this test app +// so we should treat both revoked and unknown as unauthorized. +- (void) getCredentialState +{ + char* userId = Siwa_GetUserId(); + NSString* user_id_string = [[NSString alloc] initWithUTF8String:userId]; + + [self.m_idProvider getCredentialStateForUserID: user_id_string + completion: ^(ASAuthorizationAppleIDProviderCredentialState credentialState, NSError* error) { + + // TODO: docs provide no information about what type of errors we can expect: + // https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidprovider/3175423-getcredentialstateforuserid?language=objc + if (error) { + NSString *errorMessage = [NSString stringWithFormat: @"getCredentialStateForUserID completed with error: %@", [error localizedDescription]]; + dmLogError([errorMessage UTF8String]); + } + + SiwaCredentialState state = STATE_UNKNOWN; + switch(credentialState) { + case ASAuthorizationAppleIDProviderCredentialAuthorized: + dmLogInfo("credential state: ASAuthorizationAppleIDProviderCredentialAuthorized"); + state = STATE_AUTHORIZED; + break; + case ASAuthorizationAppleIDProviderCredentialRevoked: + dmLogInfo("credential state: ASAuthorizationAppleIDProviderCredentialRevoked"); + state = STATE_REVOKED; + break; + case ASAuthorizationAppleIDProviderCredentialNotFound: + dmLogInfo("credential state: ASAuthorizationAppleIDProviderCredentialNotFound"); + state = STATE_NOT_FOUND; + break; + default: + dmLogInfo("credential state: unknown!!!"); + break; + } + + Siwa_QueueCredentialCallback(userId, state); + }]; +} + +// triggers the sign in with Apple native ui flow to begin. +- (void) loginWithUI +{ + ASAuthorizationAppleIDRequest* request = [self.m_idProvider createRequest]; + request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail]; + + ASAuthorizationController* authController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]]; + authController.presentationContextProvider = self; + authController.delegate = self; + + [authController performRequests]; +} + +// the Auth controller needs to specify where to display the native login. +// this is the function our SiwaManager delegate has to implement to provide that information. +- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller { + UIWindow *window = [UIApplication sharedApplication].keyWindow; + return window; +} + +// the Auth controller callback for getting a response back from apple for a sign in +- (void)authorizationController:(ASAuthorizationController *)controller +didCompleteWithAuthorization:(ASAuthorization *)authorization { + if ([authorization.credential class] == [ASAuthorizationAppleIDCredential class]) { + ASAuthorizationAppleIDCredential* appleIdCredential = ((ASAuthorizationAppleIDCredential*) authorization.credential); + + const char* appleUserId = [appleIdCredential.user UTF8String]; + const char* email = [appleIdCredential.email UTF8String]; + const char* givenName = [appleIdCredential.fullName.givenName UTF8String]; + const char* familyName = [appleIdCredential.fullName.familyName UTF8String]; + SiwaUserDetectionStatus userDetectionStatus = STATUS_UNSUPPORTED; + if (appleIdCredential.realUserStatus == ASUserDetectionStatusLikelyReal) + { + userDetectionStatus = STATUS_LIKELY_REAL; + } + else if (appleIdCredential.realUserStatus == ASUserDetectionStatusUnknown) + { + userDetectionStatus = STATUS_UNKNOWN; + } + + appleIdCredential.realUserStatus; + NSString* tokenString = [[NSString alloc] initWithData:appleIdCredential.identityToken encoding:NSUTF8StringEncoding]; + const char* identityToken = [tokenString UTF8String]; + + Siwa_QueueAuthSuccessCallback(identityToken, appleUserId, email, givenName, familyName, userDetectionStatus); + } + else + { + Siwa_QueueAuthFailureCallback("authorization failed!"); + } +} + +// The Auth controller callback for getting an error during authorization +- (void)authorizationController:(ASAuthorizationController *)controller +didCompleteWithError:(NSError *)error { + NSString *errorMessage = [NSString stringWithFormat: @"Authorization error: %@", [error localizedDescription]]; + Siwa_QueueAuthFailureCallback([errorMessage UTF8String]); +} + +@end + + +API_AVAILABLE(ios(13.0)) +static SiwaManager* g_SiwaManager = nil; + +API_AVAILABLE(ios(13.0)) +SiwaManager* GetSiwaManager() +{ + if(g_SiwaManager == nil) + { + g_SiwaManager = [[SiwaManager alloc] init]; + } + + return g_SiwaManager; +} + +API_AVAILABLE(ios(13.0)) +// Kicks off the request to get the credential state of a provided user id. +void Siwa_PlatformDoGetCredentialState() { + SiwaManager *siwaMan = GetSiwaManager(); + [siwaMan getCredentialState]; + return; +} + +API_AVAILABLE(ios(13.0)) +// Kicks off the sign in with apple flow. +void Siwa_PlatformDoAuthenticateWithApple() { + SiwaManager *siwaMan = GetSiwaManager(); + [siwaMan loginWithUI]; +} + +// Checks if Siwa is supported on this device by seeing if the main +// class involved in all the siwa requests we use exists. +bool Siwa_PlatformIsSupported() +{ + return ([ASAuthorizationAppleIDProvider class] != nil); +} + +void Siwa_PlatformGetCredentialState() { + Siwa_PlatformDoGetCredentialState(); +} + +void Siwa_PlatformAuthenticateWithApple() { + Siwa_PlatformDoAuthenticateWithApple(); +} + +#endif diff --git a/siwa/src/siwa_null.cpp b/siwa/src/siwa_null.cpp new file mode 100755 index 0000000..7e97865 --- /dev/null +++ b/siwa/src/siwa_null.cpp @@ -0,0 +1,6 @@ +#if !defined(DM_PLATFORM_IOS) +extern "C" void siwa() +{ + +} +#endif