├── .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 | 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 |
Go to /application/apps/global to engage React App
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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 63 |
64 |
65 |
66 |
67 | 68 | )} 69 |

70 | 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 |