Skip to content

Commit f718dd5

Browse files
committed
chore: system wide license file write working
1 parent e27a823 commit f718dd5

File tree

8 files changed

+207
-3
lines changed

8 files changed

+207
-3
lines changed

docs/API-Reference/command/Commands.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,12 @@ Opens support resources
740740
## HELP\_GET\_PRO
741741
Opens Phoenix Pro page
742742

743+
**Kind**: global variable
744+
<a name="HELP_MANAGE_LICENSES"></a>
745+
746+
## HELP\_MANAGE\_LICENSES
747+
Manage Pro licenses
748+
743749
**Kind**: global variable
744750
<a name="HELP_SUGGEST"></a>
745751

docs/API-Reference/utils/NodeUtils.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,40 @@ Opens a file in the default application for its type on Windows, macOS, and Linu
107107
| --- | --- | --- |
108108
| fullPath | <code>string</code> | The path to the file/folder to open. |
109109

110+
<a name="addDeviceLicense"></a>
111+
112+
## addDeviceLicense() ⇒ <code>Promise.&lt;boolean&gt;</code>
113+
Enables device license by creating a system-wide license file.
114+
On Windows, macOS, and Linux this will request elevation if needed.
115+
116+
**Kind**: global function
117+
**Returns**: <code>Promise.&lt;boolean&gt;</code> - - Resolves true if system wide defile file added, else false.
118+
**Throws**:
119+
120+
- <code>Error</code> - If called from the browser
121+
122+
<a name="removeDeviceLicense"></a>
123+
124+
## removeDeviceLicense() ⇒ <code>Promise.&lt;boolean&gt;</code>
125+
Removes the system-wide device license file.
126+
On Windows, macOS, and Linux this will request elevation if needed.
127+
128+
**Kind**: global function
129+
**Returns**: <code>Promise.&lt;boolean&gt;</code> - - Resolves true if system wide defile file removed, else false.
130+
**Throws**:
131+
132+
- <code>Error</code> - If called from the browser
133+
134+
<a name="isLicensedDevice"></a>
135+
136+
## isLicensedDevice() ⇒ <code>Promise.&lt;boolean&gt;</code>
137+
Checks if the current machine is licensed.
138+
This validates that the system-wide license file exists,
139+
contains valid JSON, and has `licensedDevice: true`.
140+
141+
**Kind**: global function
142+
**Returns**: <code>Promise.&lt;boolean&gt;</code> - - Resolves with `true` if the device is licensed, `false` otherwise.
143+
**Throws**:
144+
145+
- <code>Error</code> - If called from the browser.
146+

src-node/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-node/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"lmdb": "^2.9.2",
2727
"mime-types": "^2.1.35",
2828
"cross-spawn": "^7.0.6",
29-
"which": "^2.0.1"
29+
"which": "^2.0.1",
30+
"@expo/sudo-prompt": "^9.3.2"
3031
}
31-
}
32+
}

src-node/utils.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ const fs = require('fs');
44
const fsPromise = require('fs').promises;
55
const path = require('path');
66
const os = require('os');
7+
const sudo = require('@expo/sudo-prompt');
78
const {lintFile} = require("./ESLint/service");
89
let openModule, open; // dynamic import when needed
910

