Skip to content

Commit cdbce01

Browse files
committed
chore: getAIEntitlement api and showAIUpsellDialog
1 parent 7cf6e7b commit cdbce01

File tree

5 files changed

+184
-5
lines changed

5 files changed

+184
-5
lines changed

src/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"app_title": "Phoenix Code",
44
"app_name_about": "Phoenix Code",
55
"main_pro_plan": "Phoenix Pro",
6+
"ai_brand_name": "Phoenix AI",
67
"about_icon": "styles/images/phoenix-icon.svg",
78
"account_url": "https://account.phcode.dev/",
89
"promotions_url": "https://promotions.phcode.dev/dev/",

src/nls/root/strings.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1717,9 +1717,15 @@ define({
17171717
"LICENSE_ENTER_KEY": "Please enter a license key",
17181718
"LICENSE_REAPPLY_TO_DEVICE": "Already activated? Reapply system-wide",
17191719
// AI CONTROL
1720+
"AI_LOGIN_DIALOG_TITLE": "Sign In to Use AI Edits",
1721+
"AI_LOGIN_DIALOG_MESSAGE": "Please log in to use AI-powered edits",
1722+
"AI_LOGIN_DIALOG_BUTTON": "Get AI Access",
1723+
"AI_DISABLED_DIALOG_TITLE": "AI is disabled",
17201724
"AI_CONTROL_ALL_ALLOWED_NO_CONFIG": "No AI config file found in system. AI is enabled for all users.",
17211725
"AI_CONTROL_ALL_ALLOWED": "AI is enabled for all users.",
17221726
"AI_CONTROL_USER_ALLOWED": "AI is enabled for user ({0}) but disabled for others",
17231727
"AI_CONTROL_ADMIN_DISABLED": "AI access has been disabled by your system administrator",
1724-
"AI_CONTROL_ADMIN_DISABLED_CONTACT": "AI access has been disabled by your system administrator. Please contact {0} for assistance."
1728+
"AI_CONTROL_ADMIN_DISABLED_CONTACT": "AI access has been disabled by your system administrator. Please contact {0} for assistance.",
1729+
"AI_UPSELL_DIALOG_TITLE": "Continue with {0}?",
1730+
"AI_UPSELL_DIALOG_MESSAGE": "You’ve discovered AI-powered edits. To proceed, you’ll need an AI subscription or credits."
17251731
});

src/services/EntitlementsManager.js

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ define(function (require, exports, module) {
3232

3333
const EventDispatcher = require("utils/EventDispatcher"),
3434
AIControl = require("./ai-control"),
35-
Strings = require("strings");
35+
Strings = require("strings"),
36+
StringUtils = require("utils/StringUtils");
3637

3738
const MS_IN_DAY = 24 * 60 * 60 * 1000;
3839
const FREE_PLAN_VALIDITY_DAYS = 10000;
@@ -169,6 +170,117 @@ define(function (require, exports, module) {
169170
};
170171
}
171172

173+
/**
174+
* Get AI is enabled for user, based on his logged in pro-user/trial status.
175+
*
176+
* @returns {Promise<Object>} AI entitlement object with the following shape:
177+
* @returns {Promise<boolean>} entitlement.activated - If true, enable AI features. If false, check upsellDialog.
178+
* @returns {Promise<boolean>} [entitlement.needsLogin] - If true, user needs to login first.
179+
* @returns {Promise<string>} [entitlement.aiBrandName] - The brand name used for AI. Eg: `Phoenix AI`
180+
* @returns {Promise<string>} [entitlement.buyURL] - URL to subscribe/purchase if not activated. Can be null if AI
181+
* is not purchasable.
182+
* @returns {Promise<string>} [entitlement.upgradeToPlan] - Plan name that includes AI entitlement
183+
* @returns {Promise<number>} [entitlement.validTill] - Timestamp when entitlement expires (if from server)
184+
* @returns {Promise<Object>} [entitlement.upsellDialog] - Dialog configuration if user needs to be shown an upsell.
185+
* Only present when activated is false.
186+
* @returns {Promise<string>} [entitlement.upsellDialog.title] - Dialog title
187+
* @returns {Promise<string>} [entitlement.upsellDialog.message] - Dialog message
188+
* @returns {Promise<string>} [entitlement.upsellDialog.buyURL] - Purchase URL. If present, dialog shows
189+
* "Get AI Access" button. If absent, shows only OK button.
190+
*
191+
* @example
192+
* const aiEntitlement = await EntitlementsManager.getAIEntitlement();
193+
* if (aiEntitlement.activated) {
194+
* // Enable AI features
195+
* enableAIFeature();
196+
* } else if (aiEntitlement.upsellDialog) {
197+
* // Show upsell dialog when user tries to use AI
198+
* promotions.showAIUpsellDialog(aiEntitlement);
199+
* }
200+
*/
201+
async function getAIEntitlement() {
202+
if(!isLoggedIn()) {
203+
return {
204+
needsLogin: true,
205+
activated: false,
206+
upsellDialog: {
207+
title: Strings.AI_LOGIN_DIALOG_TITLE,
208+
message: Strings.AI_LOGIN_DIALOG_MESSAGE
209+
// no buy url as it is a sign in hint. only ok button will be there in this dialog.
210+
}
211+
};
212+
}
213+
const aiControlStatus = await EntitlementsManager.getAIControlStatus();
214+
if(!aiControlStatus.aiEnabled) {
215+
return {
216+
activated: false,
217+
upsellDialog: {
218+
title: Strings.AI_DISABLED_DIALOG_TITLE,
219+
// Eg. AI is disabled by school admin/root user.
220+
// no buyURL as ai is disabled explicitly. only ok button will be there in this dialog.
221+
message: aiControlStatus.message || Strings.AI_CONTROL_ADMIN_DISABLED
222+
}
223+
};
224+
}
225+
const defaultAIBrandName = brackets.config.ai_brand_name,
226+
defaultPurchaseURL = brackets.config.purchase_url,
227+
defaultUpsellTitle = StringUtils.format(Strings.AI_UPSELL_DIALOG_TITLE, defaultAIBrandName);
228+
const entitlements = await _getEffectiveEntitlements();
229+
if(!entitlements || !entitlements.entitlements || !entitlements.entitlements.aiAgent) {
230+
return {
231+
activated: false,
232+
aiBrandName: defaultAIBrandName,
233+
buyURL: defaultPurchaseURL,
234+
upgradeToPlan: defaultAIBrandName,
235+
upsellDialog: {
236+
title: defaultUpsellTitle,
237+
message: Strings.AI_UPSELL_DIALOG_MESSAGE,
238+
buyURL: defaultPurchaseURL
239+
}
240+
};
241+
}
242+
const aiEntitlement = entitlements.entitlements.aiAgent;
243+
// entitlements.entitlements.aiAgent: {
244+
// activated: boolean,
245+
// aiBrandName: string,
246+
// subscribeURL: string,
247+
// upgradeToPlan: string,
248+
// validTill: number,
249+
// upsellDialog: {
250+
// title: "if activated is false, server can send a custom upsell dialog to show",
251+
// message: "this is the message to show",
252+
// buyURL: "if this url is present from server, this will be shown to as buy link"
253+
// }
254+
// }
255+
256+
if(aiEntitlement.activated) {
257+
return {
258+
activated: true,
259+
aiBrandName: aiEntitlement.aiBrandName,
260+
buyURL: aiEntitlement.subscribeURL,
261+
upgradeToPlan: aiEntitlement.upgradeToPlan,
262+
validTill: aiEntitlement.validTill
263+
// no upsellDialog, as it need not be shown.
264+
};
265+
}
266+
267+
const upsellTitle = StringUtils.format(Strings.AI_UPSELL_DIALOG_TITLE,
268+
aiEntitlement.aiBrandName || defaultAIBrandName);
269+
const upsellDialog = aiEntitlement.upsellDialog || {};
270+
return {
271+
activated: false,
272+
aiBrandName: aiEntitlement.aiBrandName,
273+
buyURL: aiEntitlement.subscribeURL || defaultPurchaseURL,
274+
upgradeToPlan: aiEntitlement.upgradeToPlan,
275+
validTill: aiEntitlement.validTill,
276+
upsellDialog: {
277+
title: upsellDialog.title || upsellTitle,
278+
message: upsellDialog.message || Strings.AI_UPSELL_DIALOG_MESSAGE,
279+
buyURL: upsellDialog.buyURL || aiEntitlement.subscribeURL || defaultPurchaseURL
280+
}
281+
};
282+
}
283+
172284
let inited = false;
173285
function init() {
174286
if(inited){
@@ -209,5 +321,6 @@ define(function (require, exports, module) {
209321
EntitlementsManager.getTrialRemainingDays = getTrialRemainingDays;
210322
EntitlementsManager.getRawEntitlements = getRawEntitlements;
211323
EntitlementsManager.getLiveEditEntitlement = getLiveEditEntitlement;
324+
EntitlementsManager.getAIEntitlement = getAIEntitlement;
212325
EntitlementsManager.EVENT_ENTITLEMENTS_CHANGED = EVENT_ENTITLEMENTS_CHANGED;
213326
});

src/services/login-service.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -547,12 +547,17 @@ define(function (require, exports, module) {
547547
* upgradeToPlan: string, // Plan name that includes this entitlement
548548
* validTill: number // Timestamp when entitlement expires
549549
* },
550-
* liveEditAI: {
550+
* aiAgent: {
551551
* activated: boolean,
552+
* aiBrandName: string,
552553
* subscribeURL: string,
553-
* purchaseCreditsURL: string, // URL to purchase AI credits
554554
* upgradeToPlan: string,
555-
* validTill: number
555+
* validTill: number,
556+
* upsellDialog: {
557+
* title: "if activated is false, server can send a custom upsell dialog to show",
558+
* message: "this is the message to show",
559+
* buyURL: "if this url is present from server, this will be shown to as buy link"
560+
* }
556561
* }
557562
* }
558563
* }
@@ -612,6 +617,8 @@ define(function (require, exports, module) {
612617
trialDaysRemaining: trialDaysRemaining,
613618
entitlements: {
614619
...serverEntitlements.entitlements,
620+
// below we only override things we grant in trial. AI which is not part of trial
621+
// is always server injected. the EntitlementsManager will resolve it appropriately.
615622
liveEdit: {
616623
activated: true,
617624
subscribeURL: brackets.config.purchase_url,
@@ -633,6 +640,8 @@ define(function (require, exports, module) {
633640
isInProTrial: true,
634641
trialDaysRemaining: trialDaysRemaining,
635642
entitlements: {
643+
// below we only override things we grant in trial. AI which is not part of trial
644+
// is always server injected. the EntitlementsManager will resolve it appropriately.
636645
liveEdit: {
637646
activated: true,
638647
subscribeURL: brackets.config.purchase_url,

src/services/pro-dialogs.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
*/
2727

2828
define(function (require, exports, module) {
29+
const KernalModeTrust = window.KernalModeTrust;
30+
if(!KernalModeTrust){
31+
// integrated extensions will have access to kernal mode, but not external extensions
32+
throw new Error("pro-dialogs.js should have access to KernalModeTrust. Cannot boot without trust ring");
33+
}
34+
2935
const proTitle = `<span class="phoenix-pro-title">
3036
<span class="pro-plan-name">${brackets.config.main_pro_plan}</span>
3137
<i class="fa-solid fa-feather orange-gold" style="margin-left: 3px;"></i>
@@ -165,6 +171,49 @@ define(function (require, exports, module) {
165171
}
166172
}
167173

174+
function showAIUpsellDialog(getAIEntitlementResponse) {
175+
// Only show dialog if upsellDialog field is present
176+
if (!getAIEntitlementResponse || !getAIEntitlementResponse.upsellDialog) {
177+
return;
178+
}
179+
180+
const upsellDialog = getAIEntitlementResponse.upsellDialog;
181+
const title = upsellDialog.title;
182+
const message = upsellDialog.message;
183+
const buyURL = upsellDialog.buyURL;
184+
const needsLogin = getAIEntitlementResponse.needsLogin;
185+
186+
let buttons;
187+
if (needsLogin || buyURL) {
188+
// Show primary action button and Cancel
189+
const primaryButtonText = needsLogin ? Strings.PROFILE_SIGN_IN : Strings.AI_LOGIN_DIALOG_BUTTON;
190+
buttons = [
191+
{ className: Dialogs.DIALOG_BTN_CLASS_NORMAL, id: Dialogs.DIALOG_BTN_CANCEL, text: Strings.CANCEL },
192+
{ className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: "mainAction", text: primaryButtonText }
193+
];
194+
} else {
195+
// Show only OK button (for disabled AI messages)
196+
buttons = [
197+
{ className: Dialogs.DIALOG_BTN_CLASS_PRIMARY, id: Dialogs.DIALOG_BTN_OK, text: Strings.OK }
198+
];
199+
}
200+
201+
Dialogs.showModalDialog(Dialogs.DIALOG_ID_INFO, title, message, buttons).done(function (id) {
202+
Metrics.countEvent(Metrics.EVENT_TYPE.AI, "dlgUpsell", "show");
203+
if(id === 'mainAction') {
204+
if (needsLogin) {
205+
Metrics.countEvent(Metrics.EVENT_TYPE.AI, "dlgUpsell", "signIn");
206+
KernalModeTrust.EntitlementsManager.loginToAccount();
207+
} else {
208+
Metrics.countEvent(Metrics.EVENT_TYPE.AI, "dlgUpsell", "buyClick");
209+
Phoenix.app.openURLInDefaultBrowser(buyURL);
210+
}
211+
} else {
212+
Metrics.countEvent(Metrics.EVENT_TYPE.AI, "dlgUpsell", id);
213+
}
214+
});
215+
}
216+
168217
if (Phoenix.isTestWindow) {
169218
window._test_pro_dlg_login_exports = {
170219
setFetchFn: function _setDdateNowFn(fn) {
@@ -175,6 +224,7 @@ define(function (require, exports, module) {
175224

176225
exports.showProTrialStartDialog = showProTrialStartDialog;
177226
exports.showProUpsellDialog = showProUpsellDialog;
227+
exports.showAIUpsellDialog = showAIUpsellDialog;
178228
exports.UPSELL_TYPE_PRO_TRIAL_ENDED = UPSELL_TYPE_PRO_TRIAL_ENDED;
179229
exports.UPSELL_TYPE_GET_PRO = UPSELL_TYPE_GET_PRO;
180230
exports.UPSELL_TYPE_LIVE_EDIT = UPSELL_TYPE_LIVE_EDIT;

0 commit comments

Comments
 (0)