Skip to content
Open
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
8 changes: 4 additions & 4 deletions pluginTester/scripts/framework/app.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pluginTester/scripts/framework/web.min.js

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions pluginTester/styles/appStyle.css
Original file line number Diff line number Diff line change
Expand Up @@ -1792,6 +1792,13 @@ textarea.md-input{
.iap-safe-padding {
padding-top: 20vh;
}

.store-not-accessible .iap-safe-padding {
padding-top: unset;
}
#launcherIap .store-not-accessible .iap-safe-padding {
padding-top: var(--bf-safe-area-inset-top) !important;
}

.iap-subscriptions:not(.iapMessage) #iap-wysiwyg h1,
.iap-subscriptions:not(.iapMessage) #iap-wysiwyg h2,
Expand Down Expand Up @@ -2946,6 +2953,22 @@ html:not([type="app"]) .snackbar-hero{
max-width: calc(280px + 900px) !important;
width: calc(280px + 900px) !important;
}
/* Give the IAP "store not accessible" panel the same content-column box as #appBody/#plugin,
so on wide screens its content is constrained to the 900px column instead of the full viewport.
The panel is position:fixed (from .iapMessage), so left:0/right:0 + margin:auto centers the
280+900 block; the 280px menu gutter is handled by .iap-safe-padding on the inner copy-holder. */
[type="app"] body:not(.full) .store-not-accessible {
left: 0;
right: 0;
margin: 0 auto;
width: calc(280px + 900px);
max-width: calc(280px + 900px);
}
/* Push the panel down when a footer menu is present so it isn't hidden behind it.
has-footer-menu lives on the #plugin ancestor, not on .store-not-accessible itself. */
[type="app"] #plugin.has-footer-menu .store-not-accessible {
margin-top: 66px;
}
[type="app"] body.full.menu-disabled #plugin,
[type="app"] body.full.menu-disabled .bookmarks-container{
min-width: 100vw !important;
Expand Down Expand Up @@ -5252,6 +5275,123 @@ body:not(.splash) #splashScreenPreview {
background-size: cover;
}

#pluginStudioLoadingOverlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100vw;
height: var(--bf-full-app-height, 100vh);
z-index: 999999999999;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
opacity: 1;
-webkit-transition: opacity 250ms ease;
transition: opacity 250ms ease;
}

#pluginStudioLoadingOverlay.hiding {
opacity: 0;
pointer-events: none;
}

/* square loader mirroring the control panel's plugin studio loading animation */
#pluginStudioLoadingOverlay .plugin-studio-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

#pluginStudioLoadingOverlay .ai-animation {
position: relative;
width: 75px;
height: 75px;
}

#pluginStudioLoadingOverlay .ai-text {
margin: 0;
padding-top: 16px;
font-size: 15px;
color: #101010B2;
opacity: 0;
-webkit-animation: pluginStudioAiTextFadeIn 0.5s forwards;
animation: pluginStudioAiTextFadeIn 0.5s forwards;
-webkit-animation-delay: 1s;
animation-delay: 1s;
}

#pluginStudioLoadingOverlay .square {
width: 32px;
height: 32px;
background: #1010101A;
border-radius: 6px;
display: flex;
position: absolute;
}

#pluginStudioLoadingOverlay .sq1 {
-webkit-animation: pluginStudioLoaderAnimation 3s infinite;
animation: pluginStudioLoaderAnimation 3s infinite;
}

#pluginStudioLoadingOverlay .sq2 {
-webkit-animation: pluginStudioLoaderAnimation 3s infinite;
animation: pluginStudioLoaderAnimation 3s infinite;
-webkit-animation-delay: 1s;
animation-delay: 1s;
}

#pluginStudioLoadingOverlay .sq3 {
-webkit-animation: pluginStudioLoaderAnimation 3s infinite;
animation: pluginStudioLoaderAnimation 3s infinite;
-webkit-animation-delay: 2s;
animation-delay: 2s;
}

