From aa5f88637c2e987574e3b7f4a788eabf2b1175b7 Mon Sep 17 00:00:00 2001 From: David Durman Date: Fri, 9 Feb 2024 16:14:09 +0100 Subject: [PATCH] First commit --- .firebaserc | 5 + .github/workflows/firebase-hosting-merge.yml | 21 ++++ .gitignore | 3 + firebase.json | 16 +++ github-mark.svg | 1 + index.html | 75 +++++++++++++ main.js | 108 +++++++++++++++++++ style.css | 83 ++++++++++++++ yoursaas.svg | 8 ++ 9 files changed, 320 insertions(+) create mode 100644 .firebaserc create mode 100644 .github/workflows/firebase-hosting-merge.yml create mode 100644 .gitignore create mode 100644 firebase.json create mode 100644 github-mark.svg create mode 100644 index.html create mode 100644 main.js create mode 100644 style.css create mode 100644 yoursaas.svg diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..1f93593 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "am-demo-embedded-integrations" + } +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..3ed47c5 --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,21 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +'on': + push: + branches: + - main +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: >- + ${{ secrets.FIREBASE_SERVICE_ACCOUNT_AM_DEMO_EMBEDDED_INTEGRATIONS + }} + channelId: live + projectId: am-demo-embedded-integrations diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae9e7d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Firebase cache +.firebase/ + diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..b3a5ba8 --- /dev/null +++ b/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": ".", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/github-mark.svg b/github-mark.svg new file mode 100644 index 0000000..37fa923 --- /dev/null +++ b/github-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..241924a --- /dev/null +++ b/index.html @@ -0,0 +1,75 @@ + + + + + + Appmixer Embed Integrations Demo + + + +
+
+ + +

Appmixer Embedded Integrations Demo

+

This demo shows you how to embed Appmixer Integrations in your own web application.

+ +

1. Try the Appmixer Integrations UI

+

The Appmixer Integrations UI allows end-users of your application to browse and activate integrations that you pre-built for them in the Appmixer Studio.

+

Try it yourself and activate an integration by clicking on the integration tile and configuring the integration.

+

+

Note that this integration will run in the context of an Appmixer virtual user that was created for you in the background when you opened this page.

+

The Integration marketplace is rendered in the page natively, using the Appmixer JavaScript SDK. It is not included in an iframe.

+ +

2. Trigger the integration by sending an App Event

+

Once you have activated an integration, you can trigger it by sending an App Event to Appmixer (assuming your integration starts with the OnAppEvent trigger). This is normally done using the Appmixer JavaScript SDK or an HTTP request:

+
+    
+        appmixer.api.sendAppEvent('contact-created', { first: 'David', last: 'Doe' });
+    
+    or:
+    
+        curl -XPOST \
+           -H 'Content-Type: application/json' \
+           -H "Authorization: Bearer VIRTUAL_USER_ACCESS_TOKEN" \
+           -d '{ "first": "David", "last": "Doe" }' \
+           "https://APPMIXER_TENANT_API_URL/plugins/appmixer/utils/appevents/events/contact-created"
+    
+
+

In this demo, you can send an App Event using the helper form below. This is to simulate a real-world scenario where you would send an App Event from your own application code.

