diff --git a/README.md b/README.md index a23642c..923633b 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,9 @@ Hersteller-Aliase und übernimmt unterstützte Apps automatisch in Springboard und App Store. In App-Ressourcen müssen dafür keine Sky-Vorlagen abgelegt werden. Unterstützt werden die dokumentierten Basisverträge von LB Phone, 17Movement, High Phone, Quasar Smartphone V3 und -YSeries; die Aliase `lb-phone`, `17mov_Phone`, `high-phone`, `qs-smartphone` und `yseries` werden -direkt von `sky_phone` bereitgestellt. Einrichtung und ehrliche Kompatibilitätsgrenzen stehen in der +YSeries; die Resource- und Cfx-Export-Aliase `lb-phone`, `17mov_Phone`, `high-phone`, +`qs-smartphone` und `yseries` werden direkt von `sky_phone` bereitgestellt. Einrichtung und +ehrliche Kompatibilitätsgrenzen stehen in der [deutschen Custom-App-Anleitung](docs/custom-apps.md). ## FlipTok verification diff --git a/frontend/public/img/custom-app.svg b/frontend/public/img/custom-app.svg new file mode 100644 index 0000000..8b9ec86 --- /dev/null +++ b/frontend/public/img/custom-app.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/sky_phone/source/client/custom_app_compat.lua b/sky_phone/source/client/custom_app_compat.lua index b2a4ad8..0228d99 100644 --- a/sky_phone/source/client/custom_app_compat.lua +++ b/sky_phone/source/client/custom_app_compat.lua @@ -561,11 +561,6 @@ AddEventHandler("onClientResourceStop", function(resource_name) end end) -CreateThread(function() - TriggerServerEvent("sky_phone:compat:high:server:requestSnapshot") - TriggerEvent("17mov_Phone:Client:Ready") -end) - exports("AddApplication", add_17mov_application) exports("RemoveApplication", remove_17mov_application) exports("SendAppMessage", send_app_message) @@ -579,3 +574,42 @@ exports("getCustomApps", get_quasar_apps) exports("OpenPhoneApp", open_quasar_app) exports("OpenApp", open_phone_app) exports("CloseApp", close_phone_app) + +SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "OpenApp", open_phone_app) +SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "CloseApp", close_phone_app) + +SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "AddApplication", add_17mov_application) +SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "RemoveApplication", remove_17mov_application) +SkyPhoneCompatibility.RegisterExportAlias("17mov_Phone", "SendAppMessage", send_app_message) + +SkyPhoneCompatibility.RegisterExportAlias("high-phone", "addApplication", add_high_application) +SkyPhoneCompatibility.RegisterExportAlias("high-phone", "sendAppNui", send_high_app_nui) + +SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "addCustomApp", add_quasar_app) +SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "addCustomAppsBatch", add_quasar_apps_batch) +SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "updateCustomApp", update_quasar_app) +SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "removeCustomApp", remove_quasar_app) +SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "getCustomApps", get_quasar_apps) +SkyPhoneCompatibility.RegisterExportAlias("qs-smartphone", "OpenPhoneApp", open_quasar_app) + +SkyPhoneCompatibility.RegisterExportAlias("yseries", "SendAppMessage", send_app_message) +SkyPhoneCompatibility.RegisterExportAlias("yseries", "CloseApp", close_phone_app) +SkyPhoneCompatibility.RegisterExportAlias("yseries", "GetDataLoaded", function() + return true +end) + +CreateThread(function() + TriggerServerEvent("sky_phone:compat:high:server:requestSnapshot") + TriggerEvent("17mov_Phone:Client:Ready") + + local provider_resources = { + "lb-phone", + "17mov_Phone", + "high-phone", + "qs-smartphone", + "yseries", + } + for index = 1, #provider_resources do + TriggerEvent("onResourceStart", provider_resources[index]) + end +end) diff --git a/sky_phone/source/client/custom_apps.lua b/sky_phone/source/client/custom_apps.lua index 1f95350..c098f70 100644 --- a/sky_phone/source/client/custom_apps.lua +++ b/sky_phone/source/client/custom_apps.lua @@ -26,12 +26,24 @@ local active_app_id = nil local active_app_ready = false local phone_open = false local queued_messages = {} -local default_icon_url = ("https://cfx-nui-%s/custom_apps/_sdk/default-icon.svg"):format(current_resource) +local default_icon_url = ("https://cfx-nui-%s/img/custom-app.svg"):format(current_resource) local function trim(value) return value:match("^%s*(.-)%s*$") end +local function is_callable(value) + if type(value) == "function" then + return true + end + if type(value) ~= "table" then + return false + end + + local value_metatable = getmetatable(value) + return type(value_metatable) == "table" and type(value_metatable.__call) == "function" +end + local function validate_owner_resource(owner_resource, allow_stopping) if type(owner_resource) ~= "string" or #owner_resource == 0 @@ -118,7 +130,7 @@ local function validate_asset_path(path) return true end -local function normalize_asset_url(value, original_owner, adapter_resource, required) +local function normalize_asset_url(value, original_owner, adapter_resource, asset_resource, required) if value == nil and not required then return nil end @@ -137,6 +149,9 @@ local function normalize_asset_url(value, original_owner, adapter_resource, requ if adapter_resource then allowed_owners[adapter_resource] = true end + if asset_resource then + allowed_owners[asset_resource] = true + end local cfx_owner = url:match("^https://cfx%-nui%-([^/]+)/") if cfx_owner then @@ -177,6 +192,13 @@ local function normalize_asset_url(value, original_owner, adapter_resource, requ asset_path = url:sub(#adapter_prefix + 1) end end + if asset_resource then + local asset_prefix = asset_resource .. "/" + if url:sub(1, #asset_prefix) == asset_prefix then + asset_owner = asset_resource + asset_path = url:sub(#asset_prefix + 1) + end + end if not validate_asset_path(asset_path) then return nil, "invalid_asset_url" @@ -326,14 +348,42 @@ local function normalize_external_definition(owner_resource, adapter_resource, d return nil, "invalid_category" end - local ui, ui_error = normalize_asset_url(definition.ui, owner_resource, adapter_resource, true) + local asset_resource = definition.assetResource + if asset_resource ~= nil then + if definition.compatibility == nil then + return nil, "asset_resource_not_allowed" + end + local valid_asset_resource, asset_resource_error = validate_owner_resource(asset_resource, false) + if not valid_asset_resource then + return nil, asset_resource_error + end + end + + local ui, ui_error = normalize_asset_url( + definition.ui, + owner_resource, + adapter_resource, + asset_resource, + true + ) if not ui then return nil, ui_error end - local icon, icon_error = normalize_asset_url(definition.icon, owner_resource, adapter_resource, false) + local icon, icon_error = normalize_asset_url( + definition.icon, + owner_resource, + adapter_resource, + asset_resource, + false + ) if definition.icon ~= nil and not icon then - return nil, icon_error + if definition.compatibility == nil then + return nil, icon_error + end + print(( + "[sky_phone] Custom app '%s' from '%s' uses an unavailable icon (%s); using the default icon." + ):format(definition.id, owner_resource, icon_error)) end local permissions, permissions_error = SkyPhoneApps.ValidatePermissions(definition.permissions) @@ -387,7 +437,7 @@ local function normalize_external_definition(owner_resource, adapter_resource, d local hooks = {} for hook_name in pairs(HOOK_NAMES) do local hook = definition[hook_name] - if hook ~= nil and type(hook) ~= "function" then + if hook ~= nil and not is_callable(hook) then return nil, "invalid_" .. hook_name end hooks[hook_name] = hook @@ -1280,6 +1330,12 @@ exports("SendCustomAppNotificationFromAdapter", send_custom_app_notification_fro exports("UpdateCustomApp", update_custom_app) exports("UpdateCustomAppFromAdapter", update_custom_app_from_adapter) +SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "AddCustomApp", add_custom_app_export) +SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "RemoveCustomApp", remove_custom_app) +SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "SendCustomAppMessage", send_custom_app_message) +SkyPhoneCompatibility.RegisterExportAlias("yseries", "AddCustomApp", add_custom_app_export) +SkyPhoneCompatibility.RegisterExportAlias("yseries", "RemoveCustomApp", remove_custom_app) + RegisterNUICallback("custom-app:lifecycle", function(data, cb) if type(data) ~= "table" then cb({ success = false, error = "invalid_lifecycle_request" }) @@ -1358,11 +1414,11 @@ RegisterNUICallback("custom-app:lifecycle", function(data, cb) clear_active_app(false) end - local hook_error = nil - if not hook_success then - hook_error = "hook_failed" + if not hook_success and not app.catalog.compatibility then + cb({ success = false, error = "hook_failed" }) + return end - cb({ success = hook_success, error = hook_error }) + cb({ success = true }) end) AddEventHandler("onClientResourceStop", function(resource_name) diff --git a/sky_phone/source/server/custom_app_compat.lua b/sky_phone/source/server/custom_app_compat.lua index 25196d2..022c21b 100644 --- a/sky_phone/source/server/custom_app_compat.lua +++ b/sky_phone/source/server/custom_app_compat.lua @@ -136,3 +136,4 @@ AddEventHandler("onResourceStop", function(resource_name) end) exports("addApplication", add_high_application) +SkyPhoneCompatibility.RegisterExportAlias("high-phone", "addApplication", add_high_application) diff --git a/sky_phone/source/shared/custom_app_compat.lua b/sky_phone/source/shared/custom_app_compat.lua index d69b0b2..ff14ea1 100644 --- a/sky_phone/source/shared/custom_app_compat.lua +++ b/sky_phone/source/shared/custom_app_compat.lua @@ -1,5 +1,15 @@ SkyPhoneCompatibility = {} +function SkyPhoneCompatibility.RegisterExportAlias(resource_name, export_name, handler) + assert(type(resource_name) == "string" and resource_name ~= "", "Export alias resource must be a non-empty string") + assert(type(export_name) == "string" and export_name ~= "", "Export alias name must be a non-empty string") + assert(type(handler) == "function", "Export alias handler must be a function") + + AddEventHandler(("__cfx_export_%s_%s"):format(resource_name, export_name), function(set_callback) + set_callback(handler) + end) +end + SkyPhoneCompatibility.Providers = { lb = "lb_phone", seventeen = "17mov", @@ -26,6 +36,34 @@ local function normalize_at_resource_url(owner_resource, url, error_prefix) return owner_resource .. "/" .. url_path end +local function resolve_lb_asset_resource(owner_resource, app_data) + local ui = app_data.ui + if type(ui) ~= "string" then + return nil + end + + local explicit_ui_resource = ui:match("^https://cfx%-nui%-([^/]+)/") + or ui:match("^nui://([^/]+)/") + local asset_resource = explicit_ui_resource or ui:match("^([%w][%w._-]*)/") + if not asset_resource or asset_resource == owner_resource then + return nil + end + + if explicit_ui_resource or app_data.resource == asset_resource then + return asset_resource + end + + local icon = app_data.icon + local icon_resource = type(icon) == "string" and ( + icon:match("^https://cfx%-nui%-([^/]+)/") or icon:match("^nui://([^/]+)/") + ) or nil + if icon_resource == asset_resource then + return asset_resource + end + + return nil +end + local function build_locale_maps(app_name, locales) local names = {} local descriptions = {} @@ -87,6 +125,7 @@ function SkyPhoneCompatibility.BuildLbDefinition(owner_resource, app_data) developer = app_data.developer, category = app_data.game and "games" or "utilities", ui = ui, + assetResource = resolve_lb_asset_resource(owner_resource, app_data), bridgeMode = "legacy", icon = app_data.icon, defaultInstalled = app_data.defaultApp or false, diff --git a/tests/custom_app_compat_client.lua b/tests/custom_app_compat_client.lua index 0a21629..f4a5027 100644 --- a/tests/custom_app_compat_client.lua +++ b/tests/custom_app_compat_client.lua @@ -1,4 +1,8 @@ local registered_exports = {} +local registered_event_handlers = {} +local registered_nui_callbacks = {} +local triggered_events = {} +local nui_messages = {} local invoking_resource = nil Config = { @@ -43,18 +47,31 @@ function GetInvokingResource() end function GetResourceState(resource_name) - if resource_name == "missing_resource" then + if resource_name == "missing_resource" or resource_name == "ui" then return "missing" end return "started" end -function RegisterNUICallback() end +function RegisterNUICallback(callback_name, handler) + registered_nui_callbacks[callback_name] = handler +end function RegisterNetEvent() end -function AddEventHandler() end -function SendNUIMessage() end +function AddEventHandler(event_name, handler) + local handlers = registered_event_handlers[event_name] or {} + handlers[#handlers + 1] = handler + registered_event_handlers[event_name] = handlers +end +function SendNUIMessage(message) + nui_messages[#nui_messages + 1] = message +end function TriggerServerEvent() end -function TriggerEvent() end +function TriggerEvent(event_name, ...) + triggered_events[#triggered_events + 1] = { + event_name = event_name, + arguments = { ... }, + } +end function CreateThread(handler) handler() @@ -65,17 +82,110 @@ dofile("sky_phone/source/shared/custom_app_compat.lua") dofile("sky_phone/source/client/custom_apps.lua") dofile("sky_phone/source/client/custom_app_compat.lua") +local function get_alias_export(resource_name, export_name) + local event_name = ("__cfx_export_%s_%s"):format(resource_name, export_name) + local handlers = registered_event_handlers[event_name] + assert(handlers and #handlers == 1, ("Missing %s:%s export alias"):format(resource_name, export_name)) + + local alias_handler + local export_callback = setmetatable({ + __cfx_functionReference = "test-export-callback", + }, { + __call = function(_, handler) + alias_handler = handler + end, + }) + handlers[1](export_callback) + assert(type(alias_handler) == "function", ("Invalid %s:%s export alias"):format(resource_name, export_name)) + return alias_handler +end + +local function make_cfx_function_reference(reference, handler) + return setmetatable({ + __cfx_functionReference = reference, + }, { + __call = function(_, ...) + return handler(...) + end, + }) +end + +local lb_add_custom_app = get_alias_export("lb-phone", "AddCustomApp") +local lb_remove_custom_app = get_alias_export("lb-phone", "RemoveCustomApp") +local lb_send_custom_app_message = get_alias_export("lb-phone", "SendCustomAppMessage") +local lb_open_app = get_alias_export("lb-phone", "OpenApp") +local lb_close_app = get_alias_export("lb-phone", "CloseApp") +local mov_add_application = get_alias_export("17mov_Phone", "AddApplication") +local mov_remove_application = get_alias_export("17mov_Phone", "RemoveApplication") +local mov_send_app_message = get_alias_export("17mov_Phone", "SendAppMessage") +local high_add_application = get_alias_export("high-phone", "addApplication") +local high_send_app_nui = get_alias_export("high-phone", "sendAppNui") +local quasar_add_custom_app = get_alias_export("qs-smartphone", "addCustomApp") +local quasar_add_custom_apps_batch = get_alias_export("qs-smartphone", "addCustomAppsBatch") +local quasar_update_custom_app = get_alias_export("qs-smartphone", "updateCustomApp") +local quasar_remove_custom_app = get_alias_export("qs-smartphone", "removeCustomApp") +local quasar_get_custom_apps = get_alias_export("qs-smartphone", "getCustomApps") +local quasar_open_phone_app = get_alias_export("qs-smartphone", "OpenPhoneApp") +local yseries_add_custom_app = get_alias_export("yseries", "AddCustomApp") +local yseries_remove_custom_app = get_alias_export("yseries", "RemoveCustomApp") +local yseries_send_app_message = get_alias_export("yseries", "SendAppMessage") +local yseries_close_app = get_alias_export("yseries", "CloseApp") +local yseries_get_data_loaded = get_alias_export("yseries", "GetDataLoaded") + +local expected_provider_resources = { + ["lb-phone"] = true, + ["17mov_Phone"] = true, + ["high-phone"] = true, + ["qs-smartphone"] = true, + ["yseries"] = true, +} +local resource_start_events = {} +for index = 1, #triggered_events do + local event = triggered_events[index] + local resource_name = event.arguments[1] + if event.event_name == "onResourceStart" then + resource_start_events[resource_name] = true + end +end +for resource_name in pairs(expected_provider_resources) do + assert(resource_start_events[resource_name], ("Missing %s provider start signal"):format(resource_name)) +end +assert(yseries_get_data_loaded(), "YSeries must see the compatibility provider as loaded") + invoking_resource = "lb_app" -local lb_success, lb_error = registered_exports.AddCustomApp({ +local lb_success, lb_error = lb_add_custom_app({ identifier = "dispatch", name = "Dispatch", description = "Dispatch terminal", ui = "ui/index.html", + onInstall = make_cfx_function_reference("install-hook", function() + error("vendor install failure") + end), + onOpen = make_cfx_function_reference("open-hook", function() + error("vendor open failure") + end), }) assert(lb_success and lb_error == nil, "LB AddCustomApp must register through the shared export") +local lifecycle_callback = assert(registered_nui_callbacks["custom-app:lifecycle"]) +local lifecycle_response +SkyPhoneApps.SetPhoneOpen(true) +lifecycle_callback({ appId = "dispatch", event = "install" }, function(response) + lifecycle_response = response +end) +assert(lifecycle_response.success, "a vendor install hook failure must not fail installation") +lifecycle_callback({ appId = "dispatch", event = "open" }, function(response) + lifecycle_response = response +end) +assert(lifecycle_response.success, "a vendor open hook failure must not prevent opening the app") +lifecycle_callback({ appId = "dispatch", event = "ready" }, function(response) + lifecycle_response = response +end) +assert(lifecycle_response.success, "ready must complete after a failed vendor open hook") +SkyPhoneApps.SetPhoneOpen(false) + invoking_resource = "another_app" -local duplicate_success, duplicate_error = registered_exports.AddCustomApp({ +local duplicate_success, duplicate_error = lb_add_custom_app({ identifier = "dispatch", name = "Hijack", description = "Hijack", @@ -83,18 +193,61 @@ local duplicate_success, duplicate_error = registered_exports.AddCustomApp({ }) assert(not duplicate_success and duplicate_error == "duplicate_app_id", "cross-owner IDs must be rejected") -local remove_success, remove_error = registered_exports.RemoveCustomApp("dispatch") +local remove_success, remove_error = lb_remove_custom_app("dispatch") assert(not remove_success and remove_error == "app_owner_mismatch", "cross-owner removal must be rejected") invoking_resource = "lb_app" -assert(registered_exports.RemoveCustomApp("dispatch"), "the LB owner must be able to remove its app") +local inactive_message_success, inactive_message_error = lb_send_custom_app_message("dispatch", { type = "ping" }) +assert(not inactive_message_success and inactive_message_error == "app_not_active", "LB message alias must preserve app state checks") +local closed_open_success, closed_open_error = lb_open_app("dispatch") +assert(not closed_open_success and closed_open_error == "phone_closed", "LB open alias must preserve phone state checks") +local inactive_close_success, inactive_close_error = lb_close_app({ app = "dispatch" }) +assert(not inactive_close_success and inactive_close_error == "app_not_active", "LB close alias must preserve app state checks") +assert(lb_remove_custom_app("dispatch"), "the LB owner must be able to remove its app") + +invoking_resource = "phone_adapter" +assert(lb_add_custom_app({ + identifier = "manufacturer-app", + name = "Manufacturer App", + description = "Manufacturer-owned UI assets", + ui = "manufacturer_app/ui/index.html", + icon = "nui://manufacturer_app/ui/icon.png", +}), "LB resource-prefixed assets must register through an adapter") +SkyPhoneApps.SetPhoneOpen(true) +SkyPhoneApps.SendCatalog() +local catalog_message = nui_messages[#nui_messages] +local manufacturer_app +for index = 1, #catalog_message.data.apps do + if catalog_message.data.apps[index].id == "manufacturer-app" then + manufacturer_app = catalog_message.data.apps[index] + break + end +end +assert(manufacturer_app, "manufacturer app must be present in the catalog") +assert( + manufacturer_app.ui == "https://cfx-nui-manufacturer_app/ui/index.html", + "LB resource-prefixed UI must preserve its asset resource" +) +assert( + manufacturer_app.icon == "https://cfx-nui-manufacturer_app/ui/icon.png", + "LB resource-prefixed icon must preserve its asset resource" +) +SkyPhoneApps.SetPhoneOpen(false) invoking_resource = "yseries_app" -assert(registered_exports.AddCustomApp({ +assert(yseries_add_custom_app({ key = "slots", name = "Slots", ui = "https://cfx-nui-yseries_app/ui/index.html", + icon = { + yos = "https://cdn.example.com/slots.png", + }, }), "YSeries AddCustomApp must be selected from the key field") +local yseries_message_success, yseries_message_error = yseries_send_app_message("slots", { type = "ping" }) +assert(not yseries_message_success and yseries_message_error == "app_not_active", "YSeries message alias must preserve app state checks") +local yseries_close_success, yseries_close_error = yseries_close_app({ app = "slots" }) +assert(not yseries_close_success and yseries_close_error == "app_not_active", "YSeries close alias must preserve app state checks") +assert(yseries_remove_custom_app("slots"), "YSeries RemoveCustomApp must use the provider alias") local ambiguous_success, ambiguous_error = registered_exports.AddCustomApp({ id = "ambiguous", @@ -105,14 +258,17 @@ local ambiguous_success, ambiguous_error = registered_exports.AddCustomApp({ assert(not ambiguous_success and ambiguous_error == "ambiguous_app_provider", "ambiguous schemas must fail") invoking_resource = "mov_app" -assert(registered_exports.AddApplication({ +assert(mov_add_application({ name = "market", label = "Market", ui = "https://cfx-nui-mov_app/ui/index.html", }), "17mov AddApplication must register") +local mov_message_success, mov_message_error = mov_send_app_message("market", { type = "ping" }) +assert(not mov_message_success and mov_message_error == "app_not_active", "17mov message alias must preserve app state checks") +assert(mov_remove_application("market"), "17mov RemoveApplication must use the provider alias") invoking_resource = "high_app" -assert(registered_exports.addApplication("bankingv2", { +assert(high_add_application("bankingv2", { externalUrl = "@high_app/ui/index.html", }, { en = { @@ -120,9 +276,11 @@ assert(registered_exports.addApplication("bankingv2", { description = "Banking app", }, }), "High addApplication must register") +local high_message_success, high_message_error = high_send_app_nui("bankingv2", { type = "ping" }) +assert(not high_message_success and high_message_error == "app_not_active", "High message alias must preserve app state checks") invoking_resource = "quasar_app" -assert(registered_exports.addCustomApp({ +assert(quasar_add_custom_app({ id = "services", label = "Services", iframe = { @@ -130,11 +288,14 @@ assert(registered_exports.addCustomApp({ }, }), "Quasar addCustomApp must register") -local quasar_apps = registered_exports.getCustomApps() +local quasar_apps = quasar_get_custom_apps() assert(#quasar_apps == 1 and quasar_apps[1].id == "services", "Quasar getCustomApps must expose the registry") -assert(registered_exports.updateCustomApp("services", { +assert(quasar_update_custom_app("services", { label = "City Services", }), "Quasar updateCustomApp must update an owned app") -assert(registered_exports.removeCustomApp("services"), "Quasar removeCustomApp must remove an owned app") +local quasar_open_success, quasar_open_error = quasar_open_phone_app("services") +assert(not quasar_open_success and quasar_open_error == "phone_closed", "Quasar open alias must preserve phone state checks") +assert(quasar_remove_custom_app("services"), "Quasar removeCustomApp must remove an owned app") +assert(quasar_add_custom_apps_batch({}), "Quasar batch alias must accept an empty batch") print("Custom app compatibility client tests passed") diff --git a/tests/custom_app_compat_server.lua b/tests/custom_app_compat_server.lua index 2ba77f6..3348d66 100644 --- a/tests/custom_app_compat_server.lua +++ b/tests/custom_app_compat_server.lua @@ -1,4 +1,5 @@ local event_handlers = {} +local registered_event_handlers = {} local registered_exports = {} local sent_events = {} local invoking_resource = nil @@ -25,7 +26,11 @@ function RegisterNetEvent(event_name, handler) event_handlers[event_name] = handler end -function AddEventHandler() end +function AddEventHandler(event_name, handler) + local handlers = registered_event_handlers[event_name] or {} + handlers[#handlers + 1] = handler + registered_event_handlers[event_name] = handlers +end function TriggerClientEvent(event_name, target, ...) sent_events[#sent_events + 1] = { @@ -38,8 +43,21 @@ end dofile("sky_phone/source/shared/custom_app_compat.lua") dofile("sky_phone/source/server/custom_app_compat.lua") +local alias_handlers = registered_event_handlers["__cfx_export_high-phone_addApplication"] +assert(alias_handlers and #alias_handlers == 1, "Missing high-phone:addApplication export alias") +local high_add_application +local export_callback = setmetatable({ + __cfx_functionReference = "test-export-callback", +}, { + __call = function(_, handler) + high_add_application = handler + end, +}) +alias_handlers[1](export_callback) +assert(type(high_add_application) == "function", "Invalid high-phone:addApplication export alias") + invoking_resource = "high_server_app" -local add_success, add_error = registered_exports.addApplication("bankingv2", { +local add_success, add_error = high_add_application("bankingv2", { externalUrl = "@high_server_app/ui/index.html", }, { en = { @@ -53,7 +71,7 @@ assert(sent_events[1].event_name == "sky_phone:compat:high:client:syncApplicatio assert(sent_events[1].arguments[1] == "high_server_app", "High sync must preserve the owner") invoking_resource = "other_server_app" -local duplicate_success, duplicate_error = registered_exports.addApplication("bankingv2", { +local duplicate_success, duplicate_error = high_add_application("bankingv2", { externalUrl = "@other_server_app/ui/index.html", }) assert(not duplicate_success and duplicate_error == "duplicate_app_id", "High cross-owner replacement must fail")