@-webkit-keyframes pluginStudioLoaderAnimation {
0% { -webkit-transform: translate(0); transform: translate(0); }
8.33% { -webkit-transform: translate(40px, 0); transform: translate(40px, 0); }
16.66% { -webkit-transform: translate(40px, 0); transform: translate(40px, 0); }
25% { -webkit-transform: translate(40px, 0); transform: translate(40px, 0); }
33.33% { -webkit-transform: translate(40px, 40px); transform: translate(40px, 40px); }
41.66% { -webkit-transform: translate(40px, 40px); transform: translate(40px, 40px); }
50% { -webkit-transform: translate(40px, 40px); transform: translate(40px, 40px); }
58.33% { -webkit-transform: translate(0, 40px); transform: translate(0, 40px); }
66.66% { -webkit-transform: translate(0, 40px); transform: translate(0, 40px); }
75% { -webkit-transform: translate(0, 40px); transform: translate(0, 40px); }
83.33% { -webkit-transform: translate(0); transform: translate(0); }
91.66% { -webkit-transform: translate(0); transform: translate(0); }
100% { -webkit-transform: translate(0); transform: translate(0); }
}

@keyframes pluginStudioLoaderAnimation {
0% { -webkit-transform: translate(0); transform: translate(0); }
8.33% { -webkit-transform: translate(40px, 0); transform: translate(40px, 0); }
16.66% { -webkit-transform: translate(40px, 0); transform: translate(40px, 0); }
25% { -webkit-transform: translate(40px, 0); transform: translate(40px, 0); }
33.33% { -webkit-transform: translate(40px, 40px); transform: translate(40px, 40px); }
41.66% { -webkit-transform: translate(40px, 40px); transform: translate(40px, 40px); }
50% { -webkit-transform: translate(40px, 40px); transform: translate(40px, 40px); }
58.33% { -webkit-transform: translate(0, 40px); transform: translate(0, 40px); }
66.66% { -webkit-transform: translate(0, 40px); transform: translate(0, 40px); }
75% { -webkit-transform: translate(0, 40px); transform: translate(0, 40px); }
83.33% { -webkit-transform: translate(0); transform: translate(0); }
91.66% { -webkit-transform: translate(0); transform: translate(0); }
100% { -webkit-transform: translate(0); transform: translate(0); }
}

@-webkit-keyframes pluginStudioAiTextFadeIn {
to { opacity: 0.7; }
}

@keyframes pluginStudioAiTextFadeIn {
to { opacity: 0.7; }
}

