As part of an early access program (EAP), you can now build standard and advanced editions of Marketplace apps in the cloud that are Paid via Atlassian. During this stage of the EAP, participants can build and test editions of non-production apps in development, staging, and production environments.
EAP features are available to select users for demand, viability, and suitability testing. Releasing a feature as an EAP helps us gather insights that inform future development decisions.
You can create standard and advanced editions of your app under a single Marketplace listing. This is handy if you're looking to expand the presence of your app in the cloud market.
App editions only apply to existing Paid via Atlassian (PvA) apps in the cloud.
The content on this page only applies to Forge apps. For Connect apps, see Editions for Connect apps instead.
Before creating editions of your app, we assume you've already gone through Phase 1 of a three-phase process, which is the planning phase.
Phase 2 involves the technical aspects of creating editions and is comprised of the following steps:
App editions are separate from parent product editions. For instance, if you're on a Jira Software Cloud edition, you can choose to purchase an advanced edition of an app.
You may find some content specific to app editions that do not apply to partners and developers outside of the EAP. Similarly, we may link to pages outside of this EAP documentation that may not have any content about app editions.
We recommend that you list the features that would go into the standard and advanced editions of your app. This way, you have a list you can refer to while going through your app code in the next step.
Refer to the customer segmentation and strategy you will have already prepared after going through this guide in the Planning phase section.
Now that you have a list of the features for your standard and advanced editions, you need to go through your app code to introduce conditional logic for each of these features.
It's important to note that editions are only available for cloud customers who are on the improved billing experience. This means, however, that customers who are on the original billing experience will not have access to editions until they are migrated to the improved billing experience.
You'll need to continue supporting such customers as you're currently doing today. Our APIs
will only share capabilitySet
information (more on that below) for customers on the improved
billing experience. You must update your app code to handle both scenarios.
If your app has a frontend, the content in this section applies to you.
A context object is an object that contains contextual information about the current environment in which the app is running. The data available depends on the module in which your app is used.
1 2interface Context { accountId?: string; cloudId?: string; extension: ExtensionData; license?: LicenseDetails; localId: string; locale: string; moduleKey: string; siteUrl: string; timezone: string; } interface ExtensionData { [k: string]: any; } interface LicenseDetails { active: boolean; billingPeriod: string; capabilitySet: 'capabilityStandard' | 'capabilityAdvanced' | null; ccpEntitlementId: string; ccpEntitlementSlug: string; isEvaluation: boolean; subscriptionEndDate: string | null; supportEntitlementNumber: string | null; trialEndDate: string | null; type: string; }
license
is undefined
for free apps, apps
not listed on the Atlassian Marketplace, and apps in development and staging environments.
See the LicenseDetails
type for what information is available.manifest.yml
file.1 2{ "localId": "localId", "cloudId": "abcd1234-1111-1111-1111-abcdef123456", "environmentId": "11111111-1111-1111-0111-111111111111", "environmentType": "DEVELOPMENT", "moduleKey": "module-key-name", "siteUrl": "https://atlassian.net", "extension": { "type": "jira:globalPage" }, "accountId": "abcde123456", "license": { "active": true, "type": "commercial", "supportEntitlementNumber": null, "trialEndDate": null, "subscriptionEndDate": null, "isEvaluation": null, "billingPeriod": "MONTHLY", "ccpEntitlementId": null, "ccpEntitlementSlug": null, "capabilitySet": "capabilityAdvanced" }, "timezone": "Pacific/Auckland", "locale": "en_GB" }
Both Custom UI and UI Kit make use of the
getContext()
method from the @forge/bridge
library,
which returns a context object that includes the LicenseDetails
object, which in turn, contains license information.
As part of the EAP, we're introducing capabilitySet
as an additional property or constant to the
license information in the LicenseDetails
object. This is where you should introduce conditional
logic to the frontend functions of your app accordingly.
The conditional logic may look like this:
1 2import { view } from "@forge/bridge"; const context = await view.getContext(); if (context.license.capabilitySet !== "capabilityAdvanced") { console.log("App is not licensed for advanced features"); return; } // App is licensed for advanced features... continue to render...
Alternatively, in UI Kit, the useProductContext
method
from the @forge/react
package will retrieve the same context object with license information as the method above.
1 2import { useProductContext } from "@forge/react"; const context = useProductContext(); if (context.license.capabilitySet !== "capabilityAdvanced") { console.log("App is not licensed for advanced features"); return; } // App is licensed for advanced features... continue to render...
If your app uses Forge functions, the content in this section applies to you.
Forge native node.js
runtime provides
appContext that's similar to the
getContext()
method used by both Custom UI and UI Kit in the frontend.
As part of the EAP, we're introducing the license
property within that context for Forge functions,
which will evaluate the license state and capabilitySet
defined for each edition. This is where
you should introduce conditional logic to the Forge functions of your app accordingly.
1 2import { getAppContext } from "@forge/api"; const { appAri, appVersion, environmentAri, environmentType, invocationId, installationAri, moduleKey, license } = getAppContext(); console.log(appAri.toString()); // 'ari:cloud:ecosystem::app/00000000-0000-0000-0000-000000000000' console.log(appAri.appId); // '00000000-0000-0000-0000-000000000000' console.log(appVersion); // '1.0.0' console.log(environmentAri.toString()); // 'ari:cloud:ecosystem::environment/00000000-0000-0000-0000-000000000000/11111111-1111-1111-0111-111111111111' console.log(environmentAri.environmentId); // '11111111-1111-1111-0111-111111111111' console.log(environmentType); // 'DEVELOPMENT' console.log(invocationId); // '33333333-3333-3333-0333-333333333333' console.log(installationAri.toString()); // 'ari:cloud:ecosystem::installation/22222222-2222-2222-0222-222222222222' console.log(installationAri.installationId); // '22222222-2222-2222-0222-222222222222' console.log(moduleKey); // 'hello-world' console.log(JSON.stringify(license)); //{"isActive":true,"billingPeriod":"MONTHLY","ccpEntitlementId":"NULL","ccpEntitlementSlug":"NULL","isEvaluation":"NULL","subscriptionEndDate":"NULL","supportEntitlementNumber":"NULL","trialEndDate":"NULL","type":"commercial"}
The conditional logic may look like this:
1 2import { getAppContext } from "@forge/api"; const { appAri, appVersion, environmentAri, environmentType, invocationId, installationAri, moduleKey, license } = getAppContext(); if (license.capabilitySet !== "capabilityAdvanced") { console.log("App is not licensed for advanced features"); return; } // App is licensed for advanced features... continue...
If your app uses Forge remote functions, the content in this section applies to you.
Requests to your remote backend will include a Forge Invocation Token (FIT) as a bearer token
in the authorization header. The FIT includes important information about the invocation context,
which will now include the license
property. This license
property will evaluate the license state
and capabilitySet
defined for each edition.
Validate the FIT token and extract the license information from the payload to add your conditional data:
1 2export const validateContextToken = async (invocationToken, appId) => { const jwksUrl = 'https://forge.cdn.prod.atlassian-dev.net/.well-known/jwks.json'; const JWKS = jose.createRemoteJWKSet(new URL(jwksUrl)); const payload = await jose.jwtVerify(invocationToken, JWKS, {audience: appId}); return payload; } const payload = validateContextToken(invocationToken, appId) if(payload.app.license.capabilitySet !== "capabilityAdvanced") { console.log("App is not licensed for advanced features"); return; }
Ensure you've already done the following tasks before continuing with this section:
As part of the EAP, we're modifiying the forge install
command in the Forge CLI so you can ensure
your editions are behaving according to the app's conditional logic. This modification lets you
test apps under various licensing conditions without needing a license itself.
Note, you can only test editions in either DEVELOPMENT
or STAGING
environments. Testing
editions in PRODUCTION
environment is not supported.
To test your editions:
Navigate to the app's top-level directory and deploy your app by running:
1 2forge deploy
Run one of the following forge install
commands:
If you're testing the advanced edition, run:
1 2forge install --environment development --license advanced
If you're testing the standard edition, run:
1 2forge install --environment development --license standard
Note, if you're testing the edition in staging, change the environment value
from development
to staging
.
Verify that the edition you're testing is behaving as intended for either the advanced
or
standard
state.
Check out this video outlining the journey you'll take when building editions for your Forge apps.
For existing installations of an app, app editions are nonexistent until a customer switches from
one edition type to another. For example, when an app customer decides to upgrade from standard to
advanced edition, it’s only at this time that the edition itself will be set and will become available
to the app. Because of this, the value for capabilitySet
will not be returned for customers
where the edition is still nonexistent.
To handle this, when introducing conditional logic in your app code, we recommend to apply logic on either:
capabilitySetAdvanced
for features that should only be visible for customers
`on advanced editions, orcapabilitySetStandard
or the absence of app editions, which is effectively
for customers on standard editions.Rate this page: