├── .eslintrc
├── .gitignore
├── .huskyrc
├── .lintstagedrc
├── .npmrc
├── .prettierrc
├── .travis.yml
├── .vercelignore
├── CONTRIBUTING.md
├── Procfile
├── README.md
├── cypress.json
├── cypress
├── globals.d.ts
├── integration
│ ├── admin.spec.js
│ ├── iframe.spec.js
│ ├── react-menu-iframe.spec.js
│ ├── react-menu.spec.js
│ ├── react.spec.js
│ └── vue.spec.js
├── plugins
│ └── index.js
└── support
│ └── index.js
├── demo
├── admin
│ ├── index.html
│ ├── other.html
│ ├── package.json
│ ├── scripts.js
│ └── serve.json
├── angular
│ ├── .gitignore
│ ├── README.md
│ ├── angular.json
│ ├── browserslist
│ ├── package.json
│ ├── src
│ │ ├── app
│ │ │ ├── app-routing.module.ts
│ │ │ ├── app.component.ts
│ │ │ ├── app.module.ts
│ │ │ ├── index.component.ts
│ │ │ └── page.component.ts
│ │ ├── index.html
│ │ ├── main.ts
│ │ ├── polyfills.ts
│ │ └── styles.css
│ ├── tsconfig.app.json
│ └── tsconfig.json
├── fed
│ ├── package.json
│ ├── src
│ │ ├── App.js
│ │ └── index.js
│ └── webpack.config.js
├── host-web-component
│ ├── .eslintrc
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── iframeApp.js
│ │ ├── index.css
│ │ ├── index.js
│ │ └── reactApp.js
├── host
│ ├── .eslintrc
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── Router.js
│ │ ├── bootstrap.js
│ │ ├── images
│ │ │ └── ringcentral.png
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── lib
│ │ │ ├── registry.js
│ │ │ └── useAppRegistry.js
│ │ └── pages
│ │ │ ├── App.js
│ │ │ ├── FederationApp.js
│ │ │ ├── FederationDirectApp.js
│ │ │ ├── Index.js
│ │ │ ├── LoggedInWrapper.js
│ │ │ ├── MenuApp.js
│ │ │ ├── MenuAppIframe.js
│ │ │ └── NotFound.js
│ └── webpack.config.js
├── iframe
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── Router.js
│ │ ├── index.css
│ │ ├── index.js
│ │ ├── lib
│ │ └── index.js
│ │ └── pages
│ │ ├── App.js
│ │ ├── Bar.js
│ │ ├── Foo.js
│ │ ├── Index.js
│ │ ├── Lorem.js
│ │ └── NotFound.js
├── react-menu-iframe
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── Menu.js
│ │ ├── Router.js
│ │ ├── index.js
│ │ └── style.css
├── react-menu
│ ├── .babelrc
│ ├── craco.config.js
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── Menu.js
│ │ ├── Router.js
│ │ ├── index.js
│ │ └── style.css
├── react
│ ├── .babelrc
│ ├── craco.config.js
│ ├── package.json
│ ├── public
│ │ └── index.html
│ └── src
│ │ ├── App.js
│ │ ├── Modal.js
│ │ ├── index.js
│ │ ├── pages
│ │ ├── Groups.js
│ │ └── Users.js
│ │ └── styles.css
└── vue
│ ├── babel.config.js
│ ├── package.json
│ ├── public
│ └── index.html
│ ├── src
│ └── main.js
│ └── vue.config.js
├── lerna.json
├── package.json
├── packages
├── common
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── host-css
│ ├── package.json
│ └── styles.css
├── host-react
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── Application.tsx
│ │ ├── Components.tsx
│ │ └── index.tsx
│ └── tsconfig.json
├── host-web-component
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.tsx
│ └── tsconfig.json
├── host
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── react
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.tsx
│ └── tsconfig.json
├── sync-host
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── sync-iframe
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── IFrameSync.ts
│ │ ├── bundle.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── webpack.config.js
├── sync-react
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.tsx
│ └── tsconfig.json
├── sync-web-component
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.tsx
│ └── tsconfig.json
└── sync
│ ├── README.md
│ ├── package.json
│ ├── src
│ ├── Sync.ts
│ ├── events.ts
│ ├── hash.ts
│ ├── historyListener.ts
│ ├── index.ts
│ └── postMessage.ts
│ ├── tsconfig.json
│ └── webpack.config.js
├── serve.json
├── tsconfig.json
├── vercel.json
└── yarn.lock
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "ringcentral-typescript"
4 | ],
5 | "rules": {
6 | "import/no-default-export": "off",
7 | "import/no-unresolved": "off", // to capture direct deps in TS
8 | "import/no-webpack-loader-syntax": "off", // !raw-loader!./shadow.css
9 | "no-console": "off",
10 | "no-undef": "off", //FIXME @see https://github.com/eslint/typescript-eslint-parser/issues/75
11 | "react/display-name": "off", // export default () => { ... }
12 | "react/sort-comp": "off",
13 | "react/prop-types": "off",
14 | "ringcentral/specified-comment-with-task-id": "warn",
15 | "@typescript-eslint/no-unused-vars": "off",
16 | "@typescript-eslint/explicit-function-return-type": "off",
17 | "@typescript-eslint/no-explicit-any": "off",
18 | "@typescript-eslint/no-var-requires": "off" // conditional require in Externals
19 | },
20 | "env": {
21 | "browser": true,
22 | "mocha": true,
23 | "node": true
24 | },
25 | "settings": {
26 | "react": {
27 | "version": "16.6.0"
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | .cache
4 | .vercel
5 | /.env
6 | lerna-debug*
7 | node_modules
8 | npm-debug*
9 | package-lock.json
10 | package.json.lerna_backup
11 | demo/*/build
12 | demo/*/dist
13 | packages/*/dist
14 | packages/*/es
15 | packages/*/lib
16 | yarn-debug*
17 | yarn-error*
18 | cypress/screenshots
19 | cypress/videos
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "pre-commit": "yarn lint:staged && yarn test:quick"
4 | }
5 | }
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.{ts,tsx,js,jsx}": [
3 | "yarn lint",
4 | "git add"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 | save-exact=true
3 | unsafe-perm=true
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": false,
3 | "printWidth": 120,
4 | "singleQuote": true,
5 | "tabWidth": 4
6 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | dist: xenial
4 |
5 | services:
6 | - xvfb
7 |
8 | addons:
9 | apt:
10 | packages:
11 | - libgconf-2-4
12 |
13 | cache:
14 | yarn: true
15 | directories:
16 | - ~/.cache/Cypress
17 |
18 | node_js: 13.9
19 |
20 | before_install:
21 | - npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN}
22 | - export DISPLAY=:99.0
23 |
24 | before_script:
25 | - DEBUG=eslint:cli-engine yarn lint:all
26 | - yarn build
27 |
28 | deploy:
29 | - provider: script
30 | script: yarn publish:release ${TRAVIS_TAG} --yes
31 | skip_cleanup: true
32 | on:
33 | all_branches: true
34 | condition: $TRAVIS_TAG != "" && $TRAVIS_TAG != *"-"*
35 | repo: ringcentral/web-apps
36 | - provider: script
37 | script: yarn publish:release ${TRAVIS_TAG} --yes --dist-tag next
38 | skip_cleanup: true
39 | on:
40 | all_branches: true
41 | condition: $TRAVIS_TAG == *"-"*
42 | repo: ringcentral/web-apps
43 |
44 | after_success:
45 | - mkdir .vercel
46 | - echo $VERCEL_PROJECT > .vercel/project.json
47 | - if [[ ${TRAVIS_BRANCH} == "master" ]]; then yarn vercel --prod --token=$VERCEL_TOKEN --no-clipboard; fi
--------------------------------------------------------------------------------
/.vercelignore:
--------------------------------------------------------------------------------
1 | .idea
2 | cypress
3 | node_modules
4 | packages
5 |
6 | .env
7 | .eslintrc
8 | .huskyrc
9 | .lintstagedrc
10 | .npmrc
11 | .prettierrc
12 | .travis.yml
13 | CONTRIBUTING.md
14 | cypress.json
15 | lerna.json
16 | lerna-debug.log
17 | package.json
18 | Procfile
19 | README.md
20 | serve.json
21 | tsconfig.json
22 | yarn.lock
23 | yarn-error.log
24 |
25 | # dark magic to keep one file
26 | demo/admin/node_modules/*
27 | demo/admin/node_modules/@ringcentral/web-apps-sync-iframe/node_modules
28 | !demo/admin/node_modules
29 | !demo/admin/node_modules/@ringcentral
30 | !demo/admin/node_modules/@ringcentral/web-apps-sync-iframe
31 | !demo/admin/node_modules/@ringcentral/web-apps-sync-iframe/dist/ringcentral-web-apps-iframe.js
32 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Publishing
2 |
3 | 1. *Canary release* — commit to master, CI will do the rest, alternatively you may publish locally:
4 | ```bash
5 | $ yarn publish:canary
6 | ```
7 |
8 | 2. *Versioned release*:
9 | ```bash
10 | $ yarn prepare:release [-- --yes --no-git-tag-version --no-push]
11 | ```
12 | This command will run `lerna version` to update versions and push to git with appropriate tag, tag will be picked up
13 | by CI and actual publish will happen (`lerna publish`).
14 |
15 | 3. *Manual publish* — run publishing locally, it assumes you already prepared your release:
16 | ```bash
17 | $ yarn publish:fromgit
18 | ```
19 | Keep in mind that CI will fail because it will try to publish on top of your already published tags.
20 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: yarn serve
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "chromeWebSecurity": false,
3 | "experimentalShadowDomSupport": true
4 | }
5 |
--------------------------------------------------------------------------------
/cypress/globals.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/cypress/integration/admin.spec.js:
--------------------------------------------------------------------------------
1 | context('Admin', () => {
2 | beforeEach(() => {
3 | cy.visit('localhost:3000');
4 | cy.get('web-app-react')
5 | .shadow()
6 | .find('a[href*="/admin"]')
7 | .click();
8 | });
9 | it.skip('Basics', () => {
10 | // Travis does not like it...
11 | cy.iframe('#iFrameResizer1')
12 | .contains('other page')
13 | .click();
14 | cy.iframe('#iFrameResizer1')
15 | .contains('index page')
16 | .click();
17 | cy.iframe('#iFrameResizer1')
18 | .contains('other page')
19 | .click();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/cypress/integration/iframe.spec.js:
--------------------------------------------------------------------------------
1 | context('IFrame', () => {
2 | beforeEach(() => {
3 | cy.visit('localhost:3000');
4 | cy.get('web-app-react')
5 | .shadow()
6 | .find('a[href*="/iframe"]')
7 | .click();
8 | });
9 |
10 | it('Navigation to Foo', () => {
11 | cy.iframe('#iFrameResizer1')
12 | .contains('Foo')
13 | .click();
14 | cy.iframe('#iFrameResizer1')
15 | .contains('The standard')
16 | .should('be.visible');
17 | });
18 |
19 | it('Navigation to Bar', () => {
20 | cy.iframe('#iFrameResizer1')
21 | .contains('Bar')
22 | .click();
23 | cy.iframe('#iFrameResizer1')
24 | .contains('BAR')
25 | .should('be.visible');
26 | });
27 |
28 | it('Navigation to Vue', () => {
29 | cy.iframe('#iFrameResizer1')
30 | .contains('Vue')
31 | .click();
32 | cy.get('web-app-vue')
33 | .shadow()
34 | .find('a[href*="/global/groups"]') // FIXME Proper assertion
35 | .should('be.visible');
36 | });
37 |
38 | it('Popup', () => {
39 | cy.wait(500); // redirect
40 |
41 | cy.iframe('#iFrameResizer1')
42 | .contains('Popup')
43 | .click();
44 | cy.get('div.app-popup').should('be.visible');
45 |
46 | cy.get('div.app-popup-bg').click({force: true}); // not visible...
47 | cy.iframe('#iFrameResizer1')
48 | .get('div.modal')
49 | .should('not.exist');
50 | });
51 |
52 | it('Message to host', () => {
53 | cy.iframe('#iFrameResizer1')
54 | .contains('Send message')
55 | .click();
56 | cy.iframe('#iFrameResizer1')
57 | .get('input[name=messages]')
58 | .should('have.value', '[{"fromIframe":"pew"}]');
59 | cy.get('input[name=messages]').should('have.value', '[{"fromIframe":"pew"}]');
60 | });
61 |
62 | it('Message to app', () => {
63 | cy.iframe('#iFrameResizer1').contains('Send message'); // wait for app to load
64 |
65 | cy.get('div.border-success')
66 | .contains('Send message')
67 | .click();
68 | cy.iframe('#iFrameResizer1')
69 | .get('input[name=messages]')
70 | .should('have.value', '[{"toApp":"message to app"}]');
71 | cy.get('input[name=messages]').should('have.value', '[{"toApp":"message to app"}]');
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/cypress/integration/react-menu-iframe.spec.js:
--------------------------------------------------------------------------------
1 | context('React Menu IFrame', () => {
2 | beforeEach(() => {
3 | cy.visit('localhost:3000');
4 | });
5 | it('Navigation propagates to React Menu', () => {
6 | cy.get('web-app-react')
7 | .shadow()
8 | .find('a[href*="/vue"]')
9 | .click();
10 | cy.location('pathname').should('include', '/vue');
11 | cy.iframe('#iFrameResizer0')
12 | .find('a[href*="/vue"]')
13 | .should('have.class', 'active');
14 |
15 | cy.get('web-app-react')
16 | .shadow()
17 | .find('a[href*="/global"]')
18 | .click();
19 | cy.location('pathname').should('include', '/global');
20 | cy.iframe('#iFrameResizer0')
21 | .find('a[href*="/global"]')
22 | .should('have.class', 'active');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/cypress/integration/react-menu.spec.js:
--------------------------------------------------------------------------------
1 | import {iframe} from './utils';
2 |
3 | context('React Menu', () => {
4 | beforeEach(() => {
5 | cy.visit('localhost:3000');
6 | });
7 | it('Navigation propagates to React Menu IFrame', () => {
8 | cy.iframe('#iFrameResizer0')
9 | .contains('Vue')
10 | .click();
11 | cy.location('pathname').should('include', '/vue');
12 | cy.get('web-app-react')
13 | .shadow()
14 | .find('a[href*="/vue"]')
15 | .should('have.class', 'active');
16 |
17 | cy.iframe('#iFrameResizer0')
18 | .contains('IFrame')
19 | .click();
20 | cy.location('pathname').should('include', '/iframe');
21 | cy.get('web-app-react')
22 | .shadow()
23 | .find('a[href*="/iframe"]')
24 | .should('have.class', 'active');
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/cypress/integration/react.spec.js:
--------------------------------------------------------------------------------
1 | context('React', () => {
2 | beforeEach(() => {
3 | cy.visit('localhost:3000');
4 | cy.get('web-app-react')
5 | .shadow()
6 | .find('a[href*="/global"]')
7 | .click();
8 | });
9 |
10 | it('Basic history', () => {
11 | cy.get('a[href*="/groups"]').click();
12 |
13 | cy.location('pathname').should('include', '/groups');
14 |
15 | cy.go('back');
16 | cy.location('pathname').should('not.include', '/groups');
17 | cy.location('pathname').should('include', '/application/apps/global');
18 |
19 | cy.go('forward');
20 | cy.location('pathname').should('include', '/groups');
21 | });
22 |
23 | it('Deep link to Vue', () => {
24 | cy.get('a[href*="/vue"]').click();
25 | cy.location('pathname').should('include', '/vue');
26 | cy.get('web-app-vue')
27 | .shadow()
28 | .find('a[href*="/global/groups"]') // FIXME Proper assertion
29 | .should('be.visible');
30 | });
31 |
32 | it('Popup', () => {
33 | cy.contains('Popup').click();
34 | cy.contains('Understood').click();
35 | cy.get('body').should('not.have.class', 'modal-open');
36 | });
37 |
38 | it('Message to host', () => {
39 | cy.get('div.border-primary')
40 | .contains('Send message')
41 | .click();
42 | cy.get('div.border-primary')
43 | .get('input[name=messages]')
44 | .should('have.value', '[{"toHost":"message to host"}]');
45 | cy.get('div.border-success')
46 | .get('input[name=messages]')
47 | .should('have.value', '[{"toHost":"message to host"}]');
48 | });
49 |
50 | it('Message to app', () => {
51 | cy.get('div.border-primary').contains('Send message'); // wait for app to load
52 |
53 | cy.get('div.border-success')
54 | .contains('Send message')
55 | .click();
56 | cy.get('div.border-primary')
57 | .get('input[name=messages]')
58 | .should('have.value', '[{"toApp":"message to app"}]');
59 | cy.get('div.border-success')
60 | .get('input[name=messages]')
61 | .should('have.value', '[{"toApp":"message to app"}]');
62 | });
63 |
64 | it('Props', () => {
65 | cy.get('div.border-primary').contains('Send message'); // wait for app to load
66 |
67 | cy.contains('Bump').click();
68 | cy.get('div.border-primary')
69 | .get('input[name=authToken]')
70 | .should('have.value', 'set-by-hostX');
71 | cy.get('div.border-success')
72 | .get('input[name=authToken]')
73 | .should('have.value', 'set-by-hostX');
74 |
75 | cy.contains('Bump').click();
76 | cy.get('div.border-primary')
77 | .get('input[name=authToken]')
78 | .should('have.value', 'set-by-hostXX');
79 | cy.get('div.border-success')
80 | .get('input[name=authToken]')
81 | .should('have.value', 'set-by-hostXX');
82 | });
83 |
84 | it('Dynamic import', () => {
85 | cy.get('div.border-primary').contains('import()');
86 | });
87 | });
88 |
--------------------------------------------------------------------------------
/cypress/integration/vue.spec.js:
--------------------------------------------------------------------------------
1 | context('Vue', () => {
2 | beforeEach(() => {
3 | cy.visit('localhost:3000');
4 | cy.get('web-app-react')
5 | .shadow()
6 | .find('a[href*="/vue"]')
7 | .click();
8 | });
9 | it('Deep link to React', () => {
10 | cy.get('web-app-vue')
11 | .shadow()
12 | .find('a[href*="/global/groups"]')
13 | .click();
14 | cy.location('pathname').should('include', '/global');
15 | cy.contains('Deep in Vue').should('be.visible');
16 | });
17 |
18 | it('Deep link to IFrame', () => {
19 | cy.get('web-app-vue')
20 | .shadow()
21 | .find('a[href*="/app/bar"]')
22 | .click();
23 | cy.iframe('#iFrameResizer1')
24 | .contains('BAR')
25 | .should('be.visible');
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {Cypress.PluginConfig}
3 | */
4 | module.exports = (on, config) => {
5 | // `on` is used to hook into various events Cypress emits
6 | // `config` is the resolved Cypress config
7 | };
8 |
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | import 'cypress-iframe';
2 |
--------------------------------------------------------------------------------
/demo/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Admin
5 |
6 |
7 | Admin
8 | Go to other page
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/admin/other.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Admin
5 |
6 |
7 |
8 |
9 | Admin > Other
10 |
11 | Go to index page
12 |
13 |
14 |
15 | The standard Lorem Ipsum passage, used since the 1500s
16 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
17 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
18 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
19 | nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
20 | anim id est laborum."
21 |
22 | Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
23 | "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem
24 | aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
25 | Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni
26 | dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit
27 | amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam
28 | aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
29 | laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea
30 | voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla
31 | pariatur?"
32 |
33 | 1914 translation by H. Rackham
34 | "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I
35 | will give you a complete account of the system, and expound the actual teachings of the great explorer of the
36 | truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it
37 | is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that
38 | are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself,
39 | because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some
40 | great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to
41 | obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure
42 | that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
43 |
44 | Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
45 | "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti
46 | atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique
47 | sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum
48 | facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil
49 | impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor
50 | repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et
51 | voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus,
52 | ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
53 |
54 | 1914 translation by H. Rackham
55 | "On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and
56 | demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain
57 | and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness
58 | of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple
59 | and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents
60 | our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in
61 | certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur
62 | that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these
63 | matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he
64 | endures pains to avoid worse pains."
65 |
66 | The standard Lorem Ipsum passage, used since the 1500s
67 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
68 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
69 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
70 | nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
71 | anim id est laborum."
72 |
73 | Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
74 | "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem
75 | aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
76 | Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni
77 | dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit
78 | amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam
79 | aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
80 | laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea
81 | voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla
82 | pariatur?"
83 |
84 | 1914 translation by H. Rackham
85 | "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I
86 | will give you a complete account of the system, and expound the actual teachings of the great explorer of the
87 | truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it
88 | is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that
89 | are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself,
90 | because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some
91 | great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to
92 | obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure
93 | that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
94 |
95 | Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
96 | "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti
97 | atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique
98 | sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum
99 | facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil
100 | impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor
101 | repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et
102 | voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus,
103 | ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
104 |
105 | 1914 translation by H. Rackham
106 | "On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and
107 | demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain
108 | and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness
109 | of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple
110 | and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents
111 | our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in
112 | certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur
113 | that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these
114 | matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he
115 | endures pains to avoid worse pains."
116 |
117 | The standard Lorem Ipsum passage, used since the 1500s
118 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
119 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
120 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
121 | nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
122 | anim id est laborum."
123 |
124 | Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
125 | "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem
126 | aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.
127 | Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni
128 | dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit
129 | amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam
130 | aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
131 | laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea
132 | voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla
133 | pariatur?"
134 |
135 | 1914 translation by H. Rackham
136 | "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I
137 | will give you a complete account of the system, and expound the actual teachings of the great explorer of the
138 | truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it
139 | is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that
140 | are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself,
141 | because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some
142 | great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to
143 | obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure
144 | that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
145 |
146 | Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
147 | "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti
148 | atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique
149 | sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum
150 | facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil
151 | impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor
152 | repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et
153 | voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus,
154 | ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."
155 |
156 | 1914 translation by H. Rackham
157 | "On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and
158 | demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain
159 | and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness
160 | of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple
161 | and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents
162 | our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in
163 | certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur
164 | that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these
165 | matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he
166 | endures pains to avoid worse pains."
167 |
168 |
169 |
170 |
171 |
--------------------------------------------------------------------------------
/demo/admin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-admin",
3 | "version": "0.6.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "serve -l $REACT_APP_ADMIN_PORT"
7 | },
8 | "dependencies": {
9 | "@ringcentral/web-apps-sync-iframe": "*"
10 | },
11 | "devDependencies": {
12 | "serve": "11.3.2"
13 | },
14 | "workspaces": {
15 | "nohoist": [
16 | "@ringcentral/web-apps-sync-iframe"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/demo/admin/scripts.js:
--------------------------------------------------------------------------------
1 | let sync = new RCApps.IFrameSDK.IFrameSync({
2 | history: 'html5',
3 | id: 'admin',
4 | sendInitialLocation: true,
5 | base: '/demo/admin',
6 | });
7 |
--------------------------------------------------------------------------------
/demo/admin/serve.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/demo/admin/**",
5 | "destination": "/"
6 | }
7 | ]
8 | }
--------------------------------------------------------------------------------
/demo/angular/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events*.json
15 | speed-measure-plugin*.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/demo/angular/README.md:
--------------------------------------------------------------------------------
1 | # DemoAngular
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.0.7.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
28 |
--------------------------------------------------------------------------------
/demo/angular/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "demo-angular": {
7 | "projectType": "application",
8 | "schematics": {},
9 | "root": "",
10 | "sourceRoot": "src",
11 | "prefix": "app",
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist",
17 | "index": "src/index.html",
18 | "main": "src/main.ts",
19 | "polyfills": "src/polyfills.ts",
20 | "tsConfig": "tsconfig.app.json",
21 | "aot": true,
22 | "assets": [
23 | "src/favicon.ico",
24 | "src/assets"
25 | ],
26 | "styles": [
27 | "src/styles.css"
28 | ],
29 | "scripts": []
30 | },
31 | "configurations": {
32 | "production": {
33 | "fileReplacements": [
34 | {
35 | "replace": "src/environments/environment.ts",
36 | "with": "src/environments/environment.prod.ts"
37 | }
38 | ],
39 | "optimization": true,
40 | "outputHashing": "all",
41 | "sourceMap": false,
42 | "extractCss": true,
43 | "namedChunks": false,
44 | "extractLicenses": true,
45 | "vendorChunk": false,
46 | "buildOptimizer": true,
47 | "budgets": [
48 | {
49 | "type": "initial",
50 | "maximumWarning": "2mb",
51 | "maximumError": "5mb"
52 | },
53 | {
54 | "type": "anyComponentStyle",
55 | "maximumWarning": "6kb",
56 | "maximumError": "10kb"
57 | }
58 | ]
59 | }
60 | }
61 | },
62 | "serve": {
63 | "builder": "@angular-devkit/build-angular:dev-server",
64 | "options": {
65 | "browserTarget": "demo-angular:build"
66 | },
67 | "configurations": {
68 | "production": {
69 | "browserTarget": "demo-angular:build:production"
70 | }
71 | }
72 | },
73 | "extract-i18n": {
74 | "builder": "@angular-devkit/build-angular:extract-i18n",
75 | "options": {
76 | "browserTarget": "demo-angular:build"
77 | }
78 | }
79 | }
80 | }
81 | },
82 | "defaultProject": "demo-angular"
83 | }
84 |
--------------------------------------------------------------------------------
/demo/angular/browserslist:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/demo/angular/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-angular",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve --port=$REACT_APP_ANGULAR_PORT",
7 | "build": "ng build --base-href /demo/angular/dist/"
8 | },
9 | "private": true,
10 | "dependencies": {
11 | "@angular/animations": "~9.0.7",
12 | "@angular/common": "~9.0.7",
13 | "@angular/compiler": "~9.0.7",
14 | "@angular/core": "~9.0.7",
15 | "@angular/forms": "~9.0.7",
16 | "@angular/platform-browser": "~9.0.7",
17 | "@angular/platform-browser-dynamic": "~9.0.7",
18 | "@angular/router": "~9.0.7",
19 | "rxjs": "~6.5.4",
20 | "tslib": "^1.10.0",
21 | "zone.js": "~0.10.2"
22 | },
23 | "devDependencies": {
24 | "@angular-devkit/build-angular": "~0.900.7",
25 | "@angular/cli": "~9.0.7",
26 | "@angular/compiler-cli": "~9.0.7",
27 | "@angular/language-service": "~9.0.7",
28 | "@types/node": "^12.11.1",
29 | "codelyzer": "^5.1.2",
30 | "ts-node": "~8.3.0",
31 | "typescript": "3.7.5"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/demo/angular/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {Routes, RouterModule} from '@angular/router';
3 | import {IndexComponent} from './index.component';
4 | import {PageComponent} from './page.component';
5 |
6 | const routes: Routes = [
7 | {path: 'application/apps/angular', component: IndexComponent},
8 | {path: 'application/apps/angular/page', component: PageComponent},
9 | ];
10 |
11 | @NgModule({
12 | imports: [RouterModule.forRoot(routes)],
13 | exports: [RouterModule],
14 | })
15 | export class AppRoutingModule {}
16 |
--------------------------------------------------------------------------------
/demo/angular/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | template: `
6 |
17 |
18 |
19 |
20 | Index
27 |
28 |
29 | Page
30 |
31 |
32 |
33 |
34 | `,
35 | })
36 | export class AppComponent {
37 | public title = 'demo-angular';
38 | }
39 |
--------------------------------------------------------------------------------
/demo/angular/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import {BrowserModule} from '@angular/platform-browser';
2 | import {NgModule} from '@angular/core';
3 |
4 | import {AppRoutingModule} from './app-routing.module';
5 | import {AppComponent} from './app.component';
6 | import {IndexComponent} from './index.component';
7 | import {PageComponent} from './page.component';
8 |
9 | @NgModule({
10 | declarations: [AppComponent, IndexComponent, PageComponent],
11 | imports: [BrowserModule, AppRoutingModule],
12 | providers: [],
13 | bootstrap: [AppComponent],
14 | })
15 | export class AppModule {}
16 |
--------------------------------------------------------------------------------
/demo/angular/src/app/index.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | template: `
5 | Index
6 | `,
7 | })
8 | export class IndexComponent {}
9 |
--------------------------------------------------------------------------------
/demo/angular/src/app/page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | template: `
5 | Page
6 | `,
7 | })
8 | export class PageComponent {}
9 |
--------------------------------------------------------------------------------
/demo/angular/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DemoAngular
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/demo/angular/src/main.ts:
--------------------------------------------------------------------------------
1 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
2 |
3 | import {AppModule} from './app/app.module';
4 |
5 | const platformRef = platformBrowserDynamic();
6 |
7 | // this does not survive hot reload, as custom element can't be redefined
8 | // @see https://stackoverflow.com/questions/47805288/modifying-a-custom-element-class-after-its-been-defined
9 | if (!customElements.get('web-app-angular'))
10 | customElements.define(
11 | 'web-app-angular',
12 | class extends HTMLElement {
13 | protected mount;
14 |
15 | public connectedCallback() {
16 | this.mount = document.createElement('app-root');
17 | this.appendChild(this.mount);
18 | platformRef.bootstrapModule(AppModule).catch(err => console.error(err));
19 | }
20 |
21 | public disconnectedCallback() {
22 | this.innerHTML = '';
23 | }
24 | },
25 | );
26 |
--------------------------------------------------------------------------------
/demo/angular/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js/dist/zone';
2 |
--------------------------------------------------------------------------------
/demo/angular/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/demo/angular/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "src/main.ts",
9 | "src/polyfills.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/demo/angular/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "esnext",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "es2015",
14 | "lib": [
15 | "es2018",
16 | "dom"
17 | ]
18 | },
19 | "angularCompilerOptions": {
20 | "fullTemplateTypeCheck": true,
21 | "strictInjectionParameters": true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/demo/fed/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-fed",
3 | "version": "0.0.0",
4 | "private": true,
5 | "devDependencies": {
6 | "@babel/core": "7.10.2",
7 | "@babel/preset-react": "7.10.1",
8 | "babel-loader": "8.1.0",
9 | "http-server": "0.12.3",
10 | "webpack": "^5.0.0-beta.17",
11 | "webpack-cli": "3.3.11",
12 | "webpack-dev-server": "3.11.0"
13 | },
14 | "scripts": {
15 | "start": "webpack-dev-server",
16 | "build": "webpack --progress"
17 | },
18 | "dependencies": {
19 | "react": "^16.13.0",
20 | "react-dom": "^16.13.0",
21 | "moment": "^2.24.0"
22 | }
23 | }
--------------------------------------------------------------------------------
/demo/fed/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import moment from 'moment';
3 |
4 | export default ({mode}) => (
5 |
14 |
15 | Global App (Federated)
16 |
17 |
18 | Mode: {mode}
, {moment().format('MMMM Do YYYY, h:mm:ss a')}
19 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/demo/fed/src/index.js:
--------------------------------------------------------------------------------
1 | import App from './App';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 |
5 | export default node => {
6 | ReactDOM.render( , node);
7 | return () => ReactDOM.unmountComponentAtNode(node);
8 | };
9 |
--------------------------------------------------------------------------------
/demo/fed/webpack.config.js:
--------------------------------------------------------------------------------
1 | const {ModuleFederationPlugin} = require('webpack').container;
2 | const path = require('path');
3 |
4 | module.exports = {
5 | entry: './src/index',
6 | mode: process.env.NODE_ENV || 'development',
7 | devServer: {
8 | contentBase: path.join(__dirname, 'dist'),
9 | port: process.env.REACT_APP_FED_PORT,
10 | },
11 | output: {
12 | publicPath:
13 | process.env.NODE_ENV === 'production'
14 | ? '/demo/fed/dist/'
15 | : `http://localhost:${process.env.REACT_APP_FED_PORT}/`,
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /\.jsx?$/,
21 | loader: 'babel-loader',
22 | exclude: /node_modules/,
23 | options: {
24 | presets: ['@babel/preset-react'],
25 | },
26 | },
27 | ],
28 | },
29 | plugins: [
30 | new ModuleFederationPlugin({
31 | name: 'web_app_federation',
32 | library: {type: 'var', name: 'web_app_federation'},
33 | filename: 'remoteEntry.js',
34 | exposes: {
35 | './App': './src/App',
36 | './index': './src/index',
37 | },
38 | shared: {
39 | 'react-dom': 'react-dom',
40 | moment: '^2.24.0',
41 | react: {
42 | import: 'react', // the "react" package will be used a provided and fallback module
43 | shareKey: 'react', // under this name the shared module will be placed in the share scope
44 | shareScope: 'default', // share scope with this name will be used
45 | singleton: true, // only a single version of the shared module is allowed
46 | },
47 | },
48 | }),
49 | ],
50 | };
51 |
--------------------------------------------------------------------------------
/demo/host-web-component/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "../../.eslintrc"
4 | ]
5 | }
--------------------------------------------------------------------------------
/demo/host-web-component/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-host-web-component",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "PORT=$REACT_APP_HOST_WC_PORT react-scripts start",
7 | "build": "react-scripts build"
8 | },
9 | "browserslist": [
10 | "IE 11"
11 | ],
12 | "dependencies": {
13 | "@ringcentral/web-apps-host-css": "*",
14 | "@ringcentral/web-apps-host-web-component": "*",
15 | "@webcomponents/webcomponentsjs": "2.2.10",
16 | "bootstrap": "4.4.1"
17 | },
18 | "devDependencies": {
19 | "react-scripts": "3.0.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/demo/host-web-component/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Service Web
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/demo/host-web-component/src/iframeApp.js:
--------------------------------------------------------------------------------
1 | import {eventType} from '@ringcentral/web-apps-common';
2 |
3 | const root = document.getElementById('app');
4 |
5 | // IFRAME APP
6 |
7 | const iframeApp = document.createElement('web-app');
8 | iframeApp.setAttribute('url', JSON.stringify([`http://localhost:${process.env.REACT_APP_IFRAME_PORT}`]));
9 | iframeApp.setAttribute('id', 'iframe');
10 | iframeApp.setAttribute('type', 'iframe');
11 | iframeApp.setAttribute('history', 'html5');
12 | iframeApp.setAttribute('class', 'app-iframe');
13 | iframeApp.addEventListener('load', () => {
14 | console.log('load', iframeApp.getEventTarget());
15 | iframeApp
16 | .getEventTarget()
17 | .addEventListener(eventType.message, event => console.log('React App got event', event.detail));
18 | });
19 | root.appendChild(iframeApp);
20 |
--------------------------------------------------------------------------------
/demo/host-web-component/src/index.css:
--------------------------------------------------------------------------------
1 | .app-iframe {
2 | border: 1px solid red;
3 | }
--------------------------------------------------------------------------------
/demo/host-web-component/src/index.js:
--------------------------------------------------------------------------------
1 | import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter';
2 | import '@webcomponents/webcomponentsjs';
3 | import '@ringcentral/web-apps-host-web-component';
4 | import 'bootstrap/dist/css/bootstrap.css';
5 | import '@ringcentral/web-apps-host-css/styles.css';
6 | import './index.css';
7 | import './iframeApp';
8 | import './reactApp';
9 |
10 | if (module.hot) module.hot.accept();
11 |
--------------------------------------------------------------------------------
/demo/host-web-component/src/reactApp.js:
--------------------------------------------------------------------------------
1 | import {makeEvent, eventType} from '@ringcentral/web-apps-common';
2 |
3 | const root = document.getElementById('app');
4 |
5 | const fetchManifest = async url => (await fetch(`${url}/asset-manifest.json`)).json();
6 |
7 | // Special treatment of Create React App manifest
8 | const getFilesFromManifest = (url, manifest) =>
9 | [
10 | 'runtime~main.js',
11 | ...Object.keys(manifest.files).filter(key => key.match(/static\/js\/.+\.chunk.js$/)),
12 | 'main.js',
13 | ].map(key => `${url}/${manifest.files[key]}`);
14 |
15 | (async () => {
16 | const url = `http://localhost:${process.env.REACT_APP_REACT_PORT}`;
17 | const resolvedUrl = getFilesFromManifest(url, await fetchManifest(url));
18 |
19 | const reactApp = document.createElement('web-app');
20 | reactApp.setAttribute('url', JSON.stringify(resolvedUrl));
21 | reactApp.setAttribute('id', 'global');
22 | reactApp.setAttribute('type', 'global');
23 | reactApp.setAttribute('history', 'html5');
24 | reactApp.setAttribute('authtoken', 'set-by-host');
25 | reactApp.addEventListener('load', () => {
26 | reactApp
27 | .getEventTarget()
28 | .addEventListener(eventType.message, event => console.log('React App got event', event.detail));
29 | });
30 | root.appendChild(reactApp);
31 |
32 | const bumpAttrButton = document.createElement('button');
33 | bumpAttrButton.innerText = 'Bump authToken';
34 | bumpAttrButton.addEventListener('click', () => {
35 | reactApp.setAttribute('authtoken', reactApp.getAttribute('authtoken') + 'X');
36 | });
37 | root.appendChild(bumpAttrButton);
38 |
39 | const sendMessageButton = document.createElement('button');
40 | sendMessageButton.innerText = 'Send message';
41 | sendMessageButton.addEventListener('click', () => {
42 | reactApp.dispatchEvent(makeEvent(eventType.message, {toApp: 'Message from host'}));
43 | });
44 | root.appendChild(sendMessageButton);
45 | })();
46 |
--------------------------------------------------------------------------------
/demo/host/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "../../.eslintrc"
4 | ]
5 | }
--------------------------------------------------------------------------------
/demo/host/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-host",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "webpack-dev-server",
7 | "build": "webpack --progress"
8 | },
9 | "browserslist": [
10 | "IE 11"
11 | ],
12 | "dependencies": {
13 | "@ringcentral/web-apps-host-css": "*",
14 | "@ringcentral/web-apps-host-react": "*",
15 | "@webcomponents/webcomponentsjs": "2.2.10",
16 | "babel-polyfill": "6.26.0",
17 | "bootstrap": "4.4.1",
18 | "history": "4.9.0",
19 | "react": "16.8.6",
20 | "react-dom": "16.8.6",
21 | "react-router-dom": "5.1.2"
22 | },
23 | "devDependencies": {
24 | "@babel/core": "7.10.2",
25 | "@babel/preset-react": "7.10.1",
26 | "babel-loader": "8.1.0",
27 | "css-loader": "^3.5.3",
28 | "html-webpack-plugin": "^4.3.0",
29 | "http-server": "0.12.3",
30 | "style-loader": "^1.2.1",
31 | "url-loader": "4.1.0",
32 | "webpack": "^5.0.0-beta.17",
33 | "webpack-cli": "3.3.11",
34 | "webpack-dev-server": "3.11.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/demo/host/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Service Web
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/host/src/Router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {createBrowserHistory} from 'history';
3 | import {Route, Switch, Redirect, Router} from 'react-router-dom';
4 | import {LocationSync} from '@ringcentral/web-apps-host-react';
5 |
6 | import LoggedInWrapper from './pages/LoggedInWrapper';
7 | import NotFound from './pages/NotFound';
8 |
9 | // This allows to block history in sub-apps, this is not required in general
10 | window.RCAppsDemoHistory = createBrowserHistory();
11 |
12 | export default () => (
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/demo/host/src/bootstrap.js:
--------------------------------------------------------------------------------
1 | import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter';
2 | import '@webcomponents/webcomponentsjs';
3 | import React from 'react';
4 | import {render} from 'react-dom';
5 | import Router from './Router';
6 |
7 | import 'bootstrap/dist/css/bootstrap.css';
8 | import '@ringcentral/web-apps-host-css/styles.css';
9 | import './index.css';
10 |
11 | const rootEl = document.getElementById('app');
12 |
13 | render( , rootEl);
14 |
15 | if (module.hot) module.hot.accept();
16 |
--------------------------------------------------------------------------------
/demo/host/src/images/ringcentral.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ringcentral/web-apps/2b5206cf3617314a74269ae2e6175fb60931d24c/demo/host/src/images/ringcentral.png
--------------------------------------------------------------------------------
/demo/host/src/index.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | font-size: 14px;
3 | }
4 |
5 | .app-iframe {
6 | border: 1px solid red;
7 | }
8 |
9 | .card, .navbar-container {
10 | margin-bottom: 1rem;
11 | }
12 |
13 | .logo {
14 | background: no-repeat url("./images/ringcentral.png") center center;
15 | background-size: contain;
16 | width: 140px;
17 | height: 50px;
18 | margin-right: 20px;
19 | display: block;
20 | }
21 |
22 | .header {
23 | margin: 10px 0;
24 | display: flex;
25 | align-items: center;
26 | }
27 |
28 | .app-popup-bg {
29 | z-index: 3 !important;
30 | }
--------------------------------------------------------------------------------
/demo/host/src/index.js:
--------------------------------------------------------------------------------
1 | import('./bootstrap');
2 |
--------------------------------------------------------------------------------
/demo/host/src/lib/registry.js:
--------------------------------------------------------------------------------
1 | // This could be any other provisioning method
2 | const fetchManifest = async url => (await fetch(`${url}/asset-manifest.json`)).json();
3 |
4 | const angularSuffix = process.env.NODE_ENV === 'production' ? '-es5' : '';
5 |
6 | // Special treatment of Create React App manifest
7 | const getFilesFromManifest = (url, manifest) =>
8 | [
9 | 'runtime~main.js',
10 | ...Object.keys(manifest.files).filter(key => key.match(/static\/js\/.+\.chunk.js$/)),
11 | 'main.js',
12 | ].map(key => `${url}/${manifest.files[key]}`);
13 |
14 | export const appRegistry = {
15 | react: {
16 | type: 'script',
17 | getUrl: async (
18 | url = process.env.NODE_ENV === 'production'
19 | ? `/demo/react-menu/build`
20 | : `http://localhost:${process.env.REACT_APP_REACT_MENU_PORT}`,
21 | ) => getFilesFromManifest(url, await fetchManifest(url)),
22 | },
23 | reactMenuIframe: {
24 | type: 'iframe',
25 | origin:
26 | process.env.NODE_ENV === 'production'
27 | ? window.location.origin
28 | : `http://localhost:${process.env.REACT_APP_REACT_MENU_IFRAME_PORT}`,
29 | getUrl: async (
30 | url = process.env.NODE_ENV === 'production'
31 | ? `/demo/react-menu-iframe/build/`
32 | : `http://localhost:${process.env.REACT_APP_REACT_MENU_IFRAME_PORT}`,
33 | ) => url,
34 | },
35 | global: {
36 | type: 'global',
37 | getUrl: async (
38 | url = process.env.NODE_ENV === 'production'
39 | ? `/demo/react/build`
40 | : `http://localhost:${process.env.REACT_APP_REACT_PORT}`,
41 | ) => getFilesFromManifest(url, await fetchManifest(url)),
42 | },
43 | vue: {
44 | type: 'script',
45 | getUrl: (
46 | url = process.env.NODE_ENV === 'production'
47 | ? `/demo/vue/dist`
48 | : `http://localhost:${process.env.REACT_APP_VUE_PORT}`,
49 | ) => [`${url}/js/app.js`, `${url}/js/chunk-vendors.js`],
50 | },
51 | iframe: {
52 | type: 'iframe',
53 | origin:
54 | process.env.NODE_ENV === 'production'
55 | ? window.location.origin
56 | : `http://localhost:${process.env.REACT_APP_IFRAME_PORT}`,
57 | getUrl: (
58 | url = process.env.NODE_ENV === 'production'
59 | ? `/demo/iframe/build`
60 | : `http://localhost:${process.env.REACT_APP_IFRAME_PORT}`,
61 | ) => url, // add # to URL enable hash history in IFRAME
62 | },
63 | admin: {
64 | type: 'iframe',
65 | origin:
66 | process.env.NODE_ENV === 'production'
67 | ? window.location.origin
68 | : `http://localhost:${process.env.REACT_APP_ADMIN_PORT}`,
69 | getUrl: (
70 | url = process.env.NODE_ENV === 'production'
71 | ? `/demo/admin`
72 | : `http://localhost:${process.env.REACT_APP_ADMIN_PORT}`,
73 | ) => url,
74 | },
75 | angular: {
76 | type: 'script',
77 | getUrl: (
78 | url = process.env.NODE_ENV === 'production'
79 | ? `/demo/angular/dist`
80 | : `http://localhost:${process.env.REACT_APP_ANGULAR_PORT}`,
81 | ) => [
82 | url + `/runtime${angularSuffix}.js`,
83 | url + `/polyfills${angularSuffix}.js`,
84 | url + `/styles${angularSuffix}.js`,
85 | url + `/vendor${angularSuffix}.js`,
86 | url + `/main${angularSuffix}.js`,
87 | ],
88 | },
89 | federation: {
90 | type: 'global',
91 | getUrl: async (
92 | url = process.env.NODE_ENV === 'production'
93 | ? `/demo/fed/dist`
94 | : `http://localhost:${process.env.REACT_APP_FED_PORT}`,
95 | ) => url + '/remoteEntry.js',
96 | options: {
97 | federation: true,
98 | },
99 | },
100 | federationDirect: {
101 | type: 'global',
102 | getUrl: async (
103 | url = process.env.NODE_ENV === 'production'
104 | ? `/demo/fed/dist`
105 | : `http://localhost:${process.env.REACT_APP_FED_PORT}`,
106 | ) => url + '/remoteEntry.js',
107 | options: {
108 | federation: true,
109 | module: './App',
110 | scope: 'web_app_federation',
111 | },
112 | },
113 | };
114 |
--------------------------------------------------------------------------------
/demo/host/src/lib/useAppRegistry.js:
--------------------------------------------------------------------------------
1 | import {useEffect, useReducer} from 'react';
2 | import {appRegistry} from './registry';
3 |
4 | const initialState = {id: null, url: null, type: null, error: null, loading: false};
5 |
6 | const reducer = (state, {type, payload}) => {
7 | switch (type) {
8 | case 'loading':
9 | return {...state, ...initialState, loading: true};
10 | case 'error':
11 | return {...state, ...initialState, error: payload};
12 | case 'success':
13 | return {...state, ...initialState, ...payload};
14 | default:
15 | return state;
16 | }
17 | };
18 |
19 | export const useAppRegistry = appId => {
20 | const [state, dispatch] = useReducer(reducer, {...initialState, loading: true});
21 |
22 | useEffect(() => {
23 | let mounted = true;
24 | (async () => {
25 | try {
26 | dispatch({type: 'loading'});
27 |
28 | if (!appRegistry[appId]) throw new Error(`App ${appId} not found in registry`);
29 |
30 | // registry itself can also be loaded from API for example
31 | const {getUrl, ...rest} = appRegistry[appId];
32 |
33 | // this allows to override url of app, useful for production to swap deployed version with local
34 | const {appsOverrides} = window.localStorage;
35 |
36 | const url = await getUrl(appsOverrides && appsOverrides[appId] && appsOverrides[appId].url);
37 |
38 | if (mounted) dispatch({type: 'success', payload: {id: appId, url, ...rest}}); // by setting ID to state we make sure that id, url and type are in sync
39 | } catch (error) {
40 | if (mounted) dispatch({type: 'error', payload: error});
41 | }
42 | })();
43 | return () => (mounted = false);
44 | }, [appId]);
45 |
46 | return state;
47 | };
48 |
--------------------------------------------------------------------------------
/demo/host/src/pages/App.js:
--------------------------------------------------------------------------------
1 | import React, {memo, useState, useCallback} from 'react';
2 | import {useApplication, eventType, useListenerEffect, dispatchEvent} from '@ringcentral/web-apps-host-react';
3 | import {useAppRegistry} from '../lib/useAppRegistry';
4 |
5 | /**
6 | * Note that this function is exactly the same as in IFRAME and React app
7 | */
8 | const Messages = ({node}) => {
9 | const [messages, setMessages] = useState([]);
10 |
11 | useListenerEffect(node, eventType.message, event => setMessages(messages => [...messages, event.detail]));
12 |
13 | const sendMessage = () => dispatchEvent(node, eventType.message, {toApp: 'message to app'});
14 |
15 | return (
16 |
17 |
18 |
19 | Send message
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | const App = ({
28 | match: {
29 | params: {appId},
30 | },
31 | }) => {
32 | const [authtoken, setAuthToken] = useState('set-by-host');
33 | const {id, url, type, error: registryError, loading: registryLoading, origin, options} = useAppRegistry(appId);
34 |
35 | const {error: appError, Component, node, loading: appLoading} = useApplication({id, type, url, options});
36 |
37 | const [popup, setPopup] = useState(false);
38 | const onPopup = useCallback(event => setPopup(popup => (popup !== event.detail ? event.detail : popup)), []);
39 | useListenerEffect(node, eventType.popup, onPopup);
40 |
41 | const onAuthError = useCallback(event => alert('App reported auth error:\n\n' + event.detail), []); //this.props.history.push('/login'); //FIXME Logout
42 | useListenerEffect(node, eventType.authError, onAuthError);
43 |
44 | // Render
45 |
46 | let render;
47 | if (registryError) render = Cannot load application: {registryError.toString()}
;
48 | else if (appError) render = Cannot render application: {appError.toString()}
;
49 | else if (registryLoading) render = Loading registry...
;
50 | else if (!Component) render = Loading app...
;
51 | else
52 | render = (
53 |
62 | );
63 |
64 | return (
65 |
66 |
dispatchEvent(node, eventType.popup, false)}
69 | style={{backgroundColor: popup}}
70 | role="presentation"
71 | />
72 |
73 |
74 |
App
75 |
76 | {render}
77 | {appLoading &&
Application is mounting
}
78 |
79 |
80 |
81 |
82 |
Host
83 |
84 |
85 |
86 | setAuthToken(authtoken + 'X')}>
87 | Bump authtoken
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | );
98 | };
99 |
100 | export default memo(App);
101 |
--------------------------------------------------------------------------------
/demo/host/src/pages/FederationApp.js:
--------------------------------------------------------------------------------
1 | import React, {memo, useEffect, useState} from 'react';
2 | import {appRegistry} from '../lib/registry';
3 | import {useApplication, useListenerEffect, eventType} from '@ringcentral/web-apps-host-react';
4 |
5 | const id = 'federation';
6 |
7 | export const FederationApp = memo(({logout}) => {
8 | const [url, setUrl] = useState(null);
9 |
10 | useEffect(() => {
11 | let mounted = true;
12 | appRegistry[id].getUrl().then(url => mounted && setUrl(url));
13 | return () => (mounted = false);
14 | }, []);
15 |
16 | const {error, loading, Component, node} = useApplication({
17 | id,
18 | type: appRegistry[id].type,
19 | url,
20 | options: appRegistry[id].options,
21 | });
22 |
23 | useListenerEffect(node, eventType.message, e => e.detail.logout && logout());
24 |
25 | let render;
26 | if (!url) render = <>Loading federation app...>;
27 | else if (error) render = <>Error in federation: {error.toString()}>;
28 | else if (loading) render = <>Loading federation app...>;
29 |
30 | return (
31 |
32 | {render && (
33 |
34 | {render}
35 |
36 | )}
37 |
38 |
39 | );
40 | });
41 |
--------------------------------------------------------------------------------
/demo/host/src/pages/FederationDirectApp.js:
--------------------------------------------------------------------------------
1 | import React, {memo, useEffect, useState} from 'react';
2 | import {appRegistry} from '../lib/registry';
3 | import {useApplication, useListenerEffect, eventType} from '@ringcentral/web-apps-host-react';
4 |
5 | const id = 'federationDirect';
6 |
7 | export const FederationDirectApp = memo(({logout}) => {
8 | const [url, setUrl] = useState(null);
9 |
10 | useEffect(() => {
11 | let mounted = true;
12 | appRegistry[id].getUrl().then(url => mounted && setUrl(url));
13 | return () => (mounted = false);
14 | }, []);
15 |
16 | const {error, loading, Component, node} = useApplication({
17 | id,
18 | type: appRegistry[id].type,
19 | url,
20 | options: appRegistry[id].options,
21 | });
22 |
23 | useListenerEffect(node, eventType.message, e => e.detail.logout && logout());
24 |
25 | let render;
26 | if (!url) render = <>Loading federation app...>;
27 | else if (error) render = <>Error in federation: {error.toString()}>;
28 | else if (loading) render = <>Loading federation app...>;
29 |
30 | return (
31 |
32 | {render && (
33 |
34 | {render}
35 |
36 | )}
37 |
38 |
39 | );
40 | });
41 |
--------------------------------------------------------------------------------
/demo/host/src/pages/Index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () =>
This is a placeholder component for index page. Please choose an app in the top menu.
;
4 |
--------------------------------------------------------------------------------
/demo/host/src/pages/LoggedInWrapper.js:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react';
2 | import {Link, Route, Switch} from 'react-router-dom';
3 | import App from './App';
4 | import Index from './Index';
5 | import {MenuApp} from './MenuApp';
6 | import {MenuAppIframe} from './MenuAppIframe';
7 | import {FederationApp} from './FederationApp';
8 | import {FederationDirectApp} from './FederationDirectApp';
9 | import logo from '../images/ringcentral.png';
10 |
11 | const logout = () => alert('Logout');
12 |
13 | const Layout = ({match}) => (
14 | <>
15 |
16 |
17 |
Web Apps
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | >
31 | );
32 |
33 | export default memo(Layout);
34 |
--------------------------------------------------------------------------------
/demo/host/src/pages/MenuApp.js:
--------------------------------------------------------------------------------
1 | import React, {memo, useEffect, useState} from 'react';
2 | import {appRegistry} from '../lib/registry';
3 | import {useApplication, useListenerEffect, eventType} from '@ringcentral/web-apps-host-react';
4 |
5 | const id = 'react';
6 |
7 | const delay = value => new Promise(res => setTimeout(() => res(value), 1000));
8 |
9 | export const MenuApp = memo(({logout}) => {
10 | const [url, setUrl] = useState(null);
11 |
12 | useEffect(() => {
13 | let mounted = true;
14 | appRegistry[id]
15 | .getUrl()
16 | .then(url => delay(url)) // delay is just to show off ;)
17 | .then(url => mounted && setUrl(url));
18 | return () => (mounted = false);
19 | }, []);
20 |
21 | const {error, loading, Component, node} = useApplication({
22 | id,
23 | type: appRegistry[id].type,
24 | url,
25 | });
26 |
27 | useListenerEffect(node, eventType.message, e => e.detail.logout && logout());
28 |
29 | let render;
30 | if (!url) render = <>Loading menu config...>;
31 | else if (error) render = <>Error in menu: {error.toString()}>;
32 | else if (loading) render = <>Loading menu app...>;
33 |
34 | return (
35 |
36 | {render && (
37 |
38 | {render}
39 |
40 | )}
41 |
42 |
43 | );
44 | });
45 |
--------------------------------------------------------------------------------
/demo/host/src/pages/MenuAppIframe.js:
--------------------------------------------------------------------------------
1 | import React, {memo, useEffect, useState} from 'react';
2 | import {appRegistry} from '../lib/registry';
3 | import {useApplication, useListenerEffect, eventType} from '@ringcentral/web-apps-host-react';
4 |
5 | const id = 'reactMenuIframe';
6 |
7 | export const MenuAppIframe = memo(({logout}) => {
8 | const [url, setUrl] = useState(null);
9 |
10 | useEffect(() => {
11 | let mounted = true;
12 | appRegistry[id].getUrl().then(url => mounted && setUrl(url));
13 | return () => (mounted = false);
14 | }, []);
15 |
16 | const {error, loading, Component, node} = useApplication({
17 | id,
18 | type: appRegistry[id].type,
19 | url,
20 | });
21 |
22 | useListenerEffect(node, eventType.message, e => e.detail.logout && logout());
23 |
24 | let render;
25 | if (!url) render = <>Loading menu config...>;
26 | else if (error) render = <>Error in menu: {error.toString()}>;
27 | else if (loading) render = <>Loading menu app...>;
28 |
29 | return (
30 |
31 | {render && (
32 |
33 | {render}
34 |
35 | )}
36 |
37 |
38 | );
39 | });
40 |
--------------------------------------------------------------------------------
/demo/host/src/pages/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => (
4 |
5 |
Not Found
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/demo/host/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin;
4 | const path = require('path');
5 |
6 | module.exports = {
7 | entry: './src/index',
8 | mode: process.env.NODE_ENV || 'development',
9 | devServer: {
10 | contentBase: path.join(__dirname, 'dist'),
11 | port: process.env.REACT_APP_HOST_PORT,
12 | historyApiFallback: true,
13 | },
14 | output: {
15 | publicPath:
16 | process.env.NODE_ENV === 'production'
17 | ? '/demo/host/dist/'
18 | : `http://localhost:${process.env.REACT_APP_HOST_PORT}/`,
19 | },
20 | module: {
21 | rules: [
22 | {
23 | test: /\.jsx?$/,
24 | loader: 'babel-loader',
25 | exclude: /node_modules/,
26 | options: {
27 | presets: ['@babel/preset-react'],
28 | },
29 | },
30 | {
31 | test: /\.css?$/,
32 | use: ['style-loader', 'css-loader'],
33 | },
34 | {
35 | test: /\.(png|jpg|gif)$/i,
36 | loader: 'url-loader',
37 | },
38 | ],
39 | },
40 | plugins: [
41 | new ModuleFederationPlugin({
42 | name: 'web-app-host',
43 | library: {type: 'var', name: 'web-app-host'},
44 | shared: ['react', 'react-dom'],
45 | }),
46 | new HtmlWebpackPlugin({
47 | template: './public/index.html',
48 | // base: process.env.NODE_ENV === 'production' ? '/demo/host/dist/' : '/',
49 | base: '/',
50 | }),
51 | new webpack.DefinePlugin({
52 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
53 | 'process.env.REACT_APP_API_SERVER': JSON.stringify(process.env.REACT_APP_API_SERVER),
54 | 'process.env.REACT_APP_HOST_CLIENT_ID': JSON.stringify(process.env.REACT_APP_HOST_CLIENT_ID),
55 | 'process.env.REACT_APP_HOST_PORT': JSON.stringify(process.env.REACT_APP_HOST_PORT),
56 | 'process.env.REACT_APP_REACT_PORT': JSON.stringify(process.env.REACT_APP_REACT_PORT),
57 | 'process.env.REACT_APP_HOST_WC_PORT': JSON.stringify(process.env.REACT_APP_HOST_WC_PORT),
58 | 'process.env.REACT_APP_VUE_PORT': JSON.stringify(process.env.REACT_APP_VUE_PORT),
59 | 'process.env.REACT_APP_IFRAME_PORT': JSON.stringify(process.env.REACT_APP_IFRAME_PORT),
60 | 'process.env.REACT_APP_ADMIN_PORT': JSON.stringify(process.env.REACT_APP_ADMIN_PORT),
61 | 'process.env.REACT_APP_REACT_MENU_PORT': JSON.stringify(process.env.REACT_APP_REACT_MENU_PORT),
62 | 'process.env.REACT_APP_ANGULAR_PORT': JSON.stringify(process.env.REACT_APP_ANGULAR_PORT),
63 | 'process.env.REACT_APP_REACT_MENU_IFRAME_PORT': JSON.stringify(
64 | process.env.REACT_APP_REACT_MENU_IFRAME_PORT,
65 | ),
66 | 'process.env.REACT_APP_FED_PORT': JSON.stringify(process.env.REACT_APP_FED_PORT),
67 | }),
68 | ],
69 | };
70 |
--------------------------------------------------------------------------------
/demo/iframe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-iframe",
3 | "version": "0.6.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "PORT=$REACT_APP_IFRAME_PORT react-scripts start",
7 | "build": "PUBLIC_URL=/demo/iframe/build/ react-scripts build"
8 | },
9 | "browserslist": [
10 | "IE 11"
11 | ],
12 | "dependencies": {
13 | "@ringcentral/web-apps-sync-iframe": "*",
14 | "@ringcentral/web-apps-react": "*",
15 | "bootstrap": "4.4.1",
16 | "history": "4.9.0",
17 | "react": "16.8.6",
18 | "react-bootstrap": "next",
19 | "react-dom": "16.8.6",
20 | "react-router-dom": "5.1.2"
21 | },
22 | "devDependencies": {
23 | "http-server": "0.12.3",
24 | "react-scripts": "3.0.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/demo/iframe/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
IFrame
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/iframe/src/Router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Route, Switch, Redirect, BrowserRouter} from 'react-router-dom';
3 |
4 | import App from './pages/App';
5 | import NotFound from './pages/NotFound';
6 |
7 | const prefix = process.env.NODE_ENV === 'production' ? '/demo/iframe/build' : '';
8 |
9 | export default () => (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/demo/iframe/src/index.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | font-size: 14px;
3 | }
4 |
5 | ul.nav-tabs {
6 | margin-bottom: 1rem;
7 | }
8 |
9 | /* This hack is needed to make sure popup is always contained within the IFRAME, bc popup cannot stretch IFRAMEs */
10 | .modal-body {
11 | max-height: calc(100vh - 200px);
12 | overflow-y: auto;
13 | }
--------------------------------------------------------------------------------
/demo/iframe/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import Router from './Router';
4 |
5 | import 'bootstrap/dist/css/bootstrap.css';
6 | import './index.css';
7 |
8 | const rootEl = document.getElementById('app');
9 |
10 | render(
, rootEl);
11 |
12 | if (module.hot) module.hot.accept();
13 |
--------------------------------------------------------------------------------
/demo/iframe/src/lib/index.js:
--------------------------------------------------------------------------------
1 | import {IFrameSync} from '@ringcentral/web-apps-sync-iframe';
2 |
3 | export const sync = new IFrameSync({
4 | history: 'html5',
5 | id: 'iframe', // must match host config
6 | origin:
7 | process.env.NODE_ENV === 'production'
8 | ? window.location.origin
9 | : `http://localhost:${process.env.REACT_APP_HOST_PORT}`, // strict mode, remove if you don't know the host
10 | base: process.env.NODE_ENV === 'production' ? '/demo/iframe/build/' : '',
11 | });
12 |
--------------------------------------------------------------------------------
/demo/iframe/src/pages/App.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import {NavLink, Route, Switch} from 'react-router-dom';
3 | import {Modal, Button} from 'react-bootstrap';
4 | import {eventType, useListenerEffect, dispatchEvent} from '@ringcentral/web-apps-react';
5 | import {sync} from '../lib';
6 | import Foo from './Foo';
7 | import Bar from './Bar';
8 | import Index from './Index';
9 | import Lorem from './Lorem';
10 |
11 | const PopupWindow = ({close}) => (
12 |
13 |
14 | Modal title
15 |
16 |
17 |
18 |
19 |
20 | Close
21 |
22 |
23 | );
24 |
25 | /**
26 | * Note that this function is exactly the same as in HOST-rendered React app
27 | */
28 | const Messages = ({node}) => {
29 | const [messages, setMessages] = useState([]);
30 |
31 | useListenerEffect(node, eventType.message, event => setMessages(messages => [...messages, event.detail]));
32 |
33 | const sendMessage = () => dispatchEvent(node, eventType.message, {fromIframe: 'pew'});
34 |
35 | return (
36 |
37 |
38 |
39 | Send message
40 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | /**
48 | * This app is rendered in IFRAME so unline in HOST-rendered React app we need to synchronize popups
49 | */
50 | const Popup = ({node}) => {
51 | const [popup, setPopup] = useState(false);
52 |
53 | const togglePopup = (popup = false) => dispatchEvent(node, eventType.popup, popup && 'rgba(0, 0, 0, 0.5)'); // sets color of host popup background
54 |
55 | useListenerEffect(node, eventType.popup, event => setPopup(event.detail));
56 |
57 | return (
58 |
59 | togglePopup(true)}>
60 | Popup
61 |
62 | {popup && togglePopup(false)} />}
63 |
64 | );
65 | };
66 |
67 | const App = ({match, location}) => {
68 | const node = sync.getEventTarget();
69 |
70 | const openOnHost = location => dispatchEvent(node, eventType.location, location); // replace origin just in case
71 |
72 | return (
73 | <>
74 |
75 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Location
115 |
116 |
117 |
118 | >
119 | );
120 | };
121 |
122 | export default App;
123 |
--------------------------------------------------------------------------------
/demo/iframe/src/pages/Bar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () =>
BAR
;
4 |
--------------------------------------------------------------------------------
/demo/iframe/src/pages/Foo.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Lorem from './Lorem';
3 |
4 | export default () => (
5 |
6 |
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/demo/iframe/src/pages/Index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () =>
Index
;
4 |
--------------------------------------------------------------------------------
/demo/iframe/src/pages/Lorem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => (
4 |
5 |
The standard Lorem Ipsum passage, used since the 1500s
6 |
7 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
8 | dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex
9 | ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
10 | nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
11 | anim id est laborum."
12 |
13 |
14 |
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
15 |
16 | "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
17 | totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt
18 | explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur
19 | magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia
20 | dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et
21 | dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam
22 | corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure
23 | reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum
24 | fugiat quo voluptas nulla pariatur?"
25 |
26 |
27 |
1914 translation by H. Rackham
28 |
29 | "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born
30 | and I will give you a complete account of the system, and expound the actual teachings of the great explorer
31 | of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself,
32 | because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter
33 | consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain
34 | pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can
35 | procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical
36 | exercise, except to obtain some advantage from it? But who has any right to find fault with a man who
37 | chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no
38 | resultant pleasure?"
39 |
40 |
41 |
Section 1.10.33 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
42 |
43 | "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
44 | deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident,
45 | similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem
46 | rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque
47 | nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor
48 | repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et
49 | voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus,
50 | ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores
51 | repellat."
52 |
53 |
54 |
1914 translation by H. Rackham
55 |
56 | "On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and
57 | demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain
58 | and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through
59 | weakness of will, which is the same as saying through shrinking from toil and pain. These cases are
60 | perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when
61 | nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain
62 | avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will
63 | frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always
64 | holds in these matters to this principle of selection: he rejects pleasures to secure other greater
65 | pleasures, or else he endures pains to avoid worse pains."
66 |
67 |
68 | );
69 |
--------------------------------------------------------------------------------
/demo/iframe/src/pages/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => (
4 |
5 |
Not Found
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/demo/react-menu-iframe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-react-menu-iframe",
3 | "version": "0.6.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "PORT=$REACT_APP_REACT_MENU_IFRAME_PORT react-scripts start",
7 | "build": "PUBLIC_URL=/demo/react-menu-iframe/build/ react-scripts build"
8 | },
9 | "browserslist": [
10 | "IE 11"
11 | ],
12 | "dependencies": {
13 | "@ringcentral/web-apps-react": "*",
14 | "@ringcentral/web-apps-sync-iframe": "*",
15 | "bootstrap": "4.4.1",
16 | "react": "16.8.6",
17 | "react-dom": "16.8.6",
18 | "react-router-dom": "5.1.2"
19 | },
20 | "devDependencies": {
21 | "react-scripts": "3.0.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/demo/react-menu-iframe/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
React Menu
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/react-menu-iframe/src/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {NavLink} from 'react-router-dom';
3 | import {dispatchEvent, eventType} from '@ringcentral/web-apps-react';
4 | import {IFrameSync} from '@ringcentral/web-apps-sync-iframe';
5 |
6 | const sync = new IFrameSync({
7 | history: 'html5',
8 | id: 'reactMenuIframe',
9 | origin:
10 | process.env.NODE_ENV === 'production'
11 | ? window.location.origin
12 | : `http://localhost:${process.env.REACT_APP_HOST_PORT}`, // strict mode, remove if you don't know the host
13 | }); // must match host config
14 |
15 | const node = sync.getEventTarget();
16 |
17 | export default () => (
18 |
19 |
20 |
21 |
22 | Home
23 |
24 |
25 |
26 |
27 | Global
28 |
29 |
30 |
31 |
32 | Vue
33 |
34 |
35 |
36 |
37 | IFrame
38 |
39 |
40 |
41 |
42 | Admin
43 |
44 |
45 |
46 |
47 | Angular
48 |
49 |
50 |
51 |
52 | Whole menu is an app that listens to routing in IFRAME
53 |
54 | dispatchEvent(node, eventType.message, {logout: true})}
57 | >
58 | Logout
59 |
60 |
61 | );
62 |
--------------------------------------------------------------------------------
/demo/react-menu-iframe/src/Router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {BrowserRouter} from 'react-router-dom';
3 | import {LocationSync} from '@ringcentral/web-apps-react';
4 | import Menu from './Menu';
5 |
6 | export default ({node}) => (
7 |
8 |
9 |
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/demo/react-menu-iframe/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 | import Router from './Router';
4 |
5 | import 'bootstrap/dist/css/bootstrap.css';
6 | import './style.css';
7 |
8 | const rootEl = document.getElementById('app');
9 |
10 | render(
, rootEl);
11 |
12 | if (module.hot) module.hot.accept();
13 |
--------------------------------------------------------------------------------
/demo/react-menu-iframe/src/style.css:
--------------------------------------------------------------------------------
1 | /* global style won't work here so we re-implement it */
2 | html, body {
3 | font-size: 14px;
4 | }
5 |
6 | .nav-link.active {
7 | border-bottom: 2px solid #007bff;
8 | margin-bottom: -2px;
9 | }
10 |
--------------------------------------------------------------------------------
/demo/react-menu/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react-app"
4 | ]
5 | }
--------------------------------------------------------------------------------
/demo/react-menu/craco.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack: {
3 | configure: {
4 | output: {
5 | // @see https://github.com/facebook/create-react-app/issues/7959
6 | jsonpFunction: 'webpackJsonpDemoReactMenu',
7 | // @see https://github.com/facebook/create-react-app/issues/8381
8 | hotUpdateFunction: 'webpackHotUpdateDemoReactMenu',
9 | publicPath: process.env.NODE_ENV === 'production' ? '/demo/react-menu/dist/' : `/`,
10 | },
11 | },
12 | },
13 | devServer: {
14 | headers: {
15 | 'Access-Control-Allow-Origin': '*',
16 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
17 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/demo/react-menu/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-react-menu",
3 | "version": "0.6.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "PORT=$REACT_APP_REACT_MENU_PORT craco start",
7 | "build": "craco build"
8 | },
9 | "browserslist": [
10 | "IE 11"
11 | ],
12 | "dependencies": {
13 | "@ringcentral/web-apps-react": "*",
14 | "bootstrap": "4.4.1",
15 | "react": "16.8.6",
16 | "react-dom": "16.8.6",
17 | "react-router-dom": "5.1.2",
18 | "react-shadow-dom-retarget-events": "1.0.10"
19 | },
20 | "devDependencies": {
21 | "@craco/craco": "5.6.3",
22 | "raw-loader": "3.0.0",
23 | "react-scripts": "3.0.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/demo/react-menu/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
React Menu
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/react-menu/src/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {NavLink} from 'react-router-dom';
3 | import {dispatchEvent, eventType} from '@ringcentral/web-apps-react';
4 |
5 | export default ({node}) => (
6 |
7 |
8 |
9 |
10 | Home
11 |
12 |
13 |
14 |
15 | Global
16 |
17 |
18 |
19 |
20 | Vue
21 |
22 |
23 |
24 |
25 | IFrame
26 |
27 |
28 |
29 |
30 | Admin
31 |
32 |
33 |
34 |
35 | Angular
36 |
37 |
38 |
39 |
40 | Whole menu is an app that listens to routing
41 |
42 | dispatchEvent(node, eventType.message, {logout: true})}
45 | >
46 | Logout
47 |
48 |
49 | );
50 |
--------------------------------------------------------------------------------
/demo/react-menu/src/Router.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Router} from 'react-router-dom';
3 | import {LocationSync} from '@ringcentral/web-apps-react';
4 | import Menu from './Menu';
5 |
6 | // This allows to block history in sub-apps, this is not required in general
7 | export default ({node}) => (
8 |
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/demo/react-menu/src/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-webpack-loader-syntax */
2 |
3 | import React from 'react';
4 | import {render, unmountComponentAtNode} from 'react-dom';
5 | import retargetEvents from 'react-shadow-dom-retarget-events';
6 | import App from './Router';
7 |
8 | const template = document.createElement('template');
9 | template.innerHTML = `
10 |
16 |
17 | `;
18 |
19 | // this does not survive hot reload, as custom element can't be redefined
20 | // @see https://stackoverflow.com/questions/47805288/modifying-a-custom-element-class-after-its-been-defined
21 | if (!customElements.get('web-app-react'))
22 | customElements.define(
23 | 'web-app-react',
24 | class extends HTMLElement {
25 | constructor() {
26 | super();
27 |
28 | this.attachShadow({mode: 'open'});
29 | this.shadowRoot.appendChild(document.importNode(template.content, true));
30 | this.mount = this.shadowRoot.querySelector('.root');
31 |
32 | retargetEvents(this.mount);
33 | }
34 |
35 | static get observedAttributes() {
36 | return ['authtoken'];
37 | }
38 |
39 | render() {
40 | // as you see we re-render every time when authtoken changes
41 | render(
, this.mount);
42 | }
43 |
44 | attributeChangedCallback(name, oldValue, newValue) {
45 | this.render();
46 | }
47 |
48 | connectedCallback() {
49 | this.render();
50 | }
51 |
52 | disconnectedCallback() {
53 | unmountComponentAtNode(this.mount);
54 | }
55 | },
56 | );
57 |
--------------------------------------------------------------------------------
/demo/react-menu/src/style.css:
--------------------------------------------------------------------------------
1 | /* global style won't work here so we re-implement it */
2 | :host {
3 | font-size: 14px;
4 | }
5 |
6 | .nav-link.active {
7 | border-bottom: 2px solid #007bff;
8 | margin-bottom: -2px;
9 | }
10 |
--------------------------------------------------------------------------------
/demo/react/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "react-app"
4 | ]
5 | }
--------------------------------------------------------------------------------
/demo/react/craco.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack: {
3 | configure: {
4 | output: {
5 | // @see https://github.com/facebook/create-react-app/issues/7959
6 | jsonpFunction: 'webpackJsonpDemoReact',
7 | // @see https://github.com/facebook/create-react-app/issues/8381
8 | hotUpdateFunction: 'webpackHotUpdateDemoReact',
9 | publicPath: process.env.NODE_ENV === 'production' ? '/demo/react/dist/' : `/`,
10 | },
11 | },
12 | },
13 | devServer: {
14 | headers: {
15 | 'Access-Control-Allow-Origin': '*',
16 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
17 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/demo/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-react",
3 | "version": "0.6.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "PORT=$REACT_APP_REACT_PORT craco start",
7 | "build": "craco build"
8 | },
9 | "browserslist": [
10 | "IE 11"
11 | ],
12 | "dependencies": {
13 | "@ringcentral/web-apps-react": "*",
14 | "bootstrap": "4.4.1",
15 | "react": "16.8.6",
16 | "react-dom": "16.8.6",
17 | "react-router-dom": "5.1.2"
18 | },
19 | "devDependencies": {
20 | "@craco/craco": "5.6.3",
21 | "react-scripts": "3.0.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/demo/react/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
React
8 |
9 |
10 |
11 |
12 |
13 |
Open /application/apps/react
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/react/src/App.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect, Suspense, lazy} from 'react';
2 | import {BrowserRouter, Router, NavLink, Route, Switch} from 'react-router-dom';
3 | import {eventType, LocationSync, dispatchEvent, useListenerEffect} from '@ringcentral/web-apps-react';
4 | import Modal from './Modal';
5 |
6 | import './styles.css'; // even though this is imported it won't make any effect in Web Component mode due to Shadow CSS
7 |
8 | const Groups = lazy(() => import('./pages/Groups'));
9 | const Users = lazy(() => import('./pages/Users'));
10 |
11 | /**
12 | * Note that this function is exactly the same as in IFRAME app
13 | */
14 | const Messages = ({node}) => {
15 | const [messages, setMessages] = useState([]);
16 |
17 | useListenerEffect(node, eventType.message, event => setMessages(messages => [...messages, event.detail]));
18 |
19 | const sendMessage = () => dispatchEvent(node, eventType.message, {toHost: 'message to host'});
20 |
21 | return (
22 |
23 |
24 |
25 | Send message
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | /**
34 | * This app is rendered in HOST frame so no need to hassle with popups since it's the same frame and DOM
35 | */
36 | const Popup = () => {
37 | const [popup, togglePopup] = useState(false);
38 |
39 | return (
40 | <>
41 | {popup && (
42 |
43 |
44 | togglePopup(false)}
49 | onKeyPress={e => null}
50 | style={{display: 'block'}}
51 | >
52 |
53 |
54 |
Sample modal
55 |
56 | togglePopup(false)}
60 | >
61 | Understood
62 |
63 |
64 |
65 |
66 |
67 |
68 | )}
69 |
70 | togglePopup(true)}>
71 | Popup
72 |
73 |
74 | >
75 | );
76 | };
77 |
78 | const App = ({node, match: {url}, authtoken}) => {
79 | useEffect(() => {
80 | console.log('REACT: Mounting'); // lifecycle demo
81 | return () => console.log('REACT: Un-Mounting'); // lifecycle demo
82 | }, []);
83 |
84 | const base = url.endsWith('/') ? url.substr(0, url.length - 1) : url;
85 |
86 | return (
87 | <>
88 |
89 |
90 |
91 | Users
92 |
93 |
94 |
95 |
96 | Groups
97 |
98 |
99 |
100 |
101 | Deep in Vue
102 |
103 |
104 |
105 |
106 |
109 | Loading page as a chunk using dynamic import()
...
110 |
111 | }
112 | >
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | authtoken (this was provided by host)
126 |
127 |
128 |
129 |
130 | >
131 | );
132 | };
133 |
134 | export default props => {
135 | const routes = (
136 | <>
137 |
138 |
} />
139 | >
140 | );
141 |
142 | // This allows to block history in sub-apps, this is not required in general
143 | return window.RCAppsDemoHistory ? (
144 | {routes}
145 | ) : (
146 | {routes}
147 | );
148 | };
149 |
--------------------------------------------------------------------------------
/demo/react/src/Modal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {createPortal} from 'react-dom';
3 |
4 | const modalRoot = document.body;
5 |
6 | export default class Modal extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.el = document.createElement('div');
10 | }
11 |
12 | componentDidMount() {
13 | modalRoot.appendChild(this.el);
14 | document.body.classList.add('modal-open');
15 | }
16 |
17 | componentWillUnmount() {
18 | modalRoot.removeChild(this.el);
19 | document.body.classList.remove('modal-open');
20 | }
21 |
22 | render() {
23 | return createPortal(this.props.children, this.el);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/demo/react/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render, unmountComponentAtNode} from 'react-dom';
3 | import {registerAppCallback} from '@ringcentral/web-apps-react';
4 | import App from './App';
5 |
6 | let MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
7 |
8 | console.log('Global App Registered'); // because it's loaded as single bundle both global and script app are delivered at once
9 |
10 | registerAppCallback('global', node => {
11 | const onChange = () => render( , node);
12 |
13 | const observer = new MutationObserver(mutations =>
14 | mutations.forEach(
15 | mutation => mutation.type === 'attributes' && onChange(), // you may also accumulate this instead of calling every time
16 | ),
17 | );
18 |
19 | node.addEventListener('remove', () => {
20 | unmountComponentAtNode(node);
21 | observer.disconnect();
22 | });
23 |
24 | observer.observe(node, {attributes: true});
25 |
26 | onChange();
27 |
28 | console.log('Global App Rendered');
29 |
30 | return () => {
31 | console.log('Global App Unmounted');
32 | };
33 | });
34 |
--------------------------------------------------------------------------------
/demo/react/src/pages/Groups.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Prompt} from 'react-router-dom';
3 |
4 | export default () => (
5 |
6 | `Are you sure you want to go to ${location.pathname}`} />
7 | Groups page loaded as a chunk using dynamic import()
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/demo/react/src/pages/Users.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => (
4 |
5 | Users page loaded as a chunk using dynamic import()
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/demo/react/src/styles.css:
--------------------------------------------------------------------------------
1 | ul.nav-tabs {
2 | margin-bottom: 1rem;
3 | }
--------------------------------------------------------------------------------
/demo/vue/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/cli-plugin-babel/preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/demo/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@web-apps/demo-vue",
3 | "version": "0.6.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "vue-cli-service serve --port $REACT_APP_VUE_PORT",
7 | "build": "vue-cli-service build"
8 | },
9 | "browserslist": "IE 11",
10 | "dependencies": {
11 | "core-js": "3.6.4",
12 | "vue": "2.6.11",
13 | "vue-router": "3.3.2"
14 | },
15 | "devDependencies": {
16 | "@vue/cli-plugin-babel": "4.4.1",
17 | "@vue/cli-plugin-router": "4.4.1",
18 | "@vue/cli-service": "4.4.1",
19 | "vue-template-compiler": "2.6.11"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/demo/vue/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vue
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/vue/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue';
2 | import VueRouter from 'vue-router';
3 |
4 | Vue.use(VueRouter);
5 |
6 | const Root = {template: 'Root!
'};
7 | const Bar = {template: 'Bar
'};
8 |
9 | const router = new VueRouter({
10 | mode: 'history',
11 | routes: [{path: '/application/apps/vue', component: Root}, {path: '/application/apps/vue/bar', component: Bar}],
12 | });
13 |
14 | const template = document.createElement('template');
15 | template.innerHTML = `
16 |
22 |
23 | `;
24 |
25 | customElements.define(
26 | 'web-app-vue',
27 | class extends HTMLElement {
28 | constructor() {
29 | super();
30 |
31 | this.attachShadow({mode: 'open'});
32 | this.shadowRoot.appendChild(document.importNode(template.content, true));
33 | this.mount = this.shadowRoot.querySelector('.container');
34 |
35 | this.app = new Vue({
36 | router,
37 | template: `
38 |
39 |
40 | Root
41 | Bar
42 |
43 |
44 |
45 |
46 | React app deep (Groups)
47 | React app
48 | Iframe app deep
49 |
50 |
51 | `.trim(),
52 | });
53 | }
54 |
55 | connectedCallback() {
56 | this.app.$mount(this.mount); // Vue REPLACES mount point
57 | }
58 |
59 | disconnectedCallback() {
60 | this.app.$destroy();
61 | }
62 | },
63 | );
64 |
--------------------------------------------------------------------------------
/demo/vue/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | configureWebpack: {
3 | output: {
4 | jsonpFunction: 'webpackJsonpDemoVue',
5 | filename: 'js/app.js', // override to prevent hash from appearing
6 | chunkFilename: 'js/[name].js', // override to prevent hash from appearing
7 | },
8 | },
9 | publicPath:
10 | process.env.NODE_ENV === 'production'
11 | ? '/demo/vue/dist/'
12 | : `http://localhost:${process.env.REACT_APP_VUE_PORT}/`,
13 | devServer: {
14 | headers: {
15 | 'Access-Control-Allow-Origin': '*',
16 | 'Access-Control-Allow-Headers': '*',
17 | },
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "npmClient": "yarn",
3 | "useWorkspaces": true,
4 | "version": "0.0.0"
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps",
3 | "version": "1.0.0",
4 | "description": "",
5 | "private": true,
6 | "scripts": {
7 | "clean": "yarn clean:packages && yarn clean:cache && yarn clean:self",
8 | "clean:packages": "lerna clean --yes",
9 | "clean:cache": "lerna exec -- rm -rf dist lib .cache false",
10 | "clean:self": "rm -rf node_modules",
11 | "test": "PORT=3000 start-server-and-test serve http://localhost:3000 cypress:run",
12 | "test:quick": "echo Not implemented yet...",
13 | "test:coverage": "echo Not implemented yet...",
14 | "build": "SKIP_PREFLIGHT_CHECK=true NODE_ENV=production dotenv lerna run build -- --stream",
15 | "build:quick": "yarn build --scope=@ringcentral/*",
16 | "start": "yarn build:quick && yarn start:quick",
17 | "start:quick": "dotenv lerna run start -- --parallel",
18 | "publish:release": "lerna publish --tag-version-prefix=\"\" --force-publish=* --no-push --no-git-tag-version",
19 | "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint --fix",
20 | "lint:all": "yarn lint './*(demo|packages)/*/src/**/*.{jsx,js,tsx,ts}'",
21 | "lint:staged": "lint-staged",
22 | "serve": "serve -l $PORT",
23 | "cypress": "cypress open",
24 | "cypress:run": "cypress run",
25 | "vercel": "vercel"
26 | },
27 | "author": "",
28 | "license": "ISC",
29 | "devDependencies": {
30 | "cypress": "4.8.0",
31 | "cypress-iframe": "1.0.1",
32 | "dotenv-cli": "2.0.1",
33 | "eslint": "5.16.0",
34 | "eslint-config-ringcentral-typescript": "1.0.0",
35 | "husky": "3.0.0",
36 | "lerna": "3.15.0",
37 | "lint-staged": "9.2.0",
38 | "serve": "11.3.2",
39 | "start-server-and-test": "1.11.0",
40 | "typescript": "3.8.3",
41 | "vercel": "19.1.0"
42 | },
43 | "workspaces": {
44 | "packages": [
45 | "demo/*",
46 | "packages/*"
47 | ],
48 | "nohoist": [
49 | "**/webpack",
50 | "**/webpack/**",
51 | "**/webpack-cli",
52 | "**/webpack-cli/**",
53 | "**/webpack-dev-server",
54 | "**/webpack-dev-server/**",
55 | "**/enhanced-resolve",
56 | "**/enhanced-resolve/**"
57 | ]
58 | },
59 | "resolutions": {
60 | "**/@babel/helper-compilation-targets": "7.10.2"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/common/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Common
2 | ===========================
3 |
4 | This library provides basic common functions like events and global Web Apps callbacks management.
5 |
6 | This library can be used both on host and on app levels.
7 |
8 | **This library can be used without Web Apps.**
--------------------------------------------------------------------------------
/packages/common/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-common",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "x-browser": "dist/ringcentral-web-apps-common.js",
7 | "scripts": {
8 | "build": "tsc",
9 | "start": "yarn build --watch --preserveWatchOutput"
10 | },
11 | "dependencies": {
12 | "custom-event-polyfill": "1.0.7"
13 | },
14 | "devDependencies": {
15 | "typescript": "3.8.3"
16 | },
17 | "publishConfig": {
18 | "access": "public"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/common/src/index.ts:
--------------------------------------------------------------------------------
1 | import 'custom-event-polyfill';
2 |
3 | // Events
4 |
5 | export enum eventType {
6 | state = 'state',
7 | message = 'message',
8 | location = 'location',
9 | popup = 'popup',
10 | authError = 'authError',
11 | }
12 |
13 | export const makeEvent = (type: eventType | string, data?) => new CustomEvent(type, {detail: data});
14 |
15 | // App callbacks
16 |
17 | const appsProperty = 'RCApps';
18 |
19 | window[appsProperty] = window[appsProperty] || {};
20 |
21 | const getObject = () => window[appsProperty];
22 |
23 | getObject().apps = getObject().apps || {};
24 |
25 | export type AppCallback = (node: HTMLElement) => any;
26 |
27 | export const registerAppCallback = (id, callback: AppCallback) => (getObject().apps[id] = callback);
28 |
29 | export const getAppCallback = (id): AppCallback => {
30 | const callback = getObject().apps[id];
31 | if (!callback)
32 | throw new Error(
33 | "Application not found in registry, make sure you've called registerAppCallback before rendering",
34 | );
35 | return callback;
36 | };
37 |
38 | export const dispatchEvent = (node: HTMLElement | any, event: eventType, data: any) => {
39 | if (!node) console.warn('Cannot dispatch event: node has not been mounted', event, data);
40 | return node && node.dispatchEvent(makeEvent(event, data));
41 | };
42 |
--------------------------------------------------------------------------------
/packages/common/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/host-css/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-host-css",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/host-css/styles.css:
--------------------------------------------------------------------------------
1 | .app-popup-bg {
2 | display: none;
3 | }
4 |
5 | .app-iframe {
6 | min-width: 100%;
7 | height: 0;
8 | background: #ffffff;
9 | }
10 |
11 | .app-popup .app-iframe {
12 | position: relative;
13 | z-index: 3;
14 | }
15 |
16 | .app-popup .app-popup-bg {
17 | display: block;
18 | position: fixed;
19 | top: 0;
20 | bottom: 0;
21 | left: 0;
22 | right: 0;
23 | z-index: 2;
24 | background: rgba(0, 0, 0, 0.3);
25 | }
--------------------------------------------------------------------------------
/packages/host-react/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Host React
2 | ===============================
3 |
4 | This is the main host package that should be used to place Applications on the host level.
5 |
6 | This is React implementation.
--------------------------------------------------------------------------------
/packages/host-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-host-react",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "yarn build --watch --preserveWatchOutput"
9 | },
10 | "dependencies": {
11 | "@ringcentral/web-apps-common": "*",
12 | "@ringcentral/web-apps-host": "*",
13 | "@ringcentral/web-apps-react": "*",
14 | "@ringcentral/web-apps-sync-react": "*"
15 | },
16 | "devDependencies": {
17 | "@types/react": "16.9.11",
18 | "react": "16.8.6",
19 | "typescript": "3.8.3"
20 | },
21 | "peerDependencies": {
22 | "react": ">=15.0.0"
23 | },
24 | "publishConfig": {
25 | "access": "public"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/host-react/src/Application.tsx:
--------------------------------------------------------------------------------
1 | import React, {FC, useEffect, useMemo, useCallback, useReducer, ReactElement, forwardRef, memo} from 'react';
2 | import {createApp, App, CreateAppInit} from '@ringcentral/web-apps-host';
3 | import {CustomElementComponent, IFrameComponent, DivComponent} from './Components';
4 |
5 | const getComponent = (type: string): any => {
6 | switch (type) {
7 | case 'script':
8 | return CustomElementComponent;
9 | case 'iframe':
10 | return IFrameComponent;
11 | case 'global':
12 | return DivComponent;
13 | default:
14 | return null;
15 | }
16 | };
17 |
18 | interface State extends CreateAppInit {
19 | app?: App;
20 | error?: Error;
21 | node?: HTMLElement;
22 | }
23 |
24 | const initialState: State = {error: null, app: null, node: null, id: null, type: null, url: null};
25 |
26 | const reducer = (state: State, {type, payload}) => {
27 | switch (type) {
28 | case 'loading':
29 | return {...state, ...initialState, ...payload};
30 | case 'error':
31 | return {...state, error: payload};
32 | case 'app':
33 | return {...state, app: payload};
34 | case 'node':
35 | return {...state, node: payload};
36 | default:
37 | return state;
38 | }
39 | };
40 |
41 | const DynamicComponent = memo(
42 | forwardRef(function DynamicComponent({type, app, ...props}, ref) {
43 | const Component = app && getComponent(type); // no app means loading
44 | return Component ? : null;
45 | }),
46 | );
47 |
48 | export interface ApplicationReturn {
49 | Component?: FC;
50 | error?: Error;
51 | loading?: boolean;
52 | node?: HTMLElement;
53 | }
54 |
55 | export const useApplication = ({id, url, type, options}: CreateAppInit): ApplicationReturn => {
56 | const [{error, app, node, type: storedType}, dispatch] = useReducer(reducer, initialState); // type is in store to make sure it's always in sync with app
57 |
58 | // Create App instance
59 |
60 | useEffect(() => {
61 | let mounted = true;
62 |
63 | console.warn('HOST: App ID has changed', id);
64 | dispatch({type: 'loading', payload: {id, url, type}});
65 |
66 | (async () => {
67 | if (!id || !url || !type) return; // maybe console.warn?
68 | try {
69 | if (mounted) dispatch({type: 'app', payload: await createApp({id, url, type, options})});
70 | } catch (error) {
71 | console.error('HOST: Cannot load app', id, error);
72 | if (mounted) dispatch({type: 'error', payload: error});
73 | }
74 | })();
75 |
76 | return () => (mounted = false);
77 | }, [id, options, type, url]);
78 |
79 | // Ref
80 |
81 | const nodeRef = useCallback(newNode => dispatch({type: 'node', payload: newNode}), []); // maybe call onReady here
82 |
83 | const Component = useMemo(
84 | () => props => , // type from store to make sure it's always in sync with app
85 | [app, nodeRef, storedType],
86 | );
87 |
88 | const loading = !node || error; // final loading status becomes true only when node is referenced & event listeners are set
89 |
90 | return {Component, error, loading, node};
91 | };
92 |
93 | export type ApplicationProps = CreateAppInit & ApplicationReturn;
94 |
95 | export const Application: FC ReactElement}> = ({children, ...props}) =>
96 | children({...props, ...useApplication(props)});
97 |
98 | export const withApplication = (defaults: CreateAppInit) => Cmp => {
99 | const WrappedComponent: FC = ({children, ...props}) => {
100 | const defaultedProps = {...defaults, ...props};
101 | const appProps = useApplication(defaultedProps);
102 | return {children} ;
103 | };
104 | WrappedComponent.displayName = `withApplication(${Cmp.displayName || Cmp.name || 'Cmp'})`;
105 | return WrappedComponent;
106 | };
107 |
--------------------------------------------------------------------------------
/packages/host-react/src/Components.tsx:
--------------------------------------------------------------------------------
1 | import React, {FC, forwardRef, useEffect, useRef} from 'react';
2 | import {IFrame, IFrameProps, useCombinedRefs} from '@ringcentral/web-apps-sync-react';
3 | import {App, ScriptApp, GlobalApp} from '@ringcentral/web-apps-host';
4 |
5 | export interface Props {
6 | app: App;
7 | }
8 |
9 | export const CustomElementComponent: FC = forwardRef(function CustomElementComponent(
10 | {app, ...props},
11 | ref,
12 | ) {
13 | const {tag} = app as ScriptApp;
14 | const Tag = tag() as any;
15 | return ;
16 | });
17 |
18 | // eslint-disable-next-line @typescript-eslint/interface-name-prefix
19 | export interface IFrameComponentProps extends Props, IFrameProps {}
20 |
21 | /**
22 | * @see https://github.com/ReactTraining/react-router/issues/6056 cannot use ref in IFrame...
23 | */
24 | export const IFrameComponent: FC = forwardRef(function IFrameComponent(
25 | {app: {url, id}, ...props},
26 | ref,
27 | ) {
28 | return (
29 |
37 | );
38 | });
39 |
40 | export interface DivComponentProps extends Props {
41 | direct: boolean;
42 | }
43 |
44 | export const DivComponent: FC = forwardRef(function DivComponent(
45 | {app, direct, ...props},
46 | ref,
47 | ) {
48 | const {id, callback} = app as GlobalApp;
49 | const node = useRef(null);
50 | const combinedRef = useCombinedRefs(ref, node);
51 | // this is used for Global apps in JSONP mode
52 | useEffect(() => (direct ? () => {} : callback(combinedRef.current)), [callback, combinedRef, direct, id]);
53 | // this is used for Global apps in direct mode
54 | const Cmp = direct && callback;
55 | return (
56 | <>
57 |
58 | {Cmp && }
59 | >
60 | );
61 | });
62 |
--------------------------------------------------------------------------------
/packages/host-react/src/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Application';
2 | export * from '@ringcentral/web-apps-react';
3 |
--------------------------------------------------------------------------------
/packages/host-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/host-web-component/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Host Web Component
2 | =======================================
3 |
4 | This is the main host package that should be used to place Applications on the host level.
5 |
6 | This is Web Component implementation.
--------------------------------------------------------------------------------
/packages/host-web-component/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-host-web-component",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "yarn build --watch --preserveWatchOutput"
9 | },
10 | "dependencies": {
11 | "@ringcentral/web-apps-common": "*",
12 | "@ringcentral/web-apps-host": "*",
13 | "@ringcentral/web-apps-sync-web-component": "*"
14 | },
15 | "devDependencies": {
16 | "typescript": "3.8.3"
17 | },
18 | "publishConfig": {
19 | "access": "public"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/host-web-component/src/index.tsx:
--------------------------------------------------------------------------------
1 | import {createMutationObserver} from '@ringcentral/web-apps-sync-web-component';
2 | import {createApp, App, ScriptApp, GlobalApp} from '@ringcentral/web-apps-host';
3 | import {makeEvent} from '@ringcentral/web-apps-common';
4 |
5 | export * from '@ringcentral/web-apps-common';
6 |
7 | const doNotSyncAttributes = ['id', 'type', 'url', 'history'];
8 |
9 | if (!customElements.get('web-app'))
10 | customElements.define(
11 | 'web-app',
12 | class extends HTMLElement {
13 | protected app: App = null;
14 | protected observer: MutationObserver = null;
15 | protected error = null;
16 |
17 | public constructor() {
18 | super();
19 | // this.attachShadow({mode: 'open'});
20 | this.observer = createMutationObserver(this.attributeChanged);
21 | }
22 |
23 | protected render({id, url, type}) {
24 | let node;
25 |
26 | switch (type) {
27 | case 'script':
28 | const {tag} = this.app as ScriptApp;
29 | const Tag = tag() as any;
30 | node = document.createElement(Tag);
31 | break;
32 |
33 | case 'iframe':
34 | const history = this.getAttribute('history');
35 | if (!history) throw new Error('History not defined');
36 | node = document.createElement('web-app-sync-iframe');
37 | node.setAttribute('id', id);
38 | node.setAttribute('url', Array.isArray(url) ? url[0] : url);
39 | node.setAttribute('history', history);
40 | break;
41 |
42 | case 'global':
43 | node = document.createElement('div');
44 | break;
45 |
46 | default:
47 | throw new Error('Unknown app type ' + type);
48 | }
49 |
50 | // could be this.shadowRoot but we WANT parent styles here to style iframes/error messages/loaders ETC
51 | this.innerHTML = '';
52 | this.appendChild(node);
53 |
54 | // Initial sync of attributes
55 | Object.values(this.attributes)
56 | .filter(attr => !doNotSyncAttributes.includes(attr.name))
57 | .forEach(attr => {
58 | node.setAttribute(attr.name, this.getAttribute(attr.name));
59 | });
60 |
61 | if (type === 'global') (this.app as GlobalApp).callback(node);
62 | }
63 |
64 | protected async loadApp() {
65 | const id = this.getAttribute('id');
66 | const rawUrl = this.getAttribute('url');
67 | const type = this.getAttribute('type');
68 |
69 | if (!id) throw new Error('ID not defined');
70 | if (!rawUrl) throw new Error('ID not defined');
71 | if (!type) throw new Error('ID not defined');
72 |
73 | const url = JSON.parse(rawUrl);
74 |
75 | this.dispatchEvent(makeEvent('beforeload', {id, type, url}));
76 | console.warn('WEB COMPONENT HOST: App ID has changed', id, type);
77 |
78 | try {
79 | //TODO [UIA-10000] Custom loading
80 | this.innerHTML = `Loading...
`;
81 | this.app = null;
82 | this.error = null;
83 | this.app = await createApp({id, url, type});
84 | await this.render({id, url, type});
85 | this.dispatchEvent(makeEvent('load', {id, type, url}));
86 | } catch (error) {
87 | this.error = error;
88 | //TODO [UIA-10000] Custom loading
89 | this.innerHTML = `Cannot load app ${id}: ${error.message}
`;
90 | console.error('WEB COMPONENT HOST: Cannot load app', id, error);
91 | this.dispatchEvent(makeEvent('error', {id, type, url, error}));
92 | }
93 | }
94 |
95 | public async connectedCallback() {
96 | await this.loadApp();
97 | this.observer.observe(this, {attributes: true});
98 | }
99 |
100 | public async disconnectedCallback() {
101 | this.observer.disconnect();
102 | }
103 |
104 | protected getNode() {
105 | return this.firstElementChild;
106 | }
107 |
108 | protected attributeChanged = async (name, newValue) => {
109 | if (name === 'id') {
110 | return await this.loadApp(); // this is handled in connectedCallback
111 | }
112 |
113 | if (doNotSyncAttributes.includes(name)) return;
114 | this.getNode().setAttribute(name, newValue);
115 | };
116 |
117 | public getEventTarget() {
118 | const node = this.getNode();
119 | //FIXME [UIA-10000] Node may change and we need uniform way to access the eventTarget
120 | return 'getEventTarget' in node ? (node as any).getEventTarget() : node;
121 | }
122 | },
123 | );
124 |
--------------------------------------------------------------------------------
/packages/host-web-component/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/host/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Host
2 | =========================
3 |
4 | This is the main host package that should be used to place Applications on the host level.
5 |
6 | This package is abstract and can be used with any framework.
--------------------------------------------------------------------------------
/packages/host/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-host",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "yarn build --watch --preserveWatchOutput"
9 | },
10 | "dependencies": {
11 | "@ringcentral/web-apps-common": "*",
12 | "little-loader": "0.2.0"
13 | },
14 | "devDependencies": {
15 | "typescript": "3.8.3"
16 | },
17 | "publishConfig": {
18 | "access": "public"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/host/src/index.ts:
--------------------------------------------------------------------------------
1 | import load from 'little-loader';
2 | import {getAppCallback} from '@ringcentral/web-apps-common';
3 |
4 | const makeKey = ({id, url, type}) => `${id}-${type}-${url}`;
5 |
6 | const retryTimeout = 100;
7 | const maxAttempts = 100;
8 | const cache = {};
9 |
10 | export interface AppInit {
11 | id: string;
12 | url: string | string[];
13 | options?: {[key: string]: any};
14 | }
15 |
16 | export interface CreateAppInit extends AppInit {
17 | type: string;
18 | }
19 |
20 | const loadScript = url =>
21 | new Promise((res, rej) => {
22 | load(url, {
23 | callback: err => {
24 | if (err) return rej(err);
25 | res();
26 | },
27 | });
28 | });
29 |
30 | const loadLink = (url, attrs) =>
31 | new Promise((res, rej) => {
32 | const id = 'link-' + url;
33 | const oldLink = document.getElementById(id);
34 | if (oldLink && oldLink.parentNode) oldLink.parentNode.removeChild(oldLink);
35 | const link = document.createElement('link');
36 | link.id = id;
37 | Object.keys(attrs).forEach(key => {
38 | link[key] = attrs[key];
39 | });
40 | link.href = url;
41 | link.onload = res;
42 | link.onerror = rej;
43 | document.head.appendChild(link);
44 | });
45 |
46 | const loadMixed = async (urls: string | string[]) => {
47 | if (!Array.isArray(urls) || typeof urls === 'string') {
48 | await loadScript(urls);
49 | return;
50 | }
51 | for (const url of urls) {
52 | if (url.includes('.css')) {
53 | await loadLink(url, {type: 'text/css', rel: 'stylesheet'});
54 | } else {
55 | await loadScript(url);
56 | }
57 | }
58 | };
59 |
60 | export abstract class App {
61 | public id: string;
62 | public url: string | string[];
63 | public options: {[key: string]: any};
64 | public loadedSource: any;
65 |
66 | public constructor({id, url, options = {}}: AppInit) {
67 | this.id = id;
68 | this.url = url;
69 | this.options = options;
70 | this.loadedSource = this.load();
71 | }
72 |
73 | public async init() {
74 | await this.loadedSource;
75 | }
76 |
77 | public abstract async load();
78 | }
79 |
80 | export abstract class JSApp extends App {
81 | public async load() {
82 | return loadMixed(this.url);
83 | }
84 | }
85 |
86 | export class ScriptApp extends JSApp {
87 | public regex = /([A-Z])/g;
88 |
89 | public tag = () => {
90 | return `web-app-${this.id.replace(this.regex, v => '-' + v.toLowerCase())}`;
91 | };
92 | }
93 |
94 | export class GlobalApp extends JSApp {
95 | public callback: (node: any) => any;
96 |
97 | public async ensureRegistered() {
98 | return new Promise((res, rej) => {
99 | let attempts = 0;
100 | const checkRegistered = () => {
101 | try {
102 | this.callback = getAppCallback(this.id);
103 | res();
104 | } catch (e) {
105 | attempts++;
106 | if (attempts > maxAttempts)
107 | rej(
108 | new Error(
109 | 'App ' + this.id + ' has not called register function within reasonable timeframe',
110 | ),
111 | );
112 | setTimeout(checkRegistered, retryTimeout);
113 | }
114 | };
115 | checkRegistered();
116 | });
117 | }
118 |
119 | public async getFederatedCallback() {
120 | const defaultScope = this.options.defaultScope || 'default';
121 | const scope = this.options.scope || `web_app_${this.id}`;
122 | const module = this.options.module || './index';
123 | const exportName = this.options.exportName || 'default';
124 |
125 | // Initializes the share scope. This fills it with known provided modules from this build and all remotes
126 | // @ts-ignore
127 | await __webpack_init_sharing__(defaultScope);
128 | // @ts-ignore
129 | await window[scope].init(__webpack_share_scopes__[defaultScope]); //eslint-disable-line
130 | // @ts-ignore
131 | const factory = await window[scope].get(module);
132 | this.callback = factory()[exportName];
133 | }
134 |
135 | public async load() {
136 | await super.load();
137 | if (this.options.federation) {
138 | await this.getFederatedCallback();
139 | } else {
140 | await this.ensureRegistered();
141 | }
142 | }
143 | }
144 |
145 | export class IFrameApp extends App {
146 | public async load() {}
147 | }
148 |
149 | export const createApp = async ({id, url, type, options}: CreateAppInit) => {
150 | const key = makeKey({id, url, type});
151 |
152 | if (!cache.hasOwnProperty(key)) {
153 | switch (type) {
154 | case 'script':
155 | cache[key] = new ScriptApp({id, url, options});
156 | break;
157 | case 'iframe':
158 | cache[key] = new IFrameApp({id, url, options});
159 | break;
160 | case 'global':
161 | cache[key] = new GlobalApp({id, url, options});
162 | break;
163 | default:
164 | throw new Error('Unknown app type ' + type);
165 | }
166 | }
167 |
168 | await cache[key].init();
169 |
170 | return cache[key];
171 | };
172 |
--------------------------------------------------------------------------------
/packages/host/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/react/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps React
2 | ===============================
3 |
4 | The purpose of this library is to synchronize page `location` with `history` object of React.
5 |
6 | The reason why it is needed is this bug of React Router: https://github.com/ReactTraining/react-router/issues/7113.
7 |
8 | ## Usage
9 |
10 | **This library can be used without Web Apps.**
11 |
12 | Simply put the `LocationSync` anywhere under `Router` in your hierarchy of components:
13 |
14 | ```js
15 | import {BrowserRouter, Route} from 'react-router-dom';
16 | import {LocationSync} from '@ringcentral/web-apps-react';
17 |
18 | export default () => (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | ```
--------------------------------------------------------------------------------
/packages/react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-react",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "yarn build --watch --preserveWatchOutput"
9 | },
10 | "dependencies": {
11 | "@ringcentral/web-apps-common": "*"
12 | },
13 | "devDependencies": {
14 | "@types/history": "4.7.4",
15 | "@types/react": "16.9.11",
16 | "@types/react-router": "5.1.4",
17 | "typescript": "3.8.3"
18 | },
19 | "peerDependencies": {
20 | "react": "*",
21 | "react-router": "*"
22 | },
23 | "publishConfig": {
24 | "access": "public"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/react/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from 'react';
2 | import {withRouter} from 'react-router';
3 | import {History} from 'history';
4 | import {eventType} from '@ringcentral/web-apps-common';
5 |
6 | export * from '@ringcentral/web-apps-common';
7 |
8 | export const makeHistoryFromRouter = (router: any): History =>
9 | // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
10 | ({
11 | listen: (...args) => router.listen(...args),
12 | replace: (...args) => router.replace(...args),
13 | push: (...args) => router.push(...args),
14 | get location() {
15 | return router.getCurrentLocation();
16 | },
17 | } as History);
18 |
19 | export const normalizeHistory = ({history, router}: any): History => (router ? makeHistoryFromRouter(router) : history);
20 |
21 | //@see https://github.com/ReactTraining/react-router/issues/7113
22 | export const LocationSync = withRouter(({history, router}: any) => {
23 | useEffect(() => {
24 | const normalHistory = normalizeHistory({history, router}); //FIXME [UIA-10000] useHistory
25 | let timeout;
26 |
27 | const check = () => {
28 | if (window.location.pathname === normalHistory.location.pathname) return; // quick check
29 |
30 | const windowHref = normalHistory.createHref(window.location);
31 | const historyHref = normalHistory.createHref(normalHistory.location);
32 |
33 | if (windowHref === historyHref) return; // more complex check
34 |
35 | console.log('Synchronized URLs', windowHref, '->', historyHref);
36 |
37 | normalHistory.replace(windowHref);
38 | };
39 |
40 | const tick = () => {
41 | check();
42 | timeout = setTimeout(tick, 100);
43 | };
44 |
45 | tick();
46 |
47 | return () => clearTimeout(timeout);
48 | }, [history, router]);
49 |
50 | return <>>;
51 | });
52 |
53 | export const useListenerEffect = (node, event: eventType, listener) => {
54 | useEffect(() => {
55 | if (!node || !listener) return;
56 | node.addEventListener(event, listener);
57 | return () => node && node.removeEventListener(event, listener);
58 | }, [node, listener, event]);
59 | };
60 |
--------------------------------------------------------------------------------
/packages/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/sync-host/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Sync Host
2 | ==============================
3 |
4 | This library can be used to synchronize parent frame and the IFRAME:
5 |
6 | - URL of parent frame and IFRAME
7 | - IFRAME size will be set according to it's content
8 | - CustomEvents on the IFRAME DOM node will be transmitted to and from the page inside IFRAME
9 |
10 | ## Usage: Host part
11 |
12 | ```js
13 | import {HostSync} from "@ringcentral/web-apps-sync/lib/host";
14 | import {makeEvent, eventType} from "@ringcentral/web-apps-common";
15 | import {createBrowserHistory} from 'history';
16 |
17 | const history = createBrowserHistory();
18 | const iframe = document.getElementById('iframe');
19 |
20 | const hostSync = new HostSync({
21 | url: 'http://path-to-iframe-page.com',
22 | iframe,
23 | history,
24 | });
25 |
26 | iframe.addEventListener(eventType.message, data => console.log(data));
27 | iframe.dispatchEvent(makeEvent(eventType.message, {foo: 'bar'}));
28 | ```
29 |
30 | Usage of `history` is optional. You can supply any history-like object with following API:
31 |
32 | ```js
33 | const history = {
34 | listen: () => {}, // listen to pushState events, should return unsubscribe callback
35 | replace: (url, state) => window.history.replaceState(state, null, url),
36 | push: (url, state) => window.history.pushState(state, null, url),
37 | get location() {
38 | return window.location;
39 | },
40 | };
41 | ```
42 |
--------------------------------------------------------------------------------
/packages/sync-host/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-sync-host",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "yarn build --watch --preserveWatchOutput"
9 | },
10 | "dependencies": {
11 | "@ringcentral/web-apps-common": "*",
12 | "@ringcentral/web-apps-sync": "*",
13 | "iframe-resizer": "4.1.1"
14 | },
15 | "devDependencies": {
16 | "@types/history": "4.7.4",
17 | "typescript": "3.8.3"
18 | },
19 | "publishConfig": {
20 | "access": "public"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/sync-host/src/index.ts:
--------------------------------------------------------------------------------
1 | import {History} from 'history';
2 | import iFrameResize from 'iframe-resizer/js/iframeResizer';
3 | import {eventType} from '@ringcentral/web-apps-common';
4 | import {Sync, addHash, removeHash, SyncInit} from '@ringcentral/web-apps-sync';
5 |
6 | export enum trackingMode {
7 | hash = 'hash',
8 | disabled = 'disabled',
9 | slave = 'slave',
10 | full = 'full',
11 | }
12 |
13 | export interface HostSyncInit extends Pick {
14 | id: string;
15 | url: string;
16 | iframe: HTMLIFrameElement;
17 | history: History;
18 | tracking?: trackingMode;
19 | minHeight?: any;
20 | }
21 |
22 | export class HostSync extends Sync {
23 | public history: History = null;
24 | public iframe: HTMLIFrameElement = null;
25 | public tracking: trackingMode = null;
26 |
27 | public constructor({
28 | id,
29 | url,
30 | iframe,
31 | history,
32 | tracking = trackingMode.hash,
33 | minHeight = 500, // needed to accomodate login
34 | origin = undefined,
35 | }) {
36 | super({
37 | id,
38 | events: {
39 | target: iframe,
40 | types: [eventType.popup, eventType.message],
41 | },
42 | postMessage: {
43 | receiver: iframe.contentWindow,
44 | target: window,
45 | handler: (...args) => this.handlePostMessage(...args),
46 | types: [eventType.popup, eventType.message, eventType.authError],
47 | origin,
48 | },
49 | history,
50 | });
51 |
52 | this.tracking = tracking;
53 |
54 | this.iframe = iframe;
55 |
56 | try {
57 | const {origin, pathname, search} = new URL(url);
58 | const updatedPathname = pathname === '/' ? '' : pathname;
59 | this.iframe.src = `${origin}${updatedPathname}${this.getState()}${search}`;
60 | } catch (error) {
61 | this.iframe.src = url + this.getState();
62 | console.error('Invalid URL of iframe', url);
63 | }
64 |
65 | !this.iframe['iFrameResizer'] &&
66 | iFrameResize(
67 | {
68 | checkOrigin: false,
69 | minHeight,
70 | heightCalculationMethod: 'min',
71 | sizeWidth: true,
72 | tolerance: 50,
73 | },
74 | this.iframe,
75 | );
76 | }
77 |
78 | public destroy = () => {
79 | super.destroy();
80 | this.iframe = null;
81 | // setTimeout(() => {
82 | // // we have to do it like this bc React will remove it and resizer too
83 | // this.iframe['iFrameResizer'].close(); // this actually does nothing besides removing from DOM
84 | // }, 1);
85 | };
86 |
87 | protected handlePostMessage = ({type, data}) => {
88 | if (type === eventType.location) {
89 | this.open(data);
90 | return true;
91 | }
92 | };
93 |
94 | protected getState = () => {
95 | switch (this.tracking) {
96 | case trackingMode.hash:
97 | return removeHash(new URL(super.getState(), location.origin).hash);
98 | case trackingMode.disabled:
99 | return '';
100 | case trackingMode.full:
101 | case trackingMode.slave:
102 | return super.getState();
103 | default:
104 | throw new Error(`Unknown tracking mode ${this.tracking}`);
105 | }
106 | };
107 |
108 | protected setState = state => {
109 | switch (this.tracking) {
110 | case trackingMode.hash:
111 | const url = new URL(super.getState(), location.origin);
112 | url.hash = addHash(state);
113 | return super.setState(url.toString().replace(location.origin, ''));
114 | case trackingMode.disabled:
115 | case trackingMode.slave:
116 | return; // ignore state change
117 | case trackingMode.full:
118 | return super.setState(state);
119 | default:
120 | throw new Error(`Unknown tracking mode ${this.tracking}`);
121 | }
122 | };
123 |
124 | protected open = location => super.setState(location);
125 | }
126 |
--------------------------------------------------------------------------------
/packages/sync-host/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/sync-iframe/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Sync IFRAME
2 | ================================
3 |
4 | This library can be used to synchronize parent frame and the IFRAME:
5 |
6 | - URL of parent frame and IFRAME
7 | - IFRAME size will be set according to it's content
8 | - CustomEvents on the IFRAME DOM node will be transmitted to and from the page inside IFRAME
9 |
10 | ## Usage: IFRAME part
11 |
12 | ```js
13 | import {IFrameSync} from "@ringcentral/web-apps-sync/lib/iframe";
14 | import {makeEvent, eventType} from "@ringcentral/web-apps-common";
15 |
16 | const iframeSync = new IFrameSync({
17 | history: 'html5', // also can be hash or custom history object
18 | sendInitialLocation: true // will trigger location event once created
19 | });
20 |
21 | iframeSync.getEventTarget().addEventListener(eventType.message, data => console.log(data));
22 | iframeSync.getEventTarget().dispatchEvent(makeEvent(eventType.message, {foo: 'bar'}));
23 | ```
24 |
25 | For react applications to simplify link between Sync and History follow this pattern:
26 |
27 | ```jsx
28 | import {createBrowserHistory} from 'history';
29 | import {Router} from 'react-router-dom';
30 |
31 | const history = createBrowserHistory();
32 |
33 | const iframeSync = new IFrameSync({history});
34 |
35 | export defult ... ;
36 | ```
--------------------------------------------------------------------------------
/packages/sync-iframe/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-sync-iframe",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "x-browser": "dist/ringcentral-web-apps-iframe.js",
7 | "scripts": {
8 | "build": "yarn build:tsc && yarn build:webpack",
9 | "build:tsc": "tsc",
10 | "build:webpack": "webpack --progress",
11 | "start": "concurrently yarn:start:*",
12 | "start:tsc": "yarn build:tsc --watch --preserveWatchOutput",
13 | "start:webpack": "yarn build:webpack --watch"
14 | },
15 | "dependencies": {
16 | "@ringcentral/web-apps-common": "*",
17 | "@ringcentral/web-apps-sync": "*",
18 | "iframe-resizer": "4.1.1"
19 | },
20 | "devDependencies": {
21 | "@types/history": "4.7.4",
22 | "concurrently": "5.0.2",
23 | "terser-webpack-plugin": "2.3.2",
24 | "ts-loader": "6.0.4",
25 | "typescript": "3.8.3",
26 | "webpack": "4.41.5",
27 | "webpack-cli": "3.3.10"
28 | },
29 | "publishConfig": {
30 | "access": "public"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/sync-iframe/src/IFrameSync.ts:
--------------------------------------------------------------------------------
1 | import {eventType} from '@ringcentral/web-apps-common';
2 | import {Sync, historyType, SyncInit} from '@ringcentral/web-apps-sync';
3 |
4 | export {historyType};
5 |
6 | // eslint-disable-next-line @typescript-eslint/interface-name-prefix
7 | export interface IFrameSyncInit extends Pick, Pick {
8 | id?: string;
9 | history?: historyType;
10 | sendInitialLocation?: boolean;
11 | }
12 |
13 | export class IFrameSync extends Sync {
14 | public constructor({
15 | id,
16 | history = null,
17 | sendInitialLocation = false,
18 | origin = undefined,
19 | base = '',
20 | }: IFrameSyncInit = {}) {
21 | super({
22 | id,
23 | events: {
24 | target: document.createElement('iframe'),
25 | types: [eventType.popup, eventType.message, eventType.location, eventType.authError],
26 | },
27 | postMessage: {
28 | receiver: window.parent,
29 | target: window,
30 | types: [eventType.popup, eventType.message],
31 | origin,
32 | },
33 | history,
34 | base,
35 | });
36 |
37 | if (sendInitialLocation) this.historyListener();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/sync-iframe/src/bundle.ts:
--------------------------------------------------------------------------------
1 | export * from './index';
2 | export * from '@ringcentral/web-apps-common';
3 |
--------------------------------------------------------------------------------
/packages/sync-iframe/src/index.ts:
--------------------------------------------------------------------------------
1 | import 'iframe-resizer/js/iframeResizer.contentWindow';
2 | export * from './IFrameSync';
3 |
--------------------------------------------------------------------------------
/packages/sync-iframe/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/sync-iframe/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'production',
6 | devtool: '#cheap-module-source-map',
7 | entry: {
8 | iframe: './src/bundle.ts',
9 | 'iframe.min': './src/bundle.ts',
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.tsx?$/,
15 | use: 'ts-loader',
16 | exclude: /node_modules/,
17 | },
18 | ],
19 | },
20 | resolve: {
21 | extensions: ['.tsx', '.ts', '.js'],
22 | },
23 | output: {
24 | filename: 'ringcentral-web-apps-[name].js',
25 | path: path.resolve(__dirname, 'dist'),
26 | library: ['RCApps', 'IFrameSDK'],
27 | libraryTarget: 'umd',
28 | // libraryExport: 'default',
29 | },
30 | externals: {
31 | react: 'React',
32 | },
33 | optimization: {
34 | minimize: true,
35 | minimizer: [
36 | new TerserPlugin({
37 | include: /\.min\.js$/,
38 | }),
39 | ],
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/packages/sync-react/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Sync React
2 | ===============================
3 |
4 | This library can be used to synchronize parent frame and the IFRAME.
5 |
6 | It is a small React wrapper for the main sync library.
7 |
8 | It also normalizes the React Router history object as there are discrepancies between versions 3 and 4+.
9 |
10 | **This library can be used without Web Apps.**
--------------------------------------------------------------------------------
/packages/sync-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-sync-react",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "yarn build --watch --preserveWatchOutput"
9 | },
10 | "dependencies": {
11 | "@ringcentral/web-apps-sync-host": "*",
12 | "@ringcentral/web-apps-react": "*"
13 | },
14 | "devDependencies": {
15 | "@types/react": "16.9.11",
16 | "@types/react-router": "5.1.4",
17 | "typescript": "3.8.3"
18 | },
19 | "peerDependencies": {
20 | "react": "*",
21 | "react-router": "*"
22 | },
23 | "publishConfig": {
24 | "access": "public"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/sync-react/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {IframeHTMLAttributes, useEffect, useRef, RefObject} from 'react';
2 | import {withRouter, RouteComponentProps} from 'react-router';
3 | import {HostSync, HostSyncInit} from '@ringcentral/web-apps-sync-host';
4 | import {normalizeHistory} from '@ringcentral/web-apps-react';
5 |
6 | /**
7 | * @see https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd
8 | */
9 | export function useCombinedRefs(...refs): RefObject {
10 | const targetRef = React.useRef();
11 |
12 | useEffect(() => {
13 | refs.forEach(ref => {
14 | if (!ref) return;
15 |
16 | if (typeof ref === 'function') {
17 | ref(targetRef.current);
18 | } else {
19 | ref.current = targetRef.current;
20 | }
21 | });
22 | }, [refs]);
23 |
24 | return targetRef;
25 | }
26 |
27 | // eslint-disable-next-line @typescript-eslint/interface-name-prefix
28 | export interface IFrameProps
29 | extends IframeHTMLAttributes,
30 | RouteComponentProps,
31 | Pick {
32 | nodeRef: any;
33 | url: string;
34 | }
35 |
36 | /**
37 | * @see https://github.com/ReactTraining/react-router/issues/6056 cannot use ref here...
38 | */
39 | export const IFrame = withRouter(({nodeRef, id, url, tracking, minHeight, origin, ...props}) => {
40 | const history = normalizeHistory(props); //FIXME [UIA-10000] useHistory
41 | const iframe = useRef(null);
42 | const combinedRef = useCombinedRefs(nodeRef, iframe);
43 |
44 | useEffect(() => {
45 | const sync = new HostSync({
46 | id,
47 | url,
48 | iframe: combinedRef.current,
49 | history,
50 | tracking,
51 | minHeight,
52 | origin,
53 | });
54 |
55 | return () => sync.destroy();
56 | }, [combinedRef, history, id, iframe, minHeight, origin, tracking, url]);
57 |
58 | delete props['staticContext']; // some router crap
59 |
60 | return (
61 |
69 | );
70 | });
71 |
--------------------------------------------------------------------------------
/packages/sync-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/sync-web-component/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Sync Web Component
2 | =======================================
3 |
4 | This library can be used to synchronize parent frame and the IFRAME.
5 |
6 | It is a small Web Component wrapper for the main sync library.
7 |
8 | It also normalizes the React Router history object as there are discrepancies between versions 3 and 4+.
9 |
10 | **This library can be used without Web Apps.**
--------------------------------------------------------------------------------
/packages/sync-web-component/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-sync-web-component",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "yarn build --watch --preserveWatchOutput"
9 | },
10 | "dependencies": {
11 | "@ringcentral/web-apps-sync-host": "*",
12 | "@ringcentral/web-apps-sync": "*"
13 | },
14 | "devDependencies": {
15 | "typescript": "3.8.3"
16 | },
17 | "publishConfig": {
18 | "access": "public"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/sync-web-component/src/index.tsx:
--------------------------------------------------------------------------------
1 | import {HostSync} from '@ringcentral/web-apps-sync-host';
2 | import {makeEvent} from '@ringcentral/web-apps-common';
3 |
4 | /**
5 | * @see https://github.com/w3c/webcomponents/issues/565#issuecomment-345556883
6 | * @param callback
7 | * @returns {MutationObserver}
8 | */
9 | export const createMutationObserver = callback =>
10 | new MutationObserver(mutations => {
11 | mutations.forEach(mutation => {
12 | if (mutation.type === 'attributes') {
13 | let newVal = mutation.target['getAttribute'](mutation.attributeName);
14 | callback(mutation.attributeName, newVal);
15 | }
16 | });
17 | });
18 |
19 | if (!customElements.get('web-app-sync-iframe'))
20 | customElements.define(
21 | 'web-app-sync-iframe',
22 | class extends HTMLElement {
23 | private sync: HostSync;
24 | private observer: MutationObserver;
25 |
26 | public constructor() {
27 | super();
28 | // this.attachShadow({mode: 'open'});
29 | this.observer = createMutationObserver(this.attributeChanged);
30 | }
31 |
32 | public getNode() {
33 | return this.firstElementChild as HTMLIFrameElement;
34 | }
35 |
36 | public connectedCallback() {
37 | // could be this.shadowRoot but we WANT parent styles here to style iframes/error messages/loaders ETC
38 | this.innerHTML = ``;
39 | const iframe = this.getNode();
40 |
41 | this.sync = new HostSync({
42 | id: this.getAttribute('id'),
43 | url: this.getAttribute('url'),
44 | iframe,
45 | history: this.getAttribute('history') as any,
46 | tracking: this.getAttribute('tracking') as any,
47 | minHeight: this.getAttribute('min-height') as any,
48 | origin: this.getAttribute('origin') as any,
49 | });
50 |
51 | this.observer.observe(this, {attributes: true});
52 | this.dispatchEvent(makeEvent('load'));
53 | }
54 |
55 | public disconnectedCallback() {
56 | this.sync.destroy();
57 | this.observer.disconnect();
58 | }
59 |
60 | public attributeChanged = (name, newValue) => {
61 | if (!this.getNode()) return;
62 | this.getNode().setAttribute(name, newValue);
63 | };
64 |
65 | public getEventTarget() {
66 | return this.getNode();
67 | }
68 | },
69 | );
70 |
--------------------------------------------------------------------------------
/packages/sync-web-component/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/sync/README.md:
--------------------------------------------------------------------------------
1 | RingCentral Web Apps Sync
2 | =========================
3 |
4 | This library can be used to synchronize parent frame and the IFRAME:
5 |
6 | - URL of parent frame and `IFRAME`
7 | - `IFRAME` size will be set according to it's content
8 | - CustomEvents on the `IFRAME` DOM node will be transmitted to and from the page inside `IFRAME`
--------------------------------------------------------------------------------
/packages/sync/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ringcentral/web-apps-sync",
3 | "version": "0.0.0",
4 | "main": "lib/index.js",
5 | "types": "lib/index.d.ts",
6 | "scripts": {
7 | "build": "tsc",
8 | "start": "yarn build --watch --preserveWatchOutput"
9 | },
10 | "dependencies": {
11 | "@ringcentral/web-apps-common": "*"
12 | },
13 | "devDependencies": {
14 | "typescript": "3.8.3"
15 | },
16 | "publishConfig": {
17 | "access": "public"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/sync/src/Sync.ts:
--------------------------------------------------------------------------------
1 | import {eventType} from '@ringcentral/web-apps-common';
2 | import {postMessageEvent, postMessageListener, sendPostMessage} from './postMessage';
3 | import {createHistoryListener, HistoryListener, historyType} from './historyListener';
4 | import {isRetransmittedEvent, makeRetransmittedEvent} from './events';
5 |
6 | export interface SyncInit {
7 | events: {
8 | target: HTMLElement;
9 | types: eventType[]; // which events to retransmit as post messages
10 | };
11 | postMessage: {
12 | receiver; // where to post message
13 | target; // where to listen to incoming post messages
14 | handler?: ({type, data}: {type: eventType; data: any}) => any;
15 | types: eventType[]; // which post messages to retransmit as events
16 | origin?: string;
17 | };
18 | id: string;
19 | base?: string;
20 | history?: historyType | any;
21 | }
22 |
23 | export abstract class Sync implements SyncInit {
24 | public id: string;
25 | public events: SyncInit['events'];
26 | public postMessage: SyncInit['postMessage'];
27 | public postMessageListener;
28 | public lastState = '';
29 | public base = '';
30 | public historyListenerObject: HistoryListener;
31 |
32 | public constructor({events, postMessage, history, id, base}: SyncInit) {
33 | if (!id) throw new Error('No ID');
34 | if (!events) throw new Error('No events config');
35 | if (!postMessage) throw new Error('No postMessage config');
36 | if (!postMessage.origin) console.warn('No origin in config of frame', window.location.origin);
37 |
38 | this.events = events;
39 | this.postMessage = postMessage;
40 | this.id = id;
41 | if (base) this.base = base;
42 |
43 | this.events.types.forEach(type => this.events.target.addEventListener(type, this.postMessageFromEvent));
44 |
45 | this.postMessageListener = postMessageListener({
46 | id,
47 | safeOrigin: this.postMessage.origin,
48 | callback: this.postMessageCallback,
49 | });
50 |
51 | //FIXME [UIA-10000] Implement props sync
52 | this.postMessage.target.addEventListener(postMessageEvent, this.postMessageListener);
53 |
54 | this.historyListenerObject = createHistoryListener({listener: this.historyListener, history});
55 | }
56 |
57 | protected destroy() {
58 | this.events.types.forEach(type => this.events.target.removeEventListener(type, this.postMessageFromEvent));
59 | this.postMessage.target.removeEventListener(postMessageEvent, this.postMessageListener);
60 | this.historyListenerObject.destroy();
61 | }
62 |
63 | public getEventTarget() {
64 | return this.events.target;
65 | }
66 |
67 | protected postMessageCallback = ({type, data}: {type: eventType; data}) => {
68 | data = JSON.parse(data);
69 |
70 | if (type === eventType.state) return this.updateStateOnPostMessage(data);
71 |
72 | if (~this.postMessage.types.indexOf(type)) return this.dispatchFromPostMessage(type, data);
73 |
74 | if (this.postMessage.handler && !this.postMessage.handler({type, data})) {
75 | throw new Error('Unknown postMessage type: ' + type);
76 | }
77 | };
78 |
79 | protected dispatchFromPostMessage = (type: eventType, data) =>
80 | this.events.target.dispatchEvent(makeRetransmittedEvent(type, data));
81 |
82 | protected postMessageFromEvent = event =>
83 | !isRetransmittedEvent(event) && this.sendPostMessage(event.type, event.detail);
84 |
85 | protected sendPostMessage = (type: eventType, data) => {
86 | if (!this.postMessage.receiver || this.postMessage.receiver === this.postMessage.target) return;
87 |
88 | sendPostMessage(
89 | this.postMessage.receiver,
90 | this.id,
91 | {
92 | type,
93 | data: JSON.stringify(data),
94 | },
95 | this.postMessage.origin,
96 | );
97 | };
98 |
99 | protected getState() {
100 | return this.historyListenerObject.getState().replace(this.base, '');
101 | }
102 |
103 | protected setState(state) {
104 | if (this.getState() === state || this.lastState === state) return; // last state check is needed to prevent reaction on own post message
105 | console.log('Setting state', this.id, this.lastState, state);
106 | this.lastState = state;
107 | this.historyListenerObject.setState(this.base + state);
108 | }
109 |
110 | protected historyListener = () => {
111 | const state = this.getState();
112 | if (this.lastState === state) return;
113 | this.lastState = state;
114 | console.log('Listener', this.id, state);
115 | this.sendPostMessage(eventType.state, state.replace(this.base, ''));
116 | };
117 |
118 | protected updateStateOnPostMessage = state => this.setState(state || '/');
119 | }
120 |
--------------------------------------------------------------------------------
/packages/sync/src/events.ts:
--------------------------------------------------------------------------------
1 | import {eventType, makeEvent} from '@ringcentral/web-apps-common';
2 |
3 | const retransmittedProperty = 'retransmitted';
4 |
5 | //FIXME We can't extend CustomEvent due to a bug https://github.com/Microsoft/TypeScript/issues/12949 and https://stackoverflow.com/q/37039638
6 | export const makeRetransmittedEvent = (type: eventType, data?) => {
7 | const event = makeEvent(type, data);
8 | event[retransmittedProperty] = true;
9 | return event;
10 | };
11 |
12 | export const isRetransmittedEvent = event => event.hasOwnProperty(retransmittedProperty);
13 |
--------------------------------------------------------------------------------
/packages/sync/src/hash.ts:
--------------------------------------------------------------------------------
1 | export const addHash = state => '#' + state;
2 |
3 | export const removeHash = state => (state.substr(0, 1) === '#' ? state.substr(1) : state);
4 |
--------------------------------------------------------------------------------
/packages/sync/src/historyListener.ts:
--------------------------------------------------------------------------------
1 | import {addHash, removeHash} from './hash';
2 |
3 | export enum historyType {
4 | html5 = 'html5',
5 | hash = 'hash',
6 | stub = 'stub',
7 | }
8 |
9 | export class HistoryListener {
10 | protected listener = null;
11 |
12 | public constructor(listener) {
13 | this.listener = listener;
14 | }
15 |
16 | protected actualListener = () => this.listener(this.getState());
17 |
18 | public destroy() {}
19 |
20 | public setState(state: string) {}
21 |
22 | public getState(): string {
23 | return '/';
24 | }
25 | }
26 |
27 | class HTML5HistoryListener extends HistoryListener {
28 | protected originalPushState = null;
29 | protected originalReplaceState = null;
30 |
31 | public constructor(listener) {
32 | super(listener);
33 |
34 | window.addEventListener('popstate', this.actualListener);
35 |
36 | this.originalPushState = history.pushState;
37 | this.originalReplaceState = history.replaceState;
38 |
39 | history.pushState = (...args) => {
40 | this.originalPushState.call(history, ...args);
41 | this.actualListener();
42 | };
43 |
44 | history.replaceState = (...args) => {
45 | this.originalReplaceState.call(history, ...args);
46 | this.actualListener();
47 | };
48 | }
49 |
50 | public destroy() {
51 | window.removeEventListener('popstate', this.actualListener);
52 | history.pushState = this.originalPushState;
53 | history.replaceState = this.originalReplaceState;
54 | }
55 |
56 | public setState(state: string) {
57 | history.replaceState(null, null, state);
58 | }
59 |
60 | public getState() {
61 | return location.toString().replace(location.origin, '');
62 | }
63 | }
64 |
65 | class HashHistoryListener extends HistoryListener {
66 | public constructor(listener) {
67 | super(listener);
68 | window.addEventListener('hashchange', this.actualListener);
69 | }
70 |
71 | public destroy() {
72 | window.removeEventListener('hashchange', this.actualListener);
73 | }
74 |
75 | public setState(state: string) {
76 | window.location.hash = addHash(state);
77 | }
78 |
79 | public getState() {
80 | return removeHash(window.location.hash);
81 | }
82 | }
83 |
84 | class CustomHistory extends HistoryListener {
85 | protected detachHistoryListener = null;
86 | protected history = null;
87 |
88 | public constructor(listener, history) {
89 | super(listener);
90 | this.history = history;
91 | this.detachHistoryListener = history.listen(this.actualListener);
92 | }
93 |
94 | public destroy() {
95 | this.detachHistoryListener();
96 | this.history = null;
97 | }
98 |
99 | public setState(state: string) {
100 | this.history.replace(state); //TODO Support push(url, state)
101 | }
102 |
103 | public getState() {
104 | const {pathname, search, hash} = this.history.location;
105 | return pathname + (search || '') + (hash || '');
106 | }
107 | }
108 |
109 | export const createHistoryListener = ({listener, history = null}: {listener: any; history: historyType | any}) => {
110 | if (history === historyType.hash) return new HashHistoryListener(listener);
111 | else if (history === historyType.html5) return new HTML5HistoryListener(listener);
112 | else if (history === historyType.stub) return new HistoryListener(listener);
113 | else if (!!history) return new CustomHistory(listener, history);
114 | throw new Error('Cannot initialize history listener');
115 | };
116 |
--------------------------------------------------------------------------------
/packages/sync/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hash';
2 | export * from './historyListener';
3 | export * from './events';
4 | export * from './postMessage';
5 | export * from './Sync';
6 |
--------------------------------------------------------------------------------
/packages/sync/src/postMessage.ts:
--------------------------------------------------------------------------------
1 | export const postMessageProperty = 'web-apps-message-';
2 | export const postMessageEvent = 'message';
3 |
4 | export const sendPostMessage = (frame, id, data, targetOrigin = '*') => {
5 | // console.log('Sending from', location.origin, {id, data, targetOrigin});
6 | frame.postMessage({[postMessageProperty + id]: data}, targetOrigin);
7 | };
8 |
9 | export const postMessageListener = ({id, safeOrigin = null, callback}) => ({origin, data}) => {
10 | // if (data && (!data.includes || !data.includes('[iFrameSizer]')))
11 | // console.log('Receiving at', location.origin, 'from', {id, origin, safeOrigin, data});
12 |
13 | if (safeOrigin && origin !== safeOrigin) return;
14 | if (!data) return;
15 |
16 | const message = data[postMessageProperty + id];
17 | if (!message) return;
18 |
19 | callback(message);
20 | };
21 |
--------------------------------------------------------------------------------
/packages/sync/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig",
3 | "compilerOptions": {
4 | "declarationDir": "lib",
5 | "outDir": "lib"
6 | },
7 | "include": [
8 | "src/**/*.ts",
9 | "src/**/*.tsx"
10 | ]
11 | }
--------------------------------------------------------------------------------
/packages/sync/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const TerserPlugin = require('terser-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'production',
6 | devtool: '#cheap-module-source-map',
7 | entry: {
8 | iframe: './src/iframe/bundle.ts',
9 | 'iframe.min': './src/iframe/bundle.ts',
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.tsx?$/,
15 | use: 'ts-loader',
16 | exclude: /node_modules/,
17 | },
18 | ],
19 | },
20 | resolve: {
21 | extensions: ['.tsx', '.ts', '.js'],
22 | },
23 | output: {
24 | filename: 'ringcentral-web-apps-[name].js',
25 | path: path.resolve(__dirname, 'dist'),
26 | library: ['RCApps', 'IFrameSDK'],
27 | libraryTarget: 'umd',
28 | // libraryExport: 'default',
29 | },
30 | externals: {
31 | react: 'React',
32 | },
33 | optimization: {
34 | minimize: true,
35 | minimizer: [
36 | new TerserPlugin({
37 | include: /\.min\.js$/,
38 | }),
39 | ],
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/serve.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/application/**",
5 | "destination": "/demo/host/dist/index.html"
6 | },
7 | {
8 | "source": "/demo/iframe/build/**",
9 | "destination": "/demo/iframe/build/index.html"
10 | },
11 | {
12 | "source": "/demo/react-menu-iframe/build/**",
13 | "destination": "/demo/react-menu-iframe/build/index.html"
14 | }
15 | ],
16 | "redirects": [
17 | {
18 | "source": "/demo/admin/",
19 | "destination": "/demo/admin/index.html"
20 | },
21 | {
22 | "source": "/",
23 | "destination": "/application/apps"
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "declaration": true,
5 | "esModuleInterop": true,
6 | "experimentalDecorators": true,
7 | "jsx": "react",
8 | "lib": [
9 | "dom",
10 | "es2015"
11 | ],
12 | "module": "commonjs",
13 | "moduleResolution": "node",
14 | "noImplicitAny": false,
15 | "preserveConstEnums": true,
16 | "pretty": true,
17 | "sourceMap": true,
18 | "target": "es5"
19 | },
20 | "exclude": [
21 | "node_modules",
22 | "lib"
23 | ]
24 | }
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ringcentral-web-apps",
3 | "scope": "kirill.konshin@gmail.com",
4 | "rewrites": [
5 | {
6 | "source": "/application/(.*)",
7 | "destination": "/demo/host/dist/index.html"
8 | },
9 | {
10 | "source": "/demo/iframe/build/(.*)",
11 | "destination": "/demo/iframe/build/index.html"
12 | },
13 | {
14 | "source": "/demo/react-menu-iframe/build/(.*)",
15 | "destination": "/demo/react-menu-iframe/build/index.html"
16 | }
17 | ],
18 | "redirects": [
19 | {
20 | "source": "/demo/admin/",
21 | "destination": "/demo/admin/index.html"
22 | },
23 | {
24 | "source": "/",
25 | "destination": "/application/apps"
26 | }
27 | ]
28 | }
--------------------------------------------------------------------------------