.action-item .action-item-label {
position: absolute !important;
display: block !important;
Expand Down
4 changes: 2 additions & 2 deletions scripts/_bundles/buildfire_lightcarousel.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/_bundles/buildfire_lightcarousel.min.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions scripts/_bundles/jquery_angular_buildfire_smartcrop.min.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions scripts/buildfire.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,8 @@ var buildfire = {
, 'geo.session._triggerOnSessionWatchChange'
, 'analytics.injectAmplitude'
, 'services.camera.triggerOnPictureFrame'
, 'services.contract._ping'
, 'services.contract._runFunction'
]
, _postMessageHandler: function (e) {
if (e.source === window) {
Expand Down Expand Up @@ -5049,6 +5051,7 @@ var buildfire = {
buildfire.loadScript({ url, scriptId }, () => {
dynamicEngine.expressions.getContext = this._prepareContext; // overwrite the getContext to be suitable for the sdk environment
dynamicEngine.getGlobalSettings = buildfire.getGlobalSettings; // overwrite the getGlobalSettings to be suitable for the sdk environment
this._prepareContractService(dynamicEngine); // overwrite contracts.invoke to run through the contract service
_executeDynamicEngineQueue(dynamicEngine);
});
}
Expand All @@ -5059,6 +5062,42 @@ var buildfire = {
this._dynamicEngineQueue = [];
};
},
/**
* _prepareContractService
* @description Overwrite dynamicEngine.contracts.invoke so context.contract expressions run through
* the contract service. The service (buildfire.services.contract) may not be loaded in a generic
* content widget, so it is loaded on demand. loadScript() calls back immediately for an in-flight
* <script> that hasn't executed yet, so concurrent invokes must share ONE load and queue until it's
* ready — otherwise every invoke but the first is called back before the service exists and fails.
* @private
*/
_prepareContractService(dynamicEngine) {
if (!dynamicEngine.contracts) return;
let contractServiceQueue = null;
const contractServiceUrl = buildfire.getContext().type == 'control'
? '../../../../scripts/buildfire/services/contract/contract.js'
: '../../../scripts/buildfire/services/contract/contract.js';
const hasContractService = () => !!(buildfire.services && buildfire.services.contract && buildfire.services.contract.invoke);
const ensureContractServiceLoaded = (callback) => {
if (hasContractService()) return callback(null);
if (contractServiceQueue) return contractServiceQueue.push(callback); // a load is already in flight
contractServiceQueue = [callback];
buildfire.loadScript({ url: contractServiceUrl, scriptId: 'buildfireContractService' }, () => {
const queued = contractServiceQueue;
contractServiceQueue = null;
const err = hasContractService() ? null : 'contract service is not available in this widget';
queued.forEach((readyCallback) => readyCallback(err));
});
};
// options is already { instanceId, functionName, options } — exactly what
// buildfire.services.contract.invoke wants, so pass it straight through
dynamicEngine.contracts.invoke = (options, callback) => {
ensureContractServiceLoaded((err) => {
if (err) return callback(err);
buildfire.services.contract.invoke(options, callback);
});
};
},
/**
* evaluate
* @description evaluate a JS expression result within a defined context
Expand Down
4 changes: 2 additions & 2 deletions scripts/buildfire.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion scripts/buildfire.min.js.map

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions scripts/buildfire/services/contract/contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
if (typeof (buildfire) === 'undefined') throw ('please add buildfire.js first to use BuildFire services');

if (typeof (buildfire.services) === 'undefined') buildfire.services = {};

buildfire.services.contract = {
/**
* List the operations registered by a plugin's contract.
* Reads the target plugin's plugin.contract.json and returns its operations.
* @param {Object} options At least one of pluginId, instanceId, or folderName must be provided.
* @param {String} [options.pluginId] The id (type token) of the plugin that owns the contract.
* @param {String} [options.instanceId] Instance id to scope the lookup to a specific instance.
* @param {String} [options.folderName] The plugin's folder name. When provided, the app fetches the
* contract directly and skips the instance lookup.
* @param {String} [options.host] Optional filter — return only operations whose host matches ("plugin" | "generic").
* @param {Function} callback Node-style callback (err, res) invoked with the list of operations.
*/
list: function (options, callback) {
if (!callback || typeof (callback) !== 'function') {
throw 'callback function is mandatory';
}

options = options || {};

if (typeof (options) !== 'object') {
return callback({ code: 'error', message: 'options must be an object' }, null);
}

if (!options.pluginId && !options.instanceId && !options.folderName) {
return callback({ code: 'error', message: 'pluginId, instanceId, or folderName is required' }, null);
}

var packet = new Packet(null, 'contract.list', options);
buildfire._sendPacket(packet, callback);
},

/**
* Invoke an operation by name. The app dispatches on the operation's declared "type":
* type: "function" → runs in the target plugin's iframe
* type: "datastore" → runs in the app (builds the query from the operation's context)
* The caller does not need to know which; it just names the operation.
* @param {Object} options
* @param {String} options.instanceId The target plugin instance id (pins the specific instance whose
* data/frame the operation runs against).
* @param {String} [options.fid] Optional — if you already prepared the frame via use(), pass its fid to
* run the function directly (skips instance lookup + contract fetch).
* @param {String} [options.pluginId] Optional plugin type token; passing it with folderName skips the instance lookup.
* @param {String} [options.folderName] Optional plugin folder name; passing it with pluginId skips the instance lookup.
* @param {String} options.functionName The operation to invoke.
* @param {Object} [options.options] Parameters passed to the operation.
* @param {Function} callback Node-style callback (err, res) invoked with the result.
*/
invoke: function (options, callback) {
if (!callback || typeof (callback) !== 'function') {
throw 'callback function is mandatory';
}

options = options || {};

if (typeof (options) !== 'object') {
return callback({ code: 'error', message: 'options must be an object' }, null);
}

if (!options.functionName) {
return callback({ code: 'error', message: 'functionName is required' }, null);
}

if (!options.instanceId && !options.fid) {
return callback({ code: 'error', message: 'instanceId or fid is required' }, null);
}

var packet = new Packet(null, 'contract.invoke', options);
buildfire._sendPacket(packet, callback);
},

/**
* Prepare (create or reuse) a server plugin frame ahead of time, so a later invoke of a
* function-type operation on that instance runs against an already-loaded frame.
* @param {Object} options
* @param {String} options.instanceId The instance id of the server plugin to prepare.
* @param {Function} callback Node-style callback (err, data) where data holds the frame handle (e.g. { fid }).
*/
use: function (options, callback) {
if (!callback || typeof (callback) !== 'function') {
throw 'callback function is mandatory';
}

options = options || {};

if (typeof (options) !== 'object') {
return callback({ code: 'error', message: 'options must be an object' }, null);
}

if (!options.instanceId) {
return callback({ code: 'error', message: 'instanceId is required' }, null);
}

var packet = new Packet(null, 'contract.use', options);
buildfire._sendPacket(packet, callback);
},

/**
* Destroy a server plugin frame previously created (via use, or on demand by invoke),
* tearing down its iframe and freeing its resources.
* @param {Object} options Identifies the frame to destroy: { fid } and/or { instanceId }.
* @param {Function} [callback] Node-style callback (err, res).
*/
destroy: function (options, callback) {
options = options || {};

if (typeof (options) !== 'object') {
if (callback) return callback({ code: 'error', message: 'options must be an object' }, null);
return;
}

if (!options.fid && !options.instanceId) {
if (callback) return callback({ code: 'error', message: 'fid or instanceId is required' }, null);
return;
}

var packet = new Packet(null, 'contract.destroy', options);
buildfire._sendPacket(packet, callback);
},

/* --------------------------------------------------------------------------
* Server side: a plugin that EXPOSES a contract implements its operations on
* window.widgetContract (in plugin.contract.js). These two handlers are the thin
* receiver the app posts into the loaded frame — _ping to know the frame is ready,
* _runFunction to run the requested method and relay its callback to the caller.
* ------------------------------------------------------------------------ */

/**
* Readiness probe. The app pings a freshly loaded server frame until this responds,
* so a function call is never posted before the frame's SDK is ready.
*/
_ping: function (options, callback) {
if (callback) callback(null, { ready: true });
},

/**
* Invoked by the app to run one of this plugin's contract operations.
* Just calls window.widgetContract[functionName](params, callback) and lets the
* plugin's own callback flow back to the caller.
* @param {Object} options { functionName, options } — the function to run and its params.
* @param {Function} callback Node-style callback relayed back to the caller plugin.
*/
_runFunction: function (options, callback) {
options = options || {};
if (!callback) callback = function () {};
if (!options.functionName) {
return callback('functionName is required');
}
var operations = (typeof (window) !== 'undefined' && window.widgetContract) ? window.widgetContract : {};
var handler = operations[options.functionName];
if (typeof (handler) !== 'function') {
return callback('contract function "' + options.functionName + '" is not implemented on Widget Contract');
}
try {
handler(options.options || {}, callback);
} catch (e) {
callback('error executing contract function: ' + (e && e.message ? e.message : e));
}
}
};
Loading