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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/website-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,15 @@ jobs:
test -f docs/website/public/playground/index.html

- name: Validate Initializr JS bundle output
run: test -f docs/website/public/initializr-app/index.html
run: |
set -euo pipefail
test -f docs/website/public/initializr-app/index.html
grep -Fq 'downloadProject' docs/website/public/initializr-app/translated_app.js
grep -Fq 'cn1-initializr-project-downloaded' \
docs/website/public/initializr-app/com_codename1_initializr_WebsiteThemeNative.js
grep -Fq 'cn1-initializr-project-downloaded' docs/website/public/initializr/index.html
node docs/website/scripts/test-initializr-download-bridge.mjs \
docs/website/public/initializr-app/com_codename1_initializr_WebsiteThemeNative.js

- name: Validate Playground JS bundle output
run: test -f docs/website/public/playground-app/index.html
Expand Down
3 changes: 3 additions & 0 deletions docs/website/assets/js/cn1-crisp.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@
crispEvents.gettingStartedDwell = (data) => scheduleCrispEvent(
"GettingStartedDwell", 20000, data
);
crispEvents.initializrProjectDownloaded = (data) => requestCrispEvent(
"InitializrProjectDownloaded", data || { page: "/initializr/" }
);
crispEvents.pricingEvaluator = (data) => {
const firePricing = () => requestCrispEvent("PricingEvaluator", data);
window.setTimeout(firePricing, 30000);
Expand Down
13 changes: 9 additions & 4 deletions docs/website/layouts/_default/initializr.html
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,17 @@
});

window.addEventListener("message", function (evt) {
if (evt.source !== frame.contentWindow || !evt.data || evt.data.type !== "cn1-initializr-ui-ready") {
if (evt.source !== frame.contentWindow || !evt.data) {
return;
}
window.requestAnimationFrame(function () {
hideLoader();
});
if (evt.data.type === "cn1-initializr-ui-ready") {
window.requestAnimationFrame(function () {
hideLoader();
});
} else if (evt.data.type === "cn1-initializr-project-downloaded" &&
window.cn1CrispEvents && window.cn1CrispEvents.initializrProjectDownloaded) {
window.cn1CrispEvents.initializrProjectDownloaded({ page: "/initializr/" });
}
});
}
})();
Expand Down
17 changes: 17 additions & 0 deletions docs/website/scripts/test-cn1-crisp-events.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,23 @@ function events(state) {
assert.equal(state.sessionStorage.getItem("cn1-crisp-ev-SigningScreenView"), "1");
}

{
const state = load("accepted");
state.window.cn1CrispEvents.initializrProjectDownloaded({ page: "/initializr/" });

const recorded = events(state);
assert.equal(recorded.length, 1, "an accepted project download should be queued once");
assert.equal(recorded[0][0], "InitializrProjectDownloaded");
assert.deepEqual(JSON.parse(JSON.stringify(recorded[0][1])), { page: "/initializr/" });
}

{
const state = load("declined");
state.window.cn1CrispEvents.initializrProjectDownloaded({ page: "/initializr/" });
assert.equal(eventCommands(state).length, 0,
"a project download must not be recorded without analytics consent");
}