11+
const options = { name: 'Phoenix Code' };
12+
const content = '{"licensedDevice": true}\n';
13+
1014
async function _importOpen() {
1115
if(open){
1216
return open;
@@ -269,6 +273,82 @@ async function getEnvironmentVariable(varName) {
269273
return process.env[varName];
270274
}
271275

276+
function getLicensePath() {
277+
switch (os.platform()) {
278+
case 'win32':
279+
return 'C:\\Program Files\\Phoenix Code Control\\device-license';
280+
case 'darwin':
281+
return '/Library/Application Support/phoenix-code-control/device-license';
282+
case 'linux':
283+
return '/etc/phoenix-code-control/device-license';
284+
default:
285+
throw new Error(`Unsupported platform: ${os.platform()}`);
286+
}
287+
}
288+
289+
function sudoExec(command) {
290+
return new Promise((resolve, reject) => {
291+
sudo.exec(command, options, (error, stdout, stderr) => {
292+
if (error) {
293+
return reject(error);
294+
}
295+
resolve({ stdout, stderr });
296+
});
297+
});
298+
}
299+
300+
function readFileUtf8(p) {
301+
return new Promise((resolve, reject) => {
302+
fs.readFile(p, 'utf8', (err, data) => (err ? reject(err) : resolve(data)));
303+
});
304+
}
305+
306+
async function addDeviceLicense() {
307+
const targetPath = getLicensePath();
308+
let command;
309+
310+
if (os.platform() === 'win32') {
311+
// PowerShell with explicit dirs and safe quoting
312+
const dir = 'C:\\Program Files\\Phoenix Code Control';
313+
const psContent = content.replace(/'/g, "''");
314+
command = `powershell -Command "New-Item -ItemType Directory -Force '${dir}' | Out-Null; Set-Content -Path '${targetPath}' -Value '${psContent}' -Encoding UTF8"`;
315+
} else {
316+
const dir = path.dirname(targetPath);
317+
// POSIX: mkdir + printf (use absolute paths)
318+
const safe = content.replace(/'/g, `'\\''`);
319+
command = `/bin/mkdir -p "${dir}" && /bin/printf '%s' '${safe}' > "${targetPath}"`;
320+
}
321+
322+
await sudoExec(command);
323+
return targetPath;
324+
}
325+
326+
async function removeDeviceLicense() {
327+
const targetPath = getLicensePath();
328+
let command;
329+
330+
if (os.platform() === 'win32') {
331+
command = `powershell -Command "if (Test-Path '${targetPath}') { Remove-Item -Path '${targetPath}' -Force }"`;
332+
} else {
333+
command = `/bin/rm -f "${targetPath}"`;
334+
}
335+
336+
await sudoExec(command);
337+
return targetPath;
338+
}
339+
340+
async function isLicensedDevice() {
341+
const targetPath = getLicensePath();
342+
try {
343+
const data = await readFileUtf8(targetPath);
344+
const json = JSON.parse(data);
345+
return json && json.licensedDevice === true;
346+
} catch {
347+
// file missing, unreadable, or invalid JSON
348+
return false;
349+
}
350+
}
351+
272352
exports.getURLContent = getURLContent;
273353
exports.setLocaleStrings = setLocaleStrings;
274354
exports.getPhoenixBinaryVersion = getPhoenixBinaryVersion;
@@ -278,5 +358,8 @@ exports.getEnvironmentVariable = getEnvironmentVariable;
278358
exports.ESLintFile = ESLintFile;
279359
exports.openNativeTerminal = openNativeTerminal;
280360
exports.openInDefaultApp = openInDefaultApp;
361+
exports.addDeviceLicense = addDeviceLicense;
362+
exports.removeDeviceLicense = removeDeviceLicense;
363+
exports.isLicensedDevice = isLicensedDevice;
281364
exports._loadNodeExtensionModule = _loadNodeExtensionModule;
282365
exports._npmInstallInFolder = _npmInstallInFolder;

src/nls/root/strings.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,7 @@ define({
17101710
"LICENSE_VALID_NEVER": "Never",
17111711
"LICENSE_STATUS_ERROR_CHECK": "Error checking license status",
17121712
"LICENSE_ACTIVATE_SUCCESS": "License activated successfully!",
1713+
"LICENSE_ACTIVATE_SUCCESS_PARTIAL": "License activated for your account only (not system-wide).",
17131714
"LICENSE_ACTIVATE_FAIL": "Failed to activate license",
17141715
"LICENSE_ENTER_KEY": "Please enter a license key"
17151716
});

src/services/manage-licenses.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ define(function (require, exports, module) {
3333
}
3434

3535
const Strings = require("strings"),
36+
NodeUtils = require("utils/NodeUtils"),
3637
Dialogs = require("widgets/Dialogs"),
3738
Mustache = require("thirdparty/mustache/mustache"),
3839
licenseManagementHTML = require("text!./html/license-management.html");
@@ -246,7 +247,10 @@ define(function (require, exports, module) {
246247
const result = await _registerDevice(licenseKey, deviceID, platform, deviceLabel);
247248

248249
if (result.isSuccess) {
249-
_showActivationMessage($dialog, true, result.message || Strings.LICENSE_ACTIVATE_SUCCESS);
250+
const addSuccess = await NodeUtils.addDeviceLicense();
251+
const successString = addSuccess ?
252+
Strings.LICENSE_ACTIVATE_SUCCESS : Strings.LICENSE_ACTIVATE_SUCCESS_PARTIAL;
253+
_showActivationMessage($dialog, true, successString);
250254

251255
// Clear the input field
252256
$dialog.find('#license-key-input').val('');

src/utils/NodeUtils.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
// @INCLUDE_IN_API_DOCS
2323

24+
/*global logger*/
25+
2426
/**
2527
* Generic node util APIs connector. see `src-node/utils.js` for node peer
2628
*/
@@ -189,6 +191,67 @@ define(function (require, exports, module) {
189191
return utilsConnector.execPeer("openInDefaultApp", window.fs.getTauriPlatformPath(fullPath));
190192
}
191193

194+
/**
195+
* Enables device license by creating a system-wide license file.
196+
* On Windows, macOS, and Linux this will request elevation if needed.
197+
*
198+
* @returns {Promise<boolean>} - Resolves true if system wide defile file added, else false.
199+
* @throws {Error} - If called from the browser
200+
*/
201+
async function addDeviceLicense() {
202+
if (!Phoenix.isNativeApp) {
203+
throw new Error("addDeviceLicense not available in browser");
204+
}
205+
try {
206+
await utilsConnector.execPeer("addDeviceLicense");
207+
return true;
208+
} catch (err) {
209+
logger.reportError(err, "system wide device license activation failed");
210+
}
211+
return false;
212+
}
213+
214+
/**
215+
* Removes the system-wide device license file.
216+
* On Windows, macOS, and Linux this will request elevation if needed.
217+
*
218+
* @returns {Promise<boolean>} - Resolves true if system wide defile file removed, else false.
219+
* @throws {Error} - If called from the browser
220+
*/
221+
async function removeDeviceLicense() {
222+
if (!Phoenix.isNativeApp) {
223+
throw new Error("removeDeviceLicense not available in browser");
224+
}
225+
try {
226+
await utilsConnector.execPeer("removeDeviceLicense");
227+
return true;
228+
} catch (err) {
229+
logger.reportError(err, "system wide device license remove failed");
230+
}
231+
return false;
232+
}
233+
234+
/**
235+
* Checks if the current machine is licensed.
236+
* This validates that the system-wide license file exists,
237+
* contains valid JSON, and has `licensedDevice: true`.
238+
*
239+
* @returns {Promise<boolean>} - Resolves with `true` if the device is licensed, `false` otherwise.
240+
* @throws {Error} - If called from the browser.
241+
*/
242+
async function isLicensedDevice() {
243+
if (!Phoenix.isNativeApp) {
244+
throw new Error("isLicensedDevice not available in browser");
245+
}
246+
try {
247+
return utilsConnector.execPeer("isLicensedDevice");
248+
} catch (err) {
249+
logger.reportError(err, "system wide device check failed");
250+
}
251+
return false;
252+
}
253+
254+
192255
if(NodeConnector.isNodeAvailable()) {
193256
// todo we need to update the strings if a user extension adds its translations. Since we dont support
194257
// node extensions for now, should consider when we support node extensions.
@@ -227,6 +290,9 @@ define(function (require, exports, module) {
227290
exports.getEnvironmentVariable = getEnvironmentVariable;
228291
exports.openNativeTerminal = openNativeTerminal;
229292
exports.openInDefaultApp = openInDefaultApp;
293+
exports.addDeviceLicense = addDeviceLicense;
294+
exports.removeDeviceLicense = removeDeviceLicense;
295+
exports.isLicensedDevice = isLicensedDevice;
230296

231297
/**
232298
* checks if Node connector is ready

0 commit comments

Comments
 (0)