+ +
+ + + +
+ + +
+
+
+
+
+
+ + + + + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..bf2da42 --- /dev/null +++ b/main.js @@ -0,0 +1,108 @@ +const QUERY_PARAMS = new URLSearchParams(window.location.search); + +// This should have the pattern https://api.YOUR_TENANT.appmixer.cloud'; +const APPMIXER_API_URL = QUERY_PARAMS.get('apiUrl'); + +let APPMIXER_VIRTUAL_USER_USERNAME = QUERY_PARAMS.get('username'); +let APPMIXER_VIRTUAL_USER_TOKEN; +let DOMAIN_FILTER = QUERY_PARAMS.get('domainFilter'); + +if (APPMIXER_VIRTUAL_USER_USERNAME) { + // If username is provided in the query string of this demo, password must be provided as well. + APPMIXER_VIRTUAL_USER_TOKEN = QUERY_PARAMS.get('token'); +} else { + // Hardcode one. Normally, you would generate this and store in your application code and pass it to the Appmixer SDK. + // Note that Appmixer username can be any, even non-existing, email address. We're using a user ID + // together with a fictional domain name. Appmixer does not send anything to these email addresses. + // They are just used as a virtual user credentials pair. Moreover, the email domain + // allows us to easily share integration templates with a specific group of users + // (An alternative to this is to use the user scopes.) + APPMIXER_VIRTUAL_USER_USERNAME = 'embed-demo-1234@appmixer-embed-integrations.com'; + APPMIXER_VIRTUAL_USER_TOKEN = '4efaa8ec-ddc5-4852-b6cc-cb3039fe17b1'; +} + +if (typeof Appmixer === 'undefined') { + alert('Appmixer SDK not loaded. Are you sure you pointed to the right appmixer.js SDK location?'); +} + +// Appmixer SDK instance. +const appmixer = new Appmixer({ + baseUrl: APPMIXER_API_URL, + theme: { + variables: { colors: { surface: '#f5f5f5' } } + } +}); + +const widgets = { + integrations: null, + wizard: null +}; + +// Learn more about Appmixer virtual users at https://docs.appmixer.com/appmixer/tutorials/appmixer-virtual-users. +async function ensureAppmixerVirtualUser(username, token) { + let auth; + try { + auth = await appmixer.api.authenticateUser(username, token); + appmixer.set('accessToken', auth.token); + } catch (err) { + if (err.response && err.response.status === 403) { + // Virtual user not yet created in Appmixer. Create one. + try { + auth = await appmixer.api.signupUser(username, token); + appmixer.set('accessToken', auth.token); + } catch (err) { + alert('Something went wrong creating a virtual user. ' + err.message); + } + } else { + alert('Something went wrong authenticating a virtual user.'); + } + } +} + +function createWidgets() { + // Create Integrations Page widget. + widgets.integrations = appmixer.ui.Integrations({ + el: '#appmixer-integrations-marketplace', + options: { + showHeader: true, + customFilter: [{ + // Show only integration templates shared with users in this demo app. + ...(DOMAIN_FILTER && { 'sharedWith.0.domain': DOMAIN_FILTER }), + type: 'integration' + }, { + userId: appmixer.get('user').id, + templateId: '>0' + //type: 'integration-instance' // Uncomment and remove line above once v6 is released. + }] + } + }); + widgets.integrations.on('integration:create', templateId => { + widgets.wizard.close(); + widgets.wizard.set('flowId', templateId); + widgets.wizard.open(); + }); + widgets.integrations.on('integration:edit', integrationId => { + widgets.wizard.close(); + widgets.wizard.set('flowId', integrationId); + widgets.wizard.open(); + }); + widgets.wizard = appmixer.ui.Wizard(); + widgets.wizard.on('flow:start-after', () => widgets.integrations.reload()); + widgets.wizard.on('flow:remove-after', () => { + widgets.integrations.reload(); + widgets.wizard.close(); + }); +} + +async function main() { + await ensureAppmixerVirtualUser(APPMIXER_VIRTUAL_USER_USERNAME, APPMIXER_VIRTUAL_USER_TOKEN); + createWidgets(); + widgets.integrations.open(); + + document.querySelector('.app-event-form').addEventListener('submit', (evt) => { + evt.preventDefault(); + appmixer.api.sendAppEvent(document.querySelector('.app-event-event').value, JSON.parse(document.querySelector('.app-event-data').value)); + }); +} + +main(); diff --git a/style.css b/style.css new file mode 100644 index 0000000..3f10355 --- /dev/null +++ b/style.css @@ -0,0 +1,83 @@ +body { + font-family: "IBM Plex Sans", "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif; +} +.container { + display: flex; + margin: 20px; +} +.guide { + flex: 2; + padding: 50px; + position: relative; +} +.app { + flex: 2; + position: relative; + border: 2px dashed #F3153C; +} +#yoursaas { + position: absolute; + right: 3px; + top: 10px; + width: 150px; +} +#github-link img { + position: absolute; + left: 50px; + top: 20px; + width: 30px; +} +.guide p { + font-size: 16px; + line-height: 1.5; + max-width: 80%; +} +.guide p.small { + font-size: 14px; + color: #666; +} +.guide p b { + color: #007bff; +} +input, select, textarea { + display: block; + box-sizing: border-box; + width: 100%; + border-radius: 0; +} +.app-event-form { + max-width: 400px; + padding: 20px; + border: 1px solid #f5f5f5; + box-shadow: 2px 8px 14px gray; +} +.app-event-form { + display: flex; + flex-direction: column; + gap: 10px; +} +.app-event-form input, +.app-event-form textarea { + padding: 10px; + border: 2px solid #007bff; + border-radius: 5px; + outline: none; +} +.app-event-form input[type="submit"] { + padding: 10px; + border: none; + border-radius: 5px; + background-color: #007bff; + color: white; + cursor: pointer; +} +.app-event-form input[type="submit"]:hover { + background-color: #0056b3; +} +#appmixer-integrations-marketplace { + position: absolute; + inset: 0; +} +.am-integrations { + padding: 50px; +} diff --git a/yoursaas.svg b/yoursaas.svg new file mode 100644 index 0000000..ccb96df --- /dev/null +++ b/yoursaas.svg @@ -0,0 +1,8 @@ + + + + + YOUR SAAS + + + \ No newline at end of file