{
const state = load(null);
state.window.cn1CrispEvents.signingScreenView({ page: "/signing/" });
Expand Down
86 changes: 86 additions & 0 deletions docs/website/scripts/test-initializr-download-bridge.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import vm from "node:vm";

const sourcePath = process.argv[2]
|| new URL("../../../scripts/initializr/javascript/src/main/javascript/com_codename1_initializr_WebsiteThemeNative.js", import.meta.url);
const source = fs.readFileSync(sourcePath, "utf8");

function loadBridge(click) {
const nativeInterfaces = {};
const posts = [];
const actions = [];
const parent = {
postMessage(message) {
actions.push("post");
posts.push(message);
}
};
const body = {
appendChild(anchor) {
actions.push("append");
anchor.parentNode = body;
},
removeChild(anchor) {
actions.push("remove");
anchor.parentNode = null;
}
};
const window = {
parent,
document: {
body,
createElement() {
return {
click() {
actions.push("click");
click();
}
};
}
},
matchMedia() {
return { matches: false };
}
};

vm.runInNewContext(source, {
window,
cn1_get_native_interfaces: () => nativeInterfaces
});

return {
bridge: nativeInterfaces.com_codename1_initializr_WebsiteThemeNative,
posts,
actions
};
}

function invokeDownload(state) {
let result;
state.bridge.downloadProject__java_lang_String_java_lang_String(
"sample.zip",
"data:application/octet-stream;base64,AA==",
{ complete(value) { result = value; } }
);
return result;
}

{
const state = loadBridge(() => {});
assert.equal(invokeDownload(state), true);
assert.deepEqual(state.actions, ["append", "click", "remove", "post"]);
assert.deepEqual(
JSON.parse(JSON.stringify(state.posts)),
[{ type: "cn1-initializr-project-downloaded" }]
);
}

{
const state = loadBridge(() => { throw new Error("blocked"); });
assert.equal(invokeDownload(state), false);
assert.deepEqual(state.actions, ["append", "click", "remove"]);
assert.deepEqual(state.posts, []);
}

console.log("Initializr download bridge tests passed");
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.initializr;

import com.codename1.system.NativeInterface;

public interface WebsiteThemeNative extends NativeInterface {
boolean isDarkMode();
void notifyUiReady();
boolean downloadProject(String fileName, String dataUrl);

/// Horizontal clearance, in CSS pixels, that the host page's chat launcher
/// (Crisp) currently needs at the bottom-right, or 0 when it is hidden. The
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
package com.codename1.initializr.model;

import com.codename1.components.ToastBar;
import com.codename1.initializr.WebsiteThemeNative;
import com.codename1.io.Log;
import com.codename1.io.Util;
import com.codename1.system.NativeLookup;
import com.codename1.util.StringUtil;
import net.sf.zipme.CRC32;
import net.sf.zipme.ZipEntry;
Expand Down Expand Up @@ -164,6 +166,9 @@ public void generate() {
ToastBar.showErrorMessage("Couldn't build the project: " + describeError(ex));
return;
}
if (downloadWebsiteProject(fileName, bytes)) {
return;
}
if (downloadBytesAsFile(fileName, bytes)) {
return;
}
Expand Down Expand Up @@ -191,6 +196,21 @@ public void generate() {
execute(filePath);
}

/** Use the website bridge so the metric is emitted only after its download handler succeeds. */
private static boolean downloadWebsiteProject(String fileName, byte[] bytes) {
WebsiteThemeNative nativeBridge = NativeLookup.create(WebsiteThemeNative.class);
if (nativeBridge == null || !nativeBridge.isSupported()) {
return false;
}
try {
String dataUrl = "data:application/octet-stream;base64,"
+ com.codename1.util.Base64.encodeNoNewline(bytes);
return nativeBridge.downloadProject(fileName, dataUrl);
} catch (Throwable t) {
return false;
}
}

private static String describeError(Throwable ex) {
String detail = ex.getMessage();
return (detail == null || detail.length() == 0) ? ex.getClass().getName() : detail;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
(function(exports){

var o = {};
Expand Down Expand Up @@ -89,6 +111,42 @@ var o = {};
}
};

o.downloadProject__java_lang_String_java_lang_String = function(fileName, dataUrl, callback) {
var anchor = null;
try {
var doc = window.document;
if (!doc || !doc.body || !dataUrl) {
callback.complete(false);
return;
}
anchor = doc.createElement("a");
anchor.href = dataUrl;
anchor.download = fileName || "codename-one-project.zip";
doc.body.appendChild(anchor);
anchor.click();
} catch (downloadError) {
callback.complete(false);
return;
} finally {
try {
if (anchor && anchor.parentNode) {
anchor.parentNode.removeChild(anchor);
}
} catch (ignored) {
// Cleanup must not change a successful download acknowledgement.
}
}

try {
if (window.parent && window.parent !== window && window.parent.postMessage) {
window.parent.postMessage({ type: "cn1-initializr-project-downloaded" }, "*");
}
} catch (ignored) {
// The download succeeded even if the optional embedding page is unavailable.
}
callback.complete(true);
};

// Horizontal clearance (CSS px) the host page's Crisp chat launcher needs at
// the bottom-right, so the generate button can be nudged left of it. The
// default Crisp launcher bubble (~64px) sits ~24px from the page edge;
Expand Down
Loading