├── .cz-config.js ├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── build-push.yml │ ├── dependabot-merger.yml │ ├── deploy-release.yml │ ├── stale.yml │ └── sync-readme-changelog.yml ├── .gitignore ├── .husky ├── pre-commit ├── pre-push └── prepare-commit-msg ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .jshintrc-test ├── .mocharc.js ├── .nycrc ├── .python-version ├── CHANGELOG.md ├── ISSUE_TEMPLATE ├── LICENSE ├── Makefile ├── README.md ├── RELEASE_DOCUMENTATION.md ├── SECURITY.md ├── bower.json ├── build_utils └── toc_generator.pl ├── deployment ├── build-example-html.sh ├── build-test.sh ├── deploy-qa.sh ├── release-s3.sh └── write-versions.sh ├── docs ├── 1_intro.md ├── 1_readme.md ├── 9_footer.md ├── images │ ├── android-web-sdk-banner-1.0.0.png │ ├── desktop-web-sdk-banner-1.0.0.png │ └── ios-web-sdk-banner-1.0.0.png └── web │ ├── 1_reference_intro.md │ └── 2_table_of_contents.md ├── examples ├── event-example.html ├── event-v2-example.html ├── example-deepview.html └── example.template.html ├── flake.lock ├── flake.nix ├── karma.conf.js ├── lint.sh ├── package-lock.json ├── package.json ├── resources └── app_id.png ├── src ├── 0_config.js ├── 0_jsonparse.js ├── 0_queue.js ├── 1_utils.js ├── 2_resources.js ├── 2_session.js ├── 2_storage.js ├── 3_api.js ├── 3_banner_utils.js ├── 4_banner_css.js ├── 4_banner_html.js ├── 5_banner.js ├── 6_branch.js ├── 7_initialization.js ├── branch_view.js ├── extern.js ├── journeys_utils.js ├── onpage.js └── stars │ ├── ic_star_black_24px.svg │ ├── ic_star_border_black_24px.svg │ └── ic_star_half_black_24px.svg ├── startDev.js ├── test ├── 0_config.js ├── 0_queue.js ├── 1_utils.js ├── 2_storage.js ├── 3_api.js ├── 6_branch.js ├── 6_branch_new.js ├── 7_integration.js ├── blob-banner-viewer.html ├── blob-banner.html ├── blob-banner.js ├── blob-interstitial-viewer.html ├── blob-interstitial.html ├── blob-interstitial.js ├── branch-deps.js ├── integration-test.html ├── integration-test.template.html ├── journeys.html ├── journeys.js ├── journeys_utils.js ├── mocha.css ├── saucelabs.js ├── test-utils.js ├── test.html └── web-config.js └── transform.js /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { value: 'feat', name: 'feat: A new feature or functionality' }, 4 | { value: 'fix', name: 'fix: A bug fix' }, 5 | { value: 'docs', name: 'docs: Documentation only changes' }, 6 | { 7 | value: 'style', 8 | name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)', 9 | }, 10 | { 11 | value: 'refactor', 12 | name: 'refactor: A code change that neither fixes a bug nor adds a feature', 13 | }, 14 | { 15 | value: 'perf', 16 | name: 'perf: A code change that improves performance', 17 | }, 18 | { value: 'test', name: 'test: Adding missing tests or correcting existing tests' }, 19 | { 20 | value: 'build', 21 | name: 'build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)', 22 | }, 23 | { 24 | value: 'ci', 25 | name: 'ci: Changes to our CI configuration files and scripts (example scopes: Github Actions, BrowserStack, SauceLabs)', 26 | }, 27 | { 28 | value: 'chore', 29 | name: 'chore: Other changes that dont modify src or test files', 30 | }, 31 | { value: 'revert', name: 'revert: Reverts a previous commit' }, 32 | { value: 'WIP', name: 'WIP: Work in progress' }, 33 | ], 34 | 35 | scopes: [{ name: 'core' }, { name: 'helper' }, { name: 'journeys' }, { name: 'other' }], 36 | 37 | usePreparedCommit: false, // to re-use commit from ./.git/COMMIT_EDITMSG 38 | allowTicketNumber: false, 39 | isTicketNumberRequired: false, 40 | ticketNumberPrefix: 'SDK-', 41 | ticketNumberRegExp: '\\d{1,5}', 42 | 43 | // it needs to match the value for field type. Eg.: 'fix' 44 | /* 45 | scopeOverrides: { 46 | fix: [ 47 | 48 | {name: 'merge'}, 49 | {name: 'style'}, 50 | {name: 'e2eTest'}, 51 | {name: 'unitTest'} 52 | ] 53 | }, 54 | */ 55 | // override the messages, defaults are as follows 56 | messages: { 57 | type: "Select the type of change that you're committing:", 58 | scope: '\nDenote the SCOPE of this change (optional):', 59 | // used if allowCustomScopes is true 60 | customScope: 'Denote the SCOPE of this change:', 61 | subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', 62 | body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', 63 | breaking: 'List any BREAKING CHANGES (optional):\n', 64 | footer: 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n', 65 | confirmCommit: 'Are you sure you want to proceed with the commit above?', 66 | }, 67 | 68 | allowCustomScopes: true, 69 | allowBreakingChanges: ['feat', 'fix'], 70 | // skip any questions you want 71 | // skipQuestions: ['scope', 'body'], 72 | 73 | // limit subject length 74 | subjectLimit: 100, 75 | // breaklineChar: '|', // It is supported for fields body and footer. 76 | // footerPrefix : 'ISSUES CLOSED:' 77 | // askForBreakingChangeFirst : true, // default is false 78 | }; 79 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | assignees: [] 3 | body: 4 | - 5 | attributes: 6 | description: "What is the problem? A clear and concise description of what the bug is." 7 | label: "Describe the bug" 8 | placeholder: "Tell us what you see!" 9 | id: description 10 | type: textarea 11 | validations: 12 | required: true 13 | - 14 | attributes: 15 | description: "Please provide as much step-by-step detail as possible.Please copy and paste any relevant code to re-produce this issue.Please include full errors, uncaught exceptions, stack traces, and relevant logs.\n" 16 | label: "Steps to reproduce" 17 | value: | 18 | 1. 19 | 2. 20 | 3. 21 | id: steps 22 | type: textarea 23 | validations: 24 | required: true 25 | - 26 | attributes: 27 | description: "What did you expect to happen?\n" 28 | label: "Expected Behavior" 29 | id: expected 30 | type: textarea 31 | validations: 32 | required: true 33 | - 34 | attributes: 35 | description: "Anything else that might be relevant for troubleshooting this bug.Any screenshots or videos that show the issue are very helpful.\n" 36 | label: "Additional Information/Context" 37 | id: context 38 | type: textarea 39 | validations: 40 | required: false 41 | - 42 | attributes: 43 | description: "What version of sdk are you seeing this issue on?" 44 | label: "SDK version used (`window.branch.sdk`)" 45 | placeholder: "2.62.0\n" 46 | id: sdk-version 47 | type: input 48 | validations: 49 | required: true 50 | - 51 | attributes: 52 | description: "What browser and operating system are you seeing this issue on? What versions?" 53 | label: Environment 54 | placeholder: "Chrome 104.0.5112.101 on macOS 12.5.1" 55 | id: env 56 | type: input 57 | validations: 58 | required: true 59 | description: "Found a bug in the Branch Web SDK? File it here." 60 | labels: 61 | - bug 62 | - needs-triage 63 | name: "🐞 Bug report" 64 | title: "(short issue description)" 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: "📕 Documentation Issue" 5 | url: https://help.branch.io/developers-hub/docs/web-sdk-overview 6 | about: Report an issue in the Branch Web SDK Reference documentation by clicking "Suggest edits" button on the documenation page 7 | - name: "Branch Support" 8 | url: https://help.branch.io/using-branch/page/submit-a-ticket 9 | about: If you are having general trouble with Branch Web SDK or your Branch Web SDK integration, please reach out to Branch Support instead. 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | description: Suggest an idea for this project 4 | title: "(short issue description)" 5 | labels: [feature-request, needs-triage] 6 | assignees: [] 7 | body: 8 | - type: textarea 9 | id: description 10 | attributes: 11 | label: Describe the feature 12 | description: A clear and concise description of the feature you are proposing. 13 | validations: 14 | required: true 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for npm 5 | - package-ecosystem: "npm" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | # Raise pull requests against the "main" branch 10 | target-branch: "main" 11 | # Allow up to 10 open pull requests 12 | open-pull-requests-limit: 10 13 | # Use custom labels on pull requests for Docker version updates 14 | labels: 15 | - "npm prod dependencies" 16 | - "dependabot-PR" 17 | 18 | # Maintain dependencies for GitHub Actions 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | ## Description 4 | 5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 6 | 7 | 8 | Fixes # (issue) 9 | 10 | ## Type of change 11 | 12 | Please delete options that are not relevant. 13 | 14 | - [ ] Bug fix (non-breaking change which fixes an issue) 15 | - [ ] New feature (non-breaking change which adds functionality) 16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | - [ ] This change requires a documentation update 18 | 19 | ## How Has This Been Tested? 20 | 21 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 22 | 23 | - [ ] Unit test 24 | - [ ] Integration test 25 | 26 | ## JS Budget Check 27 | 28 | Please mention the size in kb before abd after this PR 29 | 30 | | Files | Before | After | 31 | | ----------- | ----------- | ----------- | 32 | | dist/build.js. | | | 33 | | dist/build.min.js| | | 34 | 35 | ## Checklist: 36 | 37 | - [ ] My code follows the style guidelines of this project 38 | - [ ] I have performed a self-review of my own code 39 | - [ ] I have commented my code, particularly in hard-to-understand areas 40 | - [ ] I have made corresponding changes to the documentation 41 | - [ ] My changes generate no new warnings 42 | - [ ] I have added tests that prove my fix is effective or that my feature works 43 | - [ ] New and existing unit tests pass locally with my changes 44 | - [ ] Any dependent changes have been merged and published in downstream modules 45 | - [ ] I have checked my code and corrected any misspellings 46 | 47 | ## Mentions: 48 | List the person or team responsible for reviewing proposed changes. 49 | 50 | cc @BranchMetrics/saas-sdk-devs for visibility. 51 | -------------------------------------------------------------------------------- /.github/workflows/build-push.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push 2 | on: push 3 | 4 | env: 5 | NODE_VERSION: 18 6 | JAVA_VERSION: 11 7 | JAVA_DISTRIBUTION: 'adopt' 8 | 9 | jobs: 10 | build-push: 11 | name: run tests, build and push 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install Node ${{ env.NODE_VERSION }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ env.NODE_VERSION }} 20 | cache: 'npm' 21 | 22 | - name: Set up JDK 11 23 | uses: actions/setup-java@v4 24 | with: 25 | java-version: ${{ env.JAVA_VERSION }} 26 | distribution: ${{ env.JAVA_DISTRIBUTION }} 27 | 28 | - name: Install branch-github-actions 29 | uses: actions/checkout@v4 30 | with: 31 | repository: BranchMetrics/branch-github-actions 32 | ref: master 33 | path: .branch-github-actions 34 | token: ${{ secrets.BRANCHLET_ACCESS_TOKEN_PUBLIC }} 35 | 36 | - name: Add INPUT_SHA env var 37 | run: | 38 | export INPUT_SHA=$(git rev-parse ${{ github.ref }}) 39 | echo "INPUT_SHA=`echo $INPUT_SHA`" >> $GITHUB_ENV 40 | 41 | - name: Get next release version 42 | uses: actions/github-script@v7 43 | id: next-version 44 | with: 45 | result-encoding: string 46 | script: | 47 | const getNextVersion = require('./.branch-github-actions/custom-scripts/next-version'); 48 | const nextVersion = await getNextVersion({ 49 | core, 50 | github, 51 | context, 52 | sha: process.env.INPUT_SHA, 53 | }); 54 | return nextVersion; 55 | env: 56 | INPUT_SHA: ${{ env.INPUT_SHA }} 57 | 58 | - name: Write version to files 59 | run: | 60 | ./deployment/write-versions.sh ${{ steps.next-version.outputs.result }} 61 | 62 | - name: Install dependencies 63 | run: npm run ci 64 | 65 | - name: Lint tests 66 | run: npm run lint 67 | 68 | - name: Run tests 69 | run: npm run test-report 70 | 71 | - name: Test Report 72 | uses: dorny/test-reporter@v1 73 | if: always() 74 | with: 75 | name: Unit Tests 76 | fail-on-error: true, 77 | path: test-results.json # Path to test results 78 | reporter: mocha-json # Format of test results 79 | 80 | - name: Publish Test Results 81 | uses: EnricoMi/publish-unit-test-result-action@v2 82 | id: test-results 83 | if: always() 84 | with: 85 | files: "test-results.json" 86 | 87 | - name: Run coverage 88 | run: npm run cover 89 | 90 | - name: upload codecov 91 | uses: codecov/codecov-action@v4 92 | 93 | - name: Check whether we will be able to make the release 94 | run: make release 95 | 96 | - name: AWS, credentials setup 97 | if: ${{ github.ref == 'refs/heads/main' }} 98 | uses: aws-actions/configure-aws-credentials@v4 99 | with: 100 | aws-region: us-west-1 101 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 102 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 103 | 104 | - name: Deploy updated builds to staging 105 | if: ${{ github.ref == 'refs/heads/main' }} 106 | id: build 107 | run: | 108 | ./deployment/deploy-qa.sh -------------------------------------------------------------------------------- /.github/workflows/dependabot-merger.yml: -------------------------------------------------------------------------------- 1 | name: Merge Dependabot PRs 2 | 3 | on: 4 | schedule: 5 | - cron: "0 9 * * 1" # Run this workflow every Monday at 9:00 6 | workflow_dispatch: 7 | 8 | jobs: 9 | merge: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Get current on-call 14 | id: on-call 15 | run: | 16 | now=$(date -u +%Y-%m-%dT%H:%M:%SZ) 17 | end_time=$(date -u -d '+24 hour' +%Y-%m-%dT%H:%M:%SZ) 18 | 19 | oncall=$(curl --request GET \ 20 | --url "https://api.pagerduty.com/oncalls?since=$now&until=$end_time&schedule_ids[]=PQLHTOP" \ 21 | --header 'Accept: application/vnd.pagerduty+json;version=2' \ 22 | --header "Authorization: Token token=${{ secrets.PAGERDUTY_TOKEN }}" \ 23 | --header 'Content-Type: application/json' ) 24 | 25 | engineer_name=$(echo "$oncall" | jq -r '.oncalls[0].user.summary') 26 | 27 | declare -A engineer_to_slackid 28 | engineer_to_slackid=( 29 | ["Nipun Singh"]="U02AMC70R6E" 30 | ["Jagadeesh Karicherla"]="U038BDE0XUZ" 31 | ["Gabe De Luna"]="U02MDA0PHK5" 32 | ["Ernest Cho"]="UCV77QDSL" 33 | ["Nidhi Dixit"]="U02GDFBP88N" 34 | ) 35 | 36 | slack_id=${engineer_to_slackid["$engineer_name"]} 37 | echo "oncall_slack_id=$slack_id" >> $GITHUB_OUTPUT 38 | 39 | - name: Create PR 40 | uses: actions/github-script@v7 41 | id: create-pr 42 | with: 43 | script: | 44 | const uniqueBranchName = 'dependabot-combined-prs-' + Date.now().toString(); 45 | const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', { 46 | owner: context.repo.owner, 47 | repo: context.repo.repo 48 | }); 49 | let branchesAndPRStrings = []; 50 | let baseBranch = null; 51 | let baseBranchSHA = null; 52 | for (const pull of pulls) { 53 | const branch = pull['head']['ref']; 54 | if (branch.startsWith('dependabot/')) { 55 | console.log('Branch matched prefix. Adding to array: ' + branch); 56 | const prString = '#' + pull['number'] + ' ' + pull['title']; 57 | branchesAndPRStrings.push({ branch, prString }); 58 | baseBranch = pull['base']['ref']; 59 | baseBranchSHA = pull['base']['sha']; 60 | } 61 | } 62 | if (branchesAndPRStrings.length == 0) { 63 | core.setFailed('There are no open dependabot PRs.'); 64 | return; 65 | } 66 | try { 67 | await github.rest.git.createRef({ 68 | owner: context.repo.owner, 69 | repo: context.repo.repo, 70 | ref: 'refs/heads/' + uniqueBranchName, 71 | sha: baseBranchSHA 72 | }); 73 | } catch (error) { 74 | console.log(error); 75 | core.setFailed('Failed to create combined branch'); 76 | return; 77 | } 78 | 79 | let combinedPRs = []; 80 | let mergeFailedPRs = []; 81 | for(const { branch, prString } of branchesAndPRStrings) { 82 | try { 83 | await github.rest.repos.merge({ 84 | owner: context.repo.owner, 85 | repo: context.repo.repo, 86 | base: uniqueBranchName, 87 | head: branch, 88 | }); 89 | console.log('Merged branch ' + branch); 90 | combinedPRs.push(prString); 91 | } catch (error) { 92 | console.log('Failed to merge branch ' + branch); 93 | mergeFailedPRs.push(prString); 94 | } 95 | } 96 | 97 | console.log('Creating combined PR'); 98 | const combinedPRsString = combinedPRs.join('\n'); 99 | let body = '✅ This PR was created by the Merge Dependabot PRs action by combining the following dependabot PRs:\n' + combinedPRsString; 100 | if(mergeFailedPRs.length > 0) { 101 | const mergeFailedPRsString = mergeFailedPRs.join('\n'); 102 | body += '\n\n⚠️ The following dependabot PRs were left out due to merge conflicts:\n' + mergeFailedPRsString 103 | } 104 | let response = await github.rest.pulls.create({ 105 | owner: context.repo.owner, 106 | repo: context.repo.repo, 107 | title: 'Combined Dependabot PR', 108 | head: uniqueBranchName, 109 | base: baseBranch, 110 | body: body 111 | }); 112 | console.log('Created combined PR: ' + response.data.html_url); 113 | core.setOutput('pr_url', response.data.html_url); 114 | core.setOutput('pr_list', combinedPRsString); 115 | 116 | - name: Post to a Slack channel 117 | uses: slackapi/slack-github-action@v1.26.0 118 | id: slack 119 | with: 120 | channel-id: "C03RTLRKJQP" 121 | payload: | 122 | { 123 | "text": "Web: New Dependabot PR Awaiting Review", 124 | "blocks": [ 125 | { 126 | "type": "header", 127 | "text": { 128 | "type": "plain_text", 129 | "text":"💻🔧 Web: New Dependabot PR Awaiting Review", 130 | "emoji": true 131 | } 132 | }, 133 | { 134 | "type": "section", 135 | "text": { 136 | "type": "mrkdwn", 137 | "text": "*Included PRs:*\n${{ steps.create-pr.outputs.pr_list }}\n\n\nCurrent On-Call: *<${{ steps.on-call.outputs.oncall_slack_id }}>*" 138 | } 139 | }, 140 | { 141 | "type": "actions", 142 | "elements": [ 143 | { 144 | "type": "button", 145 | "text": { 146 | "type": "plain_text", 147 | "text": ":github-pull-request-open: View Combined PR", 148 | "emoji": true 149 | }, 150 | "value": "pr-button", 151 | "url": "${{ steps.create-pr.outputs.pr_url }}", 152 | "action_id": "link-action", 153 | "style": "primary" 154 | } 155 | ] 156 | } 157 | ] 158 | } 159 | env: 160 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_SDK_BOT_TOKEN }} 161 | -------------------------------------------------------------------------------- /.github/workflows/deploy-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Next Release (Manual) 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | NODE_VERSION: 18 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: AWS, credentials setup 16 | uses: aws-actions/configure-aws-credentials@v4 17 | with: 18 | aws-region: us-west-1 19 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 20 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 21 | 22 | - name: Add INPUT_SHA env var 23 | run: | 24 | export INPUT_SHA=$(git rev-parse ${{ github.ref }}) 25 | echo "INPUT_SHA=`echo $INPUT_SHA`" >> $GITHUB_ENV 26 | 27 | - name: Install branch-github-actions 28 | uses: actions/checkout@v4 29 | with: 30 | repository: BranchMetrics/branch-github-actions 31 | ref: master 32 | path: .branch-github-actions 33 | token: ${{ secrets.BRANCHLET_ACCESS_TOKEN_PUBLIC }} 34 | 35 | - name: Get next release version 36 | uses: actions/github-script@v7 37 | id: next-version 38 | with: 39 | result-encoding: string 40 | script: | 41 | const getNextVersion = require('./.branch-github-actions/custom-scripts/next-version'); 42 | const nextVersion = await getNextVersion({ 43 | core, 44 | github, 45 | context, 46 | sha: process.env.INPUT_SHA, 47 | }); 48 | return nextVersion; 49 | env: 50 | INPUT_SHA: ${{ env.INPUT_SHA }} 51 | 52 | - name: Install Node ${{ env.NODE_VERSION }} 53 | uses: actions/setup-node@v4 54 | with: 55 | node-version: ${{ env.NODE_VERSION }} 56 | registry-url: 'https://registry.npmjs.org' 57 | 58 | # Do this before npm configure because it writes the package.json version 59 | - name: Write version to files 60 | run: | 61 | ./deployment/write-versions.sh ${{ steps.next-version.outputs.result }} 62 | 63 | - name: Configure NPM 64 | run: npm ci 65 | 66 | - run: make release 67 | 68 | - name: Publish to s3 69 | run: | 70 | ./deployment/release-s3.sh ${{ steps.next-version.outputs.result }} 71 | 72 | - name: Publish to npm 73 | env: 74 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISHER_TOKEN }} 75 | run: npm publish 76 | 77 | - name: Create Github Release 78 | uses: actions/github-script@v7 79 | env: 80 | GE_NEXT_VERSION: ${{ steps.next-version.outputs.result }} 81 | INPUT_SHA: ${{ env.INPUT_SHA }} 82 | with: 83 | result-encoding: string 84 | script: | 85 | const createRelease = require('./.branch-github-actions/custom-scripts/create-release'); 86 | const sha = process.env.INPUT_SHA; 87 | const version = process.env.GE_NEXT_VERSION; 88 | await createRelease({ 89 | core, 90 | context, 91 | github, 92 | sha, 93 | version, 94 | }); -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues 7 | 8 | on: 9 | schedule: 10 | - cron: '0 0 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | 19 | steps: 20 | - uses: actions/stale@v9 21 | with: 22 | repo-token: ${{ github.token }} 23 | days-before-issue-stale: 60 24 | days-before-close: 7 25 | stale-issue-message: 'This issue has been automatically marked as stale due to inactivity for 60 days. If this issue is still relevant, please respond with any updates or this issue will be closed in 7 days. If you believe this is a mistake, please comment to let us know. Thank you for your contributions.' 26 | stale-issue-label: 'no-issue-activity' 27 | close-issue-message: 'This issue has been closed due to inactivity. If this issue is still relevant, please reopen it or create a new one. Thank you for your contributions.' 28 | start-date: '2023-05-22' 29 | -------------------------------------------------------------------------------- /.github/workflows/sync-readme-changelog.yml: -------------------------------------------------------------------------------- 1 | name: Update Version History on Readme 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | update-changelog: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Format and publish release notes to version history doc 13 | id: update 14 | run: | 15 | # Get release name, body, and date from the release event 16 | release_name="${{ github.event.release.tag_name }}" 17 | release_body="${{ github.event.release.body }}" 18 | release_date=$(date -d "${{ github.event.release.published_at }}" +"%Y-%m-%d") 19 | # Format release notes 20 | formatted_notes="## [$release_name] - $release_date\n\n$release_body" 21 | 22 | # Get existing version history page 23 | existing_content=$(curl --request GET \ 24 | --url https://dash.readme.com/api/v1/docs/web-version-history \ 25 | --header 'accept: application/json' \ 26 | --header "authorization: Basic ${{ secrets.readme_api_key_base64 }}" \ 27 | | jq -r '.body') 28 | 29 | # Prepend new release notes to existing content 30 | new_content=$(echo -e "$formatted_notes\n\n$existing_content") 31 | payload=$(jq -n --arg nc "$new_content" '{"body": $nc}') 32 | 33 | # Update version history page with new release notes 34 | curl --request PUT \ 35 | --url https://dash.readme.com/api/v1/docs/web-version-history \ 36 | --header 'accept: application/json' \ 37 | --header "authorization: Basic ${{ secrets.readme_api_key_base64 }}" \ 38 | --header 'content-type: application/json' \ 39 | --data "$payload" 40 | 41 | - name: Announce New Release in Slack 42 | uses: slackapi/slack-github-action@v1.26.0 43 | with: 44 | channel-id: "C063MQJMKJN" #sdk-releases 45 | payload: | 46 | { 47 | "text": "New Release: Branch Web SDK ${{ github.event.release.tag_name }}", 48 | "blocks": [ 49 | { 50 | "type": "header", 51 | "text": { 52 | "type": "plain_text", 53 | "text": ":rocket: New Release: Branch Web SDK ${{ github.event.release.tag_name }}", 54 | "emoji": true 55 | } 56 | }, 57 | { 58 | "type": "divider" 59 | }, 60 | { 61 | "type": "section", 62 | "text": { 63 | "type": "mrkdwn", 64 | "text": ":star: *What's New*" 65 | } 66 | }, 67 | { 68 | "type": "section", 69 | "text": { 70 | "type": "mrkdwn", 71 | "text": ${{ toJSON(github.event.release.body) }} 72 | } 73 | }, 74 | { 75 | "type": "divider" 76 | }, 77 | { 78 | "type": "actions", 79 | "elements": [ 80 | { 81 | "type": "button", 82 | "text": { 83 | "type": "plain_text", 84 | "text": ":git: GitHub Release", 85 | "emoji": true 86 | }, 87 | "value": "github", 88 | "action_id": "github", 89 | "url": "${{ github.event.release.html_url }}" 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | env: 96 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_SDK_BOT_TOKEN }} 97 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.*-e 2 | *.log 3 | *.pid 4 | *.seed 5 | *.sublime-project 6 | *.sublime-workspace 7 | .DS_Store 8 | .grunt 9 | .lock-wscript 10 | .map 11 | .project 12 | .settings 13 | Resources 14 | Thumbs.db 15 | aws_access_keys.sh 16 | build 17 | build.log 18 | build/Release 19 | compiler 20 | compiler-latest.zip 21 | coverage 22 | dist/web/build.min.js.gz 23 | docs/1_onpage.md 24 | docs/3_branch.md 25 | lib-cov 26 | logs 27 | node_modules 28 | npm-debug.log 29 | pids 30 | sauce_access_keys.sh 31 | tmp 32 | local-testbench 33 | index.html 34 | dist 35 | dev 36 | .nyc_output 37 | test-results.json 38 | .idea/ 39 | .direnv/ 40 | dev.config 41 | dev.html 42 | example.html 43 | dist/ -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint & npm run test 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint & npm run test 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | if [ $2 == "template" ]; then 5 | exec < /dev/tty && npx cz --hook || true 6 | fi 7 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowKeywords": [ 3 | "eval", 4 | "with" 5 | ], 6 | "disallowMixedSpacesAndTabs": true, 7 | "disallowMultipleLineStrings": true, 8 | "disallowMultipleSpaces": true, 9 | "disallowMultipleVarDecl": true, 10 | "disallowNewlineBeforeBlockStatements": true, 11 | "disallowSpaceAfterObjectKeys": true, 12 | "disallowSpaceAfterPrefixUnaryOperators": [ 13 | "!", 14 | "+", 15 | "++", 16 | "-", 17 | "--", 18 | "~" 19 | ], 20 | "disallowSpaceBeforeBinaryOperators": [ 21 | "," 22 | ], 23 | "disallowSpaceBeforePostfixUnaryOperators": [ 24 | "++", 25 | "--", 26 | ";" 27 | ], 28 | "disallowSpaceBeforeComma": true, 29 | "disallowSpaceBeforeSemicolon": true, 30 | "disallowSpacesInAnonymousFunctionExpression": { 31 | "beforeOpeningRoundBrace": true 32 | }, 33 | "disallowSpacesInFunction": { 34 | "beforeOpeningRoundBrace": true 35 | }, 36 | "disallowSpacesInFunctionDeclaration": { 37 | "beforeOpeningRoundBrace": true 38 | }, 39 | "disallowSpacesInFunctionExpression": { 40 | "beforeOpeningRoundBrace": true 41 | }, 42 | "disallowSpacesInNamedFunctionExpression": { 43 | "beforeOpeningRoundBrace": true 44 | }, 45 | "disallowSpacesInsideParentheses": true, 46 | "disallowTrailingComma": true, 47 | "disallowTrailingWhitespace": true, 48 | "disallowYodaConditions": true, 49 | "fileExtensions": [ 50 | ".js" 51 | ], 52 | "requireBlocksOnNewline": true, 53 | "requireCapitalizedConstructors": true, 54 | "requireCommaBeforeLineBreak": true, 55 | "requireCurlyBraces": [ 56 | "catch", 57 | "do", 58 | "else", 59 | "for", 60 | "if", 61 | "try", 62 | "while" 63 | ], 64 | "requireKeywordsOnNewLine": [ 65 | "else" 66 | ], 67 | "requireLineFeedAtFileEnd": true, 68 | "requireLineBreakAfterVariableAssignment": true, 69 | "requireParenthesesAroundIIFE": true, 70 | "requireSemicolons": true, 71 | "requireSpaceAfterBinaryOperators": [ 72 | "!=", 73 | "!==", 74 | "*", 75 | "+", 76 | ",", 77 | "-", 78 | "/", 79 | "=", 80 | "==", 81 | "===" 82 | ], 83 | "requireSpaceAfterKeywords": [ 84 | "catch", 85 | "do", 86 | "else", 87 | "for", 88 | "if", 89 | "return", 90 | "switch", 91 | "try", 92 | "while" 93 | ], 94 | "requireSpaceAfterLineComment": true, 95 | "requireSpaceBeforeBinaryOperators": [ 96 | "!=", 97 | "!==", 98 | "*", 99 | "+", 100 | "-", 101 | "/", 102 | "=", 103 | "==", 104 | "===" 105 | ], 106 | "requireSpaceBeforeBlockStatements": true, 107 | "requireSpacesInAnonymousFunctionExpression": { 108 | "beforeOpeningCurlyBrace": true 109 | }, 110 | "requireSpacesInConditionalExpression": { 111 | "afterConsequent": true, 112 | "afterTest": true, 113 | "beforeAlternate": true, 114 | "beforeConsequent": true 115 | }, 116 | "requireSpacesInFunction": { 117 | "beforeOpeningCurlyBrace": true 118 | }, 119 | "requireSpacesInFunctionDeclaration": { 120 | "beforeOpeningCurlyBrace": true 121 | }, 122 | "requireSpacesInFunctionExpression": { 123 | "beforeOpeningCurlyBrace": true 124 | }, 125 | "requireSpacesInNamedFunctionExpression": { 126 | "beforeOpeningCurlyBrace": true 127 | }, 128 | "requireSpacesInsideArrayBrackets": "all", 129 | "requireSpacesInsideObjectBrackets": "all", 130 | "safeContextKeyword": [ 131 | "self" 132 | ], 133 | "validateIndentation": "\t", 134 | "validateLineBreaks": "LF", 135 | "validateNewlineAfterArrayElements": true, 136 | "validateParameterSeparator": ", " 137 | } 138 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "browser": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "freeze": true, 8 | "immed": true, 9 | "latedef": true, 10 | "maxdepth": 4, 11 | "mocha": true, 12 | "noarg": true, 13 | "nocomma": true, 14 | "node": true, 15 | "nonbsp": true, 16 | "predef": [ 17 | "ActiveXObject", 18 | "console", 19 | "define", 20 | "goog", 21 | "module" 22 | ], 23 | "scripturl": true, 24 | "shadow": "inner", 25 | "strict": true, 26 | "sub": true, 27 | "trailing": true, 28 | "undef": true, 29 | "unused": true, 30 | "esversion": 6, 31 | "-W138": true, 32 | "-W073": true 33 | } 34 | -------------------------------------------------------------------------------- /.jshintrc-test: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "browser": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "freeze": true, 8 | "immed": true, 9 | "latedef": true, 10 | "maxdepth": 3, 11 | "mocha": true, 12 | "noarg": true, 13 | "nocomma": true, 14 | "node": true, 15 | "nonbsp": true, 16 | "predef": [ 17 | "ActiveXObject", 18 | "app_id", 19 | "browser_fingerprint_id", 20 | "console", 21 | "define", 22 | "goog", 23 | "identity_id", 24 | "module", 25 | "session_id", 26 | "sinon", 27 | "testUtils" 28 | ], 29 | "scripturl": true, 30 | "shadow": "inner", 31 | "strict": true, 32 | "sub": true, 33 | "trailing": true, 34 | "undef": true, 35 | "unused": true 36 | } 37 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a JavaScript-based config file containing every Mocha option plus others. 4 | // If you need conditional logic, you might want to use this type of config, 5 | // e.g. set options via environment variables 'process.env'. 6 | // Otherwise, JSON or YAML is recommended. 7 | 8 | module.exports = { 9 | require: ['./node_modules/google-closure-library', './test/branch-deps.js', 'global-jsdom/register', './test/test-utils.js', './node_modules/sinon/lib/sinon.js'], 10 | spec: ['./test/0_config.js','./test/0_queue.js','./test/1_utils.js','./test/6_branch_new.js', './test/journeys_utils.js'] 11 | }; 12 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "text-summary", 4 | "html", 5 | "lcov" 6 | ], 7 | "all": true, 8 | "include": [ 9 | "src/**" 10 | ], 11 | "exclude": [ 12 | "coverage/**", 13 | "docs/**", 14 | "packages/*/test{,s}/**", 15 | "**/*.d.ts", 16 | "test{,s}/**", 17 | "test{,-*}.{js,cjs,mjs,ts}", 18 | "**/*{.,-}test.{js,cjs,mjs,ts}", 19 | "**/__tests__/**", 20 | "**/{karma,rollup,webpack}.config.js", 21 | "**/{babel.config,.eslintrc,.mocharc}.{js,cjs}", 22 | "lib/browser/**", 23 | "package-scripts.js", 24 | "scripts/**", 25 | "dist/**", 26 | "src/onpage.js", 27 | "src/extern.js", 28 | "src/7_initialization.js" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | system 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | If there is a bug or error in this repo, please file the issue here. 2 | 3 | If you need assistance integrating a Branch SDK, please email support@branch.io. Thank you. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Branch Metrics, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | CLOSURE_COMPILER=java -jar ./node_modules/google-closure-compiler-java/compiler.jar 3 | CLOSURE_LIBRARY= ./node_modules/google-closure-library/closure 4 | 5 | COMPILER_ARGS=--js $(SOURCES) --externs $(EXTERN) --output_wrapper "(function() {%output%})();" --dependency_mode=PRUNE_LEGACY --language_out ECMASCRIPT_2015 --entry_point branch_instance 6 | COMPILER_MIN_ARGS=--compilation_level ADVANCED_OPTIMIZATIONS --language_out ECMASCRIPT_2015 7 | COMPILER_DEBUG_ARGS=--formatting=print_input_delimiter --formatting=pretty_print --warning_level=VERBOSE 8 | COMPILER_DEV_ARGS= 9 | KEY_VALUE= 10 | 11 | SOURCES=$(CLOSURE_LIBRARY)/goog/base.js\ 12 | $(CLOSURE_LIBRARY)/goog/json/json.js\ 13 | src/0_config.js\ 14 | src/0_jsonparse.js\ 15 | src/0_queue.js\ 16 | src/1_utils.js\ 17 | src/2_resources.js src/2_session.js src/2_storage.js\ 18 | src/3_api.js src/3_banner_utils.js\ 19 | src/4_banner_css.js src/4_banner_html.js\ 20 | src/5_banner.js\ 21 | src/6_branch.js\ 22 | src/7_initialization.js\ 23 | src/branch_view.js\ 24 | src/journeys_utils.js 25 | 26 | EXTERN=src/extern.js 27 | 28 | VERSION=$(shell grep "version" package.json | perl -pe 's/\s+"version": "(.*)",/$$1/') 29 | 30 | ONPAGE_RELEASE=$(subst ",\",$(shell perl -pe 'BEGIN{$$sub="https://cdn.branch.io/branch-latest.min.js"};s\#SCRIPT_URL_HERE\#$$sub\#' src/onpage.js | $(CLOSURE_COMPILER) | node transform.js branch_sdk)) 31 | ONPAGE_DEV=$(subst ",\",$(shell perl -pe 'BEGIN{$$sub="build.js"};s\#SCRIPT_URL_HERE\#$$sub\#' src/onpage.js | $(CLOSURE_COMPILER) | node transform.js branch_sdk)) 32 | ONPAGE_TEST=$(subst ",\",$(shell perl -pe 'BEGIN{$$sub="../dist/build.js"};s\#SCRIPT_URL_HERE\#$$sub\#' src/onpage.js | $(CLOSURE_COMPILER) | node transform.js branch_sdk)) 33 | 34 | .PHONY: clean 35 | 36 | all: dist/build.min.js dist/build.js test/branch-deps.js test/integration-test.html 37 | clean: 38 | rm -f dist/** docs/web/3_branch_web.md test/branch-deps.js dist/build.min.js.gz test/integration-test.html 39 | release: clean all dist/build.min.js.gz 40 | 41 | test/branch-deps.js: $(SOURCES) 42 | npx closure-make-deps \ 43 | --closure-path $(CLOSURE_LIBRARY)/goog \ 44 | --f node_modules/google-closure-library/closure/goog/deps.js \ 45 | --root src \ 46 | --root test \ 47 | --exclude test/web-config.js \ 48 | --exclude test/branch-deps.js > test/branch-deps.js 49 | 50 | dist/build.js: $(SOURCES) $(EXTERN) 51 | mkdir -p dist && \ 52 | $(CLOSURE_COMPILER) $(COMPILER_ARGS) $(COMPILER_DEBUG_ARGS) > dist/build.js 53 | 54 | dist/build.min.js: $(SOURCES) $(EXTERN) 55 | mkdir -p dist && \ 56 | $(CLOSURE_COMPILER) $(COMPILER_ARGS) $(COMPILER_MIN_ARGS) > dist/build.min.js 57 | 58 | dist/build.min.js.gz: dist/build.min.js 59 | mkdir -p dist && \ 60 | gzip -c dist/build.min.js > dist/build.min.js.gz 61 | 62 | # Documentation 63 | 64 | docs/web/3_branch_web.md: $(SOURCES) 65 | perl -pe 's/\/\*\*\ =CORDOVA/\/\*\*\*/gx' src/6_branch.js > src/3_branch_web.js 66 | perl -p -i -e 's/=WEB//gx' src/3_branch_web.js 67 | jsdox src/3_branch_web.js --output docs/web 68 | rm src/3_branch_web.js 69 | 70 | # integration test page 71 | 72 | test/integration-test.html: test/integration-test.template.html 73 | perl -pe 'BEGIN{$$a="$(ONPAGE_TEST)"}; s#// INSERT INIT CODE#$$a#' test/integration-test.template.html > test/integration-test.html 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Latest NPM Version](https://img.shields.io/npm/v/branch-sdk) 2 | ![NPM Downloads](https://img.shields.io/npm/dm/branch-sdk) 3 | ![License](https://img.shields.io/npm/l/branch-sdk) 4 | ![CircleCI](https://img.shields.io/circleci/build/github/BranchMetrics/web-branch-deep-linking-attribution) 5 | 6 | [Web Demo App]: https://help.branch.io/developers-hub/docs/web-sdk-overview#section-web-demo-app 7 | [Basic Integration]: https://help.branch.io/developers-hub/docs/web-basic-integration 8 | [Advanced Features]: https://help.branch.io/developers-hub/docs/web-advanced-features 9 | [Testing]: https://help.branch.io/developers-hub/docs/web-testing 10 | [Troubleshooting]: https://help.branch.io/developers-hub/docs/web-troubleshooting 11 | [Web Full Reference]: https://help.branch.io/developers-hub/docs/web-full-reference 12 | 13 | # Branch Web SDK 14 | 15 | Branch Metrics Deep Linking/Smart Banner Web SDK. Please see 16 | [the SDK documentation](https://help.branch.io/developers-hub/docs/web-sdk-overview) 17 | for full details. 18 | 19 | - [Web Demo App] 20 | - [Basic Integration] 21 | - [Advanced Features] 22 | - [Testing] 23 | - [Troubleshooting] 24 | - [Web Full Reference] 25 | 26 | # Running locally 27 | Download node 18 (if not using nix) 28 | 29 | ```sh 30 | node startDev.js 31 | # Navigate to http://localhost:3000/dev.html 32 | ``` -------------------------------------------------------------------------------- /RELEASE_DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Branch Web SDK Build and Release Documentation 2 | 3 | **Last Updated**: Sep. 24th, 2015 4 | 5 | The entire process for releasing new versions of the Branch SDK, and generating accompanying documentation, is entirely automated. 6 | 7 | ### Features of Build and Release Automation 8 | + **Makefile** 9 | + Building and minification with Google Closure Compiler 10 | + gzip of minified JavaScript 11 | + Embedding of Branch snippet on the Web testbed 12 | + Embedding of Branch snippet on the integration Mocha.js test page 13 | + Documentation generation from JSDoc style inline comments from `src/3_branch.js` 14 | + Concatenation of a summary README that includes an introduction, most recent Web snippet, and overview of Web Smart Banner 15 | + Concatenation of full documentation for Web of an introduction, an automatically generated table of contents, and JSDoc generated function descriptions and spec. 16 | 17 | + **Release.sh** 18 | + Version number management in `0_config.js`, `package.json`, `CHANGELOG.md`, `bower.json`, Git commit and tag 19 | + Build a release build with the Makefile 20 | + Upload both the minified and gzipped Web SDK, and the example web testbed to S3 21 | + Publish to npm 22 | + Reset everything: Changelog unreleased version at top, clean and remake, then commit 23 | + Reminder to update the Branch integration guide - soon to be automated 24 | 25 | ## Documentation 26 | 27 | **Important Note**: The majority of the documentation markdown files for the Web SDK are **generated** - meaning that any changes made directly to them will not survive running `make`, and will be overwritten. All generated filed are concated from components of the documentation in the `docs` folder, or generated from comments in `src/3_branch.js`. 28 | 29 | ### Files that are generated 30 | + `README.md` 31 | + `WEB_GUIDE.md` 32 | 33 | ### Files that can be edited directly 34 | + `CHANGELOG.md` - **Note**: The changelog should only be edited directly under the unreleased version: `## [VERSION] - unreleased` 35 | + `RELEASE_DOCUMENTATION.md` (this file) 36 | + `SMART_BANNER_GUIDE.md` - An in-depth guide explaining every feature of the smart app sharing banner 37 | 38 | #### Editing the main README (`README.md`) 39 | The markdown files concatenated together to make the main README, can be seen in the `README.md` target of the Makefile. In summary, the files consist of: 40 | 41 | | File | Edit Directly? | Content | 42 | |:--------:|:-------:|:-------:| 43 | | `docs/0_notice.md` | ✗ | A notice placed at the top of every generated document, that any edits made to the following file will be overwritten. | 44 | | `docs/readme/1_main.md` | ✔ | The content of the `README` file, along with a placeholder for the Branch init code to be put. This is done with a perl string substitution regex. **Any edits to the content of the `README` file should be made here.** | 45 | | `docs/4_footer.md` | ✗ | A generic footer placed at the end of all documentationt that includes an email where bugs and feature requests can be dropped. | 46 | 47 | #### Editing The Full Web SDK Documentation (`WEB_GUIDE.md`) 48 | The markdown files concatenated together to make the Full Web SDK documentation, can be seen in the `WEB_GUIDE.md` target of the Makefile. The introduction (`docs/web/1_intro.md`), is the only markdown file that should be edited directly. The Tabe of Contents (`docs/web/2_table_of_contents.md`), and the bulk of the readme (`docs/web/3_branch_web.md`), are both generated from the source code for the main Branch class `src/3_branch.js`. 49 | 50 | | File | Edit Directly? | Content | 51 | |:--------:|:-------:|:-------:| 52 | | `docs/0_notice.md` | ✗ | A notice placed at the top of every generated document, that any edits made to this file will be overwritten. | 53 | | `docs/web/1_intro.md` | ✔ | An introduction to the Web SDK, showing current Browser compatibility, basic installation of the Branch Web SDK, and other high level items that are important for basic implementations of the Web SDK. An up-to-date embed code is automatically placed here by the Makefile. | 54 | | `docs/web/2_table_of_contents.md` | ✗ | The Table of Contents of every function of the Branch Web SDK. This file is generated by passing `src/3_branch.js` into a simple perl program: `build_utils/toc_generator.pl`. Instructions for use of the Table of Contents generator can be found in the comments at the top of it, and examples can be seen of the exitisting Table of Content items in `src/3_branch.js` | 55 | | `docs/web/3_branch_web.md` | ✗ | The full specification of every function of the Web SDK. This entire file is generated using [JSDoc](http://usejsdoc.org/)/[JSDox](http://jsdox.org/) from the comments found in `src/3_branch.js`. Whenever a new function is added to the Branch class, or a function is edited, be sure to make the appropriate documentation changes to the comments above the function. | 56 | | `docs/4_footer.md` | ✗ | A generic footer placed at the end of all documentationt that includes an email where bugs and feature requests can be dropped. | 57 | 58 | ## Releasing the Web SDK 59 | The entire release process has been encapsulated into a single shell script: `release.sh`. The release script has a few dependencies you'll need to make sure you have installed: [AWS CLI](http://aws.amazon.com/cli/), [make](http://www.gnu.org/software/make/), and you'll need environment variables to upload the SDK and Web testbed to S3. For convenience, I've placed mine in a simple shell script that I have added to `.gitignore`: 60 | `aws_access_keys.sh` 61 | 62 | 63 | The release shell script is a simple wizard. Once your S3 environment variables are set, and you have been added to the Branch npmjs.org account, simple run :`./release.sh` and follow the steps. 64 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | If you discover a potential security issue in this project we ask that you notify Branch Security directly via email to security@branch.io 5 | Please do not open GitHub issues or pull requests - this makes the problem immediately visible to everyone, including malicious actors. 6 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Dmitri Gaskin ", 4 | "Kirk Tang " 5 | ], 6 | "name": "branch-sdk", 7 | "description": "The Branch Web SDK provides an easy way to interact with the Branch API, to track and attribute mobile app downloads and referrals.", 8 | "version": "2.71.0", 9 | "main": "dist/build.min.js", 10 | "license": "https://github.com/BranchMetrics/Web-SDK/blob/main/LICENSE", 11 | "homepage": "http://branch.io", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:BranchMetrics/Web-SDK.git" 15 | }, 16 | "ignore": [ 17 | ".gitignore", 18 | ".jscsrc", 19 | ".jshintignore", 20 | ".jshintrc", 21 | "CHANGELOG.md", 22 | "LICENSE", 23 | "Makefile", 24 | "README.md", 25 | "build.sh", 26 | "compiler", 27 | "dist/build.min.js.gz", 28 | "docs", 29 | "externs", 30 | "lint.sh", 31 | "node_modules", 32 | "package.json", 33 | "src/**", 34 | "testbeds/**", 35 | "tests", 36 | "transform.js" 37 | ], 38 | "build": "2.71.0" 39 | } 40 | -------------------------------------------------------------------------------- /build_utils/toc_generator.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # Table Of Contents Generator for the Branch Web SDK 4 | # Scott Hasbrouck, scott@branchmetrics.io 5 | # 6 | # Using this thing is pretty simple. 7 | # There are three arguments: 8 | # (1) *Required* the file you want it to read from. 9 | # (2) *Required* the file you want it to write the results to. 10 | # (3) *Required* The target you're making the Table of contents for: WEB, etc. 11 | # 12 | # Example: 13 | # $ perl toc_generator.pl YOUR_SWEET_FILE.js TARGET 14 | # *Note* The default for TARGET is 'WEB', if left blank. 15 | # 16 | # TOC Generator will read the entire contents of the file, and look for two matches, in this format: 17 | # 1. TOC headings: /*** +TOC_HEADING &Name of heading& ^TARGET ***/ 18 | # 2. TOC items: /*** +TOC_ITEM #anchor-link-on-github &.nameOfMethod()& ^TARGET ***/ 19 | # 20 | # HOT TIP OF THE DAY: We use /*** and not /** to comment the lines out because then JSDoc will ignore it :-P 21 | # 22 | # The file is read from top to bottom, and the TOC headings and items are written 23 | # to the table of contents in the order they are matched in the file, in this format: 24 | # 1. Branch Session 25 | # + [.init()](#initbranch_key-options) 26 | # + [.setIdentity()](#setidentityidentity-callback) 27 | # 28 | # Motivation for doing this: I kept forgetting to add new links at the top of the documentation for new methods, 29 | # and to update the links if I changed the name of the method or the arguments. Plus, it just 30 | # made sense to have all this in-line with the source code, since the rest of the documentation was that way. 31 | 32 | my $read_file = @ARGV[0]; 33 | my $write_file = @ARGV[1]; 34 | my $target = @ARGV[2] || "WEB"; 35 | my @file_array; 36 | my @toc_output; 37 | my $heading_count = 1; 38 | my $export_string; 39 | 40 | # Read the file in the first argument 41 | open(my $fh, "<", $read_file) or die "Sorry homie: $!\n"; 42 | while(<$fh>) { 43 | chomp; 44 | push @file_array, $_; 45 | } 46 | close $fh; 47 | 48 | # All the regex's 49 | my $heading_regex = '\\/\\*\\*\\* \\+TOC_HEADING &([\w\s.,_\(\)]+?)& \\^(.*?) \\*\\*\\*\\/'; 50 | my $item_regex = '\\/\\*\\*\\* \\+TOC_ITEM (#.*?) &([\w\s.,_\(\)]+?)& \\^(.*?) \\*\\*\\*\\/'; 51 | 52 | # Look for matches 53 | foreach $file_line (@file_array) { 54 | my @heading_matches = $file_line =~ m/$heading_regex/g; 55 | if ($#heading_matches > 0 && (@heading_matches[1] eq $target || @heading_matches[1] eq "ALL")) { 56 | if ($heading_count != 1) { $export_string .= "\n"; } # Extra new line between headings 57 | $export_string .= $heading_count.". ".@heading_matches[0]."\n"; 58 | $heading_count++; 59 | } 60 | my @item_matches = $file_line =~ m/$item_regex/g; 61 | if ($#item_matches > 0 && (@item_matches[2] eq $target || @item_matches[2] eq "ALL")) { 62 | $export_string .= ' + ['.@item_matches[1].']('.@item_matches[0].")\n"; 63 | } 64 | } 65 | # Visual horizontal rule seperator for documentation 66 | $export_string .= "\n___\n"; 67 | 68 | open(my $fh, '>', $write_file) or die "Sorry homie: $!"; 69 | print $fh $export_string; 70 | close $fh; 71 | 72 | print "Done generating table of contents\n"; 73 | print "Target: $target\n"; 74 | print "Written to: $write_file\n"; 75 | -------------------------------------------------------------------------------- /deployment/build-example-html.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script takes the example.template.html and outputs the example.html 4 | # based on the api key, the desired api, and the desired sdk build file 5 | 6 | # Used to publish to s3 for example files 7 | 8 | templateFile="examples/example.template.html" 9 | outputFile="example.html" 10 | tempFile="temp.html" # Temporary file for edits 11 | 12 | if [[ $# -ne 3 ]]; then 13 | echo "Usage: $0 " 14 | exit 1 15 | fi 16 | 17 | key_replacement="$1" 18 | api_replacement="$2" 19 | script_replacement="$3" 20 | 21 | key_placeholder="key_place_holder" 22 | api_placeholder="api_place_holder" 23 | script_placeholder="script_place_holder" 24 | 25 | if [[ ! -f "$templateFile" ]]; then 26 | echo "Template file $templateFile does not exist." 27 | exit 1 28 | fi 29 | 30 | cp "$templateFile" "$tempFile" 31 | echo "Copied $templateFile to $tempFile" 32 | 33 | sed -e "s|$key_placeholder|$key_replacement|g" \ 34 | -e "s|$api_placeholder|$api_replacement|g" \ 35 | -e "s|$script_placeholder|$script_replacement|g" \ 36 | "$tempFile" > "$outputFile" 37 | 38 | rm "$tempFile" 39 | echo "Placeholders replaced successfully in $outputFile" 40 | -------------------------------------------------------------------------------- /deployment/build-test.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # Test build 4 | 5 | set -e 6 | 7 | GREEN='\033[0;32m' 8 | NC='\033[0m' 9 | 10 | #-------------------------------------------------------------------------------------------- 11 | # Build test 12 | #-------------------------------------------------------------------------------------------- 13 | 14 | echo -en "${GREEN}init build test ...${NC}\n" 15 | 16 | make release 17 | 18 | # Exit prompts 19 | echo -en "${GREEN}Done build test ...${NC}\n" 20 | -------------------------------------------------------------------------------- /deployment/deploy-qa.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # Deploy to QA 4 | 5 | set -e 6 | 7 | RED='\033[0;31m' 8 | GREEN='\033[0;32m' 9 | NC='\033[0m' 10 | 11 | #-------------------------------------------------------------------------------------------- 12 | # Main branch (QA) Deploy 13 | #-------------------------------------------------------------------------------------------- 14 | 15 | echo -en "${GREEN}QA Release...${NC}\n" 16 | 17 | echo -en "${GREEN}make release ...${NC}\n" 18 | make release 19 | 20 | echo -en "${GREEN}Pushing to builds ...${NC}\n" 21 | aws s3 cp --content-type="text/javascript" --content-encoding="gzip" dist/build.min.js.gz s3://branch-builds/web-sdk/branch-latest.min.js 22 | aws s3 cp --content-type="text/javascript" dist/build.js s3://branch-builds/web-sdk/branch.js 23 | 24 | # External services app - ID: 436637608899006753 25 | ./deployment/build-example-html.sh "key_live_plqOidX7fW71Gzt0LdCThkemDEjCbTgx" "https://api.stage.branch.io" "https://cdn.branch.io/branch-staging-latest.min.js" 26 | aws s3 cp example.html s3://branch-cdn/example-staging.html 27 | 28 | echo -en "${GREEN}Pushing to CDN ...${NC}\n" 29 | aws s3 cp --content-type="text/javascript" --content-encoding="gzip" dist/build.min.js.gz s3://branch-cdn/branch-staging-latest.min.js --cache-control "max-age=300" 30 | 31 | echo -en "Invalidating cloudfront distribution for staging ...\n" 32 | aws configure set preview.cloudfront true 33 | aws cloudfront create-invalidation --distribution-id E10P37NG0GMER --paths /branch-staging-latest.min.js /example-staging.html 34 | 35 | # Exit prompts 36 | echo -en "${GREEN}Done deploy QA script ...${NC}\n" 37 | -------------------------------------------------------------------------------- /deployment/release-s3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [ $# -eq 0 ] && { echo "Usage: $0 1.0.0"; exit 1; } 4 | 5 | VERSION_NO_V=$1 6 | VERSION="v"$VERSION_NO_V 7 | DATE=$(date "+%Y-%m-%d") 8 | 9 | echo "Releasing Branch Web SDK" 10 | 11 | make release 12 | 13 | # Engagement Pro Production Testing App - ID: 1364963849844839205 14 | ./deployment/build-example-html.sh "key_live_gAbR03mCEte9DLh6L9GFApebvyg4mMDw" "https://api2.branch.io" "https://cdn.branch.io/branch-latest.min.js" 15 | aws s3 cp example.html s3://branch-builds/example.html 16 | aws s3 cp example.html s3://branch-cdn/example.html 17 | 18 | aws s3 cp --content-type="text/javascript" --content-encoding="gzip" dist/build.js s3://branch-cdn/branch-latest.js --cache-control "max-age=300" 19 | aws s3 cp --content-type="text/javascript" --content-encoding="gzip" dist/build.min.js.gz s3://branch-cdn/branch-$VERSION.min.js --cache-control "max-age=300" 20 | aws s3 cp --content-type="text/javascript" --content-encoding="gzip" dist/build.min.js.gz s3://branch-cdn/branch-latest.min.js --cache-control "max-age=300" 21 | 22 | echo -en "Invalidating cloudfront distribution...\n" 23 | aws configure set preview.cloudfront true 24 | aws cloudfront create-invalidation --distribution-id E10P37NG0GMER --paths /branch-latest.min.js /example.html 25 | 26 | echo "Post-release sanity checks." 27 | read -p "Can you visit https://cdn.branch.io/branch-$VERSION.min.js ?" -n 1 -r 28 | echo 29 | read -p "Is https://cdn.branch.io/example.html using the right version number $VERSION?" -n 1 -r 30 | echo 31 | read -p "Is https://www.npmjs.com/package/branch-sdk using the right version number $VERSION?" -n 1 -r 32 | echo -------------------------------------------------------------------------------- /deployment/write-versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [ $# -eq 0 ] && { echo "Usage: $0 1.0.0"; exit 1; } 4 | 5 | sed -i -e "s/\"version\":.*$/\"version\": \"$1\",/" package.json 6 | sed -i -e "s/version = '.*';$/version = '$1';/" src/0_config.js 7 | sed -i -e "s/version = '.*';$/version = '$1';/" test/web-config.js -------------------------------------------------------------------------------- /docs/1_intro.md: -------------------------------------------------------------------------------- 1 | # Branch Web SDK 2 | 3 | NOTES: 4 | 5 | For all users that are on a version earlier than v2.25.0 please update to the latest version. v2.25.0 patches a critical bug in anticipation of iOS 11. 6 | 7 | Journeys link data (data returned from Journeys event handlers) in WebSDK versions <= 2.48.0 will now contain escaped keys and values. 8 | Characters targeted for escaping include ", ', &, <, and >. These characters will be escaped to their corresponding HTML entities. 9 | Additionally URLs, in both keys and values will be URI encoded. 10 | 11 | Developers using WebSDK >= 2.49 will notice values in Journeys link data become escaped with the same rules as above. Keys will not be escaped. 12 | 13 | This README outlines the functionality of the Branch Web SDK, and how to easily incorporate it into a web app. 14 | 15 | Live demo: [https://cdn.branch.io/example.html](https://cdn.branch.io/example.html) 16 | 17 | ## Overview 18 | 19 | The Branch Web SDK provides an easy way to interact with the Branch API on your website or web app. It requires no frameworks, and is only ~13K gzipped. 20 | 21 | To use the Web SDK, you'll need to first initialize it with your Branch Key found in your [Branch dashboard](https://dashboard.branch.io/#/settings). You'll also need to register when your users login with `setIdentity`, and when they logout with `logout`. 22 | 23 | Once initialized, the Branch Web SDK allows you to create and share links with a banner, over SMS, or your own methods. 24 | 25 | ## Using the Sample Web App 26 | 27 | We provide a sample web app which demonstrates what Branch Metrics Web SDK can do. The online version can be found at . Alternatively, you can open `example.html` locally to for the same effect. 28 | 29 | To modify this local web app, edit `src/web/example.template.html` first, and then run `make`, which will automatically update `example.html`. Refrain from manually editting `example.html`. 30 | 31 | ## Installation 32 | 33 | ### Requirements 34 | 35 | This SDK requires native browser Javascript and has been tested in all modern browsers with sessionStorage capability. No 3rd party libraries are needed to make use of the SDK as is it 100% native Javascript. 36 | 37 | ### Browser Specific Support 38 | | Chrome | Firefox | Safari | IE | 39 | | ------ | ------- | ------ | ---------- | 40 | | ✔ | ✔ | ✔ | 9, 10, 11 | 41 | 42 | ### Quick Install 43 | 44 | #### Recommended Installation 45 | 46 | Add the following script within your `` tags: 47 | _Be sure to replace `BRANCH KEY` with your actual Branch Key found in your [account dashboard](https://dashboard.branch.io/#/settings)._ 48 | 49 | ```html 50 | 58 | ``` 59 | 60 | You can also get the sdk via npm or bower (branch-sdk) 61 | 62 | #### Common.JS and RequireJS compatibility 63 | 64 | In addition to working as a standalone library, the Branch SDK works great in CommonJS environments (browserify, webpack) as well as RequireJS environments (RequireJS/AMD). Just `require('branch')` or `define(['branch'], function(branch) { ... });` to get started! 65 | 66 | ### Initialization 67 | 68 | You will need to create a [Branch Metrics app](http://branch.io) to obtain your Branch Key (you will have the option to toggle between live and test modes). 69 | 70 | Initializing the SDK is an asynchronous method with a callback, so it may seem as though you would need to place any method calls that will execute immediately inside the `branch.init()` callback. We've made it even easier than that, by building in a queue to the SDK! The only thing that is required is that `branch.init()` is called prior to any other methods. All SDK methods called are guaranteed to : 1. be executed in the order that they were called, and 2. wait to execute until the previous SDK method finishes. Therefore, it is 100% allowable to do something like: 71 | ```js 72 | branch.init(...); 73 | branch.banner(...); 74 | ``` 75 | 76 | If `branch.init()` fails, all subsequent branch methods will fail. 77 | 78 | ___ 79 | -------------------------------------------------------------------------------- /docs/1_readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BranchMetrics/web-branch-deep-linking-attribution/3a65201c59a88c289fbcf46ac22c2de2457ec737/docs/1_readme.md -------------------------------------------------------------------------------- /docs/9_footer.md: -------------------------------------------------------------------------------- 1 | ## Bugs / Help / Support 2 | 3 | Feel free to report any bugs you might encounter in the repo's issues. Any support inquiries outside of bugs 4 | please send to [support@branch.io](mailto:support@branch.io). 5 | -------------------------------------------------------------------------------- /docs/images/android-web-sdk-banner-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BranchMetrics/web-branch-deep-linking-attribution/3a65201c59a88c289fbcf46ac22c2de2457ec737/docs/images/android-web-sdk-banner-1.0.0.png -------------------------------------------------------------------------------- /docs/images/desktop-web-sdk-banner-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BranchMetrics/web-branch-deep-linking-attribution/3a65201c59a88c289fbcf46ac22c2de2457ec737/docs/images/desktop-web-sdk-banner-1.0.0.png -------------------------------------------------------------------------------- /docs/images/ios-web-sdk-banner-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BranchMetrics/web-branch-deep-linking-attribution/3a65201c59a88c289fbcf46ac22c2de2457ec737/docs/images/ios-web-sdk-banner-1.0.0.png -------------------------------------------------------------------------------- /docs/web/1_reference_intro.md: -------------------------------------------------------------------------------- 1 | # Branch Web SDK 2 | 3 | ## API Reference 4 | 5 | -------------------------------------------------------------------------------- /docs/web/2_table_of_contents.md: -------------------------------------------------------------------------------- 1 | 1. Branch Session 2 | + [.init()](#initbranch_key-options-callback) 3 | + [.data()](#datacallback) 4 | + [.first()](#firstcallback) 5 | + [.setIdentity()](#setidentityidentity-callback) 6 | + [.logout()](#logoutcallback) 7 | + [.crossPlatformIds()](#crossPlatformIdscallback) 8 | + [.lastAttributedTouchData()](#lastAttributedTouchDataattribution_window-callback) 9 | 10 | 2. Event Tracking 11 | + [.track()](#trackevent-metadata-callback) 12 | + [.logEvent()](#logeventevent-event_data_and_custom_data-content_items-callback) 13 | 14 | 3. Deep Linking 15 | + [.link()](#linkdata-callback) 16 | + [.deepview()](#deepviewdata-options-callback) 17 | + [.deepviewCta()](#deepviewcta) 18 | 19 | 4. Event Listener 20 | + [.addListener()](#addlistenerevent-listener) 21 | + [.removeListener()](#removelistenerlistener) 22 | 23 | 5. Journeys Web To App 24 | + [.setBranchViewData()](#setbranchviewdatadata) 25 | + [.closeJourney()](#closejourneycallback) 26 | 27 | 6. Revenue Analytics 28 | + [.trackCommerceEvent()](#trackcommerceeventevent-commerce_data-metadata-callback) 29 | 30 | 7. User Privacy 31 | + [.disableTracking()](#disabletrackingdisabletracking) 32 | 33 | ___ 34 | -------------------------------------------------------------------------------- /examples/event-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Branch View - Branch Metrics 9 | 10 | 28 | 29 | 30 | 31 |
32 |
33 |

Branch Metrics Branch View Example

34 |
35 |
36 |
37 |

Old Banner

38 |
39 | 40 |
41 |
42 |

Branch Views - Banners

43 |
44 | 45 | 46 | 47 | 48 |
49 |
50 |

Branch Views - Interstitials

51 |
52 | 53 | 54 | 55 | 56 |
57 |

Branch Views - Free Form

58 |
59 | 60 |
61 | 62 |
63 |
64 |
65 |
66 | 67 | 68 | 75 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /examples/event-v2-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Branch V2 Events - Branch Metrics 10 | 11 | 29 | 30 | 31 | 32 |
33 |
34 |

Branch Metrics Branch V2 Events Example

35 |
36 |
37 |
38 |

Session Info

39 |
Initializing...
40 |
41 |

Request

42 |
Click a button!
43 |
44 |

Response

45 |
Click a button!
46 |
47 |
48 |

Branch V2 Events

49 |
50 | 51 |
52 | 53 |
54 | 55 | 56 |
57 |
58 |
59 |
60 | 61 | 62 | 93 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /examples/example-deepview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Branch Metrics Web SDK Example App 6 | 7 | 25 | 26 | 27 | 28 |
29 |
30 |

Branch Metrics Web SDK Deepview Example

31 |
32 |
33 |
34 |

Session Info

35 |
Reading session from .init()...
36 |
37 |

Request

38 |
Click a button!
39 |
40 |

Response

41 |
Click a button!
42 |
43 |
44 |
45 |
46 |

Methods

47 |
48 |

Session and Identity

49 |
50 | 51 | 52 | 53 |
54 |
55 |
56 |
57 | 58 | 59 | 79 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1730602179, 6 | "narHash": "sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-24.05", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "utils": "utils" 23 | } 24 | }, 25 | "systems": { 26 | "locked": { 27 | "lastModified": 1681028828, 28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 29 | "owner": "nix-systems", 30 | "repo": "default", 31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "nix-systems", 36 | "repo": "default", 37 | "type": "github" 38 | } 39 | }, 40 | "utils": { 41 | "inputs": { 42 | "systems": "systems" 43 | }, 44 | "locked": { 45 | "lastModified": 1726560853, 46 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 47 | "owner": "numtide", 48 | "repo": "flake-utils", 49 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "numtide", 54 | "repo": "flake-utils", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "web-branch-deep-linking-attribution flake"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; 6 | utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, utils }: 10 | utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | nodejs = pkgs.nodejs-18_x; 14 | in 15 | { 16 | devShell = pkgs.mkShell { 17 | nativeBuildInputs = [ 18 | nodejs 19 | ]; 20 | shellHook = '' 21 | if [ -f $HOME/.config/bin/setup-webstorm-sdk ] && [ -f ./.idea/workspace.xml ]; then 22 | $HOME/.config/bin/setup-webstorm-sdk || echo "setup-webstorm-sdk failed" 23 | fi 24 | ''; 25 | }; 26 | formatter = pkgs.nixpkgs-fmt; 27 | } 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | browserStack: { 4 | username: BROWSER_STACK_USERNAME, 5 | accessKey: BROWSER_STACK_ACCESS_KEY 6 | } 7 | 8 | customLaunchers: { 9 | bs_firefox_mac: { 10 | base: 'BrowserStack', 11 | browser: 'firefox', 12 | browser_version: '21.0', 13 | os: 'OS X', 14 | os_version: 'Mountain Lion' 15 | }, 16 | bs_iphone5: { 17 | base: 'BrowserStack', 18 | device: 'iPhone 5', 19 | os: 'ios', 20 | os_version: '6.0' 21 | } 22 | }, 23 | 24 | browsers: ['bs_firefox_mac', 'bs_iphone5'] 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BAD=0 4 | 5 | exec 5>&1 6 | 7 | for file in $( find src test | grep '\d_' ); do 8 | jshintrc=".jshintrc" 9 | if [[ $file == test* ]]; then 10 | jshintrc=".jshintrc-test" 11 | fi 12 | 13 | ERRORS=$( cat $file | perl -pe 's#goog.provide\('"'(.*)'"'\);?#/* exported $1 */ var $1;#' | perl -pe 's#goog.require\('"'(.*)'"'\);?#/* global $1:false */#' | jshint -c $jshintrc - | perl -pe "s#stdin#$file#" | tee >(cat - >&5) ) 14 | if [ "$ERRORS" != "" ]; then 15 | BAD=1 16 | fi; 17 | 18 | ERRORS=$( jscs -c .jscsrc $file | grep -v 'No code style errors' | tee >(cat - >&5) ) 19 | if [ "$ERRORS" != "" ]; then 20 | BAD=1 21 | fi; 22 | done 23 | 24 | if [ "$BAD" != 0 ]; then 25 | exit 1; 26 | fi; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "branch-sdk", 3 | "version": "0.0.0", 4 | "description": "Branch Metrics Deep Linking/Smart Banner Web SDK", 5 | "main": "dist/build.min.js", 6 | "files": [ 7 | "dist/build.min.js" 8 | ], 9 | "scripts": { 10 | "build": "make", 11 | "ci": "npm ci", 12 | "clean": "npm-run-all clean:*", 13 | "clean:dist": "rimraf dist/*", 14 | "clean:coverage": "rimraf .nyc_output coverage", 15 | "clean:test-report": "rimraf test-results.json", 16 | "clean-and-develop": "npm-run-all ci clean build cover", 17 | "commit": "git-cz", 18 | "cover": "npm-run-all cover:*", 19 | "cover:clean": "npm run clean:coverage", 20 | "cover:report": "npx nyc@latest mocha", 21 | "lint": "./lint.sh", 22 | "test": "mocha", 23 | "test-debug": "mocha --inspect-brk --require test/setup.js --reporter spec --timeout 10000 test/*.js", 24 | "test-report": "mocha --reporter json > test-results.json", 25 | "builddocs": "jsdox src/3_branch_web.js --output docs/web", 26 | "prepare": "husky install", 27 | "validate": "npm ls", 28 | "start:dev": "node startDev.js" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/BranchMetrics/web-branch-deep-linking-attribution.git" 33 | }, 34 | "author": "Dmitri Gaskin ", 35 | "contributors": [ 36 | "Jagadeesh Karicherla" 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/BranchMetrics/web-branch-deep-linking-attribution/issues", 41 | "email": "support@branch.io" 42 | }, 43 | "homepage": "https://help.branch.io/developers-hub/docs/web-sdk-overview", 44 | "devDependencies": { 45 | "commitizen": "^4.3.0", 46 | "cz-customizable": "^7.0.0", 47 | "falafel": "2.2.5", 48 | "global-jsdom": "24.0.0", 49 | "google-closure-compiler": "^20240317.0.0", 50 | "google-closure-deps": "^20230802.0.0", 51 | "google-closure-library": "^20230802.0.0", 52 | "husky": "^9.0.11", 53 | "i": "^0.3.7", 54 | "inquirer": "^9.2.15", 55 | "jscs": "2.0.0", 56 | "jsdom": "24.0.0", 57 | "jshint": "^2.13.6", 58 | "koa": "^2.15.0", 59 | "koa-static": "^5.0.0", 60 | "mocha": "^10.2.0", 61 | "npm-run-all": "^4.1.5", 62 | "precommit-hook": "3.0.0", 63 | "rimraf": "^5.0.1", 64 | "sinon": "16.0.0" 65 | }, 66 | "build": "2.71.0", 67 | "config": { 68 | "commitizen": { 69 | "path": "./node_modules/cz-customizable" 70 | } 71 | }, 72 | "pre-commit": [ 73 | "lint", 74 | "validate", 75 | "test" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /resources/app_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BranchMetrics/web-branch-deep-linking-attribution/3a65201c59a88c289fbcf46ac22c2de2457ec737/resources/app_id.png -------------------------------------------------------------------------------- /src/0_config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Just a couple of variables that shouldn't change very often... 3 | */ 4 | goog.provide('config'); 5 | /** @define {string} */ 6 | var DEFAULT_API_ENDPOINT = 'https://api2.branch.io'; 7 | 8 | config.app_service_endpoint = 'https://app.link'; 9 | config.link_service_endpoint = 'https://bnc.lt'; 10 | config.api_endpoint = DEFAULT_API_ENDPOINT; 11 | // will get overwritten by gha on actual deploy 12 | config.version = '2.85.2'; 13 | -------------------------------------------------------------------------------- /src/0_jsonparse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The default goog.json.parse uses eval. We don't want that. 3 | */ 4 | 'use strict'; 5 | 6 | goog.provide('safejson'); 7 | 8 | /*jshint unused:false*/ 9 | goog.require('goog.json'); 10 | 11 | safejson.parse = function(sJSON) { 12 | sJSON = String(sJSON); 13 | try { 14 | return JSON.parse(sJSON); 15 | } 16 | catch (e) { 17 | 18 | } 19 | 20 | throw Error("Invalid JSON string: " + sJSON); 21 | }; 22 | 23 | safejson.stringify = function(objJSON) { 24 | try { 25 | return (typeof JSON === 'object' && typeof JSON.stringify === 'function') ? JSON.stringify(objJSON) : goog.json.serialize(objJSON); 26 | } 27 | catch (e) { 28 | 29 | } 30 | 31 | throw Error("Could not stringify object"); 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /src/0_queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple blocking queue for the API requests 3 | */ 4 | 'use strict'; 5 | goog.provide('task_queue'); 6 | 7 | /** 8 | * @returns {function(function(function()))} 9 | */ 10 | task_queue = function() { 11 | var queue = []; 12 | var next = function() { 13 | if (queue.length) { 14 | queue[0](function() { 15 | queue.shift(); 16 | next(); 17 | }); 18 | } 19 | }; 20 | 21 | return function(task) { 22 | queue.push(task); 23 | if (queue.length === 1) { 24 | next(); 25 | } 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/2_session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | goog.provide('session'); 3 | 4 | /*jshint unused:false*/ 5 | goog.require('goog.json'); 6 | goog.require('utils'); 7 | goog.require('safejson'); 8 | goog.require('storage'); 9 | 10 | /** 11 | * @param {storage} storage 12 | * @param {boolean=} first 13 | * @return {Object} 14 | */ 15 | session.get = function(storage, first) { 16 | var sessionString = first ? 'branch_session_first' : 'branch_session'; 17 | try { 18 | var data = safejson.parse(storage.get(sessionString, first)) || null; 19 | return utils.decodeBFPs(data); 20 | } 21 | catch (e) { 22 | return null; 23 | } 24 | }; 25 | 26 | /** 27 | * @param {storage} storage 28 | * @param {Object} data 29 | * @param {boolean=} first 30 | */ 31 | session.set = function(storage, data, first) { 32 | 33 | if (first && data['referring_link'] && utils.userPreferences.enableExtendedJourneysAssist) { 34 | var now = new Date(); 35 | data['referringLinkExpiry'] = now.getTime() + utils.extendedJourneysAssistExpiryTime; 36 | } 37 | data = utils.encodeBFPs(data); 38 | storage.set('branch_session', goog.json.serialize(data)); 39 | if (first) { 40 | storage.set('branch_session_first', goog.json.serialize(data), true); 41 | } 42 | }; 43 | 44 | /** 45 | * @param {storage} storage 46 | * @param {Object} newData 47 | */ 48 | session.update = function(storage, newData) { 49 | if (!newData) { 50 | return; 51 | } 52 | var currentData = session.get(storage) || {}; 53 | var data = goog.json.serialize(utils.encodeBFPs(utils.merge(currentData, newData))); 54 | storage.set('branch_session', data); 55 | }; 56 | 57 | /** 58 | * Patches a field in localStorage or sessionStorage or both. 59 | * @param {storage} storage 60 | * @param {Object} data 61 | * @param {boolean=} updateLocalStorage 62 | * @param {boolean=} removeNull delete null or undefined entries instead of inserting 63 | */ 64 | session.patch = function(storage, data, updateLocalStorage, removeNull) { 65 | var merge = function(source, patch) { 66 | return utils.encodeBFPs(utils.merge(safejson.parse(source), patch, removeNull)); 67 | }; 68 | 69 | var session = storage.get('branch_session', false) || {}; 70 | storage.set('branch_session', goog.json.serialize(merge(session, data))); 71 | 72 | if (updateLocalStorage) { 73 | var sessionFirst = storage.get('branch_session_first', true) || {}; 74 | storage.set('branch_session_first', goog.json.serialize(merge(sessionFirst, data)), true); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/2_storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A custom storage abstraction class that implements: 3 | * sessionStorage, localStorage, cookies, and a plain 4 | * old javascript object as a fallback 5 | */ 6 | 'use strict'; 7 | 8 | goog.provide('storage'); 9 | /*jshint unused:false*/ 10 | goog.require('goog.json'); 11 | goog.require('utils'); 12 | 13 | /*globals Ti */ 14 | 15 | var COOKIE_MS = 365 * 24 * 60 * 60 * 1000; 16 | var BRANCH_KEY_PREFIX = 'BRANCH_WEBSDK_KEY'; 17 | 18 | /** @typedef {undefined|{get:function(string, boolean=), set:function(string, (string|boolean), boolean=), 19 | * remove:function(string), clear:function(), isEnabled:function()}} */ 20 | 21 | /** 22 | * @class BranchStorage 23 | * @constructor 24 | */ 25 | storage.BranchStorage = function(storageMethods) { 26 | for (var i = 0; i < storageMethods.length; i++) { 27 | var storageMethod = this[storageMethods[i]]; 28 | storageMethod = (typeof storageMethod === 'function') ? storageMethod() : storageMethod; 29 | if (storageMethod.isEnabled()) { 30 | storageMethod._store = { }; 31 | return storageMethod; 32 | } 33 | } 34 | }; 35 | 36 | var prefix = function(key) { 37 | return (key === 'branch_session' || key === 'branch_session_first') ? 38 | key : 39 | BRANCH_KEY_PREFIX + key; 40 | }; 41 | 42 | var trimPrefix = function(key) { 43 | return key.replace(BRANCH_KEY_PREFIX, ''); 44 | }; 45 | 46 | var retrieveValue = function(value) { 47 | if (value === 'true') { 48 | return true; 49 | } 50 | if (value === 'false') { 51 | return false; 52 | } 53 | return value; 54 | }; 55 | 56 | var hasBranchPrefix = function(key) { 57 | return key.indexOf(BRANCH_KEY_PREFIX) === 0; 58 | }; 59 | 60 | var isBranchCookie = function(key) { 61 | return key === 'branch_session' || key === 'branch_session_first' || hasBranchPrefix(key); 62 | }; 63 | 64 | var processCookie = function(row) { 65 | var cookie = row.trim(); 66 | var firstEqualSign = cookie.indexOf("="); 67 | return { 68 | name: cookie.substring(0, firstEqualSign), 69 | value: retrieveValue(cookie.substring(firstEqualSign + 1, cookie.length)) 70 | }; 71 | }; 72 | 73 | var webStorage = function(perm) { 74 | var storageMethod; 75 | try { 76 | storageMethod = perm && localStorage ? localStorage : sessionStorage; 77 | } 78 | catch (err) { 79 | return { 80 | isEnabled: function() { 81 | return false; 82 | } 83 | }; 84 | } 85 | return { 86 | getAll: function() { 87 | if (typeof storageMethod === 'undefined') { 88 | return null; 89 | } 90 | 91 | var allKeyValues = null; 92 | for (var key in storageMethod) { 93 | if (key.indexOf(BRANCH_KEY_PREFIX) === 0) { 94 | if (allKeyValues === null) { 95 | allKeyValues = { }; 96 | } 97 | allKeyValues[trimPrefix(key)] = retrieveValue(storageMethod.getItem(key)); 98 | } 99 | } 100 | return utils.decodeBFPs(allKeyValues); 101 | }, 102 | get: function(key, perm_override) { 103 | // Make sure that browser_fingerprint_id gets decoded every time it is accessed. 104 | if (key === 'browser_fingerprint_id' || key === "alternative_browser_fingerprint_id") { 105 | return perm_override && localStorage ? 106 | utils.base64Decode(localStorage.getItem(prefix(key))) : 107 | utils.base64Decode(storageMethod.getItem(prefix(key))); 108 | } 109 | return retrieveValue( 110 | perm_override && localStorage ? 111 | localStorage.getItem(prefix(key)) : 112 | storageMethod.getItem(prefix(key)) 113 | ); 114 | }, 115 | set: function(key, value, perm_override) { 116 | if (perm_override && localStorage) { 117 | localStorage.setItem(prefix(key), value); 118 | } 119 | else { 120 | storageMethod.setItem(prefix(key), value); 121 | } 122 | }, 123 | remove: function(key, perm_override) { 124 | if (perm_override && localStorage) { 125 | localStorage.removeItem(prefix(key)); 126 | } 127 | else { 128 | storageMethod.removeItem(prefix(key)); 129 | } 130 | }, 131 | clear: function() { 132 | Object.keys(storageMethod).forEach(function(item) { 133 | if (item.indexOf(BRANCH_KEY_PREFIX) === 0) { 134 | storageMethod.removeItem(item); 135 | } 136 | }); 137 | }, 138 | isEnabled: function() { 139 | try { 140 | storageMethod.setItem('test', ''); 141 | storageMethod.removeItem('test'); 142 | return true; 143 | } 144 | catch (err) { 145 | return false; 146 | } 147 | } 148 | }; 149 | }; 150 | 151 | /** @type {storage} */ 152 | storage.BranchStorage.prototype['local'] = function() { 153 | return webStorage(true); 154 | }; 155 | 156 | /** @type {storage} */ 157 | storage.BranchStorage.prototype['session'] = function() { 158 | return webStorage(false); 159 | }; 160 | 161 | var cookies = function() { 162 | var setCookie = function(key, value) { 163 | document.cookie = key + '=' + value + '; path=/'; 164 | }; 165 | var removeCookie = function(key, addPrefix) { 166 | var expires = 'Thu, 01 Jan 1970 00:00:01 GMT'; 167 | if (addPrefix) { 168 | key = prefix(key); 169 | } 170 | document.cookie = key + '=; expires=' + expires + '; path=/'; 171 | }; 172 | return { 173 | getAll: function() { 174 | var returnCookieObject = { }; 175 | var cookieArray = document.cookie.split(';'); 176 | for (var i = 0; i < cookieArray.length; i++) { 177 | var cookie = processCookie(cookieArray[i]); 178 | if (cookie && cookie.hasOwnProperty('name') && cookie.hasOwnProperty('value') && isBranchCookie(cookie['name'])) { 179 | returnCookieObject[trimPrefix(cookie['name'])] = cookie['value']; 180 | } 181 | } 182 | return returnCookieObject; 183 | }, 184 | get: function(key) { 185 | key = prefix(key); 186 | var cookieArray = document.cookie.split(';'); 187 | for (var i = 0; i < cookieArray.length; i++) { 188 | var cookie = processCookie(cookieArray[i]); 189 | if (cookie && cookie.hasOwnProperty('name') && cookie.hasOwnProperty('value') && cookie['name'] === key) { 190 | return cookie['value']; 191 | } 192 | } 193 | return null; 194 | }, 195 | set: function(key, value) { 196 | setCookie(prefix(key), value); 197 | }, 198 | remove: function(key) { 199 | removeCookie(key, true); 200 | }, 201 | clear: function() { 202 | var cookieArray = document.cookie.split(';'); 203 | for (var i = 0; i < cookieArray.length; i++) { 204 | var cookie = processCookie(cookieArray[i]); 205 | if (cookie && cookie.hasOwnProperty('name') && isBranchCookie(cookie['name'])) { 206 | removeCookie(cookie['name'], false); 207 | } 208 | } 209 | }, 210 | isEnabled: function() { 211 | return navigator.cookieEnabled; 212 | } 213 | }; 214 | }; 215 | 216 | storage.BranchStorage.prototype['cookie'] = function() { 217 | return cookies(); 218 | }; 219 | 220 | /** @type {storage} */ 221 | storage.BranchStorage.prototype['pojo'] = { 222 | getAll: function() { 223 | return this._store; 224 | }, 225 | get: function(key) { 226 | return this._store[key] || null; 227 | }, 228 | set: function(key, value) { 229 | this._store[key] = value; 230 | }, 231 | remove: function(key) { 232 | delete this._store[key]; 233 | }, 234 | clear: function() { 235 | this._store = { }; 236 | }, 237 | isEnabled: function() { 238 | return true; 239 | } 240 | }; 241 | -------------------------------------------------------------------------------- /src/3_banner_utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | goog.provide('banner_utils'); 4 | 5 | goog.require('storage'); // jshint unused:false 6 | goog.require('utils'); 7 | goog.require('safejson'); 8 | 9 | /** @typedef {{icon:string, 10 | * title:string, 11 | * buttonBackgroundColor:string, 12 | * buttonBackgroundColorHover:string, 13 | * buttonBorderColor:string, 14 | * buttonBorderColorHover:string, 15 | * buttonFontColor:string, 16 | * buttonFontColorHover:string, 17 | * description:string, 18 | * openAppButtonText:string, 19 | * downloadAppButtonText:string, 20 | * iframe:boolean, 21 | * showiOS:boolean, 22 | * showiPad:boolean, 23 | * showAndroid:boolean, 24 | * showBlackberry:boolean, 25 | * showWindowsPhone:boolean, 26 | * showKindle:boolean, 27 | * forgetHide:boolean, 28 | * disableHide:boolean, 29 | * make_new_link:boolean, 30 | * customCSS:string, 31 | * mobileSticky:boolean, 32 | * position:string, 33 | * rating:number, 34 | * reviewCount:number, 35 | * open_app:boolean, 36 | * append_deeplink_path:boolean}} */ 37 | banner_utils.options; // jshint ignore:line 38 | 39 | // UI Animation transition speed in ms. 40 | /** @type {number} */ 41 | banner_utils.animationSpeed = 250; 42 | 43 | // UIAnimation delay between juxtaposed elements. 44 | /** @type {number} */ 45 | banner_utils.animationDelay = 20; 46 | 47 | // Height of banner. 48 | /** @type {string} */ 49 | banner_utils.bannerHeight = '76px'; 50 | 51 | // How long to show red error state 52 | /** @type {number} */ 53 | banner_utils.error_timeout = 2000; 54 | 55 | 56 | /** 57 | * @param {Object} element 58 | */ 59 | banner_utils.removeElement = function(element) { 60 | if (element) { 61 | element.parentNode.removeChild(element); 62 | } 63 | }; 64 | 65 | 66 | banner_utils.hasClass = function(element, className) { 67 | return !!element.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)')); 68 | }; 69 | 70 | banner_utils.addClass = function(element, className) { 71 | if (!element) { 72 | return; 73 | } 74 | if (!banner_utils.hasClass(element, className)) { 75 | element.className += ' ' + className; 76 | } 77 | }; 78 | 79 | banner_utils.removeClass = function(element, className) { 80 | if (!element) { 81 | return; 82 | } 83 | if (banner_utils.hasClass(element, className)) { 84 | var reg = new RegExp('(\\s|^)' + className + '(\\s|$)'); 85 | element.className = element.className.replace(reg, ' '); 86 | } 87 | }; 88 | 89 | banner_utils.getDate = function(days) { 90 | var currentDate = new Date(); 91 | return currentDate.setDate(currentDate.getDate() + days); 92 | }; 93 | 94 | banner_utils.getBodyStyle = function(style) { 95 | if (document.body.currentStyle) { 96 | return document.body.currentStyle[utils.snakeToCamel(style)]; 97 | } 98 | else { 99 | return window.getComputedStyle(document.body).getPropertyValue(style); 100 | } 101 | }; 102 | 103 | banner_utils.addCSSLengths = function(length1, length2) { 104 | var convertToUnitlessPixels = function(input) { 105 | if (!input) { 106 | return 0; 107 | } 108 | var unit = input.replace(/[0-9,\.]/g, ''); 109 | var inputArray = input.match(/\d+/g); 110 | var value = parseInt(inputArray.length > 0 ? inputArray[0] : '0', 10); 111 | var vw = function() { 112 | return Math.max(document.documentElement.clientWidth, window.innerWidth || 0) / 100; 113 | }; 114 | var vh = function() { 115 | return Math.max(document.documentElement.clientHeight, window.innerHeight || 0) / 100; 116 | }; 117 | return parseInt( 118 | { 119 | "px": function(value) { 120 | return value; 121 | }, 122 | "em": function(value) { 123 | if (document.body.currentStyle) { 124 | return value * convertToUnitlessPixels(document.body.currentStyle.fontSize); 125 | } 126 | else { 127 | return value * parseFloat(window.getComputedStyle(document.body).fontSize); 128 | } 129 | }, 130 | "rem": function(value) { 131 | if (document.documentElement.currentStyle) { 132 | return value * 133 | convertToUnitlessPixels(document.documentElement.currentStyle.fontSize); 134 | } 135 | else { 136 | return value * 137 | parseFloat(window.getComputedStyle(document.documentElement).fontSize); 138 | } 139 | }, 140 | "vw": function(value) { 141 | return value * vw(); 142 | }, 143 | "vh": function(value) { 144 | return value * vh(); 145 | }, 146 | "vmin": function(value) { 147 | return value * Math.min(vh(), vw()); 148 | }, 149 | "vmax": function(value) { 150 | return value * Math.max(vh(), vw()); 151 | }, 152 | "%": function() { 153 | return (document.body.clientWidth / 100) * value; 154 | } 155 | }[unit](value), 156 | 10 157 | ); 158 | }; 159 | return (convertToUnitlessPixels(length1) + convertToUnitlessPixels(length2)).toString() + 'px'; 160 | }; 161 | 162 | /** 163 | * @param {storage} storage 164 | * @param {banner_utils.options} options 165 | * @return {boolean} 166 | */ 167 | banner_utils.shouldAppend = function(storage, options) { 168 | var hideBanner = storage.get('hideBanner', true); 169 | 170 | if (options.respectDNT && navigator && !!Number(navigator['doNotTrack'])) { 171 | return false; 172 | } 173 | try { 174 | if (typeof hideBanner === 'string') { 175 | hideBanner = safejson.parse(hideBanner); 176 | } 177 | } 178 | catch (e) { 179 | hideBanner = false; 180 | } 181 | if (typeof hideBanner === 'number') { 182 | hideBanner = new Date() >= new Date(hideBanner); 183 | } 184 | else { 185 | hideBanner = !hideBanner; 186 | } 187 | 188 | var forgetHide = options.forgetHide; 189 | if (typeof forgetHide === 'number') { 190 | forgetHide = false; 191 | } 192 | 193 | return !document.getElementById('branch-banner') && 194 | !document.getElementById('branch-banner-iframe') && 195 | (hideBanner || forgetHide) && 196 | ( 197 | (options.showAndroid && utils.getPlatformByUserAgent() === 'android') || 198 | (options.showiPad && utils.getPlatformByUserAgent() === 'ipad') || 199 | (options.showiOS && utils.getPlatformByUserAgent() === 'ios') || 200 | (options.showBlackberry && utils.getPlatformByUserAgent() === 'blackberry') || 201 | (options.showWindowsPhone && utils.getPlatformByUserAgent() === 'windows_phone') || 202 | (options.showKindle && utils.getPlatformByUserAgent() === 'kindle') 203 | ); 204 | }; 205 | -------------------------------------------------------------------------------- /src/4_banner_html.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | goog.provide('banner_html'); 3 | 4 | goog.require('banner_utils'); 5 | goog.require('utils'); 6 | goog.require('session'); 7 | goog.require('storage'); // jshint unused:false 8 | 9 | /** 10 | * @param {banner_utils.options} options 11 | * @param {string} action 12 | */ 13 | banner_html.banner = function(options, action) { 14 | return '
' + 15 | '
' + 16 | action + 17 | '
' + 18 | '
' + 19 | (!options.disableHide ? 20 | '
×
' : 21 | '') + 22 | '
' + 23 | 'Application icon' + 24 | '
' + 25 | '
' + 26 | '
' + options.title + '
' + 27 | ((options.rating || options.reviewCount) ? 28 | ('
' + 29 | (options.rating ? 30 | ('' + 31 | (function() { 32 | var stars = ""; 33 | for (var i = 0; i < 5; i++) { 34 | stars += '' + 35 | '' + 36 | '' + 37 | '' + 38 | '' + 39 | '' + 40 | '' + 41 | ''; 42 | if (options.rating > i) { 43 | stars += (i + 1 > options.rating && options.rating % 1) ? 44 | '' + 45 | '' + 46 | '' + 47 | '' + 48 | '' + 49 | '' + 50 | '' + 51 | '' + 52 | '' + 53 | '' + 54 | '' + 55 | '' + 56 | '' + 57 | '' : 58 | '' + 59 | '' + 60 | '' + 61 | '' + 62 | '' + 63 | '' + 64 | '' + 65 | '' + 66 | '' + 67 | ' '; 68 | } 69 | stars += ''; 70 | } 71 | return stars; 72 | })() + 73 | '') : 74 | '') + 75 | (options.reviewCount ? 76 | ('' + options.reviewCount + '') : 77 | '') + 78 | '
') : 79 | '') + 80 | '
' + options.description + '
' + 81 | '
' + 82 | '
' + 83 | '
'; 84 | }; 85 | 86 | 87 | /** 88 | * @param {banner_utils.options} options 89 | * @param {storage} storage 90 | */ 91 | banner_html.mobileAction = function(options, storage) { 92 | return '' + 93 | ((session.get(storage) || {})['has_app'] ? 94 | options.openAppButtonText : 95 | options.downloadAppButtonText) + 96 | ''; 97 | }; 98 | 99 | banner_html.checkmark = function() { 100 | if (window.ActiveXObject) { 101 | return ''; 102 | } 103 | else { 104 | return '' + 106 | '' + 110 | ''; 111 | } 112 | }; 113 | 114 | /** 115 | * @param {banner_utils.options} options 116 | */ 117 | banner_html.iframe = function(options, action, callback) { 118 | var iframe = document.createElement('iframe'); 119 | iframe.src = 'about:blank'; // solves CORS issues, test in IE 120 | iframe.style.overflow = 'hidden'; 121 | iframe.scrolling = 'no'; 122 | iframe.id = 'branch-banner-iframe'; 123 | iframe.className = 'branch-animation'; 124 | utils.addNonceAttribute(iframe); 125 | 126 | iframe.onload = function() { 127 | var bodyClass; 128 | var userAgent = utils.getPlatformByUserAgent(); 129 | if (userAgent === 'ios' || userAgent === 'ipad') { 130 | bodyClass = 'branch-banner-ios'; 131 | } 132 | else if (userAgent === 'android') { 133 | bodyClass = 'branch-banner-android'; 134 | } 135 | else { 136 | bodyClass = 'branch-banner-other'; 137 | } 138 | 139 | var iframedoc = iframe.contentDocument || iframe.contentWindow.document; 140 | iframedoc.head = iframedoc.createElement('head'); 141 | iframedoc.body = iframedoc.createElement('body'); 142 | iframedoc.body.className = bodyClass; 143 | 144 | banner_html.div(options, action, iframedoc); 145 | 146 | callback(iframe); 147 | }; 148 | 149 | document.body.appendChild(iframe); 150 | }; 151 | 152 | /** 153 | * @param {banner_utils.options} options 154 | */ 155 | banner_html.div = function(options, action, doc) { 156 | doc = doc || document; 157 | 158 | var banner = doc.createElement('div'); 159 | banner.id = 'branch-banner'; 160 | banner.className = 'branch-animation'; 161 | banner.innerHTML = banner_html.banner(options, action); 162 | doc.body.appendChild(banner); 163 | 164 | return banner; 165 | }; 166 | 167 | /** 168 | * @param {banner_utils.options} options 169 | * @param {storage} storage 170 | */ 171 | banner_html.markup = function(options, storage, callback) { 172 | var action = '
' + 173 | banner_html.mobileAction(options, storage) + 174 | '
'; 175 | 176 | if (options.iframe) { 177 | banner_html.iframe(options, action, callback); 178 | } 179 | else { 180 | var markup_div = banner_html.div(options, action, document); 181 | callback(markup_div); 182 | } 183 | }; 184 | -------------------------------------------------------------------------------- /src/5_banner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This provides the markup, styles, and helper functions for all Banner UI Elements 3 | */ 4 | 'use strict'; 5 | goog.provide('banner'); 6 | 7 | goog.require('utils'); 8 | goog.require('banner_utils'); 9 | goog.require('banner_css'); 10 | goog.require('banner_html'); 11 | 12 | 13 | /** 14 | * @param {Object} branch 15 | * @param {banner_utils.options} options 16 | * @param {Object} linkData 17 | * @param {storage} storage 18 | */ 19 | banner = function(branch, options, linkData, storage) { 20 | if (!banner_utils.shouldAppend(storage, options)) { 21 | branch._publishEvent('willNotShowBanner'); 22 | return null; 23 | } 24 | 25 | branch._publishEvent('willShowBanner'); 26 | 27 | var element; 28 | var bodyMarginTopInline = document.body.style.marginTop; 29 | var bodyMarginBottomInline = document.body.style.marginBottom; 30 | 31 | var closeBanner = function(closeOptions, callback) { 32 | if (typeof closeOptions === 'function') { 33 | callback = closeOptions; 34 | closeOptions = {}; 35 | } 36 | closeOptions = closeOptions || {}; 37 | 38 | if (options.position === 'top') { 39 | element.style.top = '-' + banner_utils.bannerHeight; 40 | } 41 | else if (options.position === 'bottom') { 42 | element.style.bottom = '-' + banner_utils.bannerHeight; 43 | } 44 | 45 | if (typeof options.forgetHide === 'number') { 46 | storage.set('hideBanner', banner_utils.getDate(options.forgetHide), true); 47 | } 48 | else { 49 | storage.set('hideBanner', true, true); 50 | } 51 | 52 | if (closeOptions.immediate) { 53 | if (options.position === 'top') { 54 | document.body.style.marginTop = bodyMarginTopInline; 55 | } 56 | else if (options.position === 'bottom') { 57 | document.body.style.marginBottom = bodyMarginBottomInline; 58 | } 59 | banner_utils.removeClass(document.body, 'branch-banner-is-active'); 60 | banner_utils.removeElement(element); 61 | banner_utils.removeElement(document.getElementById('branch-css')); 62 | callback(); 63 | } 64 | else { 65 | setTimeout(function() { 66 | banner_utils.removeElement(element); 67 | banner_utils.removeElement(document.getElementById('branch-css')); 68 | callback(); 69 | }, banner_utils.animationSpeed + banner_utils.animationDelay); 70 | 71 | setTimeout(function() { 72 | if (options.position === 'top') { 73 | document.body.style.marginTop = bodyMarginTopInline; 74 | } 75 | else if (options.position === 'bottom') { 76 | document.body.style.marginBottom = bodyMarginBottomInline; 77 | } 78 | banner_utils.removeClass(document.body, 'branch-banner-is-active'); 79 | }, banner_utils.animationDelay); 80 | } 81 | }; 82 | 83 | var finalHookupsCallback = function(markup) { 84 | element = markup; 85 | // Add CSS 86 | banner_css.css(options, element); 87 | // Attach actions 88 | linkData['channel'] = linkData['channel'] || 'app banner'; 89 | 90 | var doc = options.iframe ? element.contentWindow.document : document; 91 | var platform = utils.getPlatformByUserAgent(); 92 | if (![ "other", "desktop" ].includes(platform)) { 93 | options['open_app'] = options.open_app; 94 | options['append_deeplink_path'] = options.append_deeplink_path; 95 | options['make_new_link'] = options.make_new_link; 96 | options['deepview_type'] = 'banner'; 97 | branch['deepview'](linkData, options); 98 | var cta = doc.getElementById('branch-mobile-action'); 99 | if (cta) { 100 | cta.onclick = function(ev) { 101 | ev.preventDefault(); 102 | branch['deepviewCta'](); 103 | }; 104 | } 105 | } 106 | 107 | var bodyMarginTopComputed = banner_utils.getBodyStyle('margin-top'); 108 | var bodyMarginBottomComputed = banner_utils.getBodyStyle('margin-bottom'); 109 | 110 | // Trigger animation 111 | banner_utils.addClass(document.body, 'branch-banner-is-active'); 112 | if (options.position === 'top') { 113 | document.body.style.marginTop = 114 | banner_utils.addCSSLengths(banner_utils.bannerHeight, bodyMarginTopComputed); 115 | } 116 | else if (options.position === 'bottom') { 117 | document.body.style.marginBottom = 118 | banner_utils.addCSSLengths(banner_utils.bannerHeight, bodyMarginBottomComputed); 119 | } 120 | 121 | var closeButton = doc.getElementById('branch-banner-close'); 122 | 123 | if (closeButton) { 124 | closeButton.onclick = function(ev) { 125 | ev.preventDefault(); 126 | branch._publishEvent('willCloseBanner'); 127 | closeBanner({}, function() { 128 | branch._publishEvent('didCloseBanner'); 129 | }); 130 | }; 131 | } 132 | 133 | var modalBackground = doc.getElementById('branch-banner-modal-background'); 134 | 135 | if (modalBackground) { 136 | modalBackground.onclick = function(ev) { 137 | ev.preventDefault(); 138 | branch._publishEvent('willCloseBanner'); 139 | closeBanner({}, function() { 140 | branch._publishEvent('didCloseBanner'); 141 | }); 142 | }; 143 | } 144 | 145 | function onAnimationEnd() { 146 | if (options.position === 'top') { 147 | element.style.top = '0'; 148 | } 149 | else if (options.position === 'bottom') { 150 | element.style.bottom = '0'; 151 | } 152 | branch._publishEvent('didShowBanner'); 153 | } 154 | 155 | if (options.immediate) { 156 | onAnimationEnd(); 157 | } 158 | else { 159 | setTimeout(onAnimationEnd, banner_utils.animationDelay); 160 | } 161 | 162 | }; 163 | 164 | // Create markup 165 | banner_html.markup(options, storage, finalHookupsCallback); 166 | 167 | return closeBanner; 168 | }; 169 | -------------------------------------------------------------------------------- /src/7_initialization.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file initialzes the main branch instance, and re-runs any tasks that 3 | * were any tasks that were executed on the dummy branch object before real 4 | * branch was loaded. 5 | */ 6 | 'use strict'; 7 | goog.provide('branch_instance'); 8 | 9 | goog.require('Branch'); 10 | goog.require('config'); // jshint unused:false 11 | 12 | branch_instance = new Branch(); 13 | 14 | if (window['branch'] && window['branch']['_q']) { 15 | var queue = window['branch']['_q']; 16 | for (var i = 0; i < queue.length; i++) { 17 | var task = queue[i]; 18 | branch_instance[task[0]].apply(branch_instance, task[1]); 19 | } 20 | } 21 | 22 | // Provides a UMD-style module wrapper for the branch instance, meaning 23 | // that the SDK can be used in any CommonJS, RequireJS, and vanilla JS environment. 24 | 25 | // AMD 26 | if (typeof define === 'function' && define.amd) { 27 | define( 28 | 'branch', 29 | function() { 30 | return branch_instance; 31 | } 32 | ); 33 | } 34 | // CommonJS-like environments that support module.exports 35 | else if (typeof exports === 'object') { 36 | module.exports = branch_instance; 37 | } 38 | 39 | // Always make a global. 40 | if (window) { 41 | window['branch'] = branch_instance; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/branch_view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | goog.provide('branch_view'); 3 | goog.require('utils'); 4 | goog.require('banner_css'); 5 | goog.require('safejson'); 6 | goog.require('journeys_utils'); 7 | 8 | function checkPreviousBanner() { 9 | // if banner already exists, don't add another 10 | if (document.getElementById('branch-banner') || 11 | document.getElementById('branch-banner-iframe') || 12 | document.getElementById('branch-banner-container')) { 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | /** 19 | * @param {Object} parent 20 | * @param {string} html 21 | * @param {boolean} hasApp 22 | */ 23 | function renderHtmlBlob(parent, html, hasApp, iframeLoadedCallback) { 24 | 25 | var ctaText = hasApp ? 'OPEN' : 'GET'; 26 | 27 | journeys_utils.setPositionAndHeight(html); 28 | // Get metadata, css and js from html blob then remove them 29 | var metadata = journeys_utils.getMetadata(html); 30 | if (metadata) { 31 | ctaText = journeys_utils.getCtaText(metadata, hasApp); 32 | journeys_utils.findInsertionDiv(parent, metadata); 33 | } 34 | var cssInsideIframe = journeys_utils.getCss(html); 35 | journeys_utils.getJsAndAddToParent(html); 36 | var cssIframeContainer = journeys_utils.getIframeCss(html); 37 | html = journeys_utils.removeScriptAndCss(html); 38 | 39 | // create iframe element, add html, add css, add ctaText 40 | var iframeContainer = document.createElement("div"); 41 | iframeContainer.id = "branch-banner-iframe-embed"; 42 | var iframe = journeys_utils.createIframe(); 43 | iframe.onload = function() { 44 | journeys_utils.addHtmlToIframe(iframe, html, utils.getPlatformByUserAgent()); 45 | journeys_utils.addIframeOuterCSS(cssIframeContainer, metadata); 46 | journeys_utils.addIframeInnerCSS(iframe, cssInsideIframe); 47 | journeys_utils.addDynamicCtaText(iframe, ctaText); 48 | const eventData = Object.assign({}, journeys_utils.journeyLinkData); 49 | eventData['bannerHeight'] = journeys_utils.bannerHeight; 50 | eventData['isFullPageBanner'] = journeys_utils.isFullPage; 51 | eventData['bannerPagePlacement'] = journeys_utils.position; 52 | eventData['isBannerInline'] = journeys_utils.sticky === 'absolute'; 53 | eventData['isBannerSticky'] = journeys_utils.sticky === 'fixed'; 54 | journeys_utils.branch._publishEvent('willShowJourney', eventData); 55 | 56 | journeys_utils.animateBannerEntrance(iframe, cssIframeContainer); 57 | iframeLoadedCallback(iframe); 58 | } 59 | if(journeys_utils.isDesktopJourney) 60 | { 61 | iframeContainer.appendChild(iframe); 62 | document.body.appendChild(iframeContainer); 63 | } 64 | else 65 | { 66 | document.body.prepend(iframe); 67 | } 68 | return iframe; 69 | }; 70 | 71 | /** 72 | * Checks if a journey should show based on dismiss time 73 | * @param {Object} branch 74 | * @return {boolean} 75 | */ 76 | function _areJourneysDismissedGlobally(branch) { 77 | var globalDismissEndTimestamp = branch._storage.get('globalJourneysDismiss', true); 78 | 79 | if (globalDismissEndTimestamp === true || globalDismissEndTimestamp > Date.now()) { 80 | return true; 81 | } 82 | 83 | branch._storage.remove('globalJourneysDismiss', true); 84 | return false; 85 | } 86 | 87 | branch_view.shouldDisplayJourney = function(eventResponse, options, journeyInTestMode) { 88 | if ( checkPreviousBanner() || 89 | utils.getPlatformByUserAgent() == "other" || 90 | !eventResponse['event_data'] || 91 | !eventResponse['template'] 92 | ) { 93 | return false; 94 | } 95 | 96 | if (journeyInTestMode) { 97 | return true; 98 | } 99 | 100 | if ( 101 | !eventResponse['event_data']['branch_view_data']['id'] || 102 | (options && options['no_journeys']) || 103 | _areJourneysDismissedGlobally(journeys_utils.branch) 104 | ) { 105 | // resets the callback index so that auto-open works the next time a Journey is rendered 106 | branch_view.callback_index = 1; 107 | return false; 108 | } 109 | return true; 110 | }; 111 | 112 | branch_view.incrementPageviewAnalytics = function(branchViewData) { 113 | var requestData = { 114 | "event": "pageview", 115 | "journey_displayed": true, 116 | "audience_rule_id": branchViewData['audience_rule_id'], 117 | "branch_view_id": branchViewData['branch_view_id'] 118 | }; 119 | 120 | var sessionStorage = session.get(journeys_utils.branch._storage) || {}; 121 | var identity = sessionStorage.hasOwnProperty('identity') ? sessionStorage['identity'] : null; 122 | requestData = utils.addPropertyIfNotNull(requestData, 'identity', identity); 123 | 124 | journeys_utils.branch._api( 125 | resources.pageview, 126 | requestData, 127 | function (err, data) { 128 | // do nothing with response 129 | } 130 | ); 131 | }; 132 | 133 | branch_view.displayJourney = function(html, requestData, templateId, branchViewData, testModeEnabled, journeyLinkData) { 134 | if(journeys_utils.exitAnimationIsRunning){ 135 | return; 136 | } 137 | 138 | journeys_utils.branchViewId = templateId; 139 | journeys_utils.setJourneyLinkData(journeyLinkData); 140 | 141 | var audienceRuleId = branchViewData['audience_rule_id']; 142 | 143 | // this code removes any leftover css from previous banner 144 | var branchCSS = document.getElementById('branch-iframe-css') 145 | if (branchCSS) { 146 | branchCSS.parentElement.removeChild(branchCSS) 147 | } 148 | 149 | var placeholder = document.createElement('div'); 150 | placeholder.id = 'branch-banner'; 151 | document.body.insertBefore(placeholder, null); 152 | banner_utils.addClass(placeholder, 'branch-banner-is-active'); 153 | 154 | var failed = false; 155 | var callbackString = requestData['callback_string']; 156 | var banner = null; 157 | var cta = null; 158 | var storage = journeys_utils.branch._storage; 159 | 160 | if (html) { 161 | var metadata = journeys_utils.getMetadata(html) || {}; 162 | 163 | html = journeys_utils.tryReplaceJourneyCtaLink(html); 164 | 165 | var timeoutTrigger = window.setTimeout( 166 | function() { 167 | window[callbackString] = function() { }; 168 | }, 169 | utils.timeout 170 | ); 171 | 172 | window[callbackString] = function(data) { 173 | window.clearTimeout(timeoutTrigger); 174 | if (failed) { 175 | return; 176 | } 177 | cta = data; 178 | 179 | journeys_utils.finalHookups(templateId, audienceRuleId, storage, cta, banner, metadata, testModeEnabled, branch_view); 180 | }; 181 | 182 | var finalHookupsOnIframeLoaded = function (banner) { 183 | journeys_utils.banner = banner; 184 | 185 | if (banner === null) { 186 | failed = true; 187 | return; 188 | } 189 | 190 | journeys_utils.finalHookups(templateId, audienceRuleId, storage, cta, banner, metadata, testModeEnabled, branch_view); 191 | 192 | if (utils.navigationTimingAPIEnabled) { 193 | utils.instrumentation['journey-load-time'] = utils.timeSinceNavigationStart(); 194 | } 195 | 196 | document.body.removeChild(placeholder); 197 | 198 | if (!utils.userPreferences.trackingDisabled && !testModeEnabled) { 199 | branch_view.incrementPageviewAnalytics(branchViewData); 200 | } 201 | } 202 | renderHtmlBlob(document.body, html, requestData['has_app_websdk'], finalHookupsOnIframeLoaded); 203 | } else { 204 | document.body.removeChild(placeholder); 205 | 206 | if (!utils.userPreferences.trackingDisabled && !testModeEnabled) { 207 | branch_view.incrementPageviewAnalytics(branchViewData); 208 | } 209 | } 210 | }; 211 | 212 | branch_view._getPageviewRequestData = function(metadata, options, branch, isDismissEvent) { 213 | 214 | journeys_utils.branch = branch; 215 | 216 | if (!options) { 217 | options = {}; 218 | } 219 | 220 | if (!metadata) { 221 | metadata = {}; 222 | } 223 | 224 | journeys_utils.entryAnimationDisabled = options['disable_entry_animation'] || false; 225 | journeys_utils.exitAnimationDisabled = options['disable_exit_animation'] || false; 226 | 227 | // starts object off with data from setBranchViewData() call 228 | var obj = utils.merge({}, branch._branchViewData); 229 | var sessionStorage = session.get(branch._storage) || {}; 230 | var has_app = sessionStorage.hasOwnProperty('has_app') ? sessionStorage['has_app'] : false; 231 | var identity = sessionStorage.hasOwnProperty('identity') ? sessionStorage['identity'] : null; 232 | var journeyDismissals = branch._storage.get('journeyDismissals', true); 233 | var userLanguage = (options['user_language'] || utils.getBrowserLanguageCode() || 'en').toLowerCase() || null; 234 | var initialReferrer = utils.getInitialReferrer(branch._referringLink()); 235 | var branchViewId = options['branch_view_id'] || utils.getParameterByName('_branch_view_id') || null; 236 | var linkClickId = !options['make_new_link'] ? utils.getClickIdAndSearchStringFromLink(branch._referringLink(true)) : null; 237 | var SessionlinkClickId = sessionStorage.hasOwnProperty('session_link_click_id') ? sessionStorage['session_link_click_id'] : null; 238 | 239 | // adds root level keys for v1/event 240 | obj['event'] = !isDismissEvent ? 'pageview' : 'dismiss'; 241 | obj['metadata'] = metadata; 242 | obj = utils.addPropertyIfNotNull(obj, 'initial_referrer', initialReferrer); 243 | 244 | // adds root level keys for v1/branchview 245 | obj = utils.addPropertyIfNotNull(obj, 'branch_view_id', branchViewId); 246 | obj = utils.addPropertyIfNotNull(obj, 'no_journeys', options['no_journeys']); 247 | obj = utils.addPropertyIfNotNull(obj, 'is_iframe', utils.isIframe()); 248 | obj = utils.addPropertyIfNotNull(obj, 'journey_dismissals', journeyDismissals); 249 | obj = utils.addPropertyIfNotNull(obj, 'identity', identity); 250 | obj = utils.addPropertyIfNotNull(obj, 'session_link_click_id', SessionlinkClickId); 251 | obj['user_language'] = userLanguage; 252 | obj['open_app'] = options['open_app'] || false; 253 | obj['has_app_websdk'] = has_app; 254 | obj['feature'] = 'journeys'; 255 | obj['callback_string'] = 'branch_view_callback__' + (journeys_utils._callback_index++); 256 | 257 | if (!obj.data) { 258 | obj.data = {}; 259 | } 260 | 261 | // builds data object for v1/branchview 262 | obj.data = utils.merge(utils.getHostedDeepLinkData(), obj.data); 263 | obj.data = utils.merge(utils.whiteListJourneysLanguageData(sessionStorage || {}), obj.data); 264 | if (linkClickId) { 265 | obj.data['link_click_id'] = linkClickId; 266 | } 267 | var linkData = sessionStorage['data'] ? safejson.parse(sessionStorage['data']) : null; 268 | if (linkData && linkData['+referrer']) { 269 | obj.data['+referrer'] = linkData['+referrer']; 270 | } 271 | obj['session_referring_link_data'] = sessionStorage['data'] || null; 272 | obj = utils.cleanLinkData(obj); 273 | return obj; 274 | }; 275 | -------------------------------------------------------------------------------- /src/extern.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is just for the closure compiler, to tell it the types of various 3 | * external functions. @type{?} means it's "optional". 4 | */ 5 | 'use strict'; 6 | 7 | /** @type {?} */ 8 | var define; 9 | /** @type {?} */ 10 | define.amd; 11 | 12 | /** @type {?} */ 13 | var exports; 14 | 15 | var module = { 16 | exports: {} 17 | }; 18 | 19 | // var console = { log: function() {}, error: function() {} }; 20 | 21 | var sessionStorage = { 22 | clear: function() {}, 23 | getItem: function() {}, 24 | setItem: function() {} 25 | }; 26 | 27 | // Used in Cordova 28 | var require = function(module) {}; 29 | 30 | var localStorage = { 31 | clear: function() {}, 32 | getItem: function() {}, 33 | removeItem: function() {}, 34 | setItem: function() {} 35 | }; 36 | 37 | -------------------------------------------------------------------------------- /src/onpage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the actual embed script that people put on their page. I use the 3 | * technique of passing in variables as function parameters, even if not 4 | * defined, to remove the need for a `var` statement. 5 | * 6 | * This script creates a window.branch object with a number of calls. When you 7 | * call them, it saves your call for later. 8 | */ 9 | 'use strict'; 10 | 11 | (function(root, doc, scriptStr, branchStr, createCallback, branchSdk, funcs, i, scriptTag, firstScript) { 12 | if (!root[branchStr] || !root[branchStr]._q) { 13 | while (i < funcs.length) { 14 | createCallback(branchSdk, funcs[i++]); 15 | } 16 | 17 | scriptTag = doc.createElement(scriptStr); 18 | scriptTag.async = 1; 19 | scriptTag.src = 'SCRIPT_URL_HERE'; 20 | firstScript = doc.getElementsByTagName(scriptStr)[0]; 21 | firstScript.parentNode.insertBefore(scriptTag, firstScript); 22 | 23 | root[branchStr] = branchSdk; 24 | } 25 | })( 26 | window, document, 'script', 'branch', function(branch, name) { 27 | branch[name] = function() { 28 | branch._q.push([ name, arguments ]); 29 | }; 30 | }, 31 | { 32 | _q: [], // _q: the "queue" of calls 33 | _v: 1 // _v: the "version" of the embed script 34 | }, 35 | [ 36 | 'addListener', 37 | 'banner', 38 | 'closeBanner', 39 | 'closeJourney', 40 | 'data', 41 | 'deepview', 42 | 'deepviewCta', 43 | 'first', 44 | 'init', 45 | 'link', 46 | 'logout', 47 | 'removeListener', 48 | 'setBranchViewData', 49 | 'setIdentity', 50 | 'track', 51 | 'trackCommerceEvent', 52 | 'logEvent', 53 | 'disableTracking', 54 | 'getBrowserFingerprintId', 55 | 'crossPlatformIds', 56 | 'lastAttributedTouchData', 57 | 'setAPIResponseCallback', 58 | 'qrCode', 59 | 'setRequestMetaData', 60 | 'setDMAParamsForEEA', 61 | 'setAPIUrl', 62 | 'getAPIUrl' 63 | ], 64 | 0 65 | ); 66 | -------------------------------------------------------------------------------- /src/stars/ic_star_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/stars/ic_star_border_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/stars/ic_star_half_black_24px.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /startDev.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const fs = require('fs').promises; 3 | const Koa = require('koa'); 4 | const serve = require('koa-static'); 5 | const path = require('path'); 6 | 7 | const defaultDev = { 8 | "APIEndpoint": "https://api.stage.branch.io", 9 | "sdkKey": 'key_live_plqOidX7fW71Gzt0LdCThkemDEjCbTgx', 10 | "port": "3000" 11 | }; 12 | 13 | const TEMPLATE_FILE = 'examples/example.template.html'; 14 | const DEV_HTML = 'dev.html'; 15 | 16 | async function fileExists(filePath) { 17 | try { 18 | await fs.access(filePath); 19 | return true; 20 | } catch (e) { 21 | return false; 22 | } 23 | } 24 | 25 | async function getDevConfig() { 26 | const devConfigPath = 'dev.config'; 27 | if (await fileExists(devConfigPath)) { 28 | try { 29 | const data = await fs.readFile(devConfigPath, 'utf8'); 30 | const config = JSON.parse(data); 31 | return config; 32 | } catch (error) { 33 | console.error('Error reading dev.config:', error); 34 | } 35 | } else { 36 | await fs.writeFile(devConfigPath, JSON.stringify( 37 | defaultDev 38 | )); 39 | return defaultDev; 40 | } 41 | } 42 | 43 | async function writeDevConfig(config) { 44 | const devConfigPath = 'dev.config'; 45 | await fs.writeFile(devConfigPath, JSON.stringify(config, null, 4), 'utf8'); 46 | } 47 | 48 | async function processTemplate(templateFile, outputFile, replacements) { 49 | try { 50 | let content = await fs.readFile(templateFile, 'utf8'); 51 | 52 | // Replace each placeholder with its replacement 53 | for (const [placeholder, replacement] of Object.entries(replacements)) { 54 | const regex = new RegExp(placeholder, 'g'); // Replace all occurrences 55 | content = content.replace(regex, replacement); 56 | } 57 | 58 | // Write the updated content back to the output file 59 | await fs.writeFile(outputFile, content, 'utf8'); 60 | } catch (error) { 61 | console.error(`Error processing file: ${error}`); 62 | } 63 | } 64 | 65 | async function writeExampleHtml(config) { 66 | // Define your file paths and replacements 67 | const templateFile = path.join(__dirname, TEMPLATE_FILE); 68 | const outputFile = path.join(__dirname, DEV_HTML); 69 | const replacements = { 70 | 'key_place_holder': config.sdkKey, 71 | 'api_place_holder': config.APIEndpoint, 72 | 'script_place_holder': './dist/build.js', 73 | }; 74 | 75 | await processTemplate(templateFile, outputFile, replacements); 76 | } 77 | 78 | function executeBuild(config) { 79 | console.log('Building build...'); 80 | exec(`make`, (error, stdout, stderr) => { 81 | if (error) { 82 | console.error(`Error executing makefile: ${error.message}`); 83 | return; 84 | } 85 | console.log(`Dev websdk build successful, ${DEV_HTML} built from ${TEMPLATE_FILE}`); 86 | const app = new Koa(); 87 | app.use(serve('.')); 88 | app.listen(config.port, () => { 89 | console.log('Server started successfully.'); 90 | console.log(`Example page running at http://localhost:${config.port}/${DEV_HTML}. To edit, update ${TEMPLATE_FILE}`); 91 | }); 92 | }); 93 | } 94 | 95 | async function startDev() { 96 | const config = await getDevConfig(); 97 | 98 | if (!config.APIEndpoint || !config.sdkKey || !config.port) { 99 | const questions = [ 100 | { 101 | type: 'input', 102 | name: 'apiEndpoint', 103 | message: 'Enter API endpoint URL (press Enter to use default):', 104 | default: 'https://api2.branch.io' 105 | }, 106 | { 107 | type: 'input', 108 | name: 'sdkKey', 109 | message: 'Enter SDK key:', 110 | validate: function(input) { 111 | if (input.trim() === '') { 112 | return 'SDK key is required'; 113 | } 114 | return true; 115 | } 116 | }, 117 | { 118 | type: 'input', 119 | name: 'port', 120 | message: 'Enter port number (press Enter to use default):', 121 | default: '3000' 122 | } 123 | ]; 124 | const { default: inquirer } = await import('inquirer'); 125 | const answers = await inquirer.prompt(questions); 126 | const { apiEndpoint, sdkKey, port } = answers; 127 | config.APIEndpoint = apiEndpoint; 128 | config.sdkKey = sdkKey; 129 | config.port = port; 130 | await writeDevConfig(config); 131 | } 132 | await writeExampleHtml(config); 133 | executeBuild(config); 134 | } 135 | 136 | startDev(); 137 | -------------------------------------------------------------------------------- /test/0_config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | goog.require('config'); 4 | 5 | 6 | describe('config', function() { 7 | var assert = testUtils.unplanned(); 8 | describe('app_service_endpoint', function() { 9 | it('app_service_endpoint should be defined', function() { 10 | assert.isDefined(config.app_service_endpoint, 'app_service_endpoint is un-defined'); 11 | }); 12 | it('app_service_endpoint value is correct', function() { 13 | assert.isDefined(config.app_service_endpoint, 'https://app.link', 'app_service_endpoint is incorrect'); 14 | }); 15 | }); 16 | 17 | describe('link_service_endpoint', function() { 18 | it('link_service_endpoint should be defined', function() { 19 | assert.isDefined(config.link_service_endpoint, 'link_service_endpoint is un-defined'); 20 | }); 21 | it('link_service_endpoint value is correct', function() { 22 | assert.isDefined(config.link_service_endpoint, 'https://bnc.lt', 'link_service_endpoint is incorrect'); 23 | }); 24 | }); 25 | 26 | describe('api_endpoint', function() { 27 | it('api_endpoint should be defined', function() { 28 | assert.isDefined(config.api_endpoint, 'api_endpoint is un-defined'); 29 | }); 30 | it('api_endpoint value is correct', function() { 31 | assert.isDefined(config.api_endpoint, 'https://api2.branch.io', 'api_endpoint is incorrect'); 32 | }); 33 | }); 34 | 35 | describe('version', function() { 36 | it('version should be defined', function() { 37 | assert.isDefined(config.version, 'version is un-defined'); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/0_queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*jshint -W079 */ 3 | var sinon = require('sinon'); 4 | 5 | goog.require('task_queue'); 6 | 7 | describe('task_queue', function() { 8 | var queue; 9 | var orderCalled; 10 | var clock; 11 | var assert = testUtils.unplanned(); 12 | beforeEach(function() { 13 | queue = task_queue(); 14 | clock = sinon.useFakeTimers(); 15 | orderCalled = []; 16 | queue(function(next) { 17 | setTimeout(function() { 18 | orderCalled.push(0); 19 | next(); 20 | }, 10); 21 | }); 22 | assert.strictEqual(orderCalled[0], undefined, 'Has not yet called function'); 23 | }); 24 | afterEach(function() { 25 | clock.restore(); 26 | }); 27 | 28 | it('should queue a function and call it', function(done) { 29 | clock.tick(11); 30 | assert.strictEqual(orderCalled[0], 0, 'Function called'); 31 | done(); 32 | }); 33 | 34 | it('should enqueue two functions, and call them in order', function(done) { 35 | queue(function(next) { 36 | setTimeout(function() { 37 | orderCalled.push(1); 38 | next(); 39 | }, 10); 40 | }); 41 | clock.tick(11); 42 | assert.strictEqual(orderCalled[0], 0, 'Called first function'); 43 | assert.strictEqual(orderCalled[1], undefined, 'Has not yet called second function'); 44 | clock.tick(11); 45 | assert.strictEqual(orderCalled[1], 1, 'Called second function'); 46 | done(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/2_storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | goog.require('storage'); // jshint ignore:line 4 | 5 | var BRANCH_KEY_PREFIX = 'BRANCH_WEBSDK_KEY'; 6 | var ITEM_KEY = 'key'; 7 | var ITEM_KEY_UNSTORED = 'key unstored'; 8 | var ITEM_VALUE = 'value'; 9 | 10 | describe('session storage', function() { 11 | var storage = new BranchStorage([ 'session' ]); // jshint ignore:line 12 | var assert = testUtils.unplanned(); 13 | beforeEach(function() { 14 | storage.clear(); 15 | }); 16 | 17 | it('should set an item', function() { 18 | storage.set(ITEM_KEY, ITEM_VALUE); 19 | assert.strictEqual( 20 | sessionStorage.getItem(BRANCH_KEY_PREFIX + ITEM_KEY), 21 | ITEM_VALUE, 22 | 'key / vaue stored' 23 | ); 24 | }); 25 | 26 | it('should get stored item with key', function() { 27 | storage.set(ITEM_KEY, ITEM_VALUE); 28 | var item = storage.get(ITEM_KEY); 29 | assert.strictEqual(item, ITEM_VALUE, 'correct value with key'); 30 | }); 31 | 32 | it('should return null for an unstored item', function() { 33 | var item = storage.get(ITEM_KEY_UNSTORED); 34 | assert.strictEqual(item, null, 'returned null'); 35 | }); 36 | 37 | it('should remove an item', function() { 38 | storage.set(ITEM_KEY, ITEM_VALUE, 'session'); 39 | storage.remove(ITEM_KEY); 40 | var item = storage.get(ITEM_KEY); 41 | assert.strictEqual(item, null, 'returned null'); 42 | }); 43 | 44 | it('should clear all items', function() { 45 | storage.set(ITEM_KEY, ITEM_VALUE); 46 | storage.clear(); 47 | assert.deepEqual(sessionStorage.getItem(ITEM_KEY), null, 'Storage cleared'); 48 | }); 49 | }); 50 | 51 | describe('local storage', function() { 52 | var storage = new BranchStorage([ 'local' ]); // jshint ignore:line 53 | var assert = testUtils.unplanned(); 54 | beforeEach(function() { 55 | storage.clear(); 56 | }); 57 | 58 | it('should set an item in localStorage', function() { 59 | storage.set(ITEM_KEY, ITEM_VALUE); 60 | assert.strictEqual( 61 | localStorage.getItem(BRANCH_KEY_PREFIX + ITEM_KEY), 62 | ITEM_VALUE, 63 | 'key / vaue stored' 64 | ); 65 | }); 66 | 67 | it('should get stored item with key', function() { 68 | storage.set(ITEM_KEY, ITEM_VALUE); 69 | var item = storage.get(ITEM_KEY); 70 | assert.strictEqual(item, ITEM_VALUE, 'correct value with key'); 71 | }); 72 | 73 | it('should return null for an unstored item', function() { 74 | var item = storage.get(ITEM_KEY_UNSTORED); 75 | assert.strictEqual(item, null, 'returned null'); 76 | }); 77 | 78 | it('should remove an item', function() { 79 | storage.set(ITEM_KEY, ITEM_VALUE, 'session'); 80 | storage.remove(ITEM_KEY); 81 | var item = storage.get(ITEM_KEY); 82 | assert.strictEqual(item, null, 'returned null'); 83 | }); 84 | 85 | it('should clear all items', function() { 86 | storage.set(ITEM_KEY, ITEM_VALUE); 87 | storage.clear(); 88 | assert.deepEqual(localStorage.getItem(ITEM_KEY), null, 'Storage cleared'); 89 | }); 90 | }); 91 | 92 | describe('cookie storage', function() { 93 | var storage = new BranchStorage([ 'cookie' ]); // jshint ignore:line 94 | var ITEM_KEY = 'branch_session'; 95 | var ITEM_VALUE = 'test_val'; 96 | var assert = testUtils.unplanned(); 97 | 98 | // sets non-Branch cookies 99 | document.cookie = "non_branch_cookie_1=abc"; 100 | document.cookie = "non_branch_cookie_2=def"; 101 | document.cookie = "non_branch_cookie_3=ghi"; 102 | 103 | it('should get stored item with key', function() { 104 | storage.set(ITEM_KEY, ITEM_VALUE); 105 | var item = storage.get(ITEM_KEY); 106 | assert.strictEqual(item, ITEM_VALUE, 'Cookie not stored. [This may not work in some browsers with a file: URL, e.g. Chrome.]'); 107 | }); 108 | 109 | it('should return null for an un-stored item', function() { 110 | var item = storage.get('not_an_item'); 111 | assert.strictEqual(item, null, 'returned null'); 112 | }); 113 | 114 | it('should remove a Branch cookie', function() { 115 | storage.remove(ITEM_KEY); 116 | var item = storage.get(ITEM_KEY); 117 | assert.strictEqual(item, null, 'returned null'); 118 | }); 119 | 120 | it('should remove a Branch cookie that is not branch_session or branch_session_first', function() { 121 | var cookieName = "test_1"; 122 | var cookieValue = "123"; 123 | storage.set(cookieName, cookieValue); 124 | storage.remove(cookieName); 125 | var item = storage.get(cookieName); 126 | assert.strictEqual(item, null, ' returned null'); 127 | }); 128 | 129 | it('should clear all Branch cookies', function() { 130 | var testCookies = { 131 | "key_1": "val_1", 132 | "branch_session": "val_2", 133 | "branch_session_first": "val_3" 134 | }; 135 | for (var key in testCookies) { 136 | if (testCookies.hasOwnProperty(key)) { 137 | storage.set(key, testCookies[key]); 138 | } 139 | } 140 | storage.clear(); 141 | var item = null; 142 | for (key in testCookies) { 143 | if (testCookies.hasOwnProperty(key)) { 144 | item = storage.get(testCookies[key]); 145 | assert.strictEqual(item, null, ' returned null'); 146 | } 147 | } 148 | }); 149 | 150 | it('should return all Branch cookies', function() { 151 | var expected = { 152 | "key_1": "val_1", 153 | "branch_session": "val_2", 154 | "branch_session_first": "val_3" 155 | }; 156 | for (var key in expected) { 157 | if (expected.hasOwnProperty(key)) { 158 | storage.set(key, expected[key]); 159 | } 160 | } 161 | var actual = storage.getAll(); 162 | assert.strictEqual(Object.keys(expected).length, Object.keys(actual).length, 'Cookie not stored. [This may not work in some browsers with a file: URL, e.g. Chrome.]'); 163 | for (key in actual) { 164 | if (actual.hasOwnProperty(key)) { 165 | assert.strictEqual(actual[key], expected[key], ' correct value for key'); 166 | } 167 | } 168 | var nonBranchCookies = { 169 | 'non_branch_cookie_1':'abc', 170 | 'non_branch_cookie_2':'def', 171 | 'non_branch_cookie_3':'ghi' 172 | }; 173 | for (key in nonBranchCookies) { // check whether original Branch cookies are returned 174 | if (nonBranchCookies.hasOwnProperty(key)) { 175 | assert.strictEqual(actual.hasOwnProperty(key), false, 'Cookie not stored. [This may not work in some browsers with a file: URL, e.g. Chrome.]'); 176 | } 177 | } 178 | }); 179 | 180 | it('non-Branch cookies should remain after clearing storage', function() { 181 | storage.clear(); 182 | var expected = { 183 | 'non_branch_cookie_1':'abc', 184 | 'non_branch_cookie_2':'def', 185 | 'non_branch_cookie_3':'ghi' 186 | }; 187 | var cookiesArray = document.cookie.split(';'); 188 | var cookiesFound = 0; 189 | for (var i = 0; i < cookiesArray.length; i++) { 190 | var cookie = cookiesArray[i].trim(); 191 | var firstEqualSign = cookie.indexOf("="); 192 | var cookieName = cookie.substring(0, firstEqualSign); 193 | var cookieValue = cookie.substring(firstEqualSign + 1, cookie.length); 194 | if (expected.hasOwnProperty(cookieName) && expected[cookieName] === cookieValue) { 195 | cookiesFound +=1; 196 | } 197 | } 198 | assert.strictEqual(3, cookiesFound, 'Cookie not stored. [This may not work in some browsers with a file: URL, e.g. Chrome.]'); 199 | }); 200 | }); 201 | 202 | describe('pojo storage', function() { 203 | var storage = new BranchStorage([ 'pojo' ]); // jshint ignore:line 204 | var assert = testUtils.unplanned(); 205 | beforeEach(function() { 206 | storage.clear(); 207 | }); 208 | 209 | it('should set a temporary item', function() { 210 | storage.set(ITEM_KEY, ITEM_VALUE); 211 | assert.strictEqual(storage._store[ITEM_KEY], ITEM_VALUE, 'key / vaue stored'); 212 | }); 213 | 214 | it('should get stored item with key', function() { 215 | storage.set(ITEM_KEY, ITEM_VALUE); 216 | var item = storage.get(ITEM_KEY); 217 | assert.strictEqual(item, ITEM_VALUE, 'correct value with key'); 218 | }); 219 | 220 | it('should return null for an unstored item', function() { 221 | storage.set(ITEM_KEY, ITEM_VALUE); 222 | var item = storage.get(ITEM_KEY_UNSTORED); 223 | assert.strictEqual(item, null, 'returned null'); 224 | }); 225 | 226 | it('should remove an item', function() { 227 | storage.set(ITEM_KEY, ITEM_VALUE, 'session'); 228 | storage.remove(ITEM_KEY); 229 | var item = storage.get(ITEM_KEY); 230 | assert.strictEqual(item, null, 'returned null'); 231 | }); 232 | 233 | it('should clear all items', function() { 234 | storage.set(ITEM_KEY, ITEM_VALUE); 235 | storage.clear(); 236 | assert.deepEqual(storage._store[ITEM_KEY], null, 'Storage cleared'); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /test/6_branch_new.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*jshint -W079 */ 3 | /*jshint esversion: 6 */ 4 | var sinon = require('sinon'); 5 | 6 | goog.require('Branch'); 7 | goog.require('utils'); 8 | goog.require('task_queue'); 9 | 10 | describe('Branch - new', function() { 11 | const sandbox = sinon.createSandbox(); 12 | const branch_instance = new Branch(); 13 | const assert = testUtils.unplanned(); 14 | afterEach(function() { 15 | sandbox.restore(); 16 | sinon.restore(); 17 | }); 18 | describe('referringLink', function() { 19 | it('test method exists', function() { 20 | sinon.assert.match(typeof branch_instance.referringLink, "function"); 21 | }); 22 | }); 23 | describe('setRequestMetaData', function() { 24 | var addPropertyIfNotNullSpy; 25 | beforeEach(function() { 26 | addPropertyIfNotNullSpy = sinon.spy(utils, 'addPropertyIfNotNull'); 27 | }); 28 | it('test method exists', function() { 29 | sinon.assert.match(typeof branch_instance.setRequestMetaData, "function"); 30 | }); 31 | it('should set metadata for a valid key and value', function() { 32 | var key = 'validKey'; 33 | var value = 'validValue'; 34 | var requestMetadata = { 35 | }; 36 | var result = branch_instance.setRequestMetaData.call({ requestMetadata: requestMetadata }, key, value); 37 | assert.strictEqual(result, undefined); 38 | sinon.assert.calledOnce(addPropertyIfNotNullSpy); 39 | assert.deepEqual(requestMetadata, { "validKey": "validValue" }); 40 | }); 41 | 42 | it('should delete metadata for a key when value is null', function() { 43 | var requestMetadata = { "keyToDelete": "value" }; 44 | branch_instance.setRequestMetaData.call({ requestMetadata: requestMetadata }, "keyToDelete", null); 45 | assert.deepEqual(requestMetadata, {}); 46 | }); 47 | 48 | it('should not modify metadata for an invalid key or undefined value', function() { 49 | var invalidKey = null; 50 | var undefinedValue; 51 | var requestMetadata = { "key": "value" }; 52 | 53 | var result1 = branch_instance.setRequestMetaData.call({ requestMetadata: requestMetadata }, invalidKey, 'validValue'); 54 | var result2 = branch_instance.setRequestMetaData.call({ requestMetadata: requestMetadata }, 'validKey', undefinedValue); 55 | assert.strictEqual(result1, undefined); 56 | assert.strictEqual(result2, undefined); 57 | sinon.assert.notCalled(addPropertyIfNotNullSpy); 58 | assert.deepEqual(requestMetadata, { "key": "value" }); 59 | }); 60 | }); 61 | describe('setDMAParamsForEEA', function() { 62 | it('test method exists', function() { 63 | sinon.assert.match(typeof branch_instance.setDMAParamsForEEA, "function"); 64 | }); 65 | it('should store dma params inside branch_dma_data of storage', function() { 66 | const thisObj = { 67 | _storage: { 68 | set: () => {} 69 | }, 70 | _queue: task_queue() 71 | }; 72 | const storageSetStub = sandbox.stub(thisObj._storage, 'set'); 73 | const dmaObj = {}; 74 | dmaObj.eeaRegion = true; 75 | dmaObj.adPersonalizationConsent = true; 76 | dmaObj.adUserDataUsageConsent = true; 77 | const stringifieddmaObj = JSON.stringify(dmaObj); 78 | branch_instance.setDMAParamsForEEA.call(thisObj, dmaObj.eeaRegion, dmaObj.adPersonalizationConsent, dmaObj.adUserDataUsageConsent); 79 | sinon.assert.calledWith(storageSetStub, 'branch_dma_data', stringifieddmaObj, true); 80 | }); 81 | it('should not store dma params inside branch_dma_data of storage if eeaRegion is not set', function() { 82 | const thisObj = { 83 | _storage: { 84 | set: () => {} 85 | }, 86 | _queue: task_queue() 87 | }; 88 | const storageSetStub = sandbox.stub(thisObj._storage, 'set'); 89 | branch_instance.setDMAParamsForEEA.call(thisObj); 90 | sinon.assert.notCalled(storageSetStub); 91 | }); 92 | it('should not store dma params inside branch_dma_data of storage if eeaRegion is null', function() { 93 | const thisObj = { 94 | _storage: { 95 | set: () => {} 96 | }, 97 | _queue: task_queue() 98 | }; 99 | const storageSetStub = sandbox.stub(thisObj._storage, 'set'); 100 | const dmaObj = {}; 101 | dmaObj.eeaRegion = null; 102 | dmaObj.adPersonalizationConsent = true; 103 | dmaObj.adUserDataUsageConsent = true; 104 | branch_instance.setDMAParamsForEEA.call(thisObj, dmaObj.eeaRegion, dmaObj.adPersonalizationConsent, dmaObj.adUserDataUsageConsent); 105 | sinon.assert.notCalled(storageSetStub); 106 | }); 107 | it('should log warning if eeaRegion is not boolean', function() { 108 | const thisObj = { 109 | _storage: { 110 | set: () => {} 111 | }, 112 | _queue: task_queue() 113 | }; 114 | const consoleErrorStub = sandbox.stub(console, 'warn'); 115 | try { 116 | const dmaObj = {}; 117 | dmaObj.eeaRegion = null; 118 | dmaObj.adPersonalizationConsent = true; 119 | dmaObj.adUserDataUsageConsent = true; 120 | branch_instance.setDMAParamsForEEA.call(thisObj, dmaObj.eeaRegion, dmaObj.adPersonalizationConsent, dmaObj.adUserDataUsageConsent); 121 | 122 | } catch (e) { 123 | 124 | } 125 | sinon.assert.calledWith(consoleErrorStub, 'setDMAParamsForEEA: eeaRegion must be boolean, but got null'); 126 | }); 127 | it('should log warning if adPersonalizationConsent is not boolean', function() { 128 | const thisObj = { 129 | _storage: { 130 | set: () => {} 131 | }, 132 | _queue: task_queue() 133 | }; 134 | const consoleErrorStub = sandbox.stub(console, 'warn'); 135 | try { 136 | const dmaObj = {}; 137 | dmaObj.eeaRegion = true; 138 | dmaObj.adPersonalizationConsent = null; 139 | dmaObj.adUserDataUsageConsent = true; 140 | branch_instance.setDMAParamsForEEA.call(thisObj, dmaObj.eeaRegion, dmaObj.adPersonalizationConsent, dmaObj.adUserDataUsageConsent); 141 | 142 | } catch (e) { 143 | 144 | } 145 | sinon.assert.calledWith(consoleErrorStub, 'setDMAParamsForEEA: adPersonalizationConsent must be boolean, but got null'); 146 | }); 147 | it('should log warning if eeaRegion is not boolean', function() { 148 | const thisObj = { 149 | _storage: { 150 | set: () => {} 151 | }, 152 | _queue: task_queue() 153 | }; 154 | const consoleErrorStub = sandbox.stub(console, 'warn'); 155 | try { 156 | const dmaObj = {}; 157 | dmaObj.eeaRegion = true; 158 | dmaObj.adPersonalizationConsent = true; 159 | dmaObj.adUserDataUsageConsent = null; 160 | branch_instance.setDMAParamsForEEA.call(thisObj, dmaObj.eeaRegion, dmaObj.adPersonalizationConsent, dmaObj.adUserDataUsageConsent); 161 | 162 | } catch (e) { 163 | 164 | } 165 | sinon.assert.calledWith(consoleErrorStub, 'setDMAParamsForEEA: adUserDataUsageConsent must be boolean, but got null'); 166 | }); 167 | it('should catch and log exception', function() { 168 | const thisObj = { 169 | _storage: { 170 | set: () => {} 171 | }, 172 | _queue: task_queue() 173 | }; 174 | sandbox.stub(thisObj._storage, 'set').throws(new Error('Mock error')); 175 | const consoleErrorStub = sandbox.stub(console, 'error'); 176 | try { 177 | const dmaObj = {}; 178 | dmaObj.eeaRegion = false; 179 | dmaObj.adPersonalizationConsent = false; 180 | dmaObj.adUserDataUsageConsent = false; 181 | branch_instance.setDMAParamsForEEA.call(thisObj, dmaObj.eeaRegion, dmaObj.adPersonalizationConsent, dmaObj.adUserDataUsageConsent); 182 | 183 | } catch (e) { 184 | 185 | } 186 | sinon.assert.calledWith(consoleErrorStub, 'setDMAParamsForEEA::An error occurred while setting DMA parameters for EEA', sinon.match.instanceOf(Error)); 187 | }); 188 | }); 189 | describe('setAPIUrl', function() { 190 | it('test method exists', function() { 191 | sinon.assert.match(typeof branch_instance.setAPIUrl, "function"); 192 | }); 193 | }); 194 | describe('getAPIUrl', function() { 195 | it('test method exists', function() { 196 | sinon.assert.match(typeof branch_instance.getAPIUrl, "function"); 197 | }); 198 | it('test url', function() { 199 | var branch_url = 'https://api16.branch.io'; 200 | branch_instance.setAPIUrl(branch_url); 201 | assert.equal(branch_instance.getAPIUrl(), branch_url); 202 | }); 203 | }); 204 | describe('addListener', function() { 205 | it('should fire listener added using addListener for an event', function() { 206 | let listenerFired = 0; 207 | const listener = function() { 208 | listenerFired++; 209 | }; 210 | branch_instance.addListener('willShowJourney', listener); 211 | branch_instance._publishEvent('willShowJourney'); 212 | assert.equal(listenerFired, 1); 213 | }); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /test/7_integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | goog.require('config'); 4 | goog.require('goog.json'); // jshint unused:false 5 | 6 | /*globals branch_sample_key, session_id, identity_id, browser_fingerprint_id, branch */ 7 | /*globals device_fingerprint_id */ 8 | 9 | describe('Integration tests', function() { 10 | var requests = [ ]; 11 | var xhr; 12 | var clock; 13 | var jsonpCallback = 0; 14 | 15 | var clearBranchStorage = function() { 16 | sessionStorage.clear(); 17 | localStorage.clear(); 18 | var clearCookies = function(temp, perm) { 19 | var deleteCookie = function(cookie) { 20 | document.cookie = cookie.substring(0, cookie.indexOf('=')) + '=;expires=-1;path=/'; 21 | }; 22 | var cookieArray = document.cookie.split(';'); 23 | for (var i = 0; i < cookieArray.length; i++) { 24 | var cookie = cookieArray[i]; 25 | while (cookie.charAt(0) === ' ') { 26 | cookie = cookie.substring(1, cookie.length); 27 | } 28 | if (cookie.indexOf('BRANCH_WEBSDK_COOKIE') === 0) { 29 | if (temp && cookie.indexOf('branch_expiration_date=') === -1) { 30 | deleteCookie(cookie); 31 | } 32 | else if (perm && cookie.indexOf('branch_expiration_date=') > 0) { 33 | deleteCookie(cookie); 34 | } 35 | } 36 | } 37 | }; 38 | clearCookies(true, true); 39 | branch._storage._store = { }; 40 | }; 41 | 42 | before(function() { 43 | xhr = sinon.useFakeXMLHttpRequest(); 44 | clock = sinon.useFakeTimers(); 45 | xhr.onCreate = function(xhr) { 46 | requests.push(xhr); 47 | }; 48 | branch._server.createScript = function() {}; 49 | sinon.stub(branch._server, 'createScript', function(src) { 50 | requests.push({ src: src, callback: window[src.match(/callback=([^&]+)/)[1]] }); 51 | }); 52 | }); 53 | 54 | beforeEach(function() { 55 | clearBranchStorage(); 56 | testUtils.go(''); 57 | branch.branch_key = 'branch_sample_key'; 58 | branch.identity = 'foo'; 59 | branch.identity_id = identity_id.toString(); 60 | branch.device_fingerprint_id = identity_id.toString(); 61 | }); 62 | 63 | afterEach(function() { 64 | jsonpCallback++; 65 | requests = []; 66 | }); 67 | 68 | after(function() { 69 | if (typeof branch._server.createScript.restore === 'function') { 70 | branch._server.createScript.restore(); 71 | } 72 | if (typeof xhr.restore === 'function') { 73 | xhr.restore(); 74 | } 75 | if (typeof clock.restore === 'function') { 76 | clock.restore(); 77 | } 78 | }); 79 | 80 | var sampleParams = { 81 | tags: [ 'tag1', 'tag2' ], 82 | channel: 'sample app', 83 | feature: 'create link', 84 | stage: 'created link', 85 | type: 1, 86 | data: { 87 | mydata: 'bar', 88 | '$desktop_url': 'https://cdn.branch.io/example.html', 89 | '$og_title': 'Branch Metrics', 90 | '$og_description': 'Branch Metrics', 91 | '$og_image_url': 'http://branch.io/img/logo_icon_white.png' 92 | } 93 | }; 94 | 95 | var indexOfLastInitRequest = function(requestsAfterInit) { 96 | return requestsAfterInit + 1; 97 | }; 98 | 99 | var numberOfAsserts = function(assertsAfterInit) { 100 | return assertsAfterInit + 4; 101 | }; 102 | 103 | var branchInit = function(assert, callback) { 104 | branch.init.apply( 105 | branch, 106 | [ 107 | device_fingerprint_id, 108 | callback 109 | ] 110 | ); 111 | if (assert) { 112 | assert.strictEqual(requests.length, 1, 'Exactly one request was made'); 113 | assert.strictEqual( 114 | requests[0].src, 115 | config.app_service_endpoint + 116 | '/_r?sdk=web' + 117 | config.version + 118 | '&branch_key=' + 119 | branch.branch_key + 120 | '&callback=branch_callback__' + jsonpCallback.toString(), 121 | 'The first request has the right .src' 122 | ); 123 | } 124 | 125 | // _r 126 | requests[0].callback(browser_fingerprint_id); 127 | // v1/open 128 | requests[1].respond( 129 | 200, 130 | { "Content-Type": "application/json" }, 131 | '{ "identity_id":' + identity_id + 132 | ', "session_id":"123088518049178533", "device_fingerprint_id":null, ' + 133 | '"browser_fingerprint_id":"79336952217731267", ' + 134 | '"link":"https://bnc.lt/i/4LYQTXE0_k", "identity":"Branch","has_app":true }' 135 | ); 136 | // v1/event 137 | requests[2].respond( 138 | 200, 139 | { "Content-Type": "application/json" }, 140 | JSON.stringify({ 141 | branch_view_enabled: false 142 | })); 143 | 144 | if (assert) { 145 | assert.strictEqual(requests.length, 3, 'Exactly three requests were made'); 146 | 147 | var params = requests[1].requestBody.split('&'); 148 | var requestObj = params.reduce(function(a, b) { 149 | var pair = b.split('='); 150 | a[pair[0]] = pair[1]; 151 | return a; 152 | }, {}); 153 | 154 | var expectedObj = { 155 | app_id: browser_fingerprint_id, 156 | browser_fingerprint_id: browser_fingerprint_id, 157 | identity_id: identity_id, 158 | options: "%7B%7D", 159 | sdk: 'web' + config.version 160 | }; 161 | 162 | if (requestObj.initial_referrer) { 163 | expectedObj.initial_referrer = requestObj.initial_referrer; 164 | } 165 | 166 | assert.deepEqual( 167 | requestObj, 168 | expectedObj, 169 | 'The second request has the right .requestBody' 170 | ); 171 | } 172 | }; 173 | 174 | describe('init', function() { 175 | it('should call api with params and version', function(done) { 176 | var assert = testUtils.plan(numberOfAsserts(1), done); 177 | branchInit(assert, function(err, data) { 178 | assert.deepEqual(data, 179 | { 180 | data: "", 181 | data_parsed: {}, 182 | has_app: true, 183 | identity: "Branch", 184 | referring_identity: null, 185 | referring_link: null 186 | }, 187 | 'Expected response returned'); 188 | }); 189 | }); 190 | 191 | it('should support being called without a callback', function(done) { 192 | var assert = testUtils.plan(numberOfAsserts(0), done); 193 | branchInit(assert); 194 | }); 195 | 196 | it('should return error to callback', function(done) { 197 | var assert = testUtils.plan(1, done); 198 | branch.init(browser_fingerprint_id, function(err) { 199 | assert.strictEqual(err.message, 'Error in API: 400', 'Expect 400 error message'); 200 | }); 201 | requests[0].callback(browser_fingerprint_id); 202 | requests[1].respond(400); 203 | }); 204 | 205 | it('should attempt 5xx error three times total', function(done) { 206 | var assert = testUtils.plan(1, done); 207 | branch.init(browser_fingerprint_id, function(err) { 208 | assert.strictEqual(err.message, 'Error in API: 500', 'Expect 500 error message'); 209 | }); 210 | var requestCount = 0; 211 | requests[requestCount].callback(browser_fingerprint_id); 212 | requestCount++; 213 | requests[requestCount].respond(500); 214 | clock.tick(250); 215 | requestCount++; 216 | requests[requestCount].respond(500); 217 | clock.tick(250); 218 | requestCount++; 219 | requests[requestCount].respond(500); 220 | }); 221 | 222 | it('should store in session and call open with link_identifier from hash', function(done) { 223 | var assert = testUtils.plan(1, done); 224 | if (testUtils.go('#r:12345')) { 225 | branchInit(); 226 | assert( 227 | requests[indexOfLastInitRequest(0)] 228 | .requestBody 229 | .indexOf('link_identifier=12345') > -1, 230 | 'Expect link_identifier=12345' 231 | ); 232 | } 233 | else { 234 | jsonpCallback--; 235 | done(); 236 | } 237 | 238 | }); 239 | }); 240 | 241 | describe('setIdentity', function() { 242 | it('make two requests to init and set identity, and return expected data', function(done) { 243 | var assert = testUtils.plan(2, done); 244 | branchInit(); 245 | branch.setIdentity('identity', function(err, data) { 246 | assert.deepEqual(data, 247 | { 248 | "identity_id": identity_id, 249 | "link_click_id": "114750153298026746", 250 | "link": config.link_service_endpoint + "/i/4LYQTXE0_k", 251 | "referring_data_parsed": null 252 | }, 253 | 'Expected response returned' 254 | ); 255 | }); 256 | assert.strictEqual( 257 | requests.length, 258 | indexOfLastInitRequest(3), 259 | 'Expect requests length' 260 | ); 261 | requests[indexOfLastInitRequest(2)].respond( 262 | 200, 263 | { "Content-Type": "application/json" }, 264 | '{ "identity_id":' + identity_id + 265 | ', "link_click_id":"114750153298026746"' + 266 | ', "link":"https://bnc.lt/i/4LYQTXE0_k" }' 267 | ); 268 | }); 269 | }); 270 | 271 | describe('data', function() { 272 | it('should make two requests and return session data', function(done) { 273 | var assert = testUtils.plan(numberOfAsserts(2), done); 274 | branchInit(assert); 275 | branch.data(function(err, data) { 276 | assert.deepEqual( 277 | data, 278 | { 279 | data: "", 280 | data_parsed: {}, 281 | has_app: true, 282 | identity: "Branch", 283 | referring_identity: null, 284 | referring_link: null 285 | }, 286 | 'Expect data in branch.data callback' 287 | ); 288 | }); 289 | assert.strictEqual(requests.length, indexOfLastInitRequest(2)); 290 | }); 291 | }); 292 | 293 | describe('getBrowserFingerprintId', function() { 294 | it('it should return browser-fingerprint-id with value 79336952217731267', function(done) { 295 | var assert = testUtils.plan(numberOfAsserts(1), done); 296 | branchInit(assert); 297 | branch.getBrowserFingerprintId(function(err, data) { 298 | assert.strictEqual("79336952217731267", data, 'expected browser-fingerprint-id returned correctly (79336952217731267)'); 299 | }); 300 | }); 301 | it('with tracking disabled, it should return browser-fingerprint-id with value null', function(done) { 302 | var assert = testUtils.plan(numberOfAsserts(1), done); 303 | branchInit(assert); 304 | branch.disableTracking(); 305 | branch.getBrowserFingerprintId(function(err, data) { 306 | assert.strictEqual(null, data, 'expected browser-fingerprint-id returned correctly (null)'); 307 | }); 308 | }); 309 | }); 310 | 311 | describe('link', function() { 312 | it('should make three requests and return short link', function(done) { 313 | var assert = testUtils.plan(numberOfAsserts(2), done); 314 | branchInit(assert); 315 | 316 | branch.link(sampleParams, function(err, data) { 317 | assert.strictEqual( 318 | data, 319 | config.link_service_endpoint + '/l/4manXlk0AJ', 320 | 'Expect data in branch.link callback' 321 | ); 322 | }); 323 | assert.strictEqual( 324 | requests.length, 325 | indexOfLastInitRequest(3), 326 | 'Expect requests length' 327 | ); 328 | requests[indexOfLastInitRequest(2)].respond( 329 | 200, 330 | { "Content-Type": "application/json" }, 331 | '{ "url":"https://bnc.lt/l/4manXlk0AJ" }' 332 | ); 333 | }); 334 | }); 335 | }); 336 | -------------------------------------------------------------------------------- /test/blob-banner-viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/blob-banner.js: -------------------------------------------------------------------------------- 1 | var blobBanner = "
×
Demo App
1000
Branch View Banner!
"; -------------------------------------------------------------------------------- /test/blob-interstitial-viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/blob-interstitial.js: -------------------------------------------------------------------------------- 1 | var blobInterstitial = "
×
Demo App
1000
Branch View Interstitial!
"; -------------------------------------------------------------------------------- /test/branch-deps.js: -------------------------------------------------------------------------------- 1 | goog.addDependency('../../../../src/0_config.js', ['config'], []); 2 | goog.addDependency('../../../../src/0_jsonparse.js', ['safejson'], ['goog.json']); 3 | goog.addDependency('../../../../src/0_queue.js', ['task_queue'], []); 4 | goog.addDependency('../../../../src/1_utils.js', ['utils'], ['config', 'goog.json', 'safejson'], {'lang': 'es6'}); 5 | goog.addDependency('../../../../src/2_resources.js', ['resources'], ['config', 'utils']); 6 | goog.addDependency('../../../../src/2_session.js', ['session'], ['goog.json', 'safejson', 'storage', 'utils']); 7 | goog.addDependency('../../../../src/2_storage.js', ['storage'], ['goog.json', 'utils']); 8 | goog.addDependency('../../../../src/3_api.js', ['Server'], ['goog.json', 'safejson', 'storage', 'utils']); 9 | goog.addDependency('../../../../src/3_banner_utils.js', ['banner_utils'], ['safejson', 'storage', 'utils']); 10 | goog.addDependency('../../../../src/4_banner_css.js', ['banner_css'], ['banner_utils', 'utils']); 11 | goog.addDependency('../../../../src/4_banner_html.js', ['banner_html'], ['banner_utils', 'session', 'storage', 'utils']); 12 | goog.addDependency('../../../../src/5_banner.js', ['banner'], ['banner_css', 'banner_html', 'banner_utils', 'utils']); 13 | goog.addDependency('../../../../src/6_branch.js', ['Branch'], ['Server', 'banner', 'branch_view', 'config', 'goog.json', 'journeys_utils', 'resources', 'safejson', 'session', 'storage', 'task_queue', 'utils'], {'lang': 'es6'}); 14 | goog.addDependency('../../../../src/7_initialization.js', ['branch_instance'], ['Branch', 'config']); 15 | goog.addDependency('../../../../src/branch_view.js', ['branch_view'], ['banner_css', 'journeys_utils', 'safejson', 'utils'], {'lang': 'es6'}); 16 | goog.addDependency('../../../../src/extern.js', [], []); 17 | goog.addDependency('../../../../src/journeys_utils.js', ['journeys_utils'], ['banner_utils', 'safejson', 'utils'], {'lang': 'es6'}); 18 | goog.addDependency('../../../../src/onpage.js', [], []); 19 | goog.addDependency('../../../../test/0_config.js', [], ['config']); 20 | goog.addDependency('../../../../test/0_queue.js', [], ['task_queue']); 21 | goog.addDependency('../../../../test/1_utils.js', [], ['utils'], {'lang': 'es6'}); 22 | goog.addDependency('../../../../test/2_storage.js', [], ['storage']); 23 | goog.addDependency('../../../../test/3_api.js', [], ['Server', 'config', 'resources', 'safejson', 'storage', 'utils']); 24 | goog.addDependency('../../../../test/6_branch.js', [], ['Branch', 'banner_html', 'banner_utils', 'config', 'goog.json', 'resources', 'safejson', 'session', 'storage', 'utils']); 25 | goog.addDependency('../../../../test/6_branch_new.js', [], ['Branch', 'task_queue', 'utils'], {'lang': 'es6'}); 26 | goog.addDependency('../../../../test/7_integration.js', [], ['config', 'goog.json']); 27 | goog.addDependency('../../../../test/blob-banner.js', [], []); 28 | goog.addDependency('../../../../test/blob-interstitial.js', [], []); 29 | goog.addDependency('../../../../test/journeys.js', [], ['Branch', 'banner_utils', 'branch_view', 'config', 'goog.json', 'resources', 'session', 'storage', 'utils'], {'lang': 'es5'}); 30 | goog.addDependency('../../../../test/journeys_utils.js', [], ['journeys_utils'], {'lang': 'es6'}); 31 | goog.addDependency('../../../../test/saucelabs.js', [], []); 32 | goog.addDependency('../../../../test/test-utils.js', [], [], {'lang': 'es5'}); 33 | 34 | -------------------------------------------------------------------------------- /test/integration-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Integration Tests for Branch 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/integration-test.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Integration Tests for Branch 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/journeys.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tests for Branch 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/journeys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | goog.require('utils'); 4 | goog.require('Branch'); 5 | goog.require('resources'); 6 | goog.require('config'); 7 | goog.require('storage'); 8 | goog.require('session'); 9 | goog.require('branch_view'); 10 | goog.require('banner_utils'); 11 | 12 | goog.require('goog.json'); // jshint unused:false 13 | 14 | /*globals branch_sample_key, session_id, identity_id, browser_fingerprint_id, BranchStorage */ 15 | 16 | describe('Branch', function() { 17 | var storage = new BranchStorage([ 'pojo' ]); 18 | var sandbox; 19 | var requests; 20 | 21 | window.sdk_version = 'web' + config.version; 22 | 23 | beforeEach(function() { 24 | testUtils.go(''); 25 | sandbox = sinon.sandbox.create(); 26 | localStorage.clear(); 27 | sessionStorage.clear(); 28 | requests = []; 29 | }); 30 | 31 | function initBranch(runInit, keepStorage) { 32 | [ document.getElementById('branch-banner-iframe'), document.getElementById('branch-banner') ].forEach(function(el) { 33 | el && el.parentNode && el.parentNode.removeChild(el); 34 | }); 35 | 36 | if (!keepStorage) { 37 | storage.clear(); 38 | } 39 | 40 | sandbox.stub(utils, 'getPlatformByUserAgent', function() { 41 | return 'ios'; 42 | }); 43 | 44 | var branch = new Branch(); 45 | 46 | sandbox.stub(branch._server, 'request', function(resource, obj, storage, callback) { 47 | requests.push({ 48 | resource: resource, 49 | obj: obj, 50 | callback: callback 51 | }); 52 | }); 53 | 54 | if (runInit) { 55 | branch.init(branch_sample_key); 56 | requests[0].callback(null, browser_fingerprint_id); 57 | requests[1].callback( 58 | null, 59 | { 60 | browser_fingerprint_id: browser_fingerprint_id, 61 | identity_id: identity_id, 62 | session_id: session_id 63 | } 64 | ); 65 | requests[2].callback(null, {}); 66 | requests = []; 67 | } 68 | 69 | return branch; 70 | } 71 | 72 | function basicTests(call, params) { 73 | it('should fail if branch not initialized', function(done) { 74 | var branch = initBranch(false); 75 | var assert = testUtils.plan(params.length * 2, done); 76 | 77 | function basicTest(param) { 78 | var p = testUtils.nulls(param); 79 | branch[call].apply(branch, p.concat(function(err) { 80 | assert.strictEqual(err.message, 'Branch SDK not initialized'); 81 | })); 82 | assert.throws(function() { 83 | branch[call].apply(branch, p); 84 | }, 'Branch SDK not initialized'); 85 | } 86 | 87 | for (var i = 0; i < params.length; i++) { 88 | basicTest(params[i]); 89 | } 90 | }); 91 | } 92 | 93 | afterEach(function() { 94 | sandbox.restore(); 95 | }); 96 | 97 | describe('journeys', function() { 98 | 99 | it('should attempt to pass deeplink data in a banner call', function(done) { 100 | var branch = initBranch(false); 101 | var assert = testUtils.plan(3, done); 102 | 103 | var bannerDeeplinkData = { 104 | tags: [ 'custom' ], 105 | data: { 106 | mydata: 'From Banner', 107 | foo: 'bar', 108 | '$deeplink_path': 'open/item/5678' 109 | } 110 | } 111 | 112 | branch.init(branch_sample_key); 113 | branch.banner( 114 | { 115 | immediate: true, 116 | disableHide: true, 117 | forgetHide: true 118 | }, 119 | bannerDeeplinkData 120 | ); 121 | 122 | // _r 123 | requests[0].callback(null, browser_fingerprint_id); 124 | 125 | // v1/open 126 | requests[1].callback( 127 | null, 128 | { 129 | browser_fingerprint_id: browser_fingerprint_id, 130 | identity_id: identity_id, 131 | session_id: session_id 132 | } 133 | ); 134 | 135 | // v1/event 136 | requests[2].callback(null, { 137 | branch_view_enabled: false 138 | }); 139 | 140 | // v1/deepview 141 | requests[3].callback(null, { 142 | branch_view_enabled: false 143 | }); 144 | 145 | assert.strictEqual(requests[3].resource.endpoint, '/v1/deepview', 'calling deepview'); 146 | assert.strictEqual(JSON.parse(requests[3].obj.data)['mydata'], 'From Banner', 'deep link data was passed by banner'); 147 | 148 | assert.strictEqual(requests.length, 4, '4 requests made'); 149 | }); 150 | 151 | it('should attempt to pass deeplink data to a journey in a page view event', function(done) { 152 | var branch = initBranch(false); 153 | var assert = testUtils.plan(4, done); 154 | 155 | var bannerDeeplinkData = { 156 | tags: [ 'custom' ], 157 | data: { 158 | mydata: 'From Banner', 159 | foo: 'bar', 160 | '$deeplink_path': 'open/item/5678' 161 | } 162 | } 163 | 164 | sandbox.stub(branch_view, 'handleBranchViewData', function(server, branchViewData, data) { 165 | assert.isDefined(data, 'user data has been defined'); 166 | assert.strictEqual(true, banner_utils.shouldAppend(storage, { 167 | forgetHide: true, 168 | showiOS: true 169 | }), 'branch view should be displayable'); 170 | assert.strictEqual(data.data.mydata, 'From Banner', 'deep link data was passed by banner'); 171 | }); 172 | 173 | branch.init(branch_sample_key); 174 | branch.banner( 175 | { 176 | immediate: true, 177 | disableHide: true, 178 | forgetHide: true 179 | }, 180 | bannerDeeplinkData 181 | ); 182 | 183 | // _r 184 | requests[0].callback(null, browser_fingerprint_id); 185 | 186 | // v1/open 187 | requests[1].callback( 188 | null, 189 | { 190 | browser_fingerprint_id: browser_fingerprint_id, 191 | identity_id: identity_id, 192 | session_id: session_id 193 | } 194 | ); 195 | 196 | // v1/event (first time) 197 | setTimeout(function() { 198 | requests[2].callback(null, { 199 | branch_view_enabled: true, 200 | branch_view_data: { 201 | id: '345', 202 | number_of_use: 1000, 203 | // url: 'https://api.branch.io/v1/branchview/key_live_feebgAAhbH9Tv85H5wLQhpdaefiZv5Dv/279760304565736467?v=1' 204 | url: 'http://localhost:8000' 205 | } 206 | }); 207 | 208 | // v1/event (second time) 209 | requests[3].callback(null, { 210 | branch_view_enabled: true 211 | }); 212 | 213 | assert.strictEqual(requests.length, 4, '4 requests made'); 214 | }, 10); 215 | }); 216 | 217 | it('should attempt to pass deeplink data to a journey in a custom event', function(done) { 218 | var branch = initBranch(false); 219 | var assert = testUtils.plan(4, done); 220 | 221 | var bannerDeeplinkData = { 222 | tags: [ 'custom' ], 223 | data: { 224 | mydata: 'From Banner', 225 | foo: 'bar', 226 | '$deeplink_path': 'open/item/5678' 227 | } 228 | } 229 | 230 | sandbox.stub(branch_view, 'handleBranchViewData', function(server, branchViewData, data) { 231 | assert.isDefined(data, 'user data has been defined'); 232 | var test = banner_utils.shouldAppend(storage, { 233 | forgetHide: true, 234 | showiOS: true 235 | }); 236 | assert.strictEqual(false, banner_utils.shouldAppend(storage, { 237 | forgetHide: true, 238 | showiOS: true 239 | }), 'branch view should not be displayable'); 240 | assert.strictEqual(data.data.mydata, 'From Banner', 'deep link data was passed by banner'); 241 | }); 242 | 243 | branch.init(branch_sample_key); 244 | branch.banner( 245 | { 246 | immediate: true, 247 | disableHide: true, 248 | forgetHide: true 249 | }, 250 | bannerDeeplinkData 251 | ); 252 | 253 | // _r 254 | requests[0].callback(null, browser_fingerprint_id); 255 | 256 | // v1/open 257 | requests[1].callback( 258 | null, 259 | { 260 | browser_fingerprint_id: browser_fingerprint_id, 261 | identity_id: identity_id, 262 | session_id: session_id 263 | } 264 | ); 265 | 266 | // v1/event (first time) 267 | requests[2].callback(null, { 268 | branch_view_enabled: true 269 | }); 270 | 271 | setTimeout(function() { 272 | // v1/event (second time) 273 | requests[3].callback(null, { 274 | branch_view_enabled: true, 275 | branch_view_data: { 276 | id: '345', 277 | number_of_use: 1000, 278 | // url: 'https://api.branch.io/v1/branchview/key_live_feebgAAhbH9Tv85H5wLQhpdaefiZv5Dv/279760304565736467?v=1' 279 | url: 'http://localhost:8000' 280 | } 281 | }); 282 | 283 | assert.strictEqual(requests.length, 4, '4 requests made'); 284 | }, 10); 285 | }); 286 | 287 | it('should attempt to pass deeplink data in a banner call from init callback', function(done) { 288 | // An existing user with a branch.banner() call during the callback passed into branch.init(), 289 | // where a Journey view would be shown. In this case, the data most recently passed to 290 | // branch.banner() and stored in the data cache would be sent through to the /v1/branchview 291 | // call. It would be combined on the server with data set in the Dashboard. 292 | var branch = initBranch(false); 293 | var assert = testUtils.plan(4, done); 294 | 295 | var bannerDeeplinkData = { 296 | tags: [ 'custom' ], 297 | data: { 298 | mydata: 'From Banner', 299 | foo: 'bar', 300 | '$deeplink_path': 'open/item/5678' 301 | } 302 | } 303 | 304 | sandbox.stub(branch_view, 'handleBranchViewData', function(server, branchViewData, data) { 305 | assert.isDefined(data, 'user data has been defined'); 306 | assert.strictEqual(true, banner_utils.shouldAppend(storage, { 307 | forgetHide: true, 308 | showiOS: true 309 | }), 'branch view should be displayable'); 310 | assert.strictEqual(data.data.mydata, 'From Banner', 'deep link data was passed by banner'); 311 | }); 312 | 313 | branch.init(branch_sample_key, {}, function onInit(errorMessage, branchData) { 314 | branch.banner( 315 | { 316 | immediate: true, 317 | disableHide: true, 318 | forgetHide: true 319 | }, 320 | bannerDeeplinkData 321 | ); 322 | }); 323 | 324 | 325 | // _r 326 | requests[0].callback(null, browser_fingerprint_id); 327 | 328 | // v1/open 329 | requests[1].callback( 330 | null, 331 | { 332 | browser_fingerprint_id: browser_fingerprint_id, 333 | identity_id: identity_id, 334 | session_id: session_id 335 | } 336 | ); 337 | 338 | // v1/event (first time) 339 | setTimeout(function() { 340 | requests[2].callback(null, { 341 | branch_view_enabled: true, 342 | branch_view_data: { 343 | id: '345', 344 | number_of_use: 1000, 345 | url: 'http://localhost:8000' 346 | } 347 | }); 348 | 349 | assert.strictEqual(requests.length, 4, '4 requests made'); 350 | }, 10); 351 | }); 352 | 353 | }); 354 | }); 355 | 356 | -------------------------------------------------------------------------------- /test/journeys_utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | goog.require('journeys_utils'); 4 | 5 | describe('getRelativeHeightValueOrFalseFromBannerHeight', function() { 6 | const assert = testUtils.unplanned(); 7 | it('should return false when bannerHeight is in pixel values', function() { 8 | const bannerHeight = '350px'; 9 | const expected = false; 10 | assert.strictEqual(journeys_utils.getRelativeHeightValueOrFalseFromBannerHeight(bannerHeight), expected, 'false when bannerHeight is pixels'); 11 | }); 12 | 13 | it('should return the height value when bannerHeight is provided with viewHeight units - 100vh', function() { 14 | const bannerHeight = '100vh'; 15 | const expected = '100'; 16 | assert.strictEqual(journeys_utils.getRelativeHeightValueOrFalseFromBannerHeight(bannerHeight), expected, '100 from bannerHeight of 100vh'); 17 | }); 18 | 19 | it('should return the height value when bannerHeight is provided with viewHeight units - 99vh', function() { 20 | const bannerHeight = '99vh'; 21 | const expected = '99'; 22 | assert.strictEqual(journeys_utils.getRelativeHeightValueOrFalseFromBannerHeight(bannerHeight), expected, '99 from bannerHeight of 99vh'); 23 | }); 24 | 25 | it('should return the height value when bannerHeight is provided with viewHeight units - 5vh', function() { 26 | const bannerHeight = '5vh'; 27 | const expected = '5'; 28 | assert.strictEqual(journeys_utils.getRelativeHeightValueOrFalseFromBannerHeight(bannerHeight), expected, '5 from bannerHeight of 5vh'); 29 | }); 30 | 31 | it('should return the height value when bannerHeight is provided with percentage units - 100%', function() { 32 | const bannerHeight = '100%'; 33 | const expected = '100'; 34 | assert.strictEqual(journeys_utils.getRelativeHeightValueOrFalseFromBannerHeight(bannerHeight), expected, '100 from bannerHeight of 100%'); 35 | }); 36 | 37 | it('should return the height value when bannerHeight is provided with percentage units - 99%', function() { 38 | const bannerHeight = '99%'; 39 | const expected = '99'; 40 | assert.strictEqual(journeys_utils.getRelativeHeightValueOrFalseFromBannerHeight(bannerHeight), expected, '99 from bannerHeight of 99%'); 41 | }); 42 | 43 | it('should return the height value when bannerHeight is provided with percentage units - 5%', function() { 44 | const bannerHeight = '5%'; 45 | const expected = '5'; 46 | assert.strictEqual(journeys_utils.getRelativeHeightValueOrFalseFromBannerHeight(bannerHeight), expected, '5 from bannerHeight of 5%'); 47 | }); 48 | }); -------------------------------------------------------------------------------- /test/mocha.css: -------------------------------------------------------------------------------- 1 | #mocha-stats { 2 | position: static; 3 | font-size: 36px; 4 | } 5 | -------------------------------------------------------------------------------- /test/saucelabs.js: -------------------------------------------------------------------------------- 1 | // Sauce labs magic, don't worry about it 2 | function onload() { 3 | var runner = mocha.run(); 4 | 5 | var failedTests = []; 6 | runner.on('end', function() { 7 | window.mochaResults = runner.stats; 8 | window.mochaResults.reports = failedTests; 9 | }); 10 | 11 | var logFailure = function(test, err) { 12 | 13 | var flattenTitles = function(test) { 14 | var titles = []; 15 | while (test.parent.title) { 16 | titles.push(test.parent.title); 17 | test = test.parent; 18 | } 19 | return titles.reverse(); 20 | }; 21 | 22 | failedTests.push({ name: test.title, 23 | result: false, 24 | message: err.message, 25 | stack: err.stack, 26 | titles: flattenTitles(test) }); 27 | }; 28 | runner.on('fail', logFailure); 29 | }; 30 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Tests for Branch 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/web-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Just a couple of variables that shouldn't change very often... 3 | */ 4 | goog.provide('config'); 5 | 6 | config.app_service_endpoint = 'https://app.link'; 7 | config.link_service_endpoint = 'https://bnc.lt'; 8 | config.api_endpoint = 'https://api.branch.io'; 9 | // will get overwritten by gha on actual deploy 10 | config.version = '2.85.2'; 11 | -------------------------------------------------------------------------------- /transform.js: -------------------------------------------------------------------------------- 1 | var falafel = require('falafel'); 2 | 3 | process.stdin.setEncoding('utf8'); 4 | 5 | var src = ""; 6 | process.stdin.on('readable', function() { 7 | var chunk = process.stdin.read(); 8 | if (chunk !== null) { 9 | src += chunk; 10 | } 11 | }); 12 | process.stdin.on('end', function() { 13 | var to = process.argv[2]; 14 | var from = src 15 | .match(/\(function\((\w+,\s*)+(\w+)/g)[0] 16 | .replace('(function(', '') 17 | .split(',') 18 | .map( 19 | function(s) { 20 | return s.trim(); 21 | } 22 | ); 23 | var pairs = {}; 24 | for (var i = 0; i < from.length; i++) { 25 | pairs[from[i]] = to[i]; 26 | } 27 | 28 | var output = falafel(src, function(node) { 29 | var name = ( 30 | node.type === 'Identifier' ? 31 | node.name : 32 | node.type === 'Literal' ? node.value : null 33 | ); 34 | if (name && pairs[name]) { 35 | if (node.type === 'Literal') { 36 | node.update(node.raw.replace(name, pairs[name])); 37 | } 38 | else { 39 | node.update(pairs[name]); 40 | } 41 | } 42 | }); 43 | console.log(output.toString()); 44 | }); 45 | --------------------------------------------------------------------------------