From 9da0b2b9c6d33cba5f75806038d89f8b96538b5b Mon Sep 17 00:00:00 2001 From: Bruno Barbieri Date: Fri, 24 May 2019 22:41:26 -0400 Subject: [PATCH] Release v0.1.7 (#672) * Transaction errors (#550) * wip: showing plain transaction error on screen alert * handle result promise * remove listener in approval * drop package-lock * comments * Add zxcvbn for password strength estimation (#555) * add zxcvbn * use zxcvbn for password estimations * add comments * update snapshot * navar title numberoflines 1 (#557) * Reduce instances of cached contract addresses (#547) Reduce instances of cached contract addresses * poll balances when tx happens (#562) * Fix custom gas price (#566) * fix wrong custom gas value * fix calculation * fix comments * Feature: implement method registry (#571) * add handlemethoddata gaba support * update action keys methods * update tests * in tx review if method data unknown default to unknown method * bump gaba * Feature: EIP747 watchAsset (#576) * watchasset erc20 basic support * render watchasset modal * handle watchasset call * handle watchasset rejection * handle watched asset images * missing strings * snapshots * bump gaba * fix styles * snapshots * styles update * pin dep * install * Improvement: Transaction Edit (#579) * close dropdown if press outside * snapshots * missing doc * renaming * Fix asset removal (#582) * fix asset removal * clean up * fix typo * bump gaba * update package-lock * Fix missing balances (#583) * fix missing balances * Update index.js * Update currency rate too * dont poll tokens * Remove ref * bump gaba * remember biometrics preference (#584) * Feature: token deeplinks support (#588) * handling data through deeplink * handle chainId * handle token value correctly * use contract metadata to get token info is in there * showing alert while changing network * don't validate toen that user doesn't have * validate amount token to send if user doesn't have it in state * move messages to locales * fix getbalanceof * handle state token * fix typo * update locale * locales * feature: update GABA to 1.0.0-beta.71 (#590) * Two step push notification prompt (#589) * two step notification prompt * clean up * fix typo * Update deps (#591) * update react-native-push-notification * upgrade react-native to v0.59.4 * Feature: Warning when send to known asset (#593) * warning when user is sending eth or assets to known asset contract * snapshots * check contract map only on mainnet * test * Bugfix: remove and ignore assets only where is necessary (#594) * remove and ignore assets only where is necessary * bump gaba * Feature: Choose IPFS gateway (#592) * use of ipfsgateway from GABA * handle ipfs gateway selection * snapshots * snapshots * reorder ipfs gateways list * only show available ipfs gateways * snapshots * bump gaba * handle current gateway down * snapshots * Improvement: consolidate send and approval screens (#596) * consolidate send screen * consolidate approval screen * tx mode from navigation * fix renderable value being undefined when edit approval * snapshots * Remember recent addresses (#597) * kinda works * keyboard wizardry working * fix eth input * bump detox (#599) * Fix account label wallet (#600) * bump detox * fix default value * feature: add 1102 convenience methods (#602) * Improvement: update corresponding balances (#601) * existing contact bugfix * poll only when necessary * recent address bugfix and adding asset type in approval * update left button navbar when tx fails * avoid checking for asset address for undefined * Feature: collectibles api key (#603) * add opensea key from env * bump gaba * change const name * Improvement: shareable SimpleWebView (#604) * make shareable simple web view * test * check for navigation * update ios * Animations on App launch & resume (#606) * lottie working * added fadeIn / out effect * finally fix crash in debug mode * added fadeIn to other views * update unit tests * fix lock screen * adjust timing * more timing stuff * fix timing stuff * use optimized bounce animation * fix for android * update snapshots * use exact versions * bump version to 0.1.7 (#608) * update gem lockfile (#612) * use latest xcode image (#614) * unlink libRCTGeolocation (#616) * Bugfix: currency code & values with less than 5 precision decimals (#615) * remove usd dollar symbol * render low values * avoid crashing app when hot reloading * avoid rendering when no value * rollback fix * tests * comment * update animation (#619) * update animation * update snapshots * Bugfix: standardize colors (#618) * reds * blue * greays and primaryfox * greens * yellow * orange * random colors * snapshots * update colords * snapshots * Revert "update animation (#619)" (#620) This reverts commit 884a20fb8ef8392412eb1da02754f75fe41ce9f9. * Feature: switch primary currency (#624) * select primary currency * wip number methods * send ETH withprimary currency fiat * fiatNumberToTokenMinimalUnit * tokens working * avoid exponential and render input * ethinput doc * handle value from ethinput * fix fill max when fiat as primary currency * clean up of values * clean render input * clean renderAsset * render tokens according to primary currency * processreadablevalue and fill max * balances and secondary balances * snapshots * small changes * pick component * snapshots * handle periodic numbers * comments * Feature: onboarding wizard (#607) * onboarding wizard component * rendering on top of stack" * steps and close tooltips * fake navigator * indicator style * hardcoded step 1 WIP * step 1 done * step 2 done * bullet progress bar * step 4 * use redux to navigate through onboarding * step 5 * steps 6 and 7 * got it * rename tooltip to coachmark * delete unused file * transparent styled button * update step3 to change account label * skip tutorial button * handle all onboarding state from redux * progress bar from array * doc * locales * snapshots * passing content and onboarding styles * reorder texts * update spanish * render methods * render onboarding wizard only when first time in app * IOS styles * android styles * snapshots * browser first * snapshots * label padding * minor comments * check onboarding on create / import wallet * sync from extension success * sync from extension success * add to entry and login * snapshots * feature: allow transactions to be cancelled (#622) * Improvement: assets detection (#627) * use collectible contract if image collectible is empty * bump gaba * Feature: terms and conditions (#631) * terms and conditions compoenent * import from seed * webview to stack * locales * snapshots * rm unused state * Feature: Tabs (#623) * good progress on tabs UI * tabs "kinda working" * fix tab selection * snapshots * refactor into components * more refactor * animations FTW * adjust values * fix tests * fix android UI * fix bugs * good progress on multi-webview approach * working on android and ios * fix a bunch of bugs * fix thumbnails * fix tests * refactor folder structure * improve snapshot reliability * fix tab remounted issue * clean up * fix back button ios * take screenshot before showing tabs * show real tab count * bugfixzzz * code review comments * dynamic calculation for scrolling * Feature: opt-in metrics (#632) * WIP * optin metrics screen full structure working * optin screen * create wallet flow * entry flow * import from seed * sync with extension flow * docs * basic logger * metametrics settings * navbar * logoin flow * snapshots AND ICONS * update no thanks * navbar doc * navbar colors and comment/; * restore action view * scrollable optin * snapshots * Improvement: gas limit fallback (#633) * gas limit estimation fallback * log * Bugfix: asset overview balance (#635) * render value on corresponding network * handle txs loader when switching networks * snapshots * svoid balance of undefined * Bugfix: phishing modal (#637) * fix ios biometrics permission prompt (#634) * Feature: import from seed password strength (#639) * add password strength to import from seed * snapshots * padding and jump to password * Bugfix: collectibles original image (#642) * bump gaba * add collectible address * bump gaba and audit * Feature: payment request (#641) * modal payment request * add qr address modal * basic stricture done * rename * select asset payment request * basic amount input * handle payment request navigation * handle amount * handle primary currency * handle primary currency switch * get crypto amount * divide primary currency methods * add buttons * generate link * success UI * better ui * share and copy to clipboard * show and close qer modal * consolidate ui and shgow user tokens * payment request flow from asset * handle network assets and link * handle empty search and empty tokens * handle not conversion rate * handle fiat primary currency with no conversion nor exchange rate * handle decimal places * consolidate primary currency methods * fix condition * docs * more docs * qr codes update * locales * onbuy coming soon * fix android icions * update icons * snapshots * on submit next * handle payment request from qrscanner * update receive icon * snapshots * update qr codes * update qr and android * android * comments * Swap cent for bounties (#626) * Swap cent for bounties * Fix typo * Fix payment request icon (#645) * fix icon * update snapshots * Feature: WalletConnect support (#643) * working * show session prompt * support for multiple connectors * persisting sessions and list * update snapshots * clean up * clean-up * Update WalletConnect.js * Update animation (#646) * update animation * update snapshots * fix and update assets * adjust values * update snapshots * Bugfix: fiat deeplinks (#648) * fix button info type color * fix bug * drop package * make overlay tappable on connect modal (#650) * make overlay tappable on connect modal * update snapshot * Bugfix: websites title and icon (#651) * if on title query for title when adding bookmark * fix website icon state * delete didmount * snapshots * android navbar * android navbar * browser scripts * Bugfix: android icon (#653) * downgrade rn vector icons * copy address from public address * snapshots * drop fabric * space * Feature: local analytics (#656) * introduce analytics * analytics opts * introduce tracking methods * track onboarding events * track navigation events * common but navigation swipe * browser view * dapp view * wallet view * rm settings logger * tramsaction confirm and approval screens * tramsaction confirm and cancel actions * transactions signatureS * transactions signatures doc * switch accounts * track login * track connect * DRY * track settings * snapshots * id dev log * remove async storage optin from settings * drop package-lock * runafterinteractions for analytics in didmount * interactionmanager changes * move tracking to end in send * analytics * move to end * Updated tabs and browser navigation (#658) * updated tabs and browser navigation * fix android stuff * update snapshots * update snapshots * Delete comment * Update index.js * Update Sync instructions (#660) * update eng instructions * update es instructions * Feature: advanced custom rpc (#661) * add rpc target chainid ticker and nickname * use network nickname * add isETH * use of ticker * move settings component to settings * basic networks settings * basic network settings * add and edit forms * adding custom rpc from network settings * add block explorer url * handle action anabled * validate chainid * doc and locales Q * drawer view in blockexplorer * handle block explorer from tx detauls * handle tx unit with ticker * configure native currency * if no conversion rate shpiw 0 * handle send from drawer * handle render from wei * handle eth input * action keys * update snapshots * create snapshots * more locales * conditional rpc rendering in networks sett * network icon top * ticker to uppercase * android ui * small fixes * more android * getticker getblockexoplorer * parse url name * bump gaba * attempt 1 (#665) * fix approval screen (#662) * enable 64 bits builds android (#667) * Bugfix: currency rate config (#666) * configure native and current currency on engine init * from copntropller init * bump gaba * destructure CurrencyRateController * Bugfix: some ui fixes (#668) * tabs thumbnail website icon * copllectible contract title center * align onboarding wizard step 5 * align receive button from asset for android * snapshots * dont import from rn gesture handler (#669) * Bugfix: close dropdowns on scan qr (#671) * close dropdowns * snapshots * swipe to dismiss notifications (#670) * swipe to dismiss notifications * exact match version * fix tap and opacity --- .circleci/config.yml | 6 +- android/app/build.gradle | 14 +- .../java/io/metamask/MainApplication.java | 4 + android/settings.gradle | 4 + app/actions/browser/index.js | 60 + app/actions/modals/index.js | 5 +- app/actions/settings/index.js | 7 + app/actions/wizard/index.js | 9 + app/animations/bounce.json | 349 + app/animations/fox-in.json | 13503 ++++++++++++++++ app/animations/wordmark.json | 1288 ++ .../Nav/App/__snapshots__/index.test.js.snap | 60 + app/components/Nav/App/index.js | 17 + .../Nav/Main/__snapshots__/index.test.js.snap | 34 + app/components/Nav/Main/index.js | 273 +- .../__snapshots__/index.test.js.snap | 10 +- app/components/UI/AccountApproval/index.js | 86 +- .../UI/AccountApproval/index.test.js | 11 + app/components/UI/AccountInput/index.js | 116 +- app/components/UI/AccountList/index.js | 35 +- app/components/UI/AccountOverview/index.js | 53 +- app/components/UI/AccountSelect/index.js | 81 +- .../__snapshots__/index.test.js.snap | 2 +- app/components/UI/ActionModal/index.js | 2 +- .../__snapshots__/index.test.js.snap | 4 +- app/components/UI/ActionView/index.js | 38 +- .../__snapshots__/index.test.js.snap | 8 +- .../UI/AddCustomCollectible/index.js | 12 +- .../__snapshots__/index.test.js.snap | 12 +- app/components/UI/AddCustomToken/index.js | 4 +- app/components/UI/AppInformation/index.js | 4 +- .../__snapshots__/index.test.js.snap | 41 +- app/components/UI/AssetActionButtons/index.js | 28 +- .../__snapshots__/index.test.js.snap | 2 +- app/components/UI/AssetElement/index.js | 2 +- app/components/UI/AssetIcon/index.js | 8 +- .../__snapshots__/index.test.js.snap | 4 +- app/components/UI/AssetOverview/index.js | 111 +- app/components/UI/AssetOverview/index.test.js | 20 +- .../__snapshots__/index.test.js.snap | 2 +- app/components/UI/AssetSearch/index.js | 2 +- app/components/UI/BrowserFeatured/index.js | 9 +- .../Button/__snapshots__/index.test.js.snap | 2 +- app/components/UI/Button/index.js | 2 +- .../__snapshots__/index.test.js.snap | 18 +- .../CollectibleContractInformation/index.js | 10 +- .../__snapshots__/index.test.js.snap | 4 +- .../UI/CollectibleContractOverview/index.js | 7 +- .../__snapshots__/index.test.js.snap | 6 +- .../UI/CollectibleContracts/index.js | 6 +- .../UI/CollectibleOverview/index.js | 4 +- app/components/UI/Collectibles/index.js | 7 +- app/components/UI/CustomGas/index.js | 94 +- app/components/UI/CustomGas/index.test.js | 5 + app/components/UI/DrawerView/index.js | 345 +- app/components/UI/EthInput/index.js | 352 +- .../__snapshots__/index.test.js.snap | 22 + app/components/UI/FadeOutOverlay/index.js | 52 + .../UI/FadeOutOverlay/index.test.js | 10 + .../HomePage/__snapshots__/index.test.js.snap | 21 +- app/components/UI/HomePage/index.js | 36 +- app/components/UI/Identicon/index.js | 40 +- app/components/UI/Navbar/index.js | 255 +- app/components/UI/NavbarBrowserTitle/index.js | 44 +- app/components/UI/NavbarTitle/index.js | 19 +- .../__snapshots__/index.test.js.snap | 34 +- app/components/UI/NetworkList/index.js | 48 +- .../__snapshots__/index.test.js.snap | 227 + .../UI/OnboardingWizard/Coachmark/index.js | 298 + .../OnboardingWizard/Coachmark/index.test.js | 12 + .../Step1/__snapshots__/index.test.js.snap | 3 + .../UI/OnboardingWizard/Step1/index.js | 94 + .../UI/OnboardingWizard/Step1/index.test.js | 19 + .../Step2/__snapshots__/index.test.js.snap | 3 + .../UI/OnboardingWizard/Step2/index.js | 86 + .../UI/OnboardingWizard/Step2/index.test.js | 19 + .../Step3/__snapshots__/index.test.js.snap | 105 + .../UI/OnboardingWizard/Step3/index.js | 141 + .../UI/OnboardingWizard/Step3/index.test.js | 38 + .../Step4/__snapshots__/index.test.js.snap | 3 + .../UI/OnboardingWizard/Step4/index.js | 96 + .../UI/OnboardingWizard/Step4/index.test.js | 19 + .../Step5/__snapshots__/index.test.js.snap | 3 + .../UI/OnboardingWizard/Step5/index.js | 102 + .../UI/OnboardingWizard/Step5/index.test.js | 19 + .../Step6/__snapshots__/index.test.js.snap | 3 + .../UI/OnboardingWizard/Step6/index.js | 98 + .../UI/OnboardingWizard/Step6/index.test.js | 19 + .../Step7/__snapshots__/index.test.js.snap | 3 + .../UI/OnboardingWizard/Step7/index.js | 87 + .../UI/OnboardingWizard/Step7/index.test.js | 19 + .../__snapshots__/index.test.js.snap | 28 + app/components/UI/OnboardingWizard/index.js | 106 + .../UI/OnboardingWizard/index.test.js | 21 + app/components/UI/OnboardingWizard/styles.js | 24 + .../__snapshots__/index.test.js.snap | 392 + app/components/UI/OptinMetrics/index.js | 244 + app/components/UI/OptinMetrics/index.test.js | 17 + .../UI/Pager/__snapshots__/index.test.js.snap | 2 +- app/components/UI/Pager/index.js | 4 +- .../__snapshots__/index.test.js.snap | 7 + .../UI/PaymentRequest/AssetList/index.js | 109 + .../UI/PaymentRequest/AssetList/index.test.js | 17 + .../__snapshots__/index.test.js.snap | 3 + app/components/UI/PaymentRequest/index.js | 623 + .../UI/PaymentRequest/index.test.js | 18 + .../UI/PaymentRequestSuccess/index.js | 324 + .../__snapshots__/index.test.js.snap | 3 +- app/components/UI/PersonalSign/index.js | 3 +- .../__snapshots__/index.test.js.snap | 8 +- app/components/UI/PhishingModal/index.js | 8 +- .../__snapshots__/index.test.js.snap | 90 + .../ReceiveRequestAction/index.js | 80 + .../ReceiveRequestAction/index.test.js | 17 + .../__snapshots__/index.test.js.snap | 400 + app/components/UI/ReceiveRequest/index.js | 372 + .../UI/ReceiveRequest/index.test.js | 26 + app/components/UI/Screen/index.js | 2 +- .../__snapshots__/index.test.js.snap | 2 +- app/components/UI/SelectComponent/index.js | 14 +- app/components/UI/SettingsDrawer/index.js | 6 +- .../__snapshots__/index.test.js.snap | 4 +- app/components/UI/SignatureRequest/index.js | 56 +- .../UI/SignatureRequest/index.test.js | 5 + .../__snapshots__/index.test.js.snap | 20 +- .../UI/StyledButton/styledButtonStyles.js | 36 +- .../__snapshots__/index.test.js.snap | 37 + app/components/UI/Tabs/TabCountIcon/index.js | 61 + .../UI/Tabs/TabCountIcon/index.test.js | 24 + .../__snapshots__/index.test.js.snap | 158 + app/components/UI/Tabs/TabThumbnail/index.js | 178 + .../UI/Tabs/TabThumbnail/index.test.js | 15 + .../UI/Tabs/__snapshots__/index.test.js.snap | 174 + app/components/UI/Tabs/index.js | 277 + app/components/UI/Tabs/index.test.js | 12 + app/components/UI/TokenImage/index.js | 7 +- app/components/UI/Tokens/index.js | 62 +- .../__snapshots__/index.test.js.snap | 23 +- app/components/UI/TransactionEdit/index.js | 92 +- app/components/UI/TransactionEditor/index.js | 70 +- .../UI/TransactionEditor/index.test.js | 9 + .../__snapshots__/index.test.js.snap | 541 +- .../TransactionDetails/index.js | 223 +- .../TransactionDetails/index.test.js | 9 + .../__snapshots__/index.test.js.snap | 2 +- .../TransferElement/index.js | 2 +- .../__snapshots__/index.test.js.snap | 22 +- app/components/UI/TransactionElement/index.js | 66 +- .../UI/TransactionElement/index.test.js | 5 + .../UI/TransactionNotification/index.js | 36 +- .../__snapshots__/index.test.js.snap | 6 +- .../TransactionReviewData/index.js | 4 +- .../__snapshots__/index.test.js.snap | 21 +- .../TransactionReviewInformation/index.js | 29 +- .../__snapshots__/index.test.js.snap | 46 +- .../TransactionReviewSummary/index.js | 44 +- .../__snapshots__/index.test.js.snap | 9 +- app/components/UI/TransactionReview/index.js | 26 +- app/components/UI/Transactions/index.js | 21 +- .../__snapshots__/index.test.js.snap | 3 +- app/components/UI/TypedSign/index.js | 3 +- .../__snapshots__/index.test.js.snap | 290 + .../UI/WalletConnectSessionApproval/index.js | 246 + .../index.test.js | 29 + .../__snapshots__/index.test.js.snap | 212 + app/components/UI/WatchAssetRequest/index.js | 189 + .../UI/WatchAssetRequest/index.test.js | 29 + .../__snapshots__/index.test.js.snap | 30 +- app/components/UI/WebsiteIcon/index.js | 37 +- .../__snapshots__/index.test.js.snap | 2 +- app/components/UI/WebviewProgressBar/index.js | 2 +- .../__snapshots__/index.test.js.snap | 6 +- .../Views/AccountBackupStep1/index.js | 4 +- .../__snapshots__/index.test.js.snap | 32 +- .../Views/AccountBackupStep4/index.js | 12 +- .../__snapshots__/index.test.js.snap | 60 +- .../Views/AccountBackupStep5/index.js | 20 +- .../Views/AccountBackupStep6/index.js | 7 +- app/components/Views/AddAsset/index.js | 12 +- .../__snapshots__/index.test.js.snap | 8 +- app/components/Views/AddBookmark/index.js | 4 +- .../Approval/__snapshots__/index.test.js.snap | 4 +- app/components/Views/Approval/index.js | 130 +- app/components/Views/Approval/index.test.js | 10 +- app/components/Views/Asset/index.js | 3 + app/components/Views/Browser/index.js | 1714 +- .../__snapshots__/index.test.js.snap | 250 +- app/components/Views/BrowserTab/index.js | 1739 ++ .../{Browser => BrowserTab}/index.test.js | 4 +- .../__snapshots__/index.test.js.snap | 15 +- app/components/Views/ChoosePassword/index.js | 59 +- app/components/Views/Collectible/index.js | 3 + app/components/Views/CreateWallet/index.js | 33 +- .../Views/CreateWallet/index.js.rej | 14 + .../Entry/__snapshots__/index.test.js.snap | 6 +- app/components/Views/Entry/index.js | 196 +- app/components/Views/Entry/index.test.js | 9 +- .../__snapshots__/index.test.js.snap | 201 +- app/components/Views/ImportFromSeed/index.js | 404 +- .../Views/ImportFromSeed/index.js.rej | 14 + .../__snapshots__/index.test.js.snap | 6 +- .../Views/ImportPrivateKey/index.js | 4 +- .../__snapshots__/index.test.js.snap | 2 +- .../Views/ImportPrivateKeySuccess/index.js | 2 +- .../__snapshots__/index.test.js.snap | 1069 +- app/components/Views/LockScreen/index.js | 138 +- .../Login/__snapshots__/index.test.js.snap | 197 +- app/components/Views/Login/index.js | 101 +- app/components/Views/Login/index.test.js | 25 +- app/components/Views/Onboarding/index.js | 114 +- .../__snapshots__/index.test.js.snap | 99 + app/components/Views/PickComponent/index.js | 103 + .../Views/PickComponent/index.test.js | 18 + app/components/Views/QRScanner/index.js | 9 + .../__snapshots__/index.test.js.snap | 10 +- .../Views/RevealPrivateCredential/index.js | 16 +- .../Root/__snapshots__/index.test.js.snap | 2 +- app/components/Views/Root/index.js | 3 +- app/components/Views/Send/index.js | 266 +- .../__snapshots__/index.test.js.snap | 105 +- .../{ => Settings}/AdvancedSettings/index.js | 190 +- .../AdvancedSettings/index.test.js | 9 +- .../__snapshots__/index.test.js.snap | 86 + .../Settings/ExperimentalSettings/index.js | 78 + .../ExperimentalSettings/index.test.js | 34 + .../__snapshots__/index.test.js.snap | 54 +- .../{ => Settings}/GeneralSettings/index.js | 68 +- .../GeneralSettings/index.test.js | 0 .../__snapshots__/index.test.js.snap | 277 + .../NetworksSettings/NetworkSettings/index.js | 407 + .../NetworkSettings/index.test.js | 32 + .../__snapshots__/index.test.js.snap | 239 + .../Views/Settings/NetworksSettings/index.js | 172 + .../Settings/NetworksSettings/index.test.js | 32 + .../__snapshots__/index.test.js.snap | 78 +- .../{ => Settings}/SecuritySettings/index.js | 68 +- .../SecuritySettings/index.test.js | 0 .../Settings/__snapshots__/index.test.js.snap | 16 +- app/components/Views/Settings/index.js | 21 + app/components/Views/SimpleWebview/index.js | 17 + .../Views/SimpleWebview/index.test.js | 9 +- .../Views/SyncWithExtension/index.js | 13 +- .../__snapshots__/index.test.js.snap | 104 +- .../Views/SyncWithExtensionSuccess/index.js | 48 +- .../SyncWithExtensionSuccess/index.test.js | 13 +- .../__snapshots__/index.test.js.snap | 7 + .../Views/TermsAndConditions/index.js | 64 + .../Views/TermsAndConditions/index.test.js | 19 + app/components/Views/Wallet/index.js | 64 +- .../__snapshots__/index.test.js.snap | 3 + .../Views/WalletConnectSessions/index.js | 168 + .../Views/WalletConnectSessions/index.test.js | 11 + app/core/Analytics.js | 182 + app/core/AppConstants.js | 3 +- app/core/DeeplinkManager.js | 1 - app/core/Engine.js | 65 +- app/core/InpageBridge.js | 50 + app/core/InpageBridge.test.js | 3 +- app/core/TransactionsNotificationManager.js | 73 +- app/core/WalletConnect.js | 273 + app/reducers/analytics/index.js | 32 + app/reducers/browser/index.js | 34 +- app/reducers/index.js | 6 +- app/reducers/modals/index.js | 9 +- app/reducers/settings/index.js | 6 + app/reducers/transaction/index.js | 2 +- app/reducers/wizard/index.js | 22 + app/styles/common.js | 74 +- app/util/DeviceSize.js | 4 + app/util/Logger.js | 24 +- app/util/analytics.js | 286 + app/util/assets.js | 5 + app/util/browserSripts.js | 96 + app/util/custom-gas.js | 12 +- app/util/dapp-url-list.js | 2 +- app/util/eip681-link-generator.js | 47 + app/util/featured-dapp-list.js | 6 +- app/util/general.js | 15 + app/util/ipfs-gateways.json | 41 + app/util/networks.js | 39 +- app/util/number.js | 77 +- app/util/number.test.js | 86 +- app/util/transactions.js | 174 +- app/util/transactions.test.js | 44 +- ios/Gemfile.lock | 24 +- ios/MetaMask.xcodeproj/project.pbxproj | 211 +- ios/MetaMask/Base.lproj/LaunchScreen.xib | 22 +- ios/MetaMask/Images.xcassets/Contents.json | 2 +- .../metamask-logo.imageset/Contents.json | 21 - .../metamask-logo.imageset/metamask-logo.png | Bin 51434 -> 0 bytes ios/MetaMask/Info.plist | 6 +- locales/en.json | 213 +- locales/es.json | 202 +- package-lock.json | 1217 +- package.json | 21 +- scripts/build.sh | 5 +- scripts/postinstall.sh | 4 + 297 files changed, 34892 insertions(+), 4285 deletions(-) create mode 100644 app/actions/wizard/index.js create mode 100644 app/animations/bounce.json create mode 100644 app/animations/fox-in.json create mode 100644 app/animations/wordmark.json create mode 100644 app/components/UI/FadeOutOverlay/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/FadeOutOverlay/index.js create mode 100644 app/components/UI/FadeOutOverlay/index.test.js create mode 100644 app/components/UI/OnboardingWizard/Coachmark/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/Coachmark/index.js create mode 100644 app/components/UI/OnboardingWizard/Coachmark/index.test.js create mode 100644 app/components/UI/OnboardingWizard/Step1/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/Step1/index.js create mode 100644 app/components/UI/OnboardingWizard/Step1/index.test.js create mode 100644 app/components/UI/OnboardingWizard/Step2/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/Step2/index.js create mode 100644 app/components/UI/OnboardingWizard/Step2/index.test.js create mode 100644 app/components/UI/OnboardingWizard/Step3/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/Step3/index.js create mode 100644 app/components/UI/OnboardingWizard/Step3/index.test.js create mode 100644 app/components/UI/OnboardingWizard/Step4/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/Step4/index.js create mode 100644 app/components/UI/OnboardingWizard/Step4/index.test.js create mode 100644 app/components/UI/OnboardingWizard/Step5/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/Step5/index.js create mode 100644 app/components/UI/OnboardingWizard/Step5/index.test.js create mode 100644 app/components/UI/OnboardingWizard/Step6/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/Step6/index.js create mode 100644 app/components/UI/OnboardingWizard/Step6/index.test.js create mode 100644 app/components/UI/OnboardingWizard/Step7/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/Step7/index.js create mode 100644 app/components/UI/OnboardingWizard/Step7/index.test.js create mode 100644 app/components/UI/OnboardingWizard/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OnboardingWizard/index.js create mode 100644 app/components/UI/OnboardingWizard/index.test.js create mode 100644 app/components/UI/OnboardingWizard/styles.js create mode 100644 app/components/UI/OptinMetrics/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/OptinMetrics/index.js create mode 100644 app/components/UI/OptinMetrics/index.test.js create mode 100644 app/components/UI/PaymentRequest/AssetList/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/PaymentRequest/AssetList/index.js create mode 100644 app/components/UI/PaymentRequest/AssetList/index.test.js create mode 100644 app/components/UI/PaymentRequest/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/PaymentRequest/index.js create mode 100644 app/components/UI/PaymentRequest/index.test.js create mode 100644 app/components/UI/PaymentRequestSuccess/index.js create mode 100644 app/components/UI/ReceiveRequest/ReceiveRequestAction/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/ReceiveRequest/ReceiveRequestAction/index.js create mode 100644 app/components/UI/ReceiveRequest/ReceiveRequestAction/index.test.js create mode 100644 app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/ReceiveRequest/index.js create mode 100644 app/components/UI/ReceiveRequest/index.test.js create mode 100644 app/components/UI/Tabs/TabCountIcon/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/Tabs/TabCountIcon/index.js create mode 100644 app/components/UI/Tabs/TabCountIcon/index.test.js create mode 100644 app/components/UI/Tabs/TabThumbnail/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/Tabs/TabThumbnail/index.js create mode 100644 app/components/UI/Tabs/TabThumbnail/index.test.js create mode 100644 app/components/UI/Tabs/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/Tabs/index.js create mode 100644 app/components/UI/Tabs/index.test.js create mode 100644 app/components/UI/WalletConnectSessionApproval/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/WalletConnectSessionApproval/index.js create mode 100644 app/components/UI/WalletConnectSessionApproval/index.test.js create mode 100644 app/components/UI/WatchAssetRequest/__snapshots__/index.test.js.snap create mode 100644 app/components/UI/WatchAssetRequest/index.js create mode 100644 app/components/UI/WatchAssetRequest/index.test.js rename app/components/Views/{Browser => BrowserTab}/__snapshots__/index.test.js.snap (67%) create mode 100644 app/components/Views/BrowserTab/index.js rename app/components/Views/{Browser => BrowserTab}/index.test.js (64%) create mode 100644 app/components/Views/CreateWallet/index.js.rej create mode 100644 app/components/Views/ImportFromSeed/index.js.rej create mode 100644 app/components/Views/PickComponent/__snapshots__/index.test.js.snap create mode 100644 app/components/Views/PickComponent/index.js create mode 100644 app/components/Views/PickComponent/index.test.js rename app/components/Views/{ => Settings}/AdvancedSettings/__snapshots__/index.test.js.snap (77%) rename app/components/Views/{ => Settings}/AdvancedSettings/index.js (66%) rename app/components/Views/{ => Settings}/AdvancedSettings/index.test.js (76%) create mode 100644 app/components/Views/Settings/ExperimentalSettings/__snapshots__/index.test.js.snap create mode 100644 app/components/Views/Settings/ExperimentalSettings/index.js create mode 100644 app/components/Views/Settings/ExperimentalSettings/index.test.js rename app/components/Views/{ => Settings}/GeneralSettings/__snapshots__/index.test.js.snap (91%) rename app/components/Views/{ => Settings}/GeneralSettings/index.js (69%) rename app/components/Views/{ => Settings}/GeneralSettings/index.test.js (100%) create mode 100644 app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.js.snap create mode 100644 app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js create mode 100644 app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.js create mode 100644 app/components/Views/Settings/NetworksSettings/__snapshots__/index.test.js.snap create mode 100644 app/components/Views/Settings/NetworksSettings/index.js create mode 100644 app/components/Views/Settings/NetworksSettings/index.test.js rename app/components/Views/{ => Settings}/SecuritySettings/__snapshots__/index.test.js.snap (87%) rename app/components/Views/{ => Settings}/SecuritySettings/index.js (85%) rename app/components/Views/{ => Settings}/SecuritySettings/index.test.js (100%) create mode 100644 app/components/Views/TermsAndConditions/__snapshots__/index.test.js.snap create mode 100644 app/components/Views/TermsAndConditions/index.js create mode 100644 app/components/Views/TermsAndConditions/index.test.js create mode 100644 app/components/Views/WalletConnectSessions/__snapshots__/index.test.js.snap create mode 100644 app/components/Views/WalletConnectSessions/index.js create mode 100644 app/components/Views/WalletConnectSessions/index.test.js create mode 100644 app/core/Analytics.js create mode 100644 app/core/WalletConnect.js create mode 100644 app/reducers/analytics/index.js create mode 100644 app/reducers/wizard/index.js create mode 100644 app/util/analytics.js create mode 100644 app/util/browserSripts.js create mode 100644 app/util/eip681-link-generator.js create mode 100644 app/util/general.js create mode 100644 app/util/ipfs-gateways.json delete mode 100644 ios/MetaMask/Images.xcassets/metamask-logo.imageset/Contents.json delete mode 100644 ios/MetaMask/Images.xcassets/metamask-logo.imageset/metamask-logo.png diff --git a/.circleci/config.yml b/.circleci/config.yml index b7b6b2a008b..00ea29dc36c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,7 +29,7 @@ jobs: prep-deps: <<: *defaults macos: - xcode: 10.1.0 + xcode: 10.2.0 steps: - checkout - restore_cache: *restore-cache @@ -81,7 +81,7 @@ jobs: test-e2e-ios: <<: *defaults macos: - xcode: 10.1.0 + xcode: 10.2.0 steps: - checkout - attach_workspace: @@ -144,7 +144,7 @@ jobs: command: npm run build:announce publish-pre-release-ios: macos: - xcode: 10.1.0 + xcode: 10.2.0 working_directory: ~/MetaMask environment: FL_OUTPUT_DIR: output diff --git a/android/app/build.gradle b/android/app/build.gradle index 1d2e4cb89b6..f2030dee4ae 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -174,8 +174,8 @@ android { applicationId "io.metamask" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 7 - versionName "0.1.6" + versionCode 8 + versionName "0.1.7" multiDexEnabled true testBuildType System.getProperty('testBuildType', 'debug') missingDimensionStrategy "minReactNative", "minReactNative46" @@ -210,7 +210,7 @@ android { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK - include "armeabi-v7a", "x86", "arm64-v8a", "x86-64" + include "armeabi-v7a", "x86", "arm64-v8a", "x86_64" } } buildTypes { @@ -228,12 +228,16 @@ android { it.buildConfigField 'String', 'foxCode', "\"$System.env.MM_FOX_CODE\"" } + packagingOptions { + pickFirst 'lib/x86_64/libjsc.so' + pickFirst 'lib/arm64-v8a/libjsc.so' + } // applicationVariants are e.g. debug, release applicationVariants.all { variant -> variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits - def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86-64": 4] + def versionCodes = ["armeabi-v7a":1, "x86":2, "arm64-v8a": 3, "x86_64": 4] def abi = output.getFilter(OutputFile.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = @@ -244,6 +248,8 @@ android { } dependencies { + implementation project(':react-native-view-shot') + implementation project(':lottie-react-native') implementation project(':@react-native-community_async-storage') implementation project(':react-native-push-notification') implementation project(':react-native-background-timer') diff --git a/android/app/src/main/java/io/metamask/MainApplication.java b/android/app/src/main/java/io/metamask/MainApplication.java index 359885c273c..19f7c6daf61 100644 --- a/android/app/src/main/java/io/metamask/MainApplication.java +++ b/android/app/src/main/java/io/metamask/MainApplication.java @@ -4,6 +4,8 @@ import com.crashlytics.android.Crashlytics; import com.facebook.react.ReactApplication; +import fr.greweb.reactnativeviewshot.RNViewShotPackage; +import com.airbnb.android.react.lottie.LottiePackage; import com.reactnativecommunity.asyncstorage.AsyncStoragePackage; import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage; import com.ocetnik.timer.BackgroundTimerPackage; @@ -48,6 +50,8 @@ public boolean getUseDeveloperSupport() { protected List getPackages() { return Arrays.asList( new MainReactPackage(), + new RNViewShotPackage(), + new LottiePackage(), new AsyncStoragePackage(), new ReactNativePushNotificationPackage(), new BackgroundTimerPackage(), diff --git a/android/settings.gradle b/android/settings.gradle index 7f0227d7265..0a21451e91a 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,8 @@ rootProject.name = 'MetaMask' +include ':react-native-view-shot' +project(':react-native-view-shot').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-view-shot/android') +include ':lottie-react-native' +project(':lottie-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/lottie-react-native/src/android') include ':@react-native-community_async-storage' project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android') include ':react-native-push-notification' diff --git a/app/actions/browser/index.js b/app/actions/browser/index.js index babf44d33ea..3e13c02e4c6 100644 --- a/app/actions/browser/index.js +++ b/app/actions/browser/index.js @@ -33,3 +33,63 @@ export function addToWhitelist(url) { url }; } + +/** + * Closes all the opened tabs + */ +export function closeAllTabs() { + return { + type: 'CLOSE_ALL_TABS' + }; +} + +/** + * Creates a new tab + * + * @param {string} url - The website's url + */ +export function createNewTab(url) { + return { + type: 'CREATE_NEW_TAB', + url, + id: Date.now() + }; +} + +/** + * Closes an exiting tab + * + * @param {number} id - The Tab ID + */ +export function closeTab(id) { + return { + type: 'CLOSE_TAB', + id + }; +} + +/** + * Selects an exiting tab + * + * @param {number} id - The Tab ID + */ +export function setActiveTab(id) { + return { + type: 'SET_ACTIVE_TAB', + id + }; +} + +/** + * Selects an exiting tab + * + * @param {number} id - The Tab ID + * @param {string} url - The website's url + */ +export function updateTab(id, data) { + return { + type: 'UPDATE_TAB', + id, + data + }; +} diff --git a/app/actions/modals/index.js b/app/actions/modals/index.js index fcda93f97f2..4c1f43b5ac3 100644 --- a/app/actions/modals/index.js +++ b/app/actions/modals/index.js @@ -17,8 +17,9 @@ export function toggleCollectibleContractModal() { }; } -export function toggleReceiveModal() { +export function toggleReceiveModal(asset) { return { - type: 'TOGGLE_RECEIVE_MODAL' + type: 'TOGGLE_RECEIVE_MODAL', + asset }; } diff --git a/app/actions/settings/index.js b/app/actions/settings/index.js index 4892e95b72d..e731a61b7e0 100644 --- a/app/actions/settings/index.js +++ b/app/actions/settings/index.js @@ -18,3 +18,10 @@ export function setLockTime(lockTime) { lockTime }; } + +export function setPrimaryCurrency(primaryCurrency) { + return { + type: 'SET_PRIMARY_CURRENCY', + primaryCurrency + }; +} diff --git a/app/actions/wizard/index.js b/app/actions/wizard/index.js new file mode 100644 index 00000000000..1040ce526e4 --- /dev/null +++ b/app/actions/wizard/index.js @@ -0,0 +1,9 @@ +/** + * Sets onboarding wizard step + */ +export default function setOnboardingWizardStep(step) { + return { + type: 'SET_ONBOARDING_WIZARD_STEP', + step + }; +} diff --git a/app/animations/bounce.json b/app/animations/bounce.json new file mode 100644 index 00000000000..e04c6ea807a --- /dev/null +++ b/app/animations/bounce.json @@ -0,0 +1,349 @@ +{ + "v": "5.5.1", + "fr": 30, + "ip": 0, + "op": 16, + "w": 1120, + "h": 930, + "nm": "Bounce", + "ddd": 0, + "assets": [ + { + "id": "comp_0", + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Romb_shadow", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [562.688, 850.992, 0], + "e": [562.688, 730.992, 0], + "to": [0, -20, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 12, + "s": [562.688, 730.992, 0], + "e": [562.688, 850.992, 0], + "to": [0, 0, 0], + "ti": [0, -20, 0] + }, + { "t": 22 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0.188, -367.008, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-52.392, -367.008], + [0.188, -271.228], + [-52.232, -367.008], + [0.188, -462.788] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [52.608, -367.008], + [0.188, -271.228], + [-52.232, -367.008], + [0.188, -462.788] + ], + "c": true + } + ] + }, + { + "i": { "x": 0.833, "y": 1 }, + "o": { "x": 0.167, "y": 0 }, + "t": 10, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [52.608, -367.008], + [0.188, -271.228], + [-52.232, -367.008], + [0.188, -462.788] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [52.608, -367.008], + [0.188, -271.228], + [51.768, -367.008], + [0.188, -462.788] + ], + "c": true + } + ] + }, + { "t": 12 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.745802696078, 0.358373754165, 0.146384325214, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 23, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Romb", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [562.688, 850.992, 0], + "e": [562.688, 730.992, 0], + "to": [0, -20, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 12, + "s": [562.688, 730.992, 0], + "e": [562.688, 850.992, 0], + "to": [0, 0, 0], + "ti": [0, -20, 0] + }, + { "t": 22 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [0.188, -367.008, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [52.608, -367.018], + [52.608, -367.008], + [52.598, -366.998], + [0.188, -271.228], + [-52.232, -367.008], + [0.188, -462.788] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "t": 27, + "s": [0], + "e": [2] + }, + { "t": 30 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 23, + "st": 0, + "bm": 0 + } + ] + } + ], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 0, + "nm": "Bounce_no_easing", + "refId": "comp_0", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [560, 733, 0], "ix": 2 }, + "a": { "a": 0, "k": [562.5, 1218, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "tm": { + "a": 1, + "k": [ + { "i": { "x": [0], "y": [1] }, "o": { "x": [0.064], "y": [0] }, "t": 0, "s": [0], "e": [0.4] }, + { "i": { "x": [1], "y": [1] }, "o": { "x": [1], "y": [0] }, "t": 8, "s": [0.4], "e": [0.733] }, + { + "i": { "x": [0.858], "y": [1] }, + "o": { "x": [0.158], "y": [0] }, + "t": 16, + "s": [0.733], + "e": [0.733] + }, + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.037], "y": [0] }, + "t": 102, + "s": [0.733], + "e": [10] + }, + { "t": 300 } + ], + "ix": 2 + }, + "w": 1125, + "h": 2436, + "ip": 0, + "op": 21, + "st": 0, + "bm": 0 + } + ], + "markers": [{ "tm": 95, "cm": "1", "dr": 0 }] +} diff --git a/app/animations/fox-in.json b/app/animations/fox-in.json new file mode 100644 index 00000000000..3b7cabc99cb --- /dev/null +++ b/app/animations/fox-in.json @@ -0,0 +1,13503 @@ +{ + "v": "5.5.1", + "fr": 30, + "ip": 0, + "op": 41, + "w": 1120, + "h": 930, + "nm": "Fox_simpler", + "ddd": 0, + "assets": [ + { + "id": "comp_0", + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Romb_shadow", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [562.688, 378.992, 0], "ix": 2 }, + "a": { "a": 0, "k": [0.188, -367.008, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-52.392, -367.008], + [0.188, -271.228], + [-52.232, -367.008], + [0.188, -462.788] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.745802696078, 0.358373754165, 0.146384325214, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 3, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Romb", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [562.688, 378.992, 0], "ix": 2 }, + "a": { "a": 0, "k": [0.188, -367.008, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [52.608, -367.018], + [52.608, -367.008], + [52.598, -366.998], + [0.188, -271.228], + [-52.232, -367.008], + [0.188, -462.788] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "t": 3.425, + "s": [0], + "e": [2] + }, + { "t": 5.4794921875 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "Layer 4", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [536.478, 426.882, 0], "ix": 2 }, + "a": { "a": 0, "k": [-26.022, -319.118, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": -31, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [0.188, -271.228], + [-52.042, -271.228], + [-52.232, -367.008] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [0.188, -271.228], + [-19.042, -304.228], + [-52.232, -367.008] + ], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [0.188, -271.228], + [-19.042, -304.228], + [-52.232, -367.008] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [0.188, -271.228], + [-52.042, -271.228], + [-52.232, -367.008] + ], + "c": true + } + ] + }, + { "t": 5.4794921875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 3.425, + "s": [0], + "e": [2] + }, + { "t": 5.4794921875 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 0, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 5.4794921875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 51, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "Layer 3", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [588.893, 426.887, 0], "ix": 2 }, + "a": { "a": 0, "k": [26.393, -319.113, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": -31, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [52.598, -366.998], + [52.418, -271.228], + [0.188, -271.228] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [52.598, -366.998], + [16.918, -300.228], + [0.188, -271.228] + ], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [52.598, -366.998], + [16.918, -300.228], + [0.188, -271.228] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [52.598, -366.998], + [52.418, -271.228], + [0.188, -271.228] + ], + "c": true + } + ] + }, + { "t": 5.4794921875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 3.425, + "s": [0], + "e": [2] + }, + { "t": 5.4794921875 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 0, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 5.4794921875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 0, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 51, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "Layer 2", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [588.988, 331.097, 0], "ix": 2 }, + "a": { "a": 0, "k": [26.488, -414.903, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": -31, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [52.788, -462.788], + [52.608, -367.018], + [0.188, -462.788] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [16.288, -433.788], + [52.608, -367.018], + [0.188, -462.788] + ], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [16.288, -433.788], + [52.608, -367.018], + [0.188, -462.788] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [52.788, -462.788], + [52.608, -367.018], + [0.188, -462.788] + ], + "c": true + } + ] + }, + { "t": 5.4794921875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 3.425, + "s": [0], + "e": [2] + }, + { "t": 5.4794921875 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 0, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 5.4794921875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 0, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 51, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "Layer 1", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [536.383, 331.102, 0], "ix": 2 }, + "a": { "a": 0, "k": [-26.117, -414.898, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": -31, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [0.188, -462.788], + [-52.232, -367.008], + [-52.423, -462.788] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [0.188, -462.788], + [-52.232, -367.008], + [-18.923, -429.788] + ], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 0, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [0.188, -462.788], + [-52.232, -367.008], + [-18.923, -429.788] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [0.188, -462.788], + [-52.232, -367.008], + [-52.423, -462.788] + ], + "c": true + } + ] + }, + { "t": 5.4794921875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 3.425, + "s": [0], + "e": [2] + }, + { "t": 5.4794921875 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 0, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 5.4794921875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 0, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 51, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 4, + "nm": "Layer 7", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [562.344, 212.991, 0], "ix": 2 }, + "a": { "a": 0, "k": [-0.156, -533.009, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 5.479, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-22.76, -461.802], + [-52.389, -463.215], + [52.078, -463.215], + [22.449, -461.802] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-52.76, -602.802], + [-52.389, -463.215], + [52.078, -463.215], + [52.449, -602.802] + ], + "c": true + } + ] + }, + { "t": 10.958984375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 8.904, + "s": [0], + "e": [2] + }, + { "t": 10.958984375 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 5.479, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 10.958984375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 5, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 8, + "ty": 4, + "nm": "Layer 6", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [562.344, 544.143, 0], "ix": 2 }, + "a": { "a": 0, "k": [-0.156, -201.857, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 5.479, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-52.76, -271.65], + [-22.389, -271.063], + [22.078, -271.063], + [52.449, -271.65] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-52.76, -271.65], + [-52.389, -132.063], + [52.078, -132.063], + [52.449, -271.65] + ], + "c": true + } + ] + }, + { "t": 10.958984375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 8.904, + "s": [0], + "e": [2] + }, + { "t": 10.958984375 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 5.479, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 10.958984375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 5, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 9, + "ty": 4, + "nm": "Layer 5", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [562.457, 683.715, 0], "ix": 2 }, + "a": { "a": 0, "k": [-0.043, -61.785, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 10.959, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-52.648, -131.579], + [-22.276, -131.992], + [22.19, -131.992], + [52.562, -131.579] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-52.648, -131.579], + [-52.276, 8.008], + [52.19, 8.008], + [52.562, -131.579] + ], + "c": true + } + ] + }, + { "t": 16.4384765625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784375668, 0.517647087574, 0.121568635106, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 14.384, + "s": [0], + "e": [2] + }, + { "t": 16.4384765625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 10.959, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 16.4384765625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 11, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 10, + "ty": 4, + "nm": "Layer 11", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [652.651, 219.477, 0], "ix": 2 }, + "a": { "a": 0, "k": [90.151, -526.523, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 10.959, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [53.516, -567.798], + [53.546, -467.248], + [52.786, -450.248], + [52.786, -602.798] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [127.516, -602.798], + [71.546, -450.248], + [52.786, -450.248], + [52.786, -602.798] + ], + "c": true + } + ] + }, + { "t": 16.4384765625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 14.384, + "s": [0], + "e": [2] + }, + { "t": 16.4384765625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 10.959, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 16.4384765625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 11, + "op": 51, + "st": -15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 11, + "ty": 4, + "nm": "Layer 9", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [472.196, 219.477, 0], "ix": 2 }, + "a": { "a": 0, "k": [-90.304, -526.523, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 10.959, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-53.464, -602.798], + [-53.464, -450.248], + [-53.174, -459.248], + [-54.144, -573.798] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [-53.464, -602.798], + [-53.464, -450.248], + [-71.174, -450.248], + [-127.144, -602.798] + ], + "c": true + } + ] + }, + { "t": 16.4384765625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 14.384, + "s": [0], + "e": [2] + }, + { "t": 16.4384765625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 10.959, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 16.4384765625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 11, + "op": 51, + "st": -15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 12, + "ty": 4, + "nm": "Layer 10", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [624.666, 440.302, 0], "ix": 2 }, + "a": { "a": 0, "k": [62.166, -305.698, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 10.959, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [53.546, -436.248], + [52.786, -161.148], + [52.786, -450.248] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [71.546, -450.248], + [52.786, -161.148], + [52.786, -450.248] + ], + "c": true + } + ] + }, + { "t": 16.4384765625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 14.384, + "s": [0], + "e": [2] + }, + { "t": 16.4384765625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 10.959, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 16.4384765625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 11, + "op": 51, + "st": -15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 13, + "ty": 4, + "nm": "Layer 8", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [500.181, 432.897, 0], "ix": 2 }, + "a": { "a": 0, "k": [-62.319, -313.103, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 10.959, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [-53.464, -450.248], + [-53.464, -175.958], + [-53.264, -419.778] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [-53.464, -450.248], + [-53.464, -175.958], + [-71.014, -449.778] + ], + "c": true + } + ] + }, + { "t": 16.4384765625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 14.384, + "s": [0], + "e": [2] + }, + { "t": 16.4384765625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 10.959, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 16.4384765625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 11, + "op": 51, + "st": -15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 14, + "ty": 4, + "nm": "Layer 52", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [498.271, 633.545, 0], "ix": 2 }, + "a": { "a": 0, "k": [1172.271, -509.955, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 16.438, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1167.438, -628.92], + [1157.4, -560.887], + [1162.233, -390.99], + [1161.141, -518.121] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1167.438, -628.92], + [1157.4, -560.887], + [1162.233, -390.99], + [1187.141, -519.621] + ], + "c": true + } + ] + }, + { "t": 21.91796875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 16.438, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1167.438, -628.92], + [1157.4, -560.887], + [1162.233, -390.99], + [1161.141, -518.121] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1167.438, -628.92], + [1157.4, -560.887], + [1162.233, -390.99], + [1187.141, -519.621] + ], + "c": true + } + ] + }, + { "t": 21.91796875 } + ], + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 2, + "ty": "sh", + "ix": 3, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 16.438, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1167.438, -628.92], + [1157.4, -560.887], + [1162.233, -390.99], + [1161.141, -518.121] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1167.438, -628.92], + [1157.4, -560.887], + [1162.233, -390.99], + [1187.141, -519.621] + ], + "c": true + } + ] + }, + { "t": 21.91796875 } + ], + "ix": 2 + }, + "nm": "Path 3", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.458823531866, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 19.863, + "s": [0], + "e": [2] + }, + { "t": 21.91796875 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 18.493, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.458823531866, 0.145098045468, 1] + }, + { "t": 21.91796875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 5, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 16, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 15, + "ty": 4, + "nm": "Layer 21", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [626.271, 633.045, 0], "ix": 2 }, + "a": { "a": 0, "k": [1172.271, -509.955, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 16.438, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1167.438, -628.92], + [1157.4, -560.887], + [1162.233, -390.99], + [1161.141, -518.121] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1167.438, -628.92], + [1157.4, -560.887], + [1162.233, -390.99], + [1187.141, -519.621] + ], + "c": true + } + ] + }, + { "t": 21.91796875 } + ], + "ix": 2 + }, + "nm": "Path 3", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.458823531866, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 19.863, + "s": [0], + "e": [2] + }, + { "t": 21.91796875 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 18.493, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.458823531866, 0.145098045468, 1] + }, + { "t": 21.91796875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 16, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 16, + "ty": 4, + "nm": "Layer 24", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [681.765, 571.215, 0], "ix": 2 }, + "a": { "a": 0, "k": [1895.265, -569.285, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 21.918, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1845.85, -537.24], + [1852.26, -514.97], + [1832.68, -623.6] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1957.85, -546.24], + [1852.26, -514.97], + [1832.68, -623.6] + ], + "c": true + } + ] + }, + { "t": 27.3974609375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.800000011921, 0.384313732386, 0.156862750649, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 25.343, + "s": [0], + "e": [2] + }, + { "t": 27.3974609375 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 23.973, + "s": [0.462745130062, 0.243137270212, 0.101960793138, 1], + "e": [0.800000011921, 0.384313732386, 0.156862750649, 1] + }, + { "t": 27.3974609375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 22, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 17, + "ty": 4, + "nm": "Layer 25", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [725.2, 550.595, 0], "ix": 2 }, + "a": { "a": 0, "k": [1938.7, -589.905, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1902.34, -578.57], + [1958.96, -546.57], + [1957.85, -546.24], + [1832.68, -623.6], + [1832.56, -624.27] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2044.84, -633.57], + [1958.96, -546.57], + [1957.85, -546.24], + [1832.68, -623.6], + [1832.56, -624.27] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.800000011921, 0.384313732386, 0.156862750649, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 29.452, + "s": [0.462745130062, 0.243137270212, 0.101960793138, 1], + "e": [0.800000011921, 0.384313732386, 0.156862750649, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 27, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 18, + "ty": 4, + "nm": "Layer 56", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [447.2, 571.215, 0], "ix": 2 }, + "a": { "a": 0, "k": [1895.265, -569.285, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 21.918, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1845.85, -537.24], + [1852.26, -514.97], + [1832.68, -623.6] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1957.85, -546.24], + [1852.26, -514.97], + [1832.68, -623.6] + ], + "c": true + } + ] + }, + { "t": 27.3974609375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.800000011921, 0.384313732386, 0.156862750649, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 25.343, + "s": [0], + "e": [2] + }, + { "t": 27.3974609375 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 23.973, + "s": [0.462745130062, 0.243137270212, 0.101960793138, 1], + "e": [0.800000011921, 0.384313732386, 0.156862750649, 1] + }, + { "t": 27.3974609375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 22, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 19, + "ty": 4, + "nm": "Layer 55", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [403.765, 550.595, 0], "ix": 2 }, + "a": { "a": 0, "k": [1938.7, -589.905, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1902.34, -578.57], + [1958.96, -546.57], + [1957.85, -546.24], + [1832.68, -623.6], + [1832.56, -624.27] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2044.84, -633.57], + [1958.96, -546.57], + [1957.85, -546.24], + [1832.68, -623.6], + [1832.56, -624.27] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.800000011921, 0.384313732386, 0.156862750649, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 29.452, + "s": [0.462745130062, 0.243137270212, 0.101960793138, 1], + "e": [0.800000011921, 0.384313732386, 0.156862750649, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 27, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 20, + "ty": 4, + "nm": "Layer 22", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [679.655, 695.176, 0], "ix": 2 }, + "a": { "a": 0, "k": [1769.155, -805.824, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 21.918, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1725.28, -848.794], + [1704.74, -747.914], + [1705.02, -747.684], + [1729.91, -876.234] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1833.28, -821.294], + [1723.24, -735.414], + [1705.02, -747.684], + [1729.91, -876.234] + ], + "c": true + } + ] + }, + { "t": 27.3974609375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 25.343, + "s": [0], + "e": [2] + }, + { "t": 27.3974609375 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 23.288, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 27.3974609375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 22, + "op": 51, + "st": -21, + "bm": 0 + }, + { + "ddd": 0, + "ind": 21, + "ty": 4, + "nm": "Layer 23", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [693.77, 636.261, 0], "ix": 2 }, + "a": { "a": 0, "k": [1783.27, -864.739, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1797.13, -839.414], + [1833.29, -821.564], + [1729.91, -876.234], + [1729.93, -876.314] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1836.63, -907.914], + [1833.29, -821.564], + [1729.91, -876.234], + [1729.93, -876.314] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 29.452, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 27, + "op": 51, + "st": -13, + "bm": 0 + }, + { + "ddd": 0, + "ind": 22, + "ty": 4, + "nm": "Layer 54", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [445.27, 694.176, 0], "ix": 2 }, + "a": { "a": 0, "k": [1769.155, -805.824, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 21.918, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1725.28, -848.794], + [1704.74, -747.914], + [1705.02, -747.684], + [1729.91, -876.234] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1833.28, -821.294], + [1723.24, -735.414], + [1705.02, -747.684], + [1729.91, -876.234] + ], + "c": true + } + ] + }, + { "t": 27.3974609375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 25.343, + "s": [0], + "e": [2] + }, + { "t": 27.3974609375 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 23.288, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 27.3974609375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 22, + "op": 51, + "st": -21, + "bm": 0 + }, + { + "ddd": 0, + "ind": 23, + "ty": 4, + "nm": "Layer 53", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [431.155, 635.261, 0], "ix": 2 }, + "a": { "a": 0, "k": [1783.27, -864.739, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1797.13, -839.414], + [1833.29, -821.564], + [1729.91, -876.234], + [1729.93, -876.314] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1836.63, -907.914], + [1833.29, -821.564], + [1729.91, -876.234], + [1729.93, -876.314] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 29.452, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 27, + "op": 51, + "st": -13, + "bm": 0 + }, + { + "ddd": 0, + "ind": 24, + "ty": 4, + "nm": "Layer 57", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [787.104, 594.593, 0], "ix": 2 }, + "a": { "a": 0, "k": [1335.104, -551.407, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1322.992, -582.1], + [1379.716, -638.214] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1328.492, -542.1], + [1379.716, -638.214] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.458823531866, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.458823531866, 0.145098045468, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 33, + "op": 51, + "st": -31, + "bm": 0 + }, + { + "ddd": 0, + "ind": 25, + "ty": 4, + "nm": "Layer 26", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [750.26, 638.09, 0], "ix": 2 }, + "a": { "a": 0, "k": [1298.26, -507.91, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1290.492, -464.6], + [1292.716, -530.214] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1290.492, -464.6], + [1328.216, -544.214] + ], + "c": true + } + ] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.333, "y": 0 }, + "t": 38.356, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1290.492, -464.6], + [1328.216, -544.214] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1290.492, -464.6], + [1329.216, -543.464] + ], + "c": true + } + ] + }, + { "t": 41.095703125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.458823531866, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.458823531866, 0.145098045468, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 33, + "op": 51, + "st": -31, + "bm": 0 + }, + { + "ddd": 0, + "ind": 26, + "ty": 4, + "nm": "Layer 59", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [340.76, 594.093, 0], "ix": 2 }, + "a": { "a": 0, "k": [1335.104, -551.407, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1322.992, -582.1], + [1379.716, -638.214] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1328.492, -542.1], + [1379.466, -637.714] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.458823531866, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.458823531866, 0.145098045468, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 33, + "op": 51, + "st": -31, + "bm": 0 + }, + { + "ddd": 0, + "ind": 27, + "ty": 4, + "nm": "Layer 58", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [377.604, 637.59, 0], "ix": 2 }, + "a": { "a": 0, "k": [1298.26, -507.91, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1290.492, -464.6], + [1292.716, -530.214] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1293.838, -551.221], + [1295.492, -466.1], + [1329.216, -543.464] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.458823531866, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.458823531866, 0.145098045468, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 33, + "op": 51, + "st": -31, + "bm": 0 + }, + { + "ddd": 0, + "ind": 28, + "ty": 4, + "nm": "Layer 14", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [678.577, 226.205, 0], "ix": 2 }, + "a": { "a": 0, "k": [2706.077, -799.795, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 15.754, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2693.522, -824.04], + [2647.632, -717.41], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2764.522, -804.04], + [2647.632, -717.41], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ] + }, + { "t": 21.232421875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.882352948189, 0.466666668653, 0.149019613862, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 19.178, + "s": [0], + "e": [2] + }, + { "t": 21.232421875 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 17.809, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.882352948189, 0.466666668653, 0.149019613862, 1] + }, + { "t": 21.232421875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 16, + "op": 51, + "st": -23, + "bm": 0 + }, + { + "ddd": 0, + "ind": 29, + "ty": 4, + "nm": "Layer 18", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [781.63, 171.483, 0], "ix": 2 }, + "a": { "a": 0, "k": [2809.13, -854.517, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 26.712, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2764.522, -804.04], + [2747.739, -827.993], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2764.522, -804.04], + [2900.739, -904.993], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ] + }, + { "t": 32.19140625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.882352948189, 0.466666668653, 0.149019613862, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.137, + "s": [0], + "e": [2] + }, + { "t": 32.19140625 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 28.768, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.882352948189, 0.466666668653, 0.149019613862, 1] + }, + { "t": 32.19140625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 27, + "op": 51, + "st": -7, + "bm": 0 + }, + { + "ddd": 0, + "ind": 30, + "ty": 4, + "nm": "Layer 19", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [781.63, 110.518, 0], "ix": 2 }, + "a": { "a": 0, "k": [2809.13, -915.482, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 30.137, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2874.704, -900.863], + [2900.739, -904.993], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2896.704, -948.863], + [2900.739, -904.993], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ] + }, + { "t": 35.6162109375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.882352948189, 0.466666668653, 0.149019613862, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 33.562, + "s": [0], + "e": [2] + }, + { "t": 35.6162109375 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 32.191, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.882352948189, 0.466666668653, 0.149019613862, 1] + }, + { "t": 35.6162109375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 31, + "op": 51, + "st": -2, + "bm": 0 + }, + { + "ddd": 0, + "ind": 31, + "ty": 4, + "nm": "Layer 20", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [932.778, 75.383, 0], "ix": 2 }, + "a": { "a": 0, "k": [2960.278, -950.617, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 40.411, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2896.704, -948.863], + [2900.739, -904.993], + [2898.852, -931.24] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2896.704, -948.863], + [2900.739, -904.993], + [3023.852, -996.24] + ], + "c": true + } + ] + }, + { "t": 45.890625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.882352948189, 0.466666668653, 0.149019613862, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 43.836, + "s": [0], + "e": [2] + }, + { "t": 45.890625 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 42.466, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.882352948189, 0.466666668653, 0.149019613862, 1] + }, + { "t": 45.890625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 41, + "op": 51, + "st": 13, + "bm": 0 + }, + { + "ddd": 0, + "ind": 32, + "ty": 4, + "nm": "Layer 51", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [448.731, 226.205, 0], "ix": 2 }, + "a": { "a": 0, "k": [2706.077, -799.795, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 15.754, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2693.522, -824.04], + [2647.632, -717.41], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2764.522, -804.04], + [2647.632, -717.41], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ] + }, + { "t": 21.232421875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.882352948189, 0.466666668653, 0.149019613862, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 19.178, + "s": [0], + "e": [2] + }, + { "t": 21.232421875 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 17.809, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.882352948189, 0.466666668653, 0.149019613862, 1] + }, + { "t": 21.232421875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 17, + "op": 51, + "st": -23, + "bm": 0 + }, + { + "ddd": 0, + "ind": 33, + "ty": 4, + "nm": "Layer 50", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [345.677, 171.483, 0], "ix": 2 }, + "a": { "a": 0, "k": [2809.13, -854.517, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 26.712, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2764.522, -804.04], + [2747.739, -827.993], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2764.522, -804.04], + [2900.739, -904.993], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ] + }, + { "t": 32.19140625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.882352948189, 0.466666668653, 0.149019613862, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.137, + "s": [0], + "e": [2] + }, + { "t": 32.19140625 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 28.768, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.882352948189, 0.466666668653, 0.149019613862, 1] + }, + { "t": 32.19140625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -7, + "bm": 0 + }, + { + "ddd": 0, + "ind": 34, + "ty": 4, + "nm": "Layer 49", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [345.677, 110.518, 0], "ix": 2 }, + "a": { "a": 0, "k": [2809.13, -915.482, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 30.822, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2874.704, -900.863], + [2900.739, -904.993], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2896.704, -948.863], + [2900.739, -904.993], + [2717.522, -882.1], + [2717.742, -882.18] + ], + "c": true + } + ] + }, + { "t": 36.3017578125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.882352948189, 0.466666668653, 0.149019613862, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.246, + "s": [0], + "e": [2] + }, + { "t": 36.3017578125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 32.877, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.882352948189, 0.466666668653, 0.149019613862, 1] + }, + { "t": 36.3017578125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 32, + "op": 51, + "st": -1, + "bm": 0 + }, + { + "ddd": 0, + "ind": 35, + "ty": 4, + "nm": "Layer 48", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [194.53, 75.383, 0], "ix": 2 }, + "a": { "a": 0, "k": [2960.278, -950.617, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 40.411, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2896.704, -948.863], + [2900.739, -904.993], + [2898.852, -931.24] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2896.704, -948.863], + [2900.739, -904.993], + [3023.852, -996.24] + ], + "c": true + } + ] + }, + { "t": 45.890625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.882352948189, 0.466666668653, 0.149019613862, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 43.836, + "s": [0], + "e": [2] + }, + { "t": 45.890625 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 42.466, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.882352948189, 0.466666668653, 0.149019613862, 1] + }, + { "t": 45.890625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 41, + "op": 51, + "st": 13, + "bm": 0 + }, + { + "ddd": 0, + "ind": 36, + "ty": 4, + "nm": "Layer 15", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [694.7, 401.08, 0], "ix": 2 }, + "a": { "a": 0, "k": [2031.7, -859.92, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 10.959, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1951.66, -975.22], + [1950.7, -843.91], + [1949.47, -741.62], + [1948.93, -843.91] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1960.66, -975.22], + [1957.7, -843.91], + [1955.47, -744.62], + [2107.93, -843.91] + ], + "c": true + } + ] + }, + { "t": 16.4384765625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 14.384, + "s": [0], + "e": [2] + }, + { "t": 16.4384765625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 10.959, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 16.4384765625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 12, + "op": 51, + "st": -35, + "bm": 0 + }, + { + "ddd": 0, + "ind": 37, + "ty": 4, + "nm": "Layer 12", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [725.595, 466.87, 0], "ix": 2 }, + "a": { "a": 0, "k": [2061.595, -794.13, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 16.438, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2086.23, -828.65], + [1955.46, -744.35], + [1955.47, -744.62], + [2107.93, -843.91] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2167.73, -753.65], + [1955.46, -744.35], + [1955.47, -744.62], + [2107.93, -843.91] + ], + "c": true + } + ] + }, + { "t": 21.91796875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 19.863, + "s": [0], + "e": [2] + }, + { "t": 21.91796875 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 16.438, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 21.91796875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 17, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 38, + "ty": 4, + "nm": "Layer 17", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [435.595, 401.08, 0], "ix": 2 }, + "a": { "a": 0, "k": [2031.7, -859.92, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 10.959, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1960.66, -975.22], + [1957.7, -843.91], + [1955.47, -744.62], + [1957.93, -843.91] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1960.66, -975.22], + [1957.7, -843.91], + [1955.47, -744.62], + [2107.93, -843.91] + ], + "c": true + } + ] + }, + { "t": 16.4384765625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 14.384, + "s": [0], + "e": [2] + }, + { "t": 16.4384765625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 10.959, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 16.4384765625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 12, + "op": 51, + "st": -35, + "bm": 0 + }, + { + "ddd": 0, + "ind": 39, + "ty": 4, + "nm": "Layer 16", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [404.7, 466.87, 0], "ix": 2 }, + "a": { "a": 0, "k": [2061.595, -794.13, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 16.438, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2086.23, -828.65], + [1955.46, -744.35], + [1955.47, -744.62], + [2107.93, -843.91] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2167.73, -753.65], + [1955.46, -744.35], + [1955.47, -744.62], + [2107.93, -843.91] + ], + "c": true + } + ] + }, + { "t": 21.91796875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 19.863, + "s": [0], + "e": [2] + }, + { "t": 21.91796875 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 16.438, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 21.91796875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 17, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 40, + "ty": 4, + "nm": "Layer 63", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [965.2, 638.083, 0], "ix": 2 }, + "a": { "a": 0, "k": [2536.7, -353.917, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 44.521, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2468.06, -313.152], + [2579.177, -394.682], + [2529.34, -357.85] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2468.06, -313.152], + [2579.177, -394.682], + [2607.09, -312.1] + ], + "c": true + } + ] + }, + { "t": 50 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.945, + "s": [0], + "e": [2] + }, + { "t": 50 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 50 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": 3, + "bm": 0 + }, + { + "ddd": 0, + "ind": 41, + "ty": 4, + "nm": "Layer 64", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [818.722, 592.831, 0], "ix": 2 }, + "a": { "a": 0, "k": [2390.222, -399.169, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 38.356, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2346.06, -379.152], + [2312.384, -312.361], + [2401.608, -485.976] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2468.06, -313.152], + [2312.384, -312.361], + [2401.608, -485.976] + ], + "c": true + } + ] + }, + { "t": 44.5205078125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.781, + "s": [0], + "e": [2] + }, + { "t": 44.5205078125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.096, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 44.5205078125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 39, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 42, + "ty": 4, + "nm": "Layer 62", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [918.694, 592.434, 0], "ix": 2 }, + "a": { "a": 0, "k": [2490.194, -399.566, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 38.356, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2513.06, -428.152], + [2579.177, -394.682], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2468.06, -313.152], + [2579.177, -394.682], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ] + }, + { "t": 43.8359375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.781, + "s": [0], + "e": [2] + }, + { "t": 43.8359375 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.096, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 43.8359375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 39, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 43, + "ty": 4, + "nm": "Layer 61", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [918.694, 535.499, 0], "ix": 2 }, + "a": { "a": 0, "k": [2490.194, -456.501, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2539.16, -518.32], + [2482.177, -506.682], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2539.16, -518.32], + [2579.177, -394.682], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 44, + "ty": 4, + "nm": "Layer 60", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [868.955, 461.24, 0], "ix": 2 }, + "a": { "a": 0, "k": [2440.455, -530.76, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2381.16, -517.32], + [2341.75, -576.32], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2539.16, -518.32], + [2341.75, -576.32], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [2], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 29.452, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 45, + "ty": 4, + "nm": "Layer 69", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [162.906, 638.083, 0], "ix": 2 }, + "a": { "a": 0, "k": [2536.7, -353.917, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 44.521, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2468.06, -313.152], + [2579.177, -394.682], + [2529.34, -357.85] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2468.06, -313.152], + [2579.177, -394.682], + [2605.34, -313.85] + ], + "c": true + } + ] + }, + { "t": 50 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.945, + "s": [0], + "e": [2] + }, + { "t": 50 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 50 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": 3, + "bm": 0 + }, + { + "ddd": 0, + "ind": 46, + "ty": 4, + "nm": "Layer 68", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [309.384, 592.831, 0], "ix": 2 }, + "a": { "a": 0, "k": [2390.222, -399.169, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 38.356, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2346.06, -379.152], + [2312.384, -312.361], + [2401.608, -485.976] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2468.06, -313.152], + [2312.384, -312.361], + [2401.608, -485.976] + ], + "c": true + } + ] + }, + { "t": 44.5205078125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.781, + "s": [0], + "e": [2] + }, + { "t": 44.5205078125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.096, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 44.5205078125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 39, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 47, + "ty": 4, + "nm": "Layer 67", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [209.412, 592.434, 0], "ix": 2 }, + "a": { "a": 0, "k": [2490.194, -399.566, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 38.356, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2513.06, -428.152], + [2579.177, -394.682], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2468.06, -313.152], + [2579.177, -394.682], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ] + }, + { "t": 43.8359375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.781, + "s": [0], + "e": [2] + }, + { "t": 43.8359375 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.096, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 43.8359375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 39, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 48, + "ty": 4, + "nm": "Layer 66", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [209.412, 535.499, 0], "ix": 2 }, + "a": { "a": 0, "k": [2490.194, -456.501, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2539.16, -518.32], + [2482.177, -506.682], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2539.16, -518.32], + [2579.177, -394.682], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 49, + "ty": 4, + "nm": "Layer 65", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [259.151, 461.24, 0], "ix": 2 }, + "a": { "a": 0, "k": [2440.455, -530.76, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2381.16, -517.32], + [2341.75, -576.32], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2539.16, -518.32], + [2341.75, -576.32], + [2401.61, -485.98], + [2401.21, -485.2] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.960784316063, 0.517647087574, 0.121568627656, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [2], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 29.452, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.960784316063, 0.517647087574, 0.121568627656, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 50, + "ty": 4, + "nm": "Layer 71", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [427.893, 755.068, 0], "ix": 2 }, + "a": { "a": 0, "k": [1244.393, -390.432, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1180.45, -378.722], + [1231.837, -419.265], + [1290.492, -464.6] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1180.45, -378.722], + [1308.337, -316.265], + [1290.492, -464.6] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 28.082, + "s": [0.462745130062, 0.243137270212, 0.101960793138, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 51, + "ty": 4, + "nm": "Layer 70", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [697.893, 755.568, 0], "ix": 2 }, + "a": { "a": 0, "k": [1244.393, -390.432, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1180.45, -378.722], + [1231.837, -419.265], + [1290.492, -464.6] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1180.45, -378.722], + [1308.337, -316.265], + [1290.492, -464.6] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 28.082, + "s": [0.462745130062, 0.243137270212, 0.101960793138, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 52, + "ty": 4, + "nm": "Layer 72", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [800.545, 754.151, 0], "ix": 2 }, + "a": { "a": 0, "k": [1349.045, -392.849, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 31.507, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1289.749, -469.432], + [1308.337, -316.265], + [1294.842, -424.087] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1289.749, -469.432], + [1308.337, -316.265], + [1408.342, -466.087] + ], + "c": true + } + ] + }, + { "t": 36.986328125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.800000011921, 0.384313732386, 0.156862750649, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0], + "e": [2] + }, + { "t": 36.986328125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 32.877, + "s": [0.462745130062, 0.243137270212, 0.101960793138, 1], + "e": [0.800000011921, 0.384313732386, 0.156862750649, 1] + }, + { "t": 36.986328125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 32, + "op": 51, + "st": -24, + "bm": 0 + }, + { + "ddd": 0, + "ind": 53, + "ty": 4, + "nm": "Layer 73", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [325.545, 754.151, 0], "ix": 2 }, + "a": { "a": 0, "k": [1349.045, -392.849, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 31.507, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1289.749, -469.432], + [1308.337, -316.265], + [1294.842, -424.087] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1289.749, -469.432], + [1308.337, -316.265], + [1408.342, -466.087] + ], + "c": true + } + ] + }, + { "t": 36.986328125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.800000011921, 0.384313732386, 0.156862750649, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0], + "e": [2] + }, + { "t": 36.986328125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 32.877, + "s": [0.462745130062, 0.243137270212, 0.101960793138, 1], + "e": [0.800000011921, 0.384313732386, 0.156862750649, 1] + }, + { "t": 36.986328125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 32, + "op": 51, + "st": -24, + "bm": 0 + }, + { + "ddd": 0, + "ind": 54, + "ty": 4, + "nm": "Layer 83", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [561.997, 798.342, 0], "ix": 2 }, + "a": { "a": 0, "k": [1306.997, -113.158, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 16.438, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1359.23, -156.84], + [1254.764, -156.84], + [1308.223, -156.975] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1359.23, -156.84], + [1254.764, -156.84], + [1308.223, -69.475] + ], + "c": true + } + ] + }, + { "t": 21.91796875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 19.863, + "s": [2], + "e": [2] + }, + { "t": 21.91796875 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 17, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 55, + "ty": 4, + "nm": "Layer 86", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [527.014, 798.342, 0], "ix": 2 }, + "a": { "a": 0, "k": [1272.014, -113.158, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 21.918, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1283.304, -109.475], + [1254.764, -156.84], + [1308.223, -69.475] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1235.804, -69.475], + [1254.764, -156.84], + [1308.223, -69.475] + ], + "c": true + } + ] + }, + { "t": 27.3974609375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 25.343, + "s": [0], + "e": [2] + }, + { "t": 27.3974609375 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 23, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 56, + "ty": 4, + "nm": "Layer 84", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [598.207, 798.342, 0], "ix": 2 }, + "a": { "a": 0, "k": [1343.207, -113.158, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 21.918, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1359.23, -156.84], + [1332.19, -109.975], + [1308.223, -69.475] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1359.23, -156.84], + [1378.19, -69.475], + [1308.223, -69.475] + ], + "c": true + } + ] + }, + { "t": 27.3974609375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 25.343, + "s": [0], + "e": [2] + }, + { "t": 27.3974609375 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 23, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 57, + "ty": 4, + "nm": "Layer 87", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [495.823, 802.617, 0], "ix": 2 }, + "a": { "a": 0, "k": [1240.823, -108.883, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1235.804, -69.475], + [1254.764, -156.84], + [1255.048, -155.572], + [1236.382, -68.925] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1235.804, -69.475], + [1254.764, -156.84], + [1236.548, -144.572], + [1226.882, -60.925] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 58, + "ty": 4, + "nm": "Layer 85", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [628.358, 802.617, 0], "ix": 2 }, + "a": { "a": 0, "k": [1373.358, -108.883, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1359.23, -156.84], + [1378.19, -69.475], + [1377.985, -69.925], + [1359.447, -154.072] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1359.23, -156.84], + [1378.19, -69.475], + [1387.485, -60.925], + [1377.447, -144.572] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.086274512112, 0.086274512112, 0.086274512112, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 59, + "ty": 4, + "nm": "Layer 77", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [948.729, 729.739, 0], "ix": 2 }, + "a": { "a": 0, "k": [1716.729, 882.739, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 44.521, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1711.279, 890.095], + [1774.861, 934.729], + [1629.178, 830.75] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1804.279, 834.095], + [1774.861, 934.729], + [1629.178, 830.75] + ], + "c": true + } + ] + }, + { "t": 50 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.945, + "s": [0], + "e": [2] + }, + { "t": 50 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.261, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 50 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": -15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 60, + "ty": 4, + "nm": "Layer 76", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [932.723, 835.693, 0], "ix": 2 }, + "a": { "a": 0, "k": [1700.723, 988.693, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 44.521, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1774.861, 934.729], + [1705.81, 969.656] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1774.861, 934.729], + [1743.31, 1042.656] + ], + "c": true + } + ] + }, + { "t": 50 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.945, + "s": [0], + "e": [2] + }, + { "t": 50 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.261, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 50 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": -15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 61, + "ty": 4, + "nm": "Layer 75", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [932.723, 767.693, 0], "ix": 2 }, + "a": { "a": 0, "k": [1700.723, 920.693, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 40.411, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1626.861, 931.729], + [1629.178, 830.75] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1774.861, 934.729], + [1629.178, 830.75] + ], + "c": true + } + ] + }, + { "t": 45.890625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 43.836, + "s": [0], + "e": [2] + }, + { "t": 45.890625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.781, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 45.890625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 41, + "op": 51, + "st": -21, + "bm": 0 + }, + { + "ddd": 0, + "ind": 62, + "ty": 4, + "nm": "Layer 74", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [811.175, 767.693, 0], "ix": 2 }, + "a": { "a": 0, "k": [1579.175, 920.693, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 35.616, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1562.084, 931.138], + [1528.173, 983.167], + [1628.178, 834] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1529.173, 983.917], + [1628.178, 834] + ], + "c": true + } + ] + }, + { "t": 41.095703125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 39.041, + "s": [0], + "e": [2] + }, + { "t": 41.095703125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 35.616, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 41.095703125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 37, + "op": 51, + "st": -28, + "bm": 0 + }, + { + "ddd": 0, + "ind": 63, + "ty": 4, + "nm": "Layer 81", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [181.946, 727.739, 0], "ix": 2 }, + "a": { "a": 0, "k": [1716.729, 882.739, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 44.521, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1711.279, 890.095], + [1774.861, 934.729], + [1629.178, 830.75] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1804.279, 834.095], + [1774.861, 934.729], + [1629.178, 830.75] + ], + "c": true + } + ] + }, + { "t": 50 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.945, + "s": [0], + "e": [2] + }, + { "t": 50 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.261, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 50 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": -15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 64, + "ty": 4, + "nm": "Layer 80", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [197.952, 833.693, 0], "ix": 2 }, + "a": { "a": 0, "k": [1700.723, 988.693, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 44.521, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1774.861, 934.729], + [1705.81, 969.656] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1774.861, 934.729], + [1743.31, 1042.656] + ], + "c": true + } + ] + }, + { "t": 50 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.945, + "s": [0], + "e": [2] + }, + { "t": 50 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.261, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 50 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": -15, + "bm": 0 + }, + { + "ddd": 0, + "ind": 65, + "ty": 4, + "nm": "Layer 79", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [197.952, 765.693, 0], "ix": 2 }, + "a": { "a": 0, "k": [1700.723, 920.693, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 40.411, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1717.048, 933.557], + [1629.49, 834.312] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1745.954, 934.143], + [1629.334, 835.031] + ], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.15, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1745.954, 934.143], + [1629.334, 835.031] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1774.861, 934.729], + [1629.178, 830.75] + ], + "c": true + } + ] + }, + { "t": 45.890625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 43.836, + "s": [0], + "e": [2] + }, + { "t": 45.890625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.781, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 45.890625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 41, + "op": 51, + "st": -21, + "bm": 0 + }, + { + "ddd": 0, + "ind": 66, + "ty": 4, + "nm": "Layer 78", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [319.499, 765.693, 0], "ix": 2 }, + "a": { "a": 0, "k": [1579.175, 920.693, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 35.616, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1562.084, 931.138], + [1528.173, 983.167], + [1628.178, 834] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [1626.584, 1010.638], + [1529.173, 983.917], + [1628.178, 834] + ], + "c": true + } + ] + }, + { "t": 41.095703125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.886274516582, 0.46274510026, 0.145098045468, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 39.041, + "s": [0], + "e": [2] + }, + { "t": 41.095703125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 35.616, + "s": [0.800000071526, 0.384313762188, 0.156862750649, 1], + "e": [0.886274516582, 0.46274510026, 0.145098045468, 1] + }, + { "t": 41.095703125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 37, + "op": 51, + "st": -28, + "bm": 0 + }, + { + "ddd": 0, + "ind": 67, + "ty": 4, + "nm": "Layer 13", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [702.064, 306.531, 0], "ix": 2 }, + "a": { "a": 0, "k": [2331.564, 112.531, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 21.918, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2300.444, 137.781], + [2400.944, 222.781], + [2262.184, 105.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2400.944, 222.781], + [2262.184, 105.111] + ], + "c": true + } + ] + }, + { "t": 27.3974609375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 25.343, + "s": [0], + "e": [2] + }, + { "t": 27.3974609375 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 23.973, + "s": [0.327573537827, 0.153352424502, 0.041353143752, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 27.3974609375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 23, + "op": 51, + "st": -37, + "bm": 0 + }, + { + "ddd": 0, + "ind": 68, + "ty": 4, + "nm": "Layer 27", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [702.064, 306.531, 0], "ix": 2 }, + "a": { "a": 0, "k": [2331.564, 112.531, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2400.944, 222.781], + [2402.184, 97.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2400.944, 222.781], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 29.452, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -29, + "bm": 0 + }, + { + "ddd": 0, + "ind": 69, + "ty": 4, + "nm": "Layer 28", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [842.064, 193.446, 0], "ix": 2 }, + "a": { "a": 0, "k": [2471.564, -0.554, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2435.944, 26.781], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2540.944, -104.219], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -21, + "bm": 0 + }, + { + "ddd": 0, + "ind": 70, + "ty": 4, + "nm": "Layer 29", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [844.064, 377.446, 0], "ix": 2 }, + "a": { "a": 0, "k": [2473.564, 183.446, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2401.444, 220.531], + [2474.944, 162.781], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.444, 221.781], + [2542.944, 263.781], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -21, + "bm": 0 + }, + { + "ddd": 0, + "ind": 71, + "ty": 4, + "nm": "Layer 30", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [962.944, 386.02, 0], "ix": 2 }, + "a": { "a": 0, "k": [2592.444, 192.02, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 37.671, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2543.944, 143.781], + [2541.908, 235.429], + [2542.944, 264.281], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2643.944, 132.281], + [2598.408, 280.929], + [2542.944, 264.281], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 43.150390625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.096, + "s": [0], + "e": [2] + }, + { "t": 43.150390625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 40.411, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 43.150390625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 38, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 72, + "ty": 4, + "nm": "Layer 31", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [977.944, 193.696, 0], "ix": 2 }, + "a": { "a": 0, "k": [2607.444, -0.304, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 37.671, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2542.944, -4.219], + [2540.944, -104.219], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2673.726, -20.594], + [2541.944, -103.719], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 43.150390625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.096, + "s": [0], + "e": [2] + }, + { "t": 43.150390625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 39.726, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 43.150390625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 39, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 73, + "ty": 4, + "nm": "Layer 32", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [977.944, 193.696, 0], "ix": 2 }, + "a": { "a": 0, "k": [2607.444, -0.304, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.15, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2674.944, -20.719], + [2540.944, -104.719], + [2593.184, -71.389] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2673.694, -21.719], + [2541.194, -104.594], + [2627.184, -164.889] + ], + "c": true + } + ] + }, + { "t": 48.6298828125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0], + "e": [2] + }, + { "t": 48.6298828125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 45.205, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 48.6298828125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 44, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 74, + "ty": 4, + "nm": "Layer 33", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [977.944, 193.696, 0], "ix": 2 }, + "a": { "a": 0, "k": [2607.444, -0.304, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.15, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2674.319, -18.969], + [2596.944, 54.031], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2674.319, -18.969], + [2643.944, 131.031], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 48.6298828125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0], + "e": [2] + }, + { "t": 48.6298828125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 45.205, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 48.6298828125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 44, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 75, + "ty": 4, + "nm": "Layer 34", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [962.944, 386.02, 0], "ix": 2 }, + "a": { "a": 0, "k": [2592.444, 192.02, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.15, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2618.444, 213.781], + [2600.908, 278.679], + [2613.934, 230.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2618.444, 213.781], + [2600.908, 278.679], + [2642.934, 230.111] + ], + "c": true + } + ] + }, + { "t": 48.6298828125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0], + "e": [2] + }, + { "t": 48.6298828125 } + ], + "ix": 5 + }, + "lc": 3, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 45.891, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 48.6298828125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 44, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 76, + "ty": 4, + "nm": "Layer 35", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [997.421, 395.23, 0], "ix": 2 }, + "a": { "a": 0, "k": [2620.921, 246.23, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 44.521, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2624.694, 213.781], + [2609.408, 268.429], + [2619.184, 233.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2624.694, 213.781], + [2609.408, 268.429], + [2649.434, 233.861] + ], + "c": true + } + ] + }, + { "t": 50 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.945, + "s": [0], + "e": [2] + }, + { "t": 50 } + ], + "ix": 5 + }, + "lc": 3, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.261, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 50 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": -4, + "bm": 0 + }, + { + "ddd": 0, + "ind": 77, + "ty": 4, + "nm": "Layer 36", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [1004.421, 354.48, 0], "ix": 2 }, + "a": { "a": 0, "k": [2620.921, 246.23, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 45.891, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2624.694, 213.781], + [2609.408, 268.429], + [2619.184, 233.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2629.194, 218.031], + [2617.158, 257.179], + [2649.184, 232.861] + ], + "c": true + } + ] + }, + { "t": 75 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 49.315, + "s": [0], + "e": [2] + }, + { "t": 75 } + ], + "ix": 5 + }, + "lc": 3, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 48.63, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 75 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 47, + "op": 51, + "st": -2, + "bm": 0 + }, + { + "ddd": 0, + "ind": 78, + "ty": 4, + "nm": "Layer 47", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [425.358, 306.531, 0], "ix": 2 }, + "a": { "a": 0, "k": [2331.564, 112.531, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 21.918, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2300.444, 137.781], + [2400.944, 222.781], + [2262.184, 105.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2400.944, 222.781], + [2262.184, 105.111] + ], + "c": true + } + ] + }, + { "t": 27.3974609375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 25.343, + "s": [0], + "e": [2] + }, + { "t": 27.3974609375 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 23.973, + "s": [0.327573537827, 0.153352424502, 0.041353143752, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 27.3974609375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 23, + "op": 51, + "st": -37, + "bm": 0 + }, + { + "ddd": 0, + "ind": 79, + "ty": 4, + "nm": "Layer 46", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [425.358, 306.531, 0], "ix": 2 }, + "a": { "a": 0, "k": [2331.564, 112.531, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 27.397, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2400.944, 222.781], + [2402.184, 97.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2400.944, 222.781], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 32.876953125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 30.822, + "s": [0], + "e": [2] + }, + { "t": 32.876953125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 29.452, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 32.876953125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 28, + "op": 51, + "st": -29, + "bm": 0 + }, + { + "ddd": 0, + "ind": 80, + "ty": 4, + "nm": "Layer 45", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [285.358, 193.446, 0], "ix": 2 }, + "a": { "a": 0, "k": [2471.564, -0.554, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2435.944, 26.781], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.944, 2.281], + [2540.944, -104.219], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -21, + "bm": 0 + }, + { + "ddd": 0, + "ind": 81, + "ty": 4, + "nm": "Layer 44", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [283.358, 377.446, 0], "ix": 2 }, + "a": { "a": 0, "k": [2473.564, 183.446, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2401.444, 220.531], + [2474.944, 162.781], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2400.444, 221.781], + [2542.944, 263.781], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -21, + "bm": 0 + }, + { + "ddd": 0, + "ind": 82, + "ty": 4, + "nm": "Layer 43", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [164.478, 386.02, 0], "ix": 2 }, + "a": { "a": 0, "k": [2592.444, 192.02, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 37.671, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2543.944, 143.781], + [2541.908, 235.429], + [2542.944, 264.281], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [2643.944, 132.281], + [2598.408, 280.929], + [2542.944, 264.281], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 43.150390625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.096, + "s": [0], + "e": [2] + }, + { "t": 43.150390625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 40.411, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 43.150390625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 38, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 83, + "ty": 4, + "nm": "Layer 42", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [149.478, 193.696, 0], "ix": 2 }, + "a": { "a": 0, "k": [2607.444, -0.304, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 37.671, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2542.944, -4.219], + [2540.944, -104.219], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2673.726, -20.594], + [2541.944, -103.719], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 43.150390625 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.096, + "s": [0], + "e": [2] + }, + { "t": 43.150390625 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 39.726, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 43.150390625 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 38, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 84, + "ty": 4, + "nm": "Layer 41", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [149.478, 193.696, 0], "ix": 2 }, + "a": { "a": 0, "k": [2607.444, -0.304, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.15, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2674.944, -20.719], + [2540.944, -104.719], + [2593.184, -71.389] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2673.694, -21.719], + [2541.194, -104.594], + [2627.184, -164.889] + ], + "c": true + } + ] + }, + { "t": 48.6298828125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0], + "e": [2] + }, + { "t": 48.6298828125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 45.205, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 48.6298828125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 44, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 85, + "ty": 4, + "nm": "Layer 40", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [149.478, 193.696, 0], "ix": 2 }, + "a": { "a": 0, "k": [2607.444, -0.304, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.15, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2674.319, -18.969], + [2596.944, 54.031], + [2544.184, 103.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2674.319, -18.969], + [2643.944, 131.031], + [2544.184, 103.111] + ], + "c": true + } + ] + }, + { "t": 48.6298828125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0], + "e": [2] + }, + { "t": 48.6298828125 } + ], + "ix": 5 + }, + "lc": 1, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 45.205, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 48.6298828125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 44, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 86, + "ty": 4, + "nm": "Layer 39", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [164.478, 386.02, 0], "ix": 2 }, + "a": { "a": 0, "k": [2592.444, 192.02, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.15, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2618.444, 213.781], + [2600.908, 278.679], + [2613.934, 230.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2618.444, 213.781], + [2600.908, 278.679], + [2642.934, 230.111] + ], + "c": true + } + ] + }, + { "t": 48.6298828125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0], + "e": [2] + }, + { "t": 48.6298828125 } + ], + "ix": 5 + }, + "lc": 3, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 45.891, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 48.6298828125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 44, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 87, + "ty": 4, + "nm": "Layer 38", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [130.001, 395.23, 0], "ix": 2 }, + "a": { "a": 0, "k": [2620.921, 246.23, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 44.521, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2624.694, 213.781], + [2609.408, 268.429], + [2619.184, 233.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2624.694, 213.781], + [2609.408, 268.429], + [2649.434, 233.861] + ], + "c": true + } + ] + }, + { "t": 50 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.945, + "s": [0], + "e": [2] + }, + { "t": 50 } + ], + "ix": 5 + }, + "lc": 3, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.261, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 50 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": -4, + "bm": 0 + }, + { + "ddd": 0, + "ind": 88, + "ty": 4, + "nm": "Layer 37", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [123.001, 354.48, 0], "ix": 2 }, + "a": { "a": 0, "k": [2620.921, 246.23, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 45.891, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2624.694, 213.781], + [2609.408, 268.429], + [2619.184, 233.111] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [ + [2629.194, 218.031], + [2617.158, 257.179], + [2649.184, 232.861] + ], + "c": true + } + ] + }, + { "t": 75 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.462745127958, 0.243137269862, 0.101960791794, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 49.315, + "s": [0], + "e": [2] + }, + { "t": 75 } + ], + "ix": 5 + }, + "lc": 3, + "lj": 1, + "ml": 10, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 48.63, + "s": [0.329411774874, 0.152941182256, 0.043137256056, 1], + "e": [0.46274510026, 0.243137255311, 0.101960785687, 1] + }, + { "t": 75 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [-1, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 47, + "op": 51, + "st": -2, + "bm": 0 + }, + { + "ddd": 0, + "ind": 89, + "ty": 4, + "nm": "Layer 91", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [561.885, 880.721, 0], "ix": 2 }, + "a": { "a": 0, "k": [1271.385, 58.721, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1344.571, 22.438], + [1351.687, 27.492], + [1342.393, 18.942], + [1200.006, 18.942], + [1193.084, 20.492], + [1333.508, 19.5] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1350.571, 62.438], + [1351.687, 27.492], + [1342.393, 18.942], + [1200.006, 18.942], + [1191.084, 27.492], + [1343.508, 98.5] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.752941191196, 0.674509823322, 0.615686297417, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.610585153103, 0.520044922829, 0.452139794827, 1], + "e": [0.752941191196, 0.674509823322, 0.615686297417, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 90, + "ty": 4, + "nm": "Layer 92", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [557.796, 880.721, 0], "ix": 2 }, + "a": { "a": 0, "k": [1267.296, 58.721, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 38.356, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1200.006, 18.942], + [1191.084, 27.492], + [1194.827, 23.938], + [1193.891, 24], + [1338.508, 24] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1200.006, 18.942], + [1191.084, 27.492], + [1191.827, 62.438], + [1198.891, 98.5], + [1343.508, 98.5] + ], + "c": true + } + ] + }, + { "t": 43.8359375 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.752941191196, 0.674509823322, 0.615686297417, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 41.781, + "s": [0], + "e": [2] + }, + { "t": 43.8359375 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 40.411, + "s": [0.611764729023, 0.521568655968, 0.450980395079, 1], + "e": [0.752941191196, 0.674509823322, 0.615686297417, 1] + }, + { "t": 43.8359375 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 39, + "op": 51, + "st": -14, + "bm": 0 + }, + { + "ddd": 0, + "ind": 91, + "ty": 4, + "nm": "Layer 90", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [426.377, 874.401, 0], "ix": 2 }, + "a": { "a": 0, "k": [1135.877, 52.401, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.836, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1072.862, 6.302], [1153.891, 44.5], [1191.827, 62.438]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1072.862, 6.302], [1198.891, 98.5], [1191.827, 62.438]], + "c": true + } + ] + }, + { "t": 49.3154296875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.752941191196, 0.674509823322, 0.615686297417, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.261, + "s": [0], + "e": [2] + }, + { "t": 49.3154296875 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0.611764729023, 0.521568655968, 0.450980395079, 1], + "e": [0.752941191196, 0.674509823322, 0.615686297417, 1] + }, + { "t": 49.3154296875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 92, + "ty": 4, + "nm": "Layer 93", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [697.877, 874.901, 0], "ix": 2 }, + "a": { "a": 0, "k": [1135.877, 52.401, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 43.836, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1072.862, 6.302], [1153.891, 44.5], [1191.827, 62.438]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1072.862, 6.302], [1198.891, 98.5], [1191.827, 62.438]], + "c": true + } + ] + }, + { "t": 49.3154296875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.752941191196, 0.674509823322, 0.615686297417, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 47.261, + "s": [0], + "e": [2] + }, + { "t": 49.3154296875 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 46.575, + "s": [0.611764729023, 0.521568655968, 0.450980395079, 1], + "e": [0.752941191196, 0.674509823322, 0.615686297417, 1] + }, + { "t": 49.3154296875 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 45, + "op": 51, + "st": -6, + "bm": 0 + }, + { + "ddd": 0, + "ind": 93, + "ty": 4, + "nm": "Layer 88", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [426.393, 826.575, 0], "ix": 2 }, + "a": { "a": 0, "k": [1244.393, -319.425, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1308.337, -316.265], + [1180.45, -378.722], + [1227.487, -356.074], + [1252.372, -342.128] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1308.337, -316.265], + [1180.45, -378.722], + [1190.487, -295.074], + [1189.372, -260.128] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.835294127464, 0.749019622803, 0.698039233685, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.699571073055, 0.587779343128, 0.521720588207, 1], + "e": [0.835294127464, 0.749019622803, 0.698039233685, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -22, + "bm": 0 + }, + { + "ddd": 0, + "ind": 94, + "ty": 4, + "nm": "Layer 82", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [696.393, 827.575, 0], "ix": 2 }, + "a": { "a": 0, "k": [1244.393, -319.425, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 32.877, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1308.337, -316.265], + [1180.45, -378.722], + [1227.487, -356.074], + [1252.372, -342.128] + ], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0], [0, 0]], + "v": [ + [1308.337, -316.265], + [1180.45, -378.722], + [1190.487, -295.074], + [1189.372, -260.128] + ], + "c": true + } + ] + }, + { "t": 38.3564453125 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.835294127464, 0.749019622803, 0.698039233685, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 36.302, + "s": [0], + "e": [2] + }, + { "t": 38.3564453125 } + ], + "ix": 5 + }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 1, + "k": [ + { + "i": { "x": [0.667], "y": [1] }, + "o": { "x": [0.333], "y": [0] }, + "t": 34.932, + "s": [0.699571073055, 0.587779343128, 0.521720588207, 1], + "e": [0.835294127464, 0.749019622803, 0.698039233685, 1] + }, + { "t": 38.3564453125 } + ], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 34, + "op": 51, + "st": -22, + "bm": 0 + } + ] + } + ], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Layer 95", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [427.99, 576.978, 0], "ix": 2 }, + "a": { "a": 0, "k": [1240.49, -552.522, 0], "ix": 1 }, + "s": { "a": 0, "k": [-100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0, "y": 0 }, + "t": 10.255, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 19, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 33, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0 }, + "t": 34.568, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.167, "y": 0 }, + "t": 35.353, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { "t": 36.921875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.137254908681, 0.203921571374, 0.278431385756, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.203921571374, 0.278431385756, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 13, + "op": 55, + "st": -48, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Layer 94", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [692.99, 577.978, 0], "ix": 2 }, + "a": { "a": 0, "k": [1240.49, -552.522, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 1, + "k": [ + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0, "y": 0 }, + "t": 10.255, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 19, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.333, "y": 0 }, + "t": 33, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { + "i": { "x": 0.667, "y": 1 }, + "o": { "x": 0.167, "y": 0 }, + "t": 34.568, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0.167, "y": 0 }, + "t": 35.353, + "s": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1237.87, -534.423], [1293.838, -551.221]], + "c": true + } + ], + "e": [ + { + "i": [[0, 0], [0, 0], [0, 0]], + "o": [[0, 0], [0, 0], [0, 0]], + "v": [[1187.141, -519.621], [1218.37, -585.423], [1293.838, -551.221]], + "c": true + } + ] + }, + { "t": 36.921875 } + ], + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [0.137254908681, 0.203921571374, 0.278431385756, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 2, "ix": 5 }, + "lc": 2, + "lj": 2, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.203921571374, 0.278431385756, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 13, + "op": 55, + "st": -48, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 0, + "nm": "Fox_fold_unfold faster", + "refId": "comp_0", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [560, 470, 0], "ix": 2 }, + "a": { "a": 0, "k": [562.5, 483, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "tm": { + "a": 1, + "k": [ + { "i": { "x": [0.015], "y": [1] }, "o": { "x": [0.154], "y": [0] }, "t": 0, "s": [0], "e": [1.7] }, + { "t": 40 } + ], + "ix": 2 + }, + "w": 1125, + "h": 966, + "ip": 0, + "op": 74, + "st": 0, + "bm": 0 + } + ], + "markers": [{ "tm": 95, "cm": "1", "dr": 0 }] +} diff --git a/app/animations/wordmark.json b/app/animations/wordmark.json new file mode 100644 index 00000000000..0991b8d914b --- /dev/null +++ b/app/animations/wordmark.json @@ -0,0 +1,1288 @@ +{ + "v": "5.5.1", + "fr": 30, + "ip": 0, + "op": 50, + "w": 1125, + "h": 160, + "nm": "wordmark 2", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Metamask", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0, "y": 1 }, + "o": { "x": 0, "y": 0 }, + "t": 11, + "s": [562.5, 251.444, 0], + "e": [562.5, 79.444, 0], + "to": [0, -28.667, 0], + "ti": [0, 28.667, 0] + }, + { "t": 31 } + ], + "ix": 2 + }, + "a": { "a": 0, "k": [574.5, 1301.444, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.652, 0.652], + [0, 0], + [-0.326, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [-0.326, -0.652], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [-0.326, -0.326], + [0, 0], + [0.326, -0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [-0.326, 0.326], + [0, 0], + [0, -0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.652], + [0, 0], + [0, 0], + [0, 0], + [0.652, 0] + ], + "v": [ + [521.144, 145.744], + [459.171, 81.487], + [459.171, 80.509], + [514.947, 22.776], + [514.621, 21.797], + [491.789, 21.797], + [491.462, 22.123], + [444.167, 71.376], + [443.189, 71.05], + [443.189, 22.45], + [442.536, 21.797], + [424.597, 21.797], + [423.944, 22.45], + [423.944, 146.396], + [424.597, 147.048], + [442.536, 147.048], + [443.189, 146.396], + [443.189, 91.599], + [444.167, 91.272], + [497.66, 146.722], + [497.986, 147.048], + [520.818, 147.048] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.121568627656, 0.1254902035, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [1035.222, 1302.423], "ix": 2 }, + "a": { "a": 0, "k": [472.722, 84.423], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Layer 8", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0], + [0, -0.326], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0, -0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [-0.326, 0.326] + ], + "v": [ + [-337.022, 128.783], + [-337.022, 90.294], + [-336.37, 89.642], + [-288.748, 89.642], + [-288.096, 88.989], + [-288.096, 73.659], + [-288.748, 73.007], + [-336.37, 73.007], + [-337.022, 72.354], + [-337.022, 39.084], + [-336.37, 38.432], + [-282.225, 38.432], + [-281.572, 37.78], + [-281.572, 22.45], + [-282.225, 21.797], + [-337.022, 21.797], + [-355.614, 21.797], + [-356.266, 22.45], + [-356.266, 38.432], + [-356.266, 72.68], + [-356.266, 89.315], + [-356.266, 129.109], + [-356.266, 146.07], + [-355.614, 146.722], + [-337.022, 146.722], + [-279.615, 146.722], + [-278.963, 146.07], + [-278.963, 129.761], + [-279.615, 129.109], + [-336.37, 129.109] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.121568627656, 0.1254902035, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [244.885, 1302.26], "ix": 2 }, + "a": { "a": 0, "k": [-317.615, 84.26], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Layer 7", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [[0, 0.326], [0, 0], [-0.326, -0.652], [0, 0], [0.326, 0], [0, 0]], + "o": [ + [0, 0], + [0.326, -0.652], + [0, 0], + [0, 0.326], + [0, 0], + [-0.326, -0.326] + ], + "v": [ + [192.686, 92.251], + [205.733, 43.977], + [207.038, 43.977], + [220.085, 92.251], + [219.433, 93.23], + [193.339, 93.23] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 0, + "k": { + "i": [ + [-0.326, 0], + [0, 0], + [0, 0.652], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0] + ], + "v": [ + [235.741, 147.048], + [252.05, 147.048], + [252.702, 146.07], + [218.78, 21.797], + [218.128, 21.471], + [211.931, 21.471], + [200.841, 21.471], + [194.643, 21.471], + [193.991, 21.797], + [160.395, 146.07], + [161.047, 147.048], + [177.356, 147.048], + [178.008, 146.722], + [187.794, 110.517], + [188.446, 110.191], + [224.651, 110.191], + [225.304, 110.517], + [235.089, 146.722] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.121568627656, 0.1254902035, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [769.049, 1302.26], "ix": 2 }, + "a": { "a": 0, "k": [206.549, 84.26], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Layer 6", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 3, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [[0, 0.326], [0, 0], [-0.326, -0.652], [0, 0], [0.326, 0], [0, 0]], + "o": [ + [0, 0], + [0.326, -0.652], + [0, 0], + [0, 0.326], + [0, 0], + [-0.326, -0.326] + ], + "v": [ + [-85.541, 92.251], + [-72.494, 43.977], + [-71.189, 43.977], + [-58.142, 92.251], + [-58.795, 93.23], + [-84.889, 93.23] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 0, + "k": { + "i": [ + [-0.326, 0], + [0, 0], + [0, 0.652], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [0, 0] + ], + "o": [ + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [0.326, 0], + [0, 0], + [-0.326, 0] + ], + "v": [ + [-42.486, 147.048], + [-26.177, 147.048], + [-25.525, 146.07], + [-59.447, 21.797], + [-60.099, 21.471], + [-66.297, 21.471], + [-77.387, 21.471], + [-83.258, 21.471], + [-83.91, 21.797], + [-117.506, 146.07], + [-116.854, 147.048], + [-100.545, 147.048], + [-99.893, 146.722], + [-90.107, 110.517], + [-89.455, 110.191], + [-53.25, 110.191], + [-52.597, 110.517], + [-42.812, 146.722] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.121568627656, 0.1254902035, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [490.984, 1302.26], "ix": 2 }, + "a": { "a": 0, "k": [-71.515, 84.26], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Layer 5", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 4, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [0, -0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0, 0], + [0, 0.326], + [0, 0] + ], + "o": [ + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0], + [0.326, 0], + [0, 0], + [0.326, -0.326] + ], + "v": [ + [-148.167, 21.797], + [-181.763, 21.797], + [-199.702, 21.797], + [-232.972, 21.797], + [-233.624, 22.45], + [-233.624, 37.78], + [-232.972, 38.432], + [-200.355, 38.432], + [-200.355, 146.07], + [-199.702, 146.722], + [-181.763, 146.722], + [-181.11, 146.07], + [-181.11, 38.432], + [-148.493, 38.432], + [-147.84, 37.78], + [-147.84, 22.45] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.121568627656, 0.1254902035, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [371.835, 1302.26], "ix": 2 }, + "a": { "a": 0, "k": [-190.665, 84.26], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Layer 4", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 5, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0.326, 0.652], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [-0.326, -0.652], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, -0.979], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0] + ], + "o": [ + [-0.326, 0], + [0, 0], + [-0.326, 0.652], + [0, 0], + [0, -0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.652], + [0, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0], + [0.326, -0.652], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0] + ], + "v": [ + [82.765, 21.797], + [82.113, 22.123], + [67.435, 70.723], + [66.13, 70.723], + [51.452, 22.123], + [50.8, 21.797], + [23.401, 21.797], + [22.749, 22.45], + [22.749, 146.396], + [23.401, 147.048], + [41.341, 147.048], + [41.993, 146.396], + [41.993, 52.131], + [43.298, 51.805], + [58.302, 100.732], + [59.281, 103.993], + [59.933, 104.319], + [73.632, 104.319], + [74.285, 103.993], + [75.263, 100.732], + [90.267, 51.805], + [91.572, 52.131], + [91.572, 146.396], + [92.224, 147.048], + [110.164, 147.048], + [110.816, 146.396], + [110.816, 22.45], + [110.164, 21.797] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.121568627656, 0.1254902035, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [629.283, 1302.423], "ix": 2 }, + "a": { "a": 0, "k": [66.783, 84.423], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Layer 3", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 6, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0.326, 0.652], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [0, -0.326], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [-0.326, -0.652], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0], + [0, 0], + [0, 0], + [0, -0.652], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0] + ], + "o": [ + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [-0.326, 0.652], + [0, 0], + [0, -0.326], + [0, 0], + [0, 0], + [0, 0], + [-0.326, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.652], + [0, 0], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, 0], + [0, -0.652], + [0, 0], + [0, 0.326], + [0, 0], + [0.326, 0], + [0, 0], + [0, -0.326], + [0, 0] + ], + "v": [ + [-420.849, 21.797], + [-429.003, 21.797], + [-437.81, 21.797], + [-438.462, 22.123], + [-453.14, 70.723], + [-454.445, 70.723], + [-468.797, 22.123], + [-469.449, 21.797], + [-478.256, 21.797], + [-486.084, 21.797], + [-496.848, 21.797], + [-497.5, 22.45], + [-497.5, 146.396], + [-496.848, 147.048], + [-478.908, 147.048], + [-478.256, 146.396], + [-478.256, 52.131], + [-476.951, 51.805], + [-461.947, 100.732], + [-460.968, 103.993], + [-460.316, 104.319], + [-446.617, 104.319], + [-445.964, 103.993], + [-444.986, 100.732], + [-429.982, 51.805], + [-429.003, 52.131], + [-429.003, 146.396], + [-428.351, 147.048], + [-410.411, 147.048], + [-409.759, 146.396], + [-409.759, 22.45], + [-410.411, 21.797] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.121568627656, 0.1254902035, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [108.871, 1302.423], "ix": 2 }, + "a": { "a": 0, "k": [-453.629, 84.423], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Layer 2", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 7, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [12.721, 8.154], + [7.828, 4.24], + [4.24, 3.588], + [-7.828, 5.219], + [-1.957, -15.004], + [-0.326, 0], + [0, 0], + [0, 0.326], + [7.176, 5.545], + [8.481, 0], + [-23.485, -14.678], + [-8.154, -4.893], + [3.588, -7.176], + [8.154, 0.326], + [2.283, 7.828], + [0, 1.305], + [0.326, 0], + [0, 0], + [0, -0.326], + [-8.807, -6.523], + [-9.459, 0], + [-2.609, 14.352] + ], + "o": [ + [-7.176, -4.893], + [-4.893, -2.609], + [-7.176, -5.871], + [11.09, -7.176], + [0, 0.326], + [0, 0], + [0.326, 0], + [-0.978, -10.438], + [-6.85, -5.219], + [-43.707, 0], + [2.609, 1.631], + [8.154, 5.219], + [-3.262, 6.85], + [-9.133, -0.326], + [-0.326, -1.305], + [0, -0.326], + [0, 0], + [-0.326, 0], + [0, 13.047], + [8.154, 6.197], + [24.463, 0], + [1.957, -13.699] + ], + "v": [ + [357.405, 84.749], + [334.246, 72.028], + [319.895, 63.221], + [321.852, 40.063], + [352.838, 51.805], + [353.491, 52.458], + [370.126, 52.458], + [370.778, 51.805], + [358.709, 27.342], + [335.225, 19.188], + [311.088, 80.183], + [345.01, 98.448], + [352.186, 119.976], + [332.289, 130.74], + [313.697, 117.366], + [313.045, 112.148], + [312.393, 111.495], + [294.453, 111.495], + [293.801, 112.148], + [305.869, 138.894], + [332.615, 147.701], + [372.409, 119.324] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { "a": 0, "k": [0.137254908681, 0.121568627656, 0.1254902035, 1], "ix": 4 }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [895.832, 1301.444], "ix": 2 }, + "a": { "a": 0, "k": [333.332, 83.444], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Layer 1", + "np": 1, + "cix": 2, + "bm": 0, + "ix": 8, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 90, + "st": 0, + "bm": 0 + } + ], + "markers": [{ "tm": 95, "cm": "1", "dr": 0 }] +} diff --git a/app/components/Nav/App/__snapshots__/index.test.js.snap b/app/components/Nav/App/__snapshots__/index.test.js.snap index 0a3304fec67..f500d6b7f8f 100644 --- a/app/components/Nav/App/__snapshots__/index.test.js.snap +++ b/app/components/Nav/App/__snapshots__/index.test.js.snap @@ -111,6 +111,19 @@ exports[`App should render correctly 1`] = ` "getStateForAction": [Function], }, "LockScreen": null, + "PaymentRequestView": Object { + "childRouters": Object { + "PaymentRequest": null, + "PaymentRequestSuccess": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "QRScanner": null, "SendView": Object { "childRouters": Object { @@ -147,11 +160,15 @@ exports[`App should render correctly 1`] = ` "childRouters": Object { "AdvancedSettings": null, "CompanySettings": null, + "ExperimentalSettings": null, "GeneralSettings": null, + "NetworkSettings": null, + "NetworksSettings": null, "RevealPrivateCredentialView": null, "SecuritySettings": null, "Settings": null, "SyncWithExtensionView": null, + "WalletConnectSessionsView": null, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], @@ -201,6 +218,7 @@ exports[`App should render correctly 1`] = ` "ImportFromSeed": null, "ImportWallet": null, "Onboarding": null, + "OptinMetrics": null, "SyncWithExtension": null, }, "getActionCreators": [Function], @@ -213,6 +231,18 @@ exports[`App should render correctly 1`] = ` }, "QRScanner": null, "SyncWithExtensionSuccess": null, + "Webview": Object { + "childRouters": Object { + "SimpleWebview": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], @@ -459,6 +489,19 @@ exports[`App should render correctly 1`] = ` "getStateForAction": [Function], }, "LockScreen": null, + "PaymentRequestView": Object { + "childRouters": Object { + "PaymentRequest": null, + "PaymentRequestSuccess": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "QRScanner": null, "SendView": Object { "childRouters": Object { @@ -495,11 +538,15 @@ exports[`App should render correctly 1`] = ` "childRouters": Object { "AdvancedSettings": null, "CompanySettings": null, + "ExperimentalSettings": null, "GeneralSettings": null, + "NetworkSettings": null, + "NetworksSettings": null, "RevealPrivateCredentialView": null, "SecuritySettings": null, "Settings": null, "SyncWithExtensionView": null, + "WalletConnectSessionsView": null, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], @@ -549,6 +596,7 @@ exports[`App should render correctly 1`] = ` "ImportFromSeed": null, "ImportWallet": null, "Onboarding": null, + "OptinMetrics": null, "SyncWithExtension": null, }, "getActionCreators": [Function], @@ -561,6 +609,18 @@ exports[`App should render correctly 1`] = ` }, "QRScanner": null, "SyncWithExtensionSuccess": null, + "Webview": Object { + "childRouters": Object { + "SimpleWebview": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index bb76d00bf17..f3ce5921a0e 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -16,6 +16,8 @@ import Entry from '../../Views/Entry'; import LockScreen from '../../Views/LockScreen'; import Main from '../Main'; import DrawerView from '../../UI/DrawerView'; +import OptinMetrics from '../../UI/OptinMetrics'; +import SimpleWebview from '../../Views/SimpleWebview'; /** * Stack navigator responsible for the onboarding process @@ -37,6 +39,9 @@ const OnboardingNav = createStackNavigator( }, SyncWithExtension: { screen: SyncWithExtension + }, + OptinMetrics: { + screen: OptinMetrics } }, { @@ -58,6 +63,18 @@ const OnboardingRootNav = createStackNavigator( }, QRScanner: { screen: QRScanner + }, + Webview: { + screen: createStackNavigator( + { + SimpleWebview: { + screen: SimpleWebview + } + }, + { + mode: 'modal' + } + ) } }, { diff --git a/app/components/Nav/Main/__snapshots__/index.test.js.snap b/app/components/Nav/Main/__snapshots__/index.test.js.snap index 7aac3dd388e..213a43d57fa 100644 --- a/app/components/Nav/Main/__snapshots__/index.test.js.snap +++ b/app/components/Nav/Main/__snapshots__/index.test.js.snap @@ -119,6 +119,19 @@ exports[`Main should render correctly 1`] = ` "getStateForAction": [Function], }, "LockScreen": null, + "PaymentRequestView": Object { + "childRouters": Object { + "PaymentRequest": null, + "PaymentRequestSuccess": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "QRScanner": null, "SendView": Object { "childRouters": Object { @@ -155,11 +168,15 @@ exports[`Main should render correctly 1`] = ` "childRouters": Object { "AdvancedSettings": null, "CompanySettings": null, + "ExperimentalSettings": null, "GeneralSettings": null, + "NetworkSettings": null, + "NetworksSettings": null, "RevealPrivateCredentialView": null, "SecuritySettings": null, "Settings": null, "SyncWithExtensionView": null, + "WalletConnectSessionsView": null, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], @@ -366,6 +383,19 @@ exports[`Main should render correctly 1`] = ` "getStateForAction": [Function], }, "LockScreen": null, + "PaymentRequestView": Object { + "childRouters": Object { + "PaymentRequest": null, + "PaymentRequestSuccess": null, + }, + "getActionCreators": [Function], + "getActionForPathAndParams": [Function], + "getComponentForRouteName": [Function], + "getComponentForState": [Function], + "getPathAndParamsForState": [Function], + "getScreenOptions": [Function], + "getStateForAction": [Function], + }, "QRScanner": null, "SendView": Object { "childRouters": Object { @@ -402,11 +432,15 @@ exports[`Main should render correctly 1`] = ` "childRouters": Object { "AdvancedSettings": null, "CompanySettings": null, + "ExperimentalSettings": null, "GeneralSettings": null, + "NetworkSettings": null, + "NetworksSettings": null, "RevealPrivateCredentialView": null, "SecuritySettings": null, "Settings": null, "SyncWithExtensionView": null, + "WalletConnectSessionsView": null, }, "getActionCreators": [Function], "getActionForPathAndParams": [Function], diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index e686fe684a9..fb1b02db9a8 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -12,10 +12,13 @@ import AddBookmark from '../../Views/AddBookmark'; import SimpleWebview from '../../Views/SimpleWebview'; import Approval from '../../Views/Approval'; import Settings from '../../Views/Settings'; -import GeneralSettings from '../../Views/GeneralSettings'; -import AdvancedSettings from '../../Views/AdvancedSettings'; +import GeneralSettings from '../../Views/Settings/GeneralSettings'; +import AdvancedSettings from '../../Views/Settings/AdvancedSettings'; +import SecuritySettings from '../../Views/Settings/SecuritySettings'; +import ExperimentalSettings from '../../Views/Settings/ExperimentalSettings'; +import NetworksSettings from '../../Views/Settings/NetworksSettings'; +import NetworkSettings from '../../Views/Settings/NetworksSettings/NetworkSettings'; import AppInformation from '../../UI/AppInformation'; -import SecuritySettings from '../../Views/SecuritySettings'; import Wallet from '../../Views/Wallet'; import TransactionsView from '../../Views/TransactionsView'; import SyncWithExtension from '../../Views/SyncWithExtension'; @@ -25,6 +28,7 @@ import Collectible from '../../Views/Collectible'; import CollectibleView from '../../Views/CollectibleView'; import Send from '../../Views/Send'; import RevealPrivateCredential from '../../Views/RevealPrivateCredential'; +import WalletConnectSessions from '../../Views/WalletConnectSessions'; import QrScanner from '../../Views/QRScanner'; import LockScreen from '../../Views/LockScreen'; import ProtectYourAccount from '../../Views/ProtectYourAccount'; @@ -37,6 +41,8 @@ import AccountBackupStep5 from '../../Views/AccountBackupStep5'; import AccountBackupStep6 from '../../Views/AccountBackupStep6'; import ImportPrivateKey from '../../Views/ImportPrivateKey'; import ImportPrivateKeySuccess from '../../Views/ImportPrivateKeySuccess'; +import PaymentRequest from '../../UI/PaymentRequest'; +import PaymentRequestSuccess from '../../UI/PaymentRequestSuccess'; import { TransactionNotification } from '../../UI/TransactionNotification'; import TransactionsNotificationManager from '../../../core/TransactionsNotificationManager'; import Engine from '../../../core/Engine'; @@ -45,6 +51,15 @@ import PushNotification from 'react-native-push-notification'; import I18n from '../../../../locales/i18n'; import { colors } from '../../../styles/common'; import LockManager from '../../../core/LockManager'; +import OnboardingWizard from '../../UI/OnboardingWizard'; +import FadeOutOverlay from '../../UI/FadeOutOverlay'; +import { hexToBN, fromWei } from '../../../util/number'; +import { setTransactionObject } from '../../../actions/transaction'; +import PersonalSign from '../../UI/PersonalSign'; +import TypedSign from '../../UI/TypedSign'; +import Modal from 'react-native-modal'; +import WalletConnect from '../../../core/WalletConnect'; +import WalletConnectSessionApproval from '../../UI/WalletConnectSessionApproval'; const styles = StyleSheet.create({ flex: { @@ -55,6 +70,10 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center' + }, + bottomModal: { + justifyContent: 'flex-end', + margin: 0 } }); @@ -134,6 +153,15 @@ const MainNavigator = createStackNavigator( SecuritySettings: { screen: SecuritySettings }, + ExperimentalSettings: { + screen: ExperimentalSettings + }, + NetworksSettings: { + screen: NetworksSettings + }, + NetworkSettings: { + screen: NetworkSettings + }, CompanySettings: { screen: AppInformation }, @@ -142,6 +170,9 @@ const MainNavigator = createStackNavigator( }, RevealPrivateCredentialView: { screen: RevealPrivateCredential + }, + WalletConnectSessionsView: { + screen: WalletConnectSessions } }) }, @@ -188,6 +219,21 @@ const MainNavigator = createStackNavigator( LockScreen: { screen: LockScreen }, + PaymentRequestView: { + screen: createStackNavigator( + { + PaymentRequest: { + screen: PaymentRequest + }, + PaymentRequestSuccess: { + screen: PaymentRequestSuccess + } + }, + { + mode: 'modal' + } + ) + }, SetPasswordFlow: { screen: createStackNavigator( { @@ -243,11 +289,28 @@ class Main extends Component { /** * Time to auto-lock the app after it goes in background mode */ - lockTime: PropTypes.number + lockTime: PropTypes.number, + /** + * Current onboarding wizard step + */ + wizardStep: PropTypes.number, + /** + * Action that sets a transaction + */ + setTransactionObject: PropTypes.func, + /** + * Object containing the information for the current transaction + */ + transaction: PropTypes.object }; state = { - forceReload: false + forceReload: false, + signMessage: false, + signMessageParams: { data: '' }, + signType: '', + walletConnectRequest: false, + walletConnectRequestInfo: {} }; backgroundMode = false; @@ -263,13 +326,11 @@ class Main extends Component { } }; - componentDidMount() { + componentDidMount = async () => { TransactionsNotificationManager.init(this.props.navigation); this.pollForIncomingTransactions(); AppState.addEventListener('change', this.handleAppStateChange); - this.lockManager = new LockManager(this.props.navigation, this.props.lockTime); - PushNotification.configure({ requestPermissions: false, onNotification: notification => { @@ -291,7 +352,62 @@ class Main extends Component { } } }); - } + + Engine.context.TransactionController.hub.on('unapprovedTransaction', this.onUnapprovedTransaction); + + Engine.context.PersonalMessageManager.hub.on('unapprovedMessage', messageParams => { + const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; + delete messageParams.meta; + this.setState({ + signMessage: true, + signMessageParams: messageParams, + signType: 'personal', + currentPageTitle, + currentPageUrl + }); + }); + + Engine.context.TypedMessageManager.hub.on('unapprovedMessage', messageParams => { + const { title: currentPageTitle, url: currentPageUrl } = messageParams.meta; + delete messageParams.meta; + this.setState({ + signMessage: true, + signMessageParams: messageParams, + signType: 'typed', + currentPageTitle, + currentPageUrl + }); + }); + + WalletConnect.hub.on('walletconnectSessionRequest', peerInfo => { + this.setState({ walletConnectRequest: true, walletConnectRequestInfo: peerInfo }); + }); + WalletConnect.init(); + }; + + onUnapprovedTransaction = transactionMeta => { + if (this.props.transaction.value || this.props.transaction.to) { + return; + } + const { + transaction: { value, gas, gasPrice } + } = transactionMeta; + transactionMeta.transaction.value = hexToBN(value); + transactionMeta.transaction.readableValue = fromWei(transactionMeta.transaction.value); + transactionMeta.transaction.gas = hexToBN(gas); + transactionMeta.transaction.gasPrice = hexToBN(gasPrice); + this.props.setTransactionObject({ + ...{ + symbol: 'ETH', + selectedAsset: { isETH: true, symbol: 'ETH' }, + type: 'ETHER_TRANSACTION', + assetType: 'ETH', + id: transactionMeta.id + }, + ...transactionMeta.transaction + }); + this.props.navigation.push('ApprovalView'); + }; handleAppStateChange = appState => { const newModeIsBackground = appState === 'background'; @@ -342,19 +458,140 @@ class Main extends Component { componentWillUnmount() { AppState.removeEventListener('change', this.handleAppStateChange); this.lockManager.stopListening(); + Engine.context.PersonalMessageManager.hub.removeAllListeners(); + Engine.context.TypedMessageManager.hub.removeAllListeners(); + Engine.context.TransactionController.hub.removeListener('unapprovedTransaction', this.onUnapprovedTransaction); } - render = () => ( - - {!this.state.forceReload ? : this.renderLoader()} - - - - ); + /** + * Return current step of onboarding wizard if not step 5 nor 0 + */ + renderOnboardingWizard = () => { + const { wizardStep } = this.props; + return wizardStep !== 5 && wizardStep > 0 && ; + }; + + onSignAction = () => { + this.setState({ signMessage: false }); + }; + + renderSigningModal = () => { + const { signMessage, signMessageParams, signType, currentPageTitle, currentPageUrl } = this.state; + return ( + + {signType === 'personal' && ( + + )} + {signType === 'typed' && ( + + )} + + ); + }; + + onWalletConnectSessionApproval = () => { + const { peerId } = this.state.walletConnectRequestInfo; + this.setState({ + walletConnectRequest: false, + walletConnectRequestInfo: {} + }); + WalletConnect.hub.emit('walletconnectSessionRequest::approved', peerId); + }; + + onWalletConnectSessionRejected = () => { + const peerId = this.state.walletConnectRequestInfo.peerId; + this.setState({ + walletConnectRequest: false, + walletConnectRequestInfo: {} + }); + WalletConnect.hub.emit('walletconnectSessionRequest::rejected', peerId); + }; + + renderWalletConnectSessionRequestModal = () => { + const { walletConnectRequest, walletConnectRequestInfo } = this.state; + + const meta = walletConnectRequestInfo.peerMeta || null; + + return ( + + + + ); + }; + + render() { + const { forceReload } = this.state; + + return ( + + + {!forceReload ? : this.renderLoader()} + {this.renderOnboardingWizard()} + + + + + {this.renderSigningModal()} + {this.renderWalletConnectSessionRequestModal()} + + ); + } } const mapStateToProps = state => ({ - lockTime: state.settings.lockTime + lockTime: state.settings.lockTime, + wizardStep: state.wizard.step, + transaction: state.transaction +}); + +const mapDispatchToProps = dispatch => ({ + setTransactionObject: asset => dispatch(setTransactionObject(asset)) }); -export default connect(mapStateToProps)(Main); +export default connect( + mapStateToProps, + mapDispatchToProps +)(Main); diff --git a/app/components/UI/AccountApproval/__snapshots__/index.test.js.snap b/app/components/UI/AccountApproval/__snapshots__/index.test.js.snap index 73d04f09c84..1c618c51874 100644 --- a/app/components/UI/AccountApproval/__snapshots__/index.test.js.snap +++ b/app/components/UI/AccountApproval/__snapshots__/index.test.js.snap @@ -37,6 +37,8 @@ exports[`AccountApproval should render correctly 1`] = ` confirmTestID="" confirmText="CONNECT" confirmed={false} + onCancelPress={[Function]} + onConfirmPress={[Function]} showCancelButton={true} showConfirmButton={true} > @@ -127,7 +129,7 @@ exports[`AccountApproval should render correctly 1`] = ` style={ Object { "alignItems": "center", - "backgroundColor": "#5ea40c", + "backgroundColor": "#28a745", "borderRadius": 12, "height": 24, "position": "relative", @@ -152,7 +154,7 @@ exports[`AccountApproval should render correctly 1`] = ` diff --git a/app/components/UI/AccountApproval/index.js b/app/components/UI/AccountApproval/index.js index 59b1664dc6a..8dbb3b58a9b 100644 --- a/app/components/UI/AccountApproval/index.js +++ b/app/components/UI/AccountApproval/index.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { StyleSheet, Text, View } from 'react-native'; +import { StyleSheet, Text, View, InteractionManager } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import ActionView from '../ActionView'; import ElevatedView from 'react-native-elevated-view'; @@ -11,6 +11,8 @@ import { colors, fontStyles } from '../../../styles/common'; import DeviceSize from '../../../util/DeviceSize'; import WebsiteIcon from '../WebsiteIcon'; import { renderAccountName } from '../../../util/address'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; const styles = StyleSheet.create({ root: { @@ -45,7 +47,7 @@ const styles = StyleSheet.create({ permissions: { alignItems: 'center', borderBottomWidth: 1, - borderColor: colors.borderColor, + borderColor: colors.grey100, borderTopWidth: 1, display: 'flex', flexDirection: 'row', @@ -112,7 +114,7 @@ const styles = StyleSheet.create({ width: '33%' }, border: { - borderColor: colors.accentGray, + borderColor: colors.grey400, borderStyle: 'dashed', borderWidth: 1, left: 0, @@ -124,7 +126,7 @@ const styles = StyleSheet.create({ }, checkWrapper: { alignItems: 'center', - backgroundColor: colors.success, + backgroundColor: colors.green500, borderRadius: 12, height: 24, position: 'relative', @@ -168,14 +170,73 @@ class AccountApproval extends Component { /** * A string that represents the selected address */ - selectedAddress: PropTypes.string + selectedAddress: PropTypes.string, + /** + * Number of tokens + */ + tokensLength: PropTypes.number, + /** + * Number of accounts + */ + accountsLength: PropTypes.number, + /** + * A string representing the network name + */ + networkType: PropTypes.string + }; + + state = { + start: Date.now() + }; + + componentDidMount = () => { + const params = this.getTrackingParams(); + delete params.timeOpen; + InteractionManager.runAfterInteractions(() => { + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.AUTHENTICATION_CONNECT, params); + }); + }; + + /** + * Calls onConfirm callback and analytics to track connect confirmed event + */ + onConfirm = () => { + Analytics.trackEventWithParameters( + ANALYTICS_EVENT_OPTS.AUTHENTICATION_CONNECT_CONFIRMED, + this.getTrackingParams() + ); + this.props.onConfirm(); + }; + + /** + * Calls onConfirm callback and analytics to track connect canceled event + */ + onCancel = () => { + Analytics.trackEventWithParameters( + ANALYTICS_EVENT_OPTS.AUTHENTICATION_CONNECT_CANCELED, + this.getTrackingParams() + ); + this.props.onCancel(); + }; + + /** + * Returns corresponding tracking params to send + * + * @return {object} - Object containing numberOfTokens, numberOfAccounts, network and timeOpen + */ + getTrackingParams = () => { + const { tokensLength, accountsLength, networkType } = this.props; + return { + numberOfTokens: tokensLength, + numberOfAccounts: accountsLength, + network: networkType, + timeOpen: (Date.now() - this.state.start) / 1000 + }; }; render = () => { const { currentPageInformation: { title, url }, - onConfirm, - onCancel, selectedAddress, identities } = this.props; @@ -189,8 +250,8 @@ class AccountApproval extends Component { @@ -228,7 +289,7 @@ class AccountApproval extends Component { {strings('accountApproval.permission')} {strings('accountApproval.address')} - + {strings('accountApproval.warning')} @@ -240,7 +301,10 @@ class AccountApproval extends Component { const mapStateToProps = state => ({ selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, - identities: state.engine.backgroundState.PreferencesController.identities + identities: state.engine.backgroundState.PreferencesController.identities, + accountsLength: Object.keys(state.engine.backgroundState.AccountTrackerController.accounts).length, + tokensLength: state.engine.backgroundState.AssetsController.tokens.length, + networkType: state.engine.backgroundState.NetworkController.provider.type }); export default connect(mapStateToProps)(AccountApproval); diff --git a/app/components/UI/AccountApproval/index.test.js b/app/components/UI/AccountApproval/index.test.js index 22fc803ea5e..8974b51022e 100644 --- a/app/components/UI/AccountApproval/index.test.js +++ b/app/components/UI/AccountApproval/index.test.js @@ -10,6 +10,17 @@ describe('AccountApproval', () => { const initialState = { engine: { backgroundState: { + AccountTrackerController: { + accounts: { '0x2': { balance: '0' } } + }, + NetworkController: { + provider: { + type: 'ropsten' + } + }, + AssetsController: { + tokens: [] + }, PreferencesController: { selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1', identities: { '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { name: 'Account 1' } } diff --git a/app/components/UI/AccountInput/index.js b/app/components/UI/AccountInput/index.js index 7def2fbf900..db6c46c309f 100644 --- a/app/components/UI/AccountInput/index.js +++ b/app/components/UI/AccountInput/index.js @@ -2,18 +2,18 @@ import React, { Component } from 'react'; import Icon from 'react-native-vector-icons/FontAwesome'; import Identicon from '../Identicon'; import PropTypes from 'prop-types'; -import { Platform, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { ScrollView, Platform, StyleSheet, Text, TextInput, TouchableOpacity, View, Keyboard } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import { connect } from 'react-redux'; import { renderShortAddress } from '../../../util/address'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import { ScrollView } from 'react-native-gesture-handler'; import ElevatedView from 'react-native-elevated-view'; import ENS from 'ethjs-ens'; import networkMap from 'ethjs-ens/lib/network-map.json'; import Engine from '../../../core/Engine'; import { strings } from '../../../../locales/i18n'; import AppConstants from '../../../core/AppConstants'; +import { isValidAddress } from 'ethereumjs-util'; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; @@ -22,7 +22,7 @@ const styles = StyleSheet.create({ flex: 1 }, arrow: { - color: colors.inputBorderColor, + color: colors.grey100, position: 'absolute', right: 10, top: Platform.OS === 'android' ? 14 : 12 @@ -49,7 +49,7 @@ const styles = StyleSheet.create({ flex: 1, flexDirection: 'row', backgroundColor: colors.white, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1 }, @@ -74,6 +74,9 @@ const styles = StyleSheet.create({ ...fontStyles.normal, fontSize: 16 }, + accountWithoutName: { + marginTop: 4 + }, name: { flex: 1, ...fontStyles.bold, @@ -87,7 +90,7 @@ const styles = StyleSheet.create({ }, optionList: { backgroundColor: colors.white, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1, paddingBottom: 12, @@ -153,11 +156,26 @@ class AccountInput extends Component { /** * Network id */ - updateToAddressError: PropTypes.func + updateToAddressError: PropTypes.func, + /** + * Callback to open accounts dropdown + */ + openAccountSelect: PropTypes.func, + /** + * Whether accounts dropdown is opened + */ + isOpen: PropTypes.bool, + /** + * Map representing the address book + */ + addressBook: PropTypes.array, + /** + * Callback close all drowpdowns + */ + closeDropdowns: PropTypes.func }; state = { - isOpen: false, address: undefined, ensRecipient: undefined, value: undefined @@ -204,9 +222,8 @@ class AccountInput extends Component { }; onFocus = () => { - const { onFocus } = this.props; - const { isOpen } = this.state; - this.setState({ isOpen: !isOpen }); + const { onFocus, isOpen, openAccountSelect } = this.props; + openAccountSelect && openAccountSelect(!isOpen); onFocus && onFocus(); }; @@ -223,7 +240,10 @@ class AccountInput extends Component { }; selectAccount(account) { + Keyboard.dismiss(); this.onChange(account.address); + const { openAccountSelect } = this.props; + openAccountSelect && openAccountSelect(false); } getNetworkEnsSupport = () => { @@ -231,6 +251,20 @@ class AccountInput extends Component { return Boolean(networkMap[network]); }; + renderAccountName(name) { + if (name !== '') { + return ( + + + {name} + + + ); + } + + return ; + } + renderOption(account, onPress) { return ( @@ -238,11 +272,7 @@ class AccountInput extends Component { - - - {account.name} - - + {this.renderAccountName(account.name)} {renderShortAddress(account.address)} @@ -253,11 +283,43 @@ class AccountInput extends Component { ); } + getVisibleOptions = value => { + const { accounts, addressBook } = this.props; + const addressBookItems = {}; + if (addressBook.length > 0) { + addressBook.forEach(contact => { + addressBookItems[contact.address] = contact; + }); + } + + const allAddresses = { ...addressBookItems, ...accounts }; + + if (typeof value !== 'undefined' && value.toString().length > 0) { + // If it's a valid address we don't show any suggestion + if (isValidAddress(value)) { + return allAddresses; + } + + const filteredAddresses = {}; + Object.keys(allAddresses).forEach(address => { + if ( + address.toLowerCase().indexOf(value.toLowerCase()) !== -1 || + (allAddresses[address].name && + allAddresses[address].name.toLowerCase().indexOf(value.toLowerCase()) !== -1) + ) { + filteredAddresses[address] = allAddresses[address]; + } + }); + return filteredAddresses; + } + return allAddresses; + }; + renderOptionList() { - const { visibleOptions = this.props.accounts } = this.state; + const visibleOptions = this.getVisibleOptions(this.state.value); return ( - + {Object.keys(visibleOptions).map(address => this.renderOption(visibleOptions[address], () => { @@ -271,35 +333,38 @@ class AccountInput extends Component { } onChange = async value => { - const { accounts, onChange } = this.props; - const addresses = Object.keys(accounts).filter(address => address.toLowerCase().match(value.toLowerCase())); - const visibleOptions = value.length === 0 ? accounts : addresses.map(address => accounts[address]); - const match = visibleOptions.length === 1 && visibleOptions[0].address.toLowerCase() === value.toLowerCase(); + const { onChange, openAccountSelect } = this.props; this.setState({ - isOpen: (value.length === 0 || visibleOptions.length) > 0 && !match, value }); + + const filteredAccounts = this.getVisibleOptions(value); + openAccountSelect && openAccountSelect(Object.keys(filteredAccounts).length > 0); onChange && onChange(value); }; onInputFocus = () => { - this.setState({ isOpen: false }); + const { openAccountSelect } = this.props; + openAccountSelect && openAccountSelect(true); }; scan = () => { + const { openAccountSelect, closeDropdowns } = this.props; + openAccountSelect && openAccountSelect(false); this.setState({ isOpen: false }); this.props.navigation.navigate('QRScanner', { onScanSuccess: meta => { if (meta.target_address) { this.onChange(meta.target_address); + closeDropdowns && closeDropdowns(); } } }); }; render = () => { - const { isOpen, value, ensRecipient, address } = this.state; - const { placeholder } = this.props; + const { value, ensRecipient, address } = this.state; + const { placeholder, isOpen } = this.props; return ( @@ -334,6 +399,7 @@ class AccountInput extends Component { } const mapStateToProps = state => ({ + addressBook: state.engine.backgroundState.AddressBookController.addressBook, accounts: state.engine.backgroundState.PreferencesController.identities, activeAddress: state.engine.backgroundState.PreferencesController.activeAddress, network: state.engine.backgroundState.NetworkController.network diff --git a/app/components/UI/AccountList/index.js b/app/components/UI/AccountList/index.js index 342e19b4fb1..3dd23c002a6 100644 --- a/app/components/UI/AccountList/index.js +++ b/app/components/UI/AccountList/index.js @@ -20,6 +20,9 @@ import { renderFromWei } from '../../../util/number'; import { strings } from '../../../../locales/i18n'; import { toChecksumAddress } from 'ethereumjs-util'; import Logger from '../../../util/Logger'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; +import { getTicker } from '../../../util/transactions'; const styles = StyleSheet.create({ wrapper: { @@ -34,13 +37,13 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderColor + borderColor: colors.grey100 }, dragger: { width: 48, height: 5, borderRadius: 4, - backgroundColor: colors.gray, + backgroundColor: colors.grey400, opacity: Platform.OS === 'android' ? 0.6 : 0.5 }, accountsWrapper: { @@ -48,7 +51,7 @@ const styles = StyleSheet.create({ }, account: { borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderColor, + borderColor: colors.grey100, flexDirection: 'row', paddingHorizontal: 20, paddingVertical: 20, @@ -80,7 +83,7 @@ const styles = StyleSheet.create({ }, btnText: { fontSize: 14, - color: colors.primary, + color: colors.blue, ...fontStyles.normal }, footerButton: { @@ -89,10 +92,10 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', borderTopWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderColor + borderColor: colors.grey100 }, importedText: { - color: colors.another50ShadesOfGrey, + color: colors.grey400, fontSize: 10, ...fontStyles.bold }, @@ -102,7 +105,7 @@ const styles = StyleSheet.create({ paddingVertical: 3, borderRadius: 10, borderWidth: 1, - borderColor: colors.another50ShadesOfGrey + borderColor: colors.grey400 }, importedView: { flex: 0.5, @@ -147,7 +150,11 @@ export default class AccountList extends Component { /** * function to be called when importing an account */ - onImportAccount: PropTypes.func + onImportAccount: PropTypes.func, + /** + * Current provider ticker + */ + ticker: PropTypes.string }; state = { @@ -207,6 +214,11 @@ export default class AccountList extends Component { this.setState({ selectedAccountIndex: previousIndex }); Logger.error('error while trying change the selected account', e); // eslint-disable-line } + InteractionManager.runAfterInteractions(() => { + setTimeout(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ACCOUNTS_SWITCHED_ACCOUNTS); + }, 1000); + }); }; importAccount = () => { @@ -247,9 +259,10 @@ export default class AccountList extends Component { } renderItem = ({ item }) => { + const { ticker } = this.props; const { index, name, address, balance, isSelected, isImported } = item; - const selected = isSelected ? : null; + const selected = isSelected ? : null; const imported = isImported ? ( @@ -271,7 +284,7 @@ export default class AccountList extends Component { {name} - {renderFromWei(balance)} {strings('unit.eth')} + {renderFromWei(balance)} {getTicker(ticker)} {imported && {imported}} @@ -325,7 +338,7 @@ export default class AccountList extends Component { {this.state.loading ? ( - + ) : ( {strings('accounts.create_new_account')} )} diff --git a/app/components/UI/AccountOverview/index.js b/app/components/UI/AccountOverview/index.js index 6b6fa977715..b98107a6bbd 100644 --- a/app/components/UI/AccountOverview/index.js +++ b/app/components/UI/AccountOverview/index.js @@ -29,10 +29,10 @@ const styles = StyleSheet.create({ textAlign: 'center' }, data: { - textAlign: 'center' + textAlign: 'center', + paddingTop: 7 }, label: { - paddingTop: 7, fontSize: 24, textAlign: 'center', ...fontStyles.normal @@ -41,7 +41,7 @@ const styles = StyleSheet.create({ marginBottom: Platform.OS === 'android' ? -10 : 0 }, addressWrapper: { - backgroundColor: colors.blueishGrey, + backgroundColor: colors.blue000, borderRadius: 40, marginTop: 20, marginBottom: 20, @@ -50,7 +50,7 @@ const styles = StyleSheet.create({ }, address: { fontSize: 12, - color: colors.gray, + color: colors.grey400, ...fontStyles.normal, letterSpacing: 0.8 }, @@ -64,7 +64,15 @@ const styles = StyleSheet.create({ borderRadius: 80, borderWidth: 2, padding: 2, - borderColor: colors.primary + borderColor: colors.blue + }, + onboardingWizardLabel: { + borderWidth: 2, + borderRadius: 4, + borderColor: colors.blue, + paddingVertical: Platform.OS === 'ios' ? 2 : -4, + paddingHorizontal: Platform.OS === 'ios' ? 5 : 5, + top: Platform.OS === 'ios' ? 0 : -2 } }); @@ -97,11 +105,15 @@ class AccountOverview extends Component { /** * Action that toggles the accounts modal */ - toggleAccountsModal: PropTypes.func + toggleAccountsModal: PropTypes.func, + /** + * whether component is being rendered from onboarding wizard + */ + onboardingWizard: PropTypes.bool }; state = { - accountLabelEditable: true, + accountLabelEditable: false, accountLabel: '', originalAccountLabel: '' }; @@ -109,7 +121,8 @@ class AccountOverview extends Component { animatingAccountsModal = false; toggleAccountsModal = () => { - if (!this.animatingAccountsModal) { + const { onboardingWizard } = this.props; + if (!onboardingWizard && !this.animatingAccountsModal) { this.animatingAccountsModal = true; this.props.toggleAccountsModal(); setTimeout(() => { @@ -167,10 +180,11 @@ class AccountOverview extends Component { render() { const { account: { name, address }, - currentCurrency + currentCurrency, + onboardingWizard } = this.props; - const fiatBalance = `$${renderFiat(Engine.getTotalFiatAccountBalance(), currentCurrency)}`; + const fiatBalance = `${renderFiat(Engine.getTotalFiatAccountBalance(), currentCurrency)}`; if (!address) return null; const { accountLabelEditable, accountLabel } = this.state; @@ -184,13 +198,21 @@ class AccountOverview extends Component { testID={'account-overview'} > - - + + {accountLabelEditable ? ( ) : ( - + {name} diff --git a/app/components/UI/AccountSelect/index.js b/app/components/UI/AccountSelect/index.js index cfbc9551eb9..9af9bfddd66 100644 --- a/app/components/UI/AccountSelect/index.js +++ b/app/components/UI/AccountSelect/index.js @@ -2,14 +2,13 @@ import React, { Component } from 'react'; import Identicon from '../Identicon'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import PropTypes from 'prop-types'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, Text, TouchableOpacity, View, ScrollView } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import { connect } from 'react-redux'; import { hexToBN } from 'gaba/util'; import { toChecksumAddress } from 'ethereumjs-util'; import { weiToFiat, renderFromWei } from '../../../util/number'; -import { strings } from '../../../../locales/i18n'; -import { ScrollView } from 'react-native-gesture-handler'; +import { getTicker } from '../../../util/transactions'; const styles = StyleSheet.create({ root: { @@ -21,14 +20,14 @@ const styles = StyleSheet.create({ width: '100%', marginTop: 75, maxHeight: 200, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1, elevation: 11 }, activeOption: { backgroundColor: colors.white, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1, position: 'relative' @@ -58,14 +57,14 @@ const styles = StyleSheet.create({ paddingHorizontal: 8 }, arrow: { - color: colors.inputBorderColor, + color: colors.grey100, position: 'absolute', right: 10, top: 25 }, optionList: { backgroundColor: colors.white, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1, paddingBottom: 12, @@ -114,37 +113,62 @@ class AccountSelect extends Component { /** * Address of the currently-selected account */ - value: PropTypes.string + value: PropTypes.string, + /** + * Callback to open accounts dropdown + */ + openAccountSelect: PropTypes.func, + /** + * Whether accounts dropdown is opened + */ + isOpen: PropTypes.bool, + /** + * Primary currency, either ETH or Fiat + */ + primaryCurrency: PropTypes.string, + /** + * Current provider ticker + */ + ticker: PropTypes.string }; static defaultProps = { enabled: true }; - state = { isOpen: false }; - componentDidMount() { const { onChange, selectedAddress } = this.props; onChange && onChange(selectedAddress); } renderActiveOption() { - const { selectedAddress, accounts, identities, value } = this.props; + const { selectedAddress, accounts, identities, value, isOpen, openAccountSelect } = this.props; const targetAddress = toChecksumAddress(value || selectedAddress); const account = { ...identities[targetAddress], ...accounts[targetAddress] }; return ( {this.props.enabled && } {this.renderOption(account, () => { - this.setState({ isOpen: !this.state.isOpen }); + openAccountSelect && openAccountSelect(!isOpen); })} ); } renderOption(account, onPress) { - const { conversionRate, currentCurrency } = this.props; + const { conversionRate, currentCurrency, primaryCurrency, ticker } = this.props; const balance = hexToBN(account.balance); + + // render balances according to selected 'primaryCurrency' + let mainBalance, secondaryBalance; + if (primaryCurrency === 'ETH') { + mainBalance = renderFromWei(balance) + ' ' + getTicker(ticker); + secondaryBalance = weiToFiat(balance, conversionRate, currentCurrency.toUpperCase()); + } else { + mainBalance = weiToFiat(balance, conversionRate, currentCurrency.toUpperCase()); + secondaryBalance = renderFromWei(balance) + ' ' + getTicker(ticker); + } + return ( {account.name} - - {renderFromWei(balance)} {strings('unit.eth')} - - {weiToFiat(balance, conversionRate, currentCurrency).toUpperCase()} + {mainBalance} + {secondaryBalance} ); } renderOptionList() { - const { accounts, identities, onChange } = this.props; + const { accounts, identities, onChange, openAccountSelect } = this.props; return ( {Object.keys(identities).map(address => this.renderOption({ ...identities[address], ...accounts[address] }, () => { - this.setState({ isOpen: false, value: address }); + this.setState({ value: address }); + openAccountSelect && openAccountSelect(true); onChange && onChange(address); }) )} @@ -187,21 +210,19 @@ class AccountSelect extends Component { render = () => ( {this.renderActiveOption()} - {this.state.isOpen && this.props.enabled && this.renderOptionList()} + {this.props.isOpen && this.props.enabled && this.renderOptionList()} ); } -const mapStateToProps = ({ - engine: { - backgroundState: { AccountTrackerController, CurrencyRateController, PreferencesController } - } -}) => ({ - accounts: AccountTrackerController.accounts, - conversionRate: CurrencyRateController.conversionRate, - currentCurrency: CurrencyRateController.currentCurrency, - identities: PreferencesController.identities, - selectedAddress: PreferencesController.selectedAddress +const mapStateToProps = state => ({ + accounts: state.engine.backgroundState.AccountTrackerController.accounts, + conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, + identities: state.engine.backgroundState.PreferencesController.identities, + currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + primaryCurrency: state.settings.primaryCurrency, + ticker: state.engine.backgroundState.NetworkController.provider.ticker }); export default connect(mapStateToProps)(AccountSelect); diff --git a/app/components/UI/ActionModal/__snapshots__/index.test.js.snap b/app/components/UI/ActionModal/__snapshots__/index.test.js.snap index c77151af026..1550679fec6 100644 --- a/app/components/UI/ActionModal/__snapshots__/index.test.js.snap +++ b/app/components/UI/ActionModal/__snapshots__/index.test.js.snap @@ -70,7 +70,7 @@ exports[`ActionModal should render correctly 1`] = ` - - {children} + + { + if (keyboardShouldPersistTaps === 'handled') { + Keyboard.dismiss(); + } + onTouchablePress && onTouchablePress(); + }} + > + {children} + {showCancelButton && ( @@ -127,6 +144,11 @@ ActionView.propTypes = { * Called when the confirm button is clicked */ onConfirmPress: PropTypes.func, + /** + * Called when the touchable without feedback is clicked + */ + onTouchablePress: PropTypes.func, + /** * Whether cancel button is shown */ @@ -134,5 +156,9 @@ ActionView.propTypes = { /** * Whether confirm button is shown */ - showConfirmButton: PropTypes.bool + showConfirmButton: PropTypes.bool, + /** + * Determines if the keyboard should stay visible after a tap + */ + keyboardShouldPersistTaps: PropTypes.string }; diff --git a/app/components/UI/AddCustomCollectible/__snapshots__/index.test.js.snap b/app/components/UI/AddCustomCollectible/__snapshots__/index.test.js.snap index b69925254ef..44006d67073 100644 --- a/app/components/UI/AddCustomCollectible/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomCollectible/__snapshots__/index.test.js.snap @@ -50,7 +50,7 @@ exports[`AddCustomCollectible should render correctly 1`] = ` style={ Array [ Object { - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "borderRadius": 4, "borderWidth": 1, "fontFamily": "Roboto", @@ -67,7 +67,7 @@ exports[`AddCustomCollectible should render correctly 1`] = ` { @@ -59,6 +63,8 @@ class AddCustomCollectible extends Component { setTimeout(() => { this.mounted && this.setState({ inputWidth: '100%' }); }, 100); + const { collectibleContract } = this.props; + collectibleContract && this.setState({ address: collectibleContract.address }); }; componentWillUnmount = () => { diff --git a/app/components/UI/AddCustomToken/__snapshots__/index.test.js.snap b/app/components/UI/AddCustomToken/__snapshots__/index.test.js.snap index f9c752f7816..245ba9583de 100644 --- a/app/components/UI/AddCustomToken/__snapshots__/index.test.js.snap +++ b/app/components/UI/AddCustomToken/__snapshots__/index.test.js.snap @@ -50,7 +50,7 @@ exports[`AddCustomToken should render correctly 1`] = ` returnKeyType="next" style={ Object { - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "borderRadius": 4, "borderWidth": 1, "fontFamily": "Roboto", @@ -65,7 +65,7 @@ exports[`AddCustomToken should render correctly 1`] = ` {leftText && ( @@ -89,7 +98,7 @@ export default class AssetActionButtons extends Component { @@ -99,7 +108,16 @@ export default class AssetActionButtons extends Component { - + {middleType === 'add' ? ( + + ) : ( + + )} {middleText} diff --git a/app/components/UI/AssetElement/__snapshots__/index.test.js.snap b/app/components/UI/AssetElement/__snapshots__/index.test.js.snap index e9114eec1a1..81ccef7894d 100644 --- a/app/components/UI/AssetElement/__snapshots__/index.test.js.snap +++ b/app/components/UI/AssetElement/__snapshots__/index.test.js.snap @@ -7,7 +7,7 @@ exports[`AssetElement should render correctly 1`] = ` onPress={[Function]} style={ Object { - "borderBottomColor": "#CCCCCC", + "borderBottomColor": "#d6d9dc", "borderBottomWidth": 0.5, "flex": 1, "flexDirection": "row", diff --git a/app/components/UI/AssetElement/index.js b/app/components/UI/AssetElement/index.js index e8a711c5f53..16567209f20 100644 --- a/app/components/UI/AssetElement/index.js +++ b/app/components/UI/AssetElement/index.js @@ -11,7 +11,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 15, paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: colors.borderColor + borderBottomColor: colors.grey100 }, arrow: { flex: 1, diff --git a/app/components/UI/AssetIcon/index.js b/app/components/UI/AssetIcon/index.js index 776152b0332..f5e142abf24 100644 --- a/app/components/UI/AssetIcon/index.js +++ b/app/components/UI/AssetIcon/index.js @@ -18,16 +18,20 @@ const styles = StyleSheet.create({ // eslint-disable-next-line react/display-name const AssetIcon = React.memo(props => { if (!props.logo) return null; - const uri = getAssetLogoPath(props.logo); + const uri = props.watchedAsset ? props.logo : getAssetLogoPath(props.logo); const style = [styles.logo, props.customStyle]; return ; }); AssetIcon.propTypes = { /** - * String of the asset icon + * String of the asset icon to be searched in contractMap */ logo: PropTypes.string, + /** + * Whether logo has to be fetched from eth-contract-metadata + */ + watchedAsset: PropTypes.bool, /** * Custom style to apply to image */ diff --git a/app/components/UI/AssetOverview/__snapshots__/index.test.js.snap b/app/components/UI/AssetOverview/__snapshots__/index.test.js.snap index 69f6e7ef7d0..85c478562a6 100644 --- a/app/components/UI/AssetOverview/__snapshots__/index.test.js.snap +++ b/app/components/UI/AssetOverview/__snapshots__/index.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AssetOverview should render correctly 1`] = ` - `; diff --git a/app/components/UI/AssetOverview/index.js b/app/components/UI/AssetOverview/index.js index e6993db38b5..d345973e6ad 100644 --- a/app/components/UI/AssetOverview/index.js +++ b/app/components/UI/AssetOverview/index.js @@ -9,13 +9,15 @@ import AssetActionButtons from '../AssetActionButtons'; import { setTokensTransaction } from '../../../actions/transaction'; import { toggleReceiveModal } from '../../../actions/modals'; import { connect } from 'react-redux'; +import { toChecksumAddress } from 'ethereumjs-util'; +import { renderFromTokenMinimalUnit, balanceToFiat, renderFromWei, weiToFiat, hexToBN } from '../../../util/number'; const styles = StyleSheet.create({ wrapper: { flex: 1, padding: 20, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: colors.borderColor, + borderBottomColor: colors.grey100, alignContent: 'center', alignItems: 'center', paddingBottom: 30 @@ -56,6 +58,10 @@ const ethLogo = require('../../../images/eth-logo.png'); // eslint-disable-line */ class AssetOverview extends Component { static propTypes = { + /** + * Map of accounts to information objects including balances + */ + accounts: PropTypes.object, /** /* navigation object required to access the props /* passed by the parent component @@ -65,23 +71,48 @@ class AssetOverview extends Component { * Object that represents the asset to be displayed */ asset: PropTypes.object, + /** + * ETH to current currency conversion rate + */ + conversionRate: PropTypes.number, + /** + * Currency code of the currently-active currency + */ + currentCurrency: PropTypes.string, + /** + * A string that represents the selected address + */ + selectedAddress: PropTypes.string, /** * Action that sets a tokens type transaction */ setTokensTransaction: PropTypes.func.isRequired, + /** + * An object containing token balances for current account and network in the format address => balance + */ + tokenBalances: PropTypes.object, + /** + * An object containing token exchange rates in the format address => exchangeRate + */ + tokenExchangeRates: PropTypes.object, /** * Action that toggles the receive modal */ - toggleReceiveModal: PropTypes.func + toggleReceiveModal: PropTypes.func, + /** + * Primary currency, either ETH or Fiat + */ + primaryCurrency: PropTypes.string }; - onDeposit = () => { - this.props.toggleReceiveModal(); + onReceive = () => { + const { asset } = this.props; + this.props.toggleReceiveModal(asset); }; onSend = async () => { const { asset } = this.props; - if (asset.symbol === 'ETH') { + if (asset.isEth) { this.props.setTokensTransaction({ symbol: 'ETH' }); this.props.navigation.navigate('SendView'); } else { @@ -92,45 +123,93 @@ class AssetOverview extends Component { renderLogo = () => { const { - asset: { address, logo, symbol } + asset: { address, image, logo, isETH } } = this.props; - if (symbol === 'ETH') { + if (isETH) { return ; } - return logo ? : ; + const watchedAsset = image !== undefined; + return logo || image ? ( + + ) : ( + + ); }; render() { const { - asset: { symbol, balance, balanceFiat } + accounts, + asset, + primaryCurrency, + selectedAddress, + tokenExchangeRates, + tokenBalances, + conversionRate, + currentCurrency } = this.props; + let mainBalance, secondaryBalance; + const itemAddress = (asset.address && toChecksumAddress(asset.address)) || undefined; + let balance, balanceFiat; + if (asset.isETH) { + balance = renderFromWei(accounts[selectedAddress] && accounts[selectedAddress].balance); + balanceFiat = weiToFiat( + hexToBN(accounts[selectedAddress].balance), + conversionRate, + currentCurrency.toUpperCase() + ); + } else { + const exchangeRate = itemAddress in tokenExchangeRates ? tokenExchangeRates[itemAddress] : undefined; + balance = + itemAddress in tokenBalances + ? renderFromTokenMinimalUnit(tokenBalances[itemAddress], asset.decimals) + : 0; + balanceFiat = balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency); + } + // choose balances depending on 'primaryCurrency' + if (primaryCurrency === 'ETH') { + mainBalance = balance + ' ' + asset.symbol; + secondaryBalance = balanceFiat; + } else { + mainBalance = !balanceFiat ? balance + ' ' + asset.symbol : balanceFiat; + secondaryBalance = !balanceFiat ? balanceFiat : balance + ' ' + asset.symbol; + } + return ( {this.renderLogo()} - - {balance} {symbol} - - {balanceFiat} + {mainBalance} + {secondaryBalance} ); } } +const mapStateToProps = state => ({ + accounts: state.engine.backgroundState.AccountTrackerController.accounts, + conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, + currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, + primaryCurrency: state.settings.primaryCurrency, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + tokenBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, + tokenExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates +}); + const mapDispatchToProps = dispatch => ({ setTokensTransaction: asset => dispatch(setTokensTransaction(asset)), - toggleReceiveModal: () => dispatch(toggleReceiveModal()) + toggleReceiveModal: asset => dispatch(toggleReceiveModal(asset)) }); export default connect( - null, + mapStateToProps, mapDispatchToProps )(AssetOverview); diff --git a/app/components/UI/AssetOverview/index.test.js b/app/components/UI/AssetOverview/index.test.js index 95db74eb6ca..27aa7b4557f 100644 --- a/app/components/UI/AssetOverview/index.test.js +++ b/app/components/UI/AssetOverview/index.test.js @@ -2,11 +2,18 @@ import React from 'react'; import AssetOverview from './'; import configureMockStore from 'redux-mock-store'; import { shallow } from 'enzyme'; +import { Provider } from 'react-redux'; + const mockStore = configureMockStore(); +const store = mockStore({}); describe('AssetOverview', () => { it('should render correctly', () => { - const initialState = {}; + const initialState = { + settings: { + primaryCurrency: 'ETH' + } + }; const asset = { balance: 4, balanceFiat: 1500, @@ -15,9 +22,14 @@ describe('AssetOverview', () => { name: 'Ethereum' }; - const wrapper = shallow(, { - context: { store: mockStore(initialState) } - }); + const wrapper = shallow( + + + , + { + context: { store: mockStore(initialState) } + } + ); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/UI/AssetSearch/__snapshots__/index.test.js.snap b/app/components/UI/AssetSearch/__snapshots__/index.test.js.snap index a53b7388292..b17deaccf0b 100644 --- a/app/components/UI/AssetSearch/__snapshots__/index.test.js.snap +++ b/app/components/UI/AssetSearch/__snapshots__/index.test.js.snap @@ -5,7 +5,7 @@ exports[`AssetSearch should render correctly 1`] = ` style={ Object { "alignItems": "center", - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "borderRadius": 4, "borderWidth": 1, "flex": 1, diff --git a/app/components/UI/AssetSearch/index.js b/app/components/UI/AssetSearch/index.js index fdf82af65f0..65aa083be84 100644 --- a/app/components/UI/AssetSearch/index.js +++ b/app/components/UI/AssetSearch/index.js @@ -17,7 +17,7 @@ const styles = StyleSheet.create({ alignItems: 'center', borderWidth: 1, borderRadius: 4, - borderColor: colors.borderColor + borderColor: colors.grey100 }, textInput: { flex: 1, diff --git a/app/components/UI/BrowserFeatured/index.js b/app/components/UI/BrowserFeatured/index.js index 2f3d03e3597..3f39ac3e8f8 100644 --- a/app/components/UI/BrowserFeatured/index.js +++ b/app/components/UI/BrowserFeatured/index.js @@ -6,6 +6,8 @@ import Button from '../Button'; import { StyleSheet, View } from 'react-native'; import { colors } from '../../../styles/common'; import DeviceSize from '../../../util/DeviceSize'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; const styles = StyleSheet.create({ wrapper: { @@ -33,6 +35,11 @@ export default class BrowserFeatured extends Component { self = React.createRef(); + onPress = url => { + this.props.goTo(url); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.BROWSER_FEATURED_APPS_OPEN); + }; + measureMyself(cb) { this.self && this.self.current && this.self.current.measure(cb); } @@ -43,7 +50,7 @@ export default class BrowserFeatured extends Component { diff --git a/app/components/UI/Button/__snapshots__/index.test.js.snap b/app/components/UI/Button/__snapshots__/index.test.js.snap index 216dfe28e97..cd39d46d0e9 100644 --- a/app/components/UI/Button/__snapshots__/index.test.js.snap +++ b/app/components/UI/Button/__snapshots__/index.test.js.snap @@ -6,7 +6,7 @@ exports[`Button should render correctly 1`] = ` Array [ Object { "alignItems": "center", - "backgroundColor": "#008edf", + "backgroundColor": "#037dd6", "borderRadius": 4, "flex": 1, "height": 40, diff --git a/app/components/UI/Button/index.js b/app/components/UI/Button/index.js index 1abc1abd003..ab97575e6de 100644 --- a/app/components/UI/Button/index.js +++ b/app/components/UI/Button/index.js @@ -9,7 +9,7 @@ const styles = StyleSheet.create({ flex: 1, alignItems: 'center', justifyContent: 'center', - backgroundColor: colors.primary, + backgroundColor: colors.blue, paddingVertical: 10, paddingHorizontal: 15, height: 40, diff --git a/app/components/UI/CollectibleContractInformation/__snapshots__/index.test.js.snap b/app/components/UI/CollectibleContractInformation/__snapshots__/index.test.js.snap index 3e47dcf3639..c736cb01578 100644 --- a/app/components/UI/CollectibleContractInformation/__snapshots__/index.test.js.snap +++ b/app/components/UI/CollectibleContractInformation/__snapshots__/index.test.js.snap @@ -14,7 +14,7 @@ exports[`CollectibleContractInformation should render correctly 1`] = ` style={ Object { "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", } } > @@ -53,7 +53,7 @@ exports[`CollectibleContractInformation should render correctly 1`] = ` @@ -57,6 +58,7 @@ exports[`CollectibleContractOverview should render correctly 1`] = ` { - this.props.navigation.push('AddAsset', { assetType: 'collectible' }); + const { navigation, collectibleContract } = this.props; + navigation.push('AddAsset', { assetType: 'collectible', collectibleContract }); }; onSend = () => { @@ -117,6 +119,7 @@ class CollectibleContractOverview extends Component { onLeftPress={this.onSend} onMiddlePress={this.onAdd} onRightPress={this.onInfo} + middleType={'add'} /> ); diff --git a/app/components/UI/CollectibleContracts/__snapshots__/index.test.js.snap b/app/components/UI/CollectibleContracts/__snapshots__/index.test.js.snap index 1b0f061f1f8..794a4329d90 100644 --- a/app/components/UI/CollectibleContracts/__snapshots__/index.test.js.snap +++ b/app/components/UI/CollectibleContracts/__snapshots__/index.test.js.snap @@ -67,7 +67,7 @@ exports[`CollectibleContracts should render correctly 1`] = ` ( - + {strings('wallet.add_collectibles').toUpperCase()} diff --git a/app/components/UI/CollectibleOverview/index.js b/app/components/UI/CollectibleOverview/index.js index 5863ba22273..1792f201097 100644 --- a/app/components/UI/CollectibleOverview/index.js +++ b/app/components/UI/CollectibleOverview/index.js @@ -28,7 +28,7 @@ const styles = StyleSheet.create({ }, content: { fontSize: 14, - color: colors.gray, + color: colors.grey400, paddingTop: 10, ...fontStyles.normal }, @@ -41,7 +41,7 @@ const styles = StyleSheet.create({ }, tokenId: { fontSize: 12, - color: colors.gray, + color: colors.grey400, marginTop: 8, ...fontStyles.normal } diff --git a/app/components/UI/Collectibles/index.js b/app/components/UI/Collectibles/index.js index 490aec26b38..56ac2862451 100644 --- a/app/components/UI/Collectibles/index.js +++ b/app/components/UI/Collectibles/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { ScrollView, RefreshControl, FlatList, StyleSheet, Text, View } from 'react-native'; +import { Alert, ScrollView, RefreshControl, FlatList, StyleSheet, Text, View } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import ActionSheet from 'react-native-actionsheet'; @@ -41,7 +41,7 @@ const styles = StyleSheet.create({ tokenId: { fontSize: 12, marginTop: 4, - color: colors.gray, + color: colors.grey400, ...fontStyles.normal } }); @@ -109,7 +109,8 @@ export default class Collectibles extends Component { removeCollectible = () => { const { AssetsController } = Engine.context; - AssetsController.removeCollectible(this.collectibleToRemove.address, this.collectibleToRemove.tokenId); + AssetsController.removeAndIgnoreCollectible(this.collectibleToRemove.address, this.collectibleToRemove.tokenId); + Alert.alert(strings('wallet.collectible_removed_title'), strings('wallet.collectible_removed_desc')); }; createActionSheetRef = ref => { diff --git a/app/components/UI/CustomGas/index.js b/app/components/UI/CustomGas/index.js index 37da3570aaf..7ec83d24d64 100644 --- a/app/components/UI/CustomGas/index.js +++ b/app/components/UI/CustomGas/index.js @@ -8,15 +8,22 @@ import { getRenderableEthGasFee, getRenderableFiatGasFee, apiEstimateModifiedToWEI, - fetchBasicGasEstimates + fetchBasicGasEstimates, + convertApiValueToGWEI } from '../../../util/custom-gas'; import { BN } from 'ethereumjs-util'; import { fromWei } from '../../../util/number'; +import Logger from '../../../util/Logger'; +import { getTicker } from '../../../util/transactions'; + +const AVERAGE_GAS = 20; +const LOW_GAS = 10; +const FAST_GAS = 40; const styles = StyleSheet.create({ selectors: { backgroundColor: colors.white, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1, flex: 1, @@ -37,7 +44,7 @@ const styles = StyleSheet.create({ marginTop: 10 }, average: { - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRightWidth: 1, borderLeftWidth: 1 }, @@ -60,12 +67,12 @@ const styles = StyleSheet.create({ ...fontStyles.bold }, textAdvancedOptions: { - color: colors.primary + color: colors.blue }, gasInput: { ...fontStyles.bold, backgroundColor: colors.white, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1, fontSize: 16, @@ -77,7 +84,7 @@ const styles = StyleSheet.create({ marginTop: 5 }, warningText: { - color: colors.error, + color: colors.red, ...fontStyles.normal } }); @@ -106,7 +113,15 @@ class CustomGas extends Component { /** * Object BN containing estimated gas limit */ - gas: PropTypes.object + gas: PropTypes.object, + /** + * Callback to modify state in parent state + */ + onPress: PropTypes.func, + /** + * Current provider ticker + */ + ticker: PropTypes.string }; state = { @@ -120,7 +135,7 @@ class CustomGas extends Component { selected: 'average', ready: false, advancedCustomGas: false, - customGasPrice: '20', + customGasPrice: '10', customGasLimit: this.props.gas.toNumber().toString(), warningGasLimit: '', warningGasPrice: '' @@ -128,46 +143,50 @@ class CustomGas extends Component { onPressGasFast = () => { const { fastGwei } = this.state; - const { gas } = this.props; + const { gas, onPress } = this.props; + onPress && onPress(); this.setState({ gasFastSelected: true, gasAverageSelected: false, gasSlowSelected: false, selected: 'fast', - customGasPrice: fastGwei.toString() + customGasPrice: fastGwei }); this.props.handleGasFeeSelection(gas, apiEstimateModifiedToWEI(fastGwei)); }; onPressGasAverage = () => { const { averageGwei } = this.state; - const { gas } = this.props; + const { gas, onPress } = this.props; + onPress && onPress(); this.setState({ gasFastSelected: false, gasAverageSelected: true, gasSlowSelected: false, selected: 'average', - customGasPrice: averageGwei.toString() + customGasPrice: averageGwei }); this.props.handleGasFeeSelection(gas, apiEstimateModifiedToWEI(averageGwei)); }; onPressGasSlow = () => { const { safeLowGwei } = this.state; - const { gas } = this.props; + const { gas, onPress } = this.props; + onPress && onPress(); this.setState({ gasFastSelected: false, gasAverageSelected: false, gasSlowSelected: true, selected: 'slow', - customGasPrice: safeLowGwei.toString() + customGasPrice: safeLowGwei }); this.props.handleGasFeeSelection(gas, apiEstimateModifiedToWEI(safeLowGwei)); }; onAdvancedOptions = () => { const { advancedCustomGas, selected, fastGwei, averageGwei, safeLowGwei, customGasPrice } = this.state; - const { gas } = this.props; + const { gas, onPress } = this.props; + onPress && onPress(); if (advancedCustomGas) { switch (selected) { case 'slow': @@ -190,16 +209,26 @@ class CustomGas extends Component { componentDidMount = async () => { await this.handleFetchBasicEstimates(); this.onPressGasAverage(); + const { ticker } = this.props; + if (ticker && ticker !== 'ETH') { + this.setState({ advancedCustomGas: true }); + } }; handleFetchBasicEstimates = async () => { this.setState({ ready: false }); - const basicGasEstimates = await fetchBasicGasEstimates(); + let basicGasEstimates; + try { + basicGasEstimates = await fetchBasicGasEstimates(); + } catch (error) { + Logger.log('Error while trying to get gas limit estimates', error); + basicGasEstimates = { average: AVERAGE_GAS, safeLow: LOW_GAS, fast: FAST_GAS }; + } const { average, fast, safeLow } = basicGasEstimates; this.setState({ - averageGwei: average, - fastGwei: fast, - safeLowGwei: safeLow, + averageGwei: convertApiValueToGWEI(average), + fastGwei: convertApiValueToGWEI(fast), + safeLowGwei: convertApiValueToGWEI(safeLow), ready: true }); }; @@ -220,6 +249,7 @@ class CustomGas extends Component { renderCustomGasSelector = () => { const { averageGwei, fastGwei, safeLowGwei } = this.state; const { conversionRate, currentCurrency, gas } = this.props; + const ticker = getTicker(this.props.ticker); return ( {strings('transaction.gas_fee_slow')} - {getRenderableEthGasFee(safeLowGwei, gas)} {strings('unit.eth')} + {getRenderableEthGasFee(safeLowGwei, gas)} {ticker} - {getRenderableFiatGasFee(safeLowGwei, conversionRate, currentCurrency, gas).toUpperCase()} + {getRenderableFiatGasFee(safeLowGwei, conversionRate, currentCurrency.toUpperCase(), gas)} - {getRenderableEthGasFee(averageGwei, gas)} {strings('unit.eth')} + {getRenderableEthGasFee(averageGwei, gas)} {ticker} - {getRenderableFiatGasFee(averageGwei, conversionRate, currentCurrency, gas).toUpperCase()} + {getRenderableFiatGasFee(averageGwei, conversionRate, currentCurrency.toUpperCase(), gas)} {strings('transaction.gas_fee_fast')} - {getRenderableEthGasFee(fastGwei, gas)} {strings('unit.eth')} + {getRenderableEthGasFee(fastGwei, gas)} {ticker} - {getRenderableFiatGasFee(fastGwei, conversionRate, currentCurrency, gas).toUpperCase()} + {getRenderableFiatGasFee(fastGwei, conversionRate, currentCurrency.toUpperCase(), gas)} @@ -288,10 +318,11 @@ class CustomGas extends Component { renderCustomGasInput = () => { const { customGasLimit, customGasPrice, warningGasLimit, warningGasPrice } = this.state; const { totalGas } = this.props; + const ticker = getTicker(this.props.ticker); return ( - {fromWei(totalGas)} {strings('unit.eth')} + {fromWei(totalGas)} {ticker} {strings('custom_gas.gas_limit')} {warningGasPrice} @@ -341,7 +372,8 @@ class CustomGas extends Component { const mapStateToProps = state => ({ conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, - currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency + currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, + ticker: state.engine.backgroundState.NetworkController.provider.ticker }); export default connect(mapStateToProps)(CustomGas); diff --git a/app/components/UI/CustomGas/index.test.js b/app/components/UI/CustomGas/index.test.js index 23b7a3e4a7a..32615ed9f5b 100644 --- a/app/components/UI/CustomGas/index.test.js +++ b/app/components/UI/CustomGas/index.test.js @@ -16,6 +16,11 @@ describe('CustomGas', () => { CurrencyRateController: { currentCurrency: 'usd', conversionRate: 0.1 + }, + NetworkController: { + provider: { + ticker: 'ETH' + } } } } diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js index bd6aa6c1c4d..599bc8dfc45 100644 --- a/app/components/UI/DrawerView/index.js +++ b/app/components/UI/DrawerView/index.js @@ -9,18 +9,16 @@ import { StyleSheet, Text, ScrollView, - Dimensions, InteractionManager } from 'react-native'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import QRCode from 'react-native-qrcode-svg'; import Share from 'react-native-share'; // eslint-disable-line import/default import Icon from 'react-native-vector-icons/FontAwesome'; import FeatherIcon from 'react-native-vector-icons/Feather'; import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons'; import { colors, fontStyles } from '../../../styles/common'; -import { hasBlockExplorer } from '../../../util/networks'; +import { hasBlockExplorer, findBlockExplorerForRpc, getBlockExplorerName } from '../../../util/networks'; import Identicon from '../Identicon'; import StyledButton from '../StyledButton'; import AccountList from '../AccountList'; @@ -43,7 +41,13 @@ import ActionModal from '../ActionModal'; import DeviceInfo from 'react-native-device-info'; import Logger from '../../../util/Logger'; import DeviceSize from '../../../util/DeviceSize'; +import OnboardingWizard from '../OnboardingWizard'; +import ReceiveRequest from '../ReceiveRequest'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; +import URL from 'url-parse'; +const ANDROID_OFFSET = 30; const styles = StyleSheet.create({ wrapper: { flex: 1, @@ -51,7 +55,7 @@ const styles = StyleSheet.create({ }, header: { paddingTop: DeviceSize.isIphoneX() ? 60 : 24, - backgroundColor: colors.drawerBg, + backgroundColor: colors.grey000, height: DeviceSize.isIphoneX() ? 110 : 74, flexDirection: 'column', paddingBottom: 0 @@ -85,10 +89,10 @@ const styles = StyleSheet.create({ }, account: { flex: 1, - backgroundColor: colors.drawerBg + backgroundColor: colors.grey000 }, accountBgOverlay: { - borderBottomColor: colors.borderColor, + borderBottomColor: colors.grey100, borderBottomWidth: 1, padding: 17 }, @@ -101,7 +105,7 @@ const styles = StyleSheet.create({ borderRadius: 96, borderWidth: 2, padding: 2, - borderColor: colors.primary + borderColor: colors.blue }, accountNameWrapper: { flexDirection: 'row', @@ -136,7 +140,7 @@ const styles = StyleSheet.create({ }, buttons: { flexDirection: 'row', - borderBottomColor: colors.borderColor, + borderBottomColor: colors.grey100, borderBottomWidth: 1, padding: 15 }, @@ -157,7 +161,7 @@ const styles = StyleSheet.create({ marginTop: Platform.OS === 'ios' ? 0 : -23, paddingBottom: Platform.OS === 'ios' ? 0 : 3, fontSize: 15, - color: colors.primary, + color: colors.blue, ...fontStyles.normal }, buttonContent: { @@ -168,13 +172,19 @@ const styles = StyleSheet.create({ buttonIcon: { marginTop: 0 }, + buttonReceive: { + transform: + Platform.OS === 'ios' + ? [{ rotate: '90deg' }] + : [{ rotate: '90deg' }, { translateX: ANDROID_OFFSET }, { translateY: ANDROID_OFFSET }] + }, menu: {}, noTopBorder: { borderTopWidth: 0 }, menuSection: { borderTopWidth: 1, - borderColor: colors.borderColor, + borderColor: colors.grey100, paddingVertical: 10 }, menuItem: { @@ -184,20 +194,20 @@ const styles = StyleSheet.create({ paddingLeft: 17 }, selectedRoute: { - backgroundColor: colors.primaryOpacity, + backgroundColor: colors.blue000, marginRight: 10, borderTopRightRadius: 20, borderBottomRightRadius: 20 }, selectedName: { - color: colors.primary + color: colors.blue }, menuItemName: { flex: 1, - paddingLeft: 15, + paddingHorizontal: 15, paddingTop: 2, fontSize: 16, - color: colors.gray, + color: colors.grey400, ...fontStyles.normal }, noIcon: { @@ -229,40 +239,6 @@ const styles = StyleSheet.create({ textAlign: 'center', ...fontStyles.bold }, - detailsWrapper: { - padding: 10, - alignItems: 'center' - }, - qrCode: { - marginVertical: 15, - alignItems: 'center', - justifyContent: 'center', - padding: 40, - backgroundColor: colors.concrete, - borderRadius: 8 - }, - addressWrapper: { - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 15, - paddingVertical: 10, - marginTop: 10, - marginBottom: 20, - marginRight: 10, - marginLeft: 10, - borderRadius: 5, - backgroundColor: colors.concrete - }, - addressTitle: { - fontSize: 16, - marginBottom: 10, - ...fontStyles.normal - }, - address: { - fontSize: Platform.OS === 'ios' ? 17 : 20, - letterSpacing: 2, - fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' - }, secureModalText: { textAlign: 'center', fontSize: 13, @@ -282,12 +258,19 @@ const styles = StyleSheet.create({ paddingVertical: 3, borderRadius: 10, borderWidth: 1, - borderColor: colors.another50ShadesOfGrey + borderColor: colors.grey400 }, importedText: { - color: colors.another50ShadesOfGrey, + color: colors.grey400, fontSize: 10, ...fontStyles.bold + }, + onboardingContainer: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 315 - DeviceSize.getDeviceWidth() } }); @@ -368,7 +351,19 @@ class DrawerView extends Component { /** * Boolean that determines if the user has set a password before */ - passwordSet: PropTypes.bool + passwordSet: PropTypes.bool, + /** + * Wizard onboarding state + */ + wizard: PropTypes.object, + /** + * Current provider ticker + */ + ticker: PropTypes.string, + /** + * Frequent RPC list from PreferencesController + */ + frequentRpcList: PropTypes.array }; state = { @@ -429,6 +424,7 @@ class DrawerView extends Component { this.animatingAccountsModal = false; }, 500); } + !this.props.accountsModalVisible && this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_ACCOUNT_NAME); }; toggleReceiveModal = () => { @@ -459,37 +455,50 @@ class DrawerView extends Component { }; showReceiveModal = () => { - this.props.toggleReceiveModal(); + this.toggleReceiveModal(); + }; + + trackEvent = event => { + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(event); + }); }; onReceive = () => { - this.props.toggleReceiveModal(); + this.toggleReceiveModal(); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_RECEIVE); }; onSend = async () => { - this.props.setTokensTransaction({ symbol: 'ETH' }); + const { ticker } = this.props; + this.props.setTokensTransaction({ symbol: ticker, isETH: true }); this.props.navigation.navigate('SendView'); this.hideDrawer(); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_SEND); }; goToBrowser = () => { this.props.navigation.navigate('BrowserTabHome'); this.hideDrawer(); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_BROWSER); }; showWallet = () => { this.props.navigation.navigate('WalletTabHome'); this.hideDrawer(); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_WALLET); }; goToTransactionHistory = () => { this.props.navigation.navigate('TransactionsHome'); this.hideDrawer(); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_TRANSACTION_HISTORY); }; showSettings = async () => { this.props.navigation.navigate('SettingsView'); this.hideDrawer(); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_SETTINGS); }; logout = () => { @@ -510,20 +519,36 @@ class DrawerView extends Component { if (!passwordSet) { this.props.navigation.navigate('Onboarding'); } else { - this.props.navigation.navigate('Entry'); + this.props.navigation.navigate('Login'); } } } ], { cancelable: false } ); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_LOGOUT); }; viewInEtherscan = () => { - const { selectedAddress, network } = this.props; - const url = getEtherscanAddressUrl(network.provider.type, selectedAddress); - const etherscan_url = getEtherscanBaseUrl(network.provider.type).replace('https://', ''); - this.goToBrowserUrl(url, etherscan_url); + const { + selectedAddress, + network, + network: { + provider: { rpcTarget } + }, + frequentRpcList + } = this.props; + if (network.provider.type === 'rpc') { + const blockExplorer = findBlockExplorerForRpc(rpcTarget, frequentRpcList); + const url = `${blockExplorer}/address/${selectedAddress}`; + const title = new URL(blockExplorer).hostname; + this.goToBrowserUrl(url, title); + } else { + const url = getEtherscanAddressUrl(network.provider.type, selectedAddress); + const etherscan_url = getEtherscanBaseUrl(network.provider.type).replace('https://', ''); + this.goToBrowserUrl(url, etherscan_url); + } + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_VIEW_ETHERSCAN); }; submitFeedback = () => { @@ -558,6 +583,7 @@ class DrawerView extends Component { showHelp = () => { this.goToBrowserUrl('https://support.metamask.io', strings('drawer.metamask_support')); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_GET_HELP); }; goToBrowserUrl(url, title) { @@ -590,16 +616,32 @@ class DrawerView extends Component { this.hideDrawer(); }; + hasBlockExplorer = providerType => { + const { frequentRpcList } = this.props; + if (providerType === 'rpc') { + const { + network: { + provider: { rpcTarget } + } + } = this.props; + const blockExplorer = findBlockExplorerForRpc(rpcTarget, frequentRpcList); + if (blockExplorer) { + return true; + } + } + return hasBlockExplorer(providerType); + }; + getIcon(name, size) { - return ; + return ; } getFeatherIcon(name, size) { - return ; + return ; } getMaterialIcon(name, size) { - return ; + return ; } getImageIcon(name) { @@ -607,72 +649,87 @@ class DrawerView extends Component { } getSelectedIcon(name, size) { - return ; + return ; } getSelectedFeatherIcon(name, size) { - return ; + return ; } getSelectedMaterialIcon(name, size) { - return ; + return ; } getSelectedImageIcon(name) { return ; } - getSections = () => [ - [ - { - name: strings('drawer.browser'), - icon: this.getIcon('globe'), - selectedIcon: this.getSelectedIcon('globe'), - action: this.goToBrowser, - routeNames: ['BrowserView', 'AddBookmark'] - }, - { - name: strings('drawer.wallet'), - icon: this.getImageIcon('wallet'), - selectedIcon: this.getSelectedImageIcon('wallet'), - action: this.showWallet, - routeNames: ['WalletView', 'Asset', 'AddAsset', 'Collectible', 'CollectibleView'] - }, - { - name: strings('drawer.transaction_history'), - icon: this.getFeatherIcon('list'), - selectedIcon: this.getSelectedFeatherIcon('list'), - action: this.goToTransactionHistory, - routeNames: ['TransactionsView'] - } - ], - [ - { - name: strings('drawer.share_address'), - icon: this.getMaterialIcon('share-variant'), - action: this.onShare + getSections = () => { + const { + network: { + provider: { type, rpcTarget } }, - { - name: strings('drawer.view_in_etherscan'), - icon: this.getIcon('eye'), - action: this.viewInEtherscan - } - ], - [ - { - name: strings('drawer.help'), - action: this.showHelp - }, - { - name: strings('drawer.submit_feedback'), - action: this.submitFeedback - }, - { - name: strings('drawer.logout'), - action: this.logout - } - ] - ]; + frequentRpcList + } = this.props; + let blockExplorer, blockExplorerName; + if (type === 'rpc') { + blockExplorer = findBlockExplorerForRpc(rpcTarget, frequentRpcList); + blockExplorerName = getBlockExplorerName(blockExplorer); + } + return [ + [ + { + name: strings('drawer.browser'), + icon: this.getIcon('globe'), + selectedIcon: this.getSelectedIcon('globe'), + action: this.goToBrowser, + routeNames: ['BrowserView', 'AddBookmark'] + }, + { + name: strings('drawer.wallet'), + icon: this.getImageIcon('wallet'), + selectedIcon: this.getSelectedImageIcon('wallet'), + action: this.showWallet, + routeNames: ['WalletView', 'Asset', 'AddAsset', 'Collectible', 'CollectibleView'] + }, + { + name: strings('drawer.transaction_history'), + icon: this.getFeatherIcon('list'), + selectedIcon: this.getSelectedFeatherIcon('list'), + action: this.goToTransactionHistory, + routeNames: ['TransactionsView'] + } + ], + [ + { + name: strings('drawer.share_address'), + icon: this.getMaterialIcon('share-variant'), + action: this.onShare + }, + { + name: + (blockExplorer && `${strings('drawer.view_in')} ${blockExplorerName}`) || + strings('drawer.view_in_etherscan'), + icon: this.getIcon('eye'), + action: this.viewInEtherscan + } + ], + [ + { + name: strings('drawer.help'), + action: this.showHelp + }, + { + name: strings('drawer.submit_feedback'), + action: this.submitFeedback + }, + { + name: strings('drawer.logout'), + action: this.logout + } + ] + ]; + }; copyAccountToClipboard = async () => { const { selectedAddress } = this.props; @@ -695,6 +752,7 @@ class DrawerView extends Component { }).catch(err => { Logger.log('Error while trying to share address', err); }); + this.trackEvent(ANALYTICS_EVENT_OPTS.NAVIGATION_TAPS_SHARE_PUBLIC_ADDRESS); }; onSecureWalletModalAction = () => { @@ -710,8 +768,24 @@ class DrawerView extends Component { return route.routeName; } + /** + * Return step 5 of onboarding wizard if that is the current step + */ + renderOnboardingWizard = () => { + const { + wizard: { step } + } = this.props; + return ( + step === 5 && ( + + + + ) + ); + }; + render() { - const { network, accounts, identities, selectedAddress, keyrings, currentCurrency } = this.props; + const { network, accounts, identities, selectedAddress, keyrings, currentCurrency, ticker } = this.props; const account = { address: selectedAddress, ...identities[selectedAddress], ...accounts[selectedAddress] }; account.balance = (accounts[selectedAddress] && renderFromWei(accounts[selectedAddress].balance)) || 0; const fiatBalance = Engine.getTotalFiatAccountBalance(); @@ -721,7 +795,6 @@ class DrawerView extends Component { this.currentBalance = fiatBalance; const fiatBalanceStr = renderFiat(this.currentBalance, currentCurrency); const currentRoute = this.findRouteNameFromNavigatorState(this.props.navigation.state); - return ( @@ -756,7 +829,7 @@ class DrawerView extends Component { - ${fiatBalanceStr} + {fiatBalanceStr} {renderShortAddress(account.address)} {this.isCurrentAccountImported() && ( @@ -778,7 +851,7 @@ class DrawerView extends Component { {strings('drawer.send_button')} @@ -789,7 +862,12 @@ class DrawerView extends Component { containerStyle={[styles.button, styles.rightButton]} style={styles.buttonContent} > - + {strings('drawer.receive_button')} @@ -802,7 +880,7 @@ class DrawerView extends Component { {section .filter(item => { if (item.name.toLowerCase().indexOf('etherscan') !== -1) { - return hasBlockExplorer(network.provider.type); + return this.hasBlockExplorer(network.provider.type); } return true; }) @@ -830,6 +908,7 @@ class DrawerView extends Component { ? styles.selectedName : null ]} + numberOfLines={1} > {item.name} @@ -863,8 +942,10 @@ class DrawerView extends Component { keyrings={keyrings} onAccountChange={this.onAccountChange} onImportAccount={this.onImportAccount} + ticker={ticker} /> + {this.renderOnboardingWizard()} - - - - - - - {strings('drawer.public_address')} - - - {selectedAddress} - - - + {!this.props.passwordSet && ( ({ selectedAddress: toChecksumAddress(state.engine.backgroundState.PreferencesController.selectedAddress), accounts: state.engine.backgroundState.AccountTrackerController.accounts, identities: state.engine.backgroundState.PreferencesController.identities, + frequentRpcList: state.engine.backgroundState.PreferencesController.frequentRpcList, currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, keyrings: state.engine.backgroundState.KeyringController.keyrings, networkModalVisible: state.modals.networkModalVisible, accountsModalVisible: state.modals.accountsModalVisible, receiveModalVisible: state.modals.receiveModalVisible, - passwordSet: state.user.passwordSet + passwordSet: state.user.passwordSet, + wizard: state.wizard, + ticker: state.engine.backgroundState.NetworkController.provider.ticker }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/UI/EthInput/index.js b/app/components/UI/EthInput/index.js index 192d7b4b528..8e2629c7e96 100644 --- a/app/components/UI/EthInput/index.js +++ b/app/components/UI/EthInput/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Platform, StyleSheet, Text, TextInput, View, Image } from 'react-native'; +import { Keyboard, ScrollView, Platform, StyleSheet, Text, TextInput, View, Image } from 'react-native'; import { colors, fontStyles } from '../../../styles/common'; import { connect } from 'react-redux'; import { @@ -8,15 +8,21 @@ import { balanceToFiat, fromTokenMinimalUnit, renderFromTokenMinimalUnit, - renderFromWei + renderFromWei, + toTokenMinimalUnit, + fiatNumberToTokenMinimalUnit, + toWei, + fiatNumberToWei, + isDecimal, + weiToFiatNumber } from '../../../util/number'; import { strings } from '../../../../locales/i18n'; import TokenImage from '../TokenImage'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import { ScrollView } from 'react-native-gesture-handler'; import ElevatedView from 'react-native-elevated-view'; import CollectibleImage from '../CollectibleImage'; import SelectableAsset from './SelectableAsset'; +import { getTicker } from '../../../util/transactions'; const styles = StyleSheet.create({ root: { @@ -30,7 +36,7 @@ const styles = StyleSheet.create({ paddingLeft: 14, position: 'relative', backgroundColor: colors.white, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1 }, @@ -43,14 +49,15 @@ const styles = StyleSheet.create({ paddingRight: 0, paddingLeft: 0, paddingTop: 0, - maxWidth: '80%' + maxWidth: '70%' }, eth: { ...fontStyles.bold, marginRight: 30, fontSize: 16, paddingTop: Platform.OS === 'android' ? 3 : 0, - paddingLeft: 10 + paddingLeft: 10, + alignSelf: 'center' }, fiatValue: { ...fontStyles.normal, @@ -76,7 +83,7 @@ const styles = StyleSheet.create({ borderRadius: 11 }, arrow: { - color: colors.inputBorderColor, + color: colors.grey100, position: 'absolute', right: 10, top: Platform.OS === 'android' ? 20 : 13 @@ -88,7 +95,7 @@ const styles = StyleSheet.create({ }, optionList: { backgroundColor: colors.white, - borderColor: colors.inputBorderColor, + borderColor: colors.grey100, borderRadius: 4, borderWidth: 1, paddingLeft: 14, @@ -166,21 +173,45 @@ class EthInput extends Component { /** * Transaction object associated with this transaction */ - transaction: PropTypes.object + transaction: PropTypes.object, + /** + * Callback to open assets dropdown + */ + openEthInput: PropTypes.func, + /** + * Whether assets dropdown is opened + */ + isOpen: PropTypes.bool, + /** + * Primary currency, either ETH or Fiat + */ + primaryCurrency: PropTypes.string, + /** + * Current provider ticker + */ + ticker: PropTypes.string }; - state = { isOpen: false, readableValue: undefined, assets: undefined }; + state = { readableValue: undefined, assets: undefined }; + /** + * Used to 'fillMax' feature. Will process value coming from parent to render corresponding values on input + */ componentDidUpdate = () => { const { fillMax, readableValue } = this.props; if (fillMax) { - this.setState({ readableValue }); + const { processedReadableValue } = this.processValue(readableValue); + this.setState({ readableValue: processedReadableValue }); } this.props.updateFillMax(false); }; + /** + * Depending on transaction type, fill 'readableValue' and 'assets' to be rendered in dorpdown asset selector + */ componentDidMount = () => { const { transaction, collectibles } = this.props; + const { processedReadableValue } = this.processValue(transaction.readableValue); switch (transaction.type) { case 'TOKENS_TRANSACTION': this.setState({ @@ -191,7 +222,7 @@ class EthInput extends Component { }, ...this.props.tokens ], - readableValue: transaction.readableValue + readableValue: processedReadableValue }); break; case 'ETHER_TRANSACTION': @@ -202,19 +233,18 @@ class EthInput extends Component { symbol: 'ETH' } ], - readableValue: transaction.readableValue + readableValue: processedReadableValue }); break; case 'INDIVIDUAL_TOKEN_TRANSACTION': this.setState({ assets: [transaction.selectedAsset], - readableValue: transaction.readableValue + readableValue: processedReadableValue }); break; case 'INDIVIDUAL_COLLECTIBLE_TRANSACTION': this.setState({ - assets: [transaction.selectedAsset], - readableValue: transaction.readableValue + assets: [transaction.selectedAsset] }); break; case 'CONTRACT_COLLECTIBLE_TRANSACTION': { @@ -222,53 +252,86 @@ class EthInput extends Component { collectible => collectible.address.toLowerCase() === transaction.selectedAsset.address.toLowerCase() ); this.setState({ - assets: collectiblesToShow, - readableValue: transaction.readableValue + assets: collectiblesToShow }); break; } } }; + /** + * Callback to openEthInput from props + */ onFocus = () => { - const { isOpen } = this.state; - this.setState({ isOpen: !isOpen }); + const { isOpen, openEthInput } = this.props; + openEthInput && openEthInput(!isOpen); }; + /** + * Selects new asset from assets dropdown selector + * + * @param {object} asset - Asset to be selected + */ selectAsset = async asset => { - this.setState({ isOpen: false }); - const { handleUpdateAsset, onChange } = this.props; + Keyboard.dismiss(); + const { handleUpdateAsset, onChange, openEthInput } = this.props; + openEthInput && openEthInput(false); handleUpdateAsset && (await handleUpdateAsset(asset)); onChange && onChange(undefined); this.setState({ readableValue: undefined }); }; + /** + * Depending on 'assetType' return element to be rendered in assets dropdown + * + * @param {object} asset - Asset to be rendered (ETH, ERC20 or ERC721) + * @param {func} onPress - Callback called when object is pressed + * @returns {object} - 'SelectableAsset' object with corresponding asset information + */ renderAsset = (asset, onPress) => { - const { assetType } = this.props.transaction; - let title, subTitle, icon; - if (assetType === 'ERC20' || assetType === 'ETH') { - const { tokenBalances, accounts, selectedAddress } = this.props; - title = asset.symbol; - subTitle = - asset.symbol !== 'ETH' - ? asset.address in tokenBalances + const { tokenBalances, accounts, selectedAddress, ticker } = this.props; + const assetsObject = { + ETH: () => { + const subTitle = renderFromWei(accounts[selectedAddress].balance) + ' ' + getTicker(ticker); + const icon = ; + return { title: getTicker(ticker), subTitle, icon }; + }, + ERC20: () => { + const title = asset.symbol; + const subTitle = + asset.address in tokenBalances ? renderFromTokenMinimalUnit(tokenBalances[asset.address], asset.decimals) + ' ' + asset.symbol - : undefined - : renderFromWei(accounts[selectedAddress].balance) + ' ' + asset.symbol; - icon = - asset.symbol !== 'ETH' ? ( - - ) : ( - + : undefined; + const icon = ; + return { title, subTitle, icon }; + }, + ERC721: () => { + const title = asset.name; + const subTitle = + strings('collectible.collectible_token_id') + strings('unit.colon') + ' ' + asset.tokenId; + const icon = ( + ); + return { title, subTitle, icon }; + } + }; + let assetType; + if (asset.isETH) { + assetType = 'ETH'; + } else if (asset.decimals) { + assetType = 'ERC20'; } else { - title = asset.name; - subTitle = strings('collectible.collectible_token_id') + strings('unit.colon') + ' ' + asset.tokenId; - icon = ; + assetType = 'ERC721'; } + const { title, subTitle, icon } = assetsObject[assetType](); return ; }; + /** + * Render assets list in a dropdown + * + * @returns {object} - Assets dropdown object in an elevated view + */ renderAssetsList = () => { const { assets } = this.state; const { @@ -282,7 +345,7 @@ class EthInput extends Component { const assetsList = assetsLists[assetType](); return ( - + {assetsList.map(asset => ( { + const { + transaction: { selectedAsset, assetType }, + conversionRate, + primaryCurrency, + contractExchangeRates + } = this.props; + let processedValue, processedReadableValue; + const decimal = isDecimal(value); + if (decimal) { + // Only for ETH or ERC20, depending on 'primaryCurrency' selected + switch (assetType) { + case 'ETH': + if (primaryCurrency === 'ETH') { + processedValue = toWei(value); + processedReadableValue = value; + } else { + processedValue = fiatNumberToWei(value, conversionRate); + processedReadableValue = weiToFiatNumber(toWei(value), conversionRate).toString(); + } + break; + case 'ERC20': { + const exchangeRate = + selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; + if (primaryCurrency !== 'ETH' && (exchangeRate && exchangeRate !== 0)) { + processedValue = fiatNumberToTokenMinimalUnit( + value, + conversionRate, + exchangeRate, + selectedAsset.decimals + ); + processedReadableValue = balanceToFiat(value, conversionRate, exchangeRate, ''); + } else { + processedValue = toTokenMinimalUnit(value, selectedAsset.decimals); + processedReadableValue = value; + } + } + } + } + return { processedValue, processedReadableValue }; + }; + + /** + * On value change, callback to props 'onChange' and update 'readableValue' + */ onChange = value => { const { onChange } = this.props; - onChange && onChange(value); + const { processedValue } = this.processValue(value); + onChange && onChange(processedValue, value); this.setState({ readableValue: value }); }; + /** + * Returns object to be rendered as input field. Only for ETH and ERC20 tokens + * + * @param {object} image - Image object of the asset + * @param {tsring} currency - String containing currency code + * @param {string} conversionRate - String containing amount depending on primary currency + * @returns {object} - View object to render as input field + */ + renderTokenInput = (image, currency, convertedAmount) => { + const { readonly } = this.props; + const { readableValue } = this.state; + return ( + + {image} + + + + + {currency} + + + {convertedAmount && ( + + {convertedAmount} + + )} + + + ); + }; + + /** + * Returns object to render input, depending on 'assetType' + * + * @returns {object} - View object to render as input field + */ renderInput = () => { const { currentCurrency, - readonly, contractExchangeRates, conversionRate, - transaction: { assetType, selectedAsset, value } + transaction: { assetType, selectedAsset, value }, + primaryCurrency, + ticker } = this.props; - const { readableValue } = this.state; + // Depending on 'assetType' return object with corresponding 'convertedAmount', 'currency' and 'image' const inputs = { ETH: () => { - const convertedAmount = weiToFiat(value, conversionRate, currentCurrency.toUpperCase()); - return ( - - - - - - - - - {strings('unit.eth')} - - - - {convertedAmount} - - - - ); + let convertedAmount, currency; + if (primaryCurrency === 'ETH') { + convertedAmount = weiToFiat(value, conversionRate, currentCurrency.toUpperCase()); + currency = getTicker(ticker); + } else { + convertedAmount = renderFromWei(value) + ' ' + getTicker(ticker); + currency = currentCurrency.toUpperCase(); + } + const image = ; + return this.renderTokenInput(image, currency, convertedAmount); }, ERC20: () => { - const exchangeRate = contractExchangeRates[selectedAsset.address]; - let convertedAmount; - if (exchangeRate) { - convertedAmount = balanceToFiat( - (value && fromTokenMinimalUnit(value, selectedAsset.decimals)) || 0, - conversionRate, - exchangeRate, - currentCurrency.toUpperCase() - ); + const exchangeRate = + selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; + let convertedAmount, currency; + if (exchangeRate && exchangeRate !== 0) { + if (primaryCurrency === 'ETH') { + const finalValue = (value && fromTokenMinimalUnit(value, selectedAsset.decimals)) || 0; + convertedAmount = balanceToFiat(finalValue, conversionRate, exchangeRate, currentCurrency); + currency = selectedAsset.symbol; + } else { + const finalValue = (value && renderFromTokenMinimalUnit(value, selectedAsset.decimals)) || 0; + convertedAmount = finalValue + ' ' + selectedAsset.symbol; + currency = currentCurrency.toUpperCase(); + } } else { convertedAmount = strings('transaction.conversion_not_available'); } - return ( - - - - - - - - - {selectedAsset.symbol} - - - - {convertedAmount} - - - - ); + const image = ; + return this.renderTokenInput(image, currency, convertedAmount); }, ERC721: () => ( @@ -412,7 +531,8 @@ class EthInput extends Component { }; render = () => { - const { isOpen, assets } = this.state; + const { assets } = this.state; + const { isOpen } = this.props; const selectAssets = assets && assets.length > 1; return ( @@ -435,7 +555,9 @@ const mapStateToProps = state => ({ tokens: state.engine.backgroundState.AssetsController.tokens, tokenBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, collectibles: state.engine.backgroundState.AssetsController.collectibles, - transaction: state.transaction + transaction: state.transaction, + primaryCurrency: state.settings.primaryCurrency, + ticker: state.engine.backgroundState.NetworkController.provider.ticker }); export default connect(mapStateToProps)(EthInput); diff --git a/app/components/UI/FadeOutOverlay/__snapshots__/index.test.js.snap b/app/components/UI/FadeOutOverlay/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..3d8972c2691 --- /dev/null +++ b/app/components/UI/FadeOutOverlay/__snapshots__/index.test.js.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FadeOutOverlay should render correctly 1`] = ` + +`; diff --git a/app/components/UI/FadeOutOverlay/index.js b/app/components/UI/FadeOutOverlay/index.js new file mode 100644 index 00000000000..c5c6cc73db7 --- /dev/null +++ b/app/components/UI/FadeOutOverlay/index.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Platform, Animated, StyleSheet } from 'react-native'; +import { colors } from '../../../styles/common'; + +const styles = StyleSheet.create({ + view: { + backgroundColor: colors.white, + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0 + } +}); + +/** + * View that is displayed to first time (new) users + */ +export default class FadeOutOverlay extends Component { + static propTypes = { + style: PropTypes.any, + duration: PropTypes.number + }; + + state = { + done: false + }; + + opacity = new Animated.Value(1); + + componentDidMount() { + Animated.timing(this.opacity, { + toValue: 0, + duration: this.props.duration, + useNativeDriver: true, + isInteraction: false + }).start(() => { + this.setState({ done: true }); + }); + } + + render() { + if (this.state.done) return null; + return ; + } +} + +FadeOutOverlay.defaultProps = { + style: null, + duration: Platform.OS === 'android' ? 300 : 300 +}; diff --git a/app/components/UI/FadeOutOverlay/index.test.js b/app/components/UI/FadeOutOverlay/index.test.js new file mode 100644 index 00000000000..673435d9bd9 --- /dev/null +++ b/app/components/UI/FadeOutOverlay/index.test.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import FadeOutOverlay from './'; + +describe('FadeOutOverlay', () => { + it('should render correctly', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/HomePage/__snapshots__/index.test.js.snap b/app/components/UI/HomePage/__snapshots__/index.test.js.snap index b76f8bbe097..ce0585f079d 100644 --- a/app/components/UI/HomePage/__snapshots__/index.test.js.snap +++ b/app/components/UI/HomePage/__snapshots__/index.test.js.snap @@ -4,15 +4,20 @@ exports[`HomePage should render correctly 1`] = ` { // eslint-disable-next-line this.refs[refName].measureMyself((x, y, w, h, l, t) => { @@ -257,6 +268,7 @@ class HomePage extends Component { }; onInitialUrlSubmit = () => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.BROWSER_SEARCH); this.props.onInitialUrlSubmit(this.state.searchInputValue); this.setState({ searchInputValue: '' }); }; @@ -269,7 +281,7 @@ class HomePage extends Component { return ( - + diff --git a/app/components/UI/Identicon/index.js b/app/components/UI/Identicon/index.js index c4afdd3910d..55c73bb28b9 100644 --- a/app/components/UI/Identicon/index.js +++ b/app/components/UI/Identicon/index.js @@ -12,26 +12,28 @@ import { colors } from '../../../styles/common.js'; */ // eslint-disable-next-line react/display-name const Identicon = React.memo(props => { - const { diameter, address, customStyle } = props; + const { diameter, address, customStyle, noFadeIn } = props; if (!address) return null; - const uri = toDataUrl(address); - return ( - - - + const image = ( + ); + + if (noFadeIn) { + return image; + } + return {image}; }); Identicon.propTypes = { @@ -46,7 +48,11 @@ Identicon.propTypes = { /** * Custom style to apply to image */ - customStyle: PropTypes.object + customStyle: PropTypes.object, + /** + * True if render is happening without fade in + */ + noFadeIn: PropTypes.bool }; Identicon.defaultProps = { diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index a468eb8c731..2b9096c729f 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -4,15 +4,27 @@ import ModalNavbarTitle from '../ModalNavbarTitle'; import AccountRightButton from '../AccountRightButton'; import NavbarBrowserTitle from '../NavbarBrowserTitle'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import { Text, Platform, TouchableOpacity, View, StyleSheet, Image, Keyboard } from 'react-native'; +import { Text, Platform, TouchableOpacity, View, StyleSheet, Image, Keyboard, InteractionManager } from 'react-native'; import { fontStyles, colors } from '../../../styles/common'; import IonicIcon from 'react-native-vector-icons/Ionicons'; import AntIcon from 'react-native-vector-icons/AntDesign'; +import EvilIcons from 'react-native-vector-icons/EvilIcons'; +import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; import URL from 'url-parse'; import { strings } from '../../../../locales/i18n'; import AppConstants from '../../../core/AppConstants'; +import TabCountIcon from '../../UI/Tabs/TabCountIcon'; +import WalletConnect from '../../../core/WalletConnect'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; const HOMEPAGE_URL = 'about:blank'; +const trackEvent = event => { + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(event); + }); +}; + const styles = StyleSheet.create({ rightButton: { marginTop: 7, @@ -20,31 +32,45 @@ const styles = StyleSheet.create({ marginBottom: 12 }, metamaskName: { - width: 94, - height: 12 + width: 122, + height: 15 + }, + metamaskFox: { + width: 40, + height: 40, + marginRight: 10 }, metamaskNameWrapper: { marginLeft: Platform.OS === 'android' ? 20 : 0 }, closeIcon: { paddingLeft: Platform.OS === 'android' ? 22 : 18, - color: colors.primary + color: colors.blue }, backIcon: { - color: colors.primary + color: colors.blue + }, + backIconIOS: { + marginHorizontal: 5 + }, + shareIconIOS: { + marginHorizontal: -5 }, backButton: { paddingLeft: Platform.OS === 'android' ? 22 : 18, paddingRight: Platform.OS === 'android' ? 22 : 18, marginTop: 5 }, + closeButton: { + paddingHorizontal: Platform.OS === 'android' ? 22 : 18 + }, infoButton: { paddingLeft: Platform.OS === 'android' ? 22 : 18, paddingRight: Platform.OS === 'android' ? 22 : 18, marginTop: 5 }, infoIcon: { - color: colors.primary + color: colors.blue }, moreIcon: { marginRight: 15, @@ -53,11 +79,8 @@ const styles = StyleSheet.create({ flex: { flex: 1 }, - closeButton: { - paddingHorizontal: 22 - }, closeButtonText: { - color: colors.primary, + color: colors.blue, fontSize: 14, ...fontStyles.normal }, @@ -70,22 +93,35 @@ const styles = StyleSheet.create({ flex: 1 }, browserRightButtonAndroid: { - flex: 0, + flex: 1, + width: 95, flexDirection: 'row' }, browserRightButton: { flex: 1 }, browserMoreIconAndroid: { - paddingTop: 10, - marginLeft: -10 + paddingTop: 10 }, disabled: { opacity: 0.3 + }, + optinHeaderLeft: { + flexDirection: 'row', + alignItems: 'center', + marginHorizontal: Platform.OS === 'ios' ? 20 : 0 + }, + tabIconAndroid: { + marginTop: 13, + marginLeft: -10, + marginRight: 3, + width: 24, + height: 24 } }); const metamask_name = require('../../../images/metamask-name.png'); // eslint-disable-line +const metamask_fox = require('../../../images/fox.png'); // eslint-disable-line /** * Function that returns the navigation options * This is used by views that will show our custom navbar @@ -99,6 +135,7 @@ export default function getNavbarOptions(title, navigation) { function onPress() { Keyboard.dismiss(); navigation.openDrawer(); + trackEvent(ANALYTICS_EVENT_OPTS.COMMON_TAPS_HAMBURGER_MENU); } return { @@ -121,6 +158,7 @@ export default function getNavbarOptions(title, navigation) { * This is used by views that will show our custom navbar which contains Title * * @param {string} title - Title in string format + * @param {Object} navigation - Navigation object required to push new views * @returns {Object} - Corresponding navbar options containing title and headerTitleStyle */ export function getNavigationOptionsTitle(title, navigation) { @@ -131,7 +169,7 @@ export function getNavigationOptionsTitle(title, navigation) { color: colors.fontPrimary, ...fontStyles.normal }, - headerTintColor: colors.primary, + headerTintColor: colors.blue, headerLeft: ( // eslint-disable-next-line react/jsx-no-bind navigation.pop()} style={styles.backButton}> @@ -145,29 +183,92 @@ export function getNavigationOptionsTitle(title, navigation) { }; } +/** + * Function that returns the navigation options + * This is used by payment request view showing close and back buttons + * + * @param {string} title - Title in string format + * @param {Object} navigation - Navigation object required to push new views + * @returns {Object} - Corresponding navbar options containing title, headerLeft and headerRight + */ +export function getPaymentRequestOptionsTitle(title, navigation) { + const goBack = navigation.getParam('dispatch', undefined); + return { + title, + headerTitleStyle: { + fontSize: 20, + color: colors.fontPrimary, + ...fontStyles.normal + }, + headerTintColor: colors.blue, + headerLeft: goBack ? ( + // eslint-disable-next-line react/jsx-no-bind + goBack()} style={styles.backButton}> + + + ) : ( + + ), + headerRight: ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.closeButton}> + + + ) + }; +} + +/** + * Function that returns the navigation options + * This is used by payment request view showing close button + * + * @returns {Object} - Corresponding navbar options containing title, and headerRight + */ +export function getPaymentRequestSuccessOptionsTitle(navigation) { + return { + headerStyle: { + shadowColor: colors.transparent, + elevation: 0, + backgroundColor: colors.white, + borderBottomWidth: 0 + }, + headerTintColor: colors.blue, + headerLeft: , + headerRight: ( + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.closeButton}> + + + ) + }; +} + /** * Function that returns the navigation options * This is used by views that confirms transactions, showing current network * * @param {string} title - Title in string format - * @param {string} backButtonText - Back text in string format * @returns {Object} - Corresponding navbar options containing title and headerTitleStyle */ -export function getTransactionOptionsTitle(title, backButtonText, navigation) { +export function getTransactionOptionsTitle(title, navigation) { + const transactionMode = navigation.getParam('mode', ''); + const leftText = transactionMode === 'edit' ? strings('transaction.cancel') : strings('transaction.edit'); + const toEditLeftAction = navigation.getParam('dispatch', () => { + ''; + }); + const leftAction = transactionMode === 'edit' ? () => navigation.pop() : () => toEditLeftAction('edit'); return { headerTitle: , - headerLeft: - Platform.OS === 'ios' ? ( - // eslint-disable-next-line react/jsx-no-bind - navigation.pop()} style={styles.closeButton}> - {backButtonText} - - ) : ( - // eslint-disable-next-line react/jsx-no-bind - navigation.pop()} style={styles.backButton}> - - - ), + headerLeft: ( + // eslint-disable-next-line react/jsx-no-bind + + {leftText} + + ), headerRight: }; } @@ -200,6 +301,7 @@ export function getBrowserViewNavbarOptions(navigation) { function onPress() { Keyboard.dismiss(); navigation.openDrawer(); + trackEvent(ANALYTICS_EVENT_OPTS.COMMON_TAPS_HAMBURGER_MENU); } const optionsDisabled = hostname === strings('browser.title'); @@ -214,21 +316,30 @@ export function getBrowserViewNavbarOptions(navigation) { /> ), - headerTitle: , + headerTitle: , headerRight: ( {Platform.OS === 'android' ? ( - { - navigation.navigate('BrowserView', { ...navigation.state.params, showOptions: true }); - }} - style={[styles.browserMoreIconAndroid, optionsDisabled ? styles.disabled : null]} - disabled={optionsDisabled} - > - - + + { + navigation.navigate('BrowserView', { ...navigation.state.params, showTabs: true }); + }} + style={styles.tabIconAndroid} + /> + { + navigation.navigate('BrowserView', { ...navigation.state.params, showOptions: true }); + }} + style={[styles.browserMoreIconAndroid, optionsDisabled ? styles.disabled : null]} + disabled={optionsDisabled} + > + + + ) : null} ) @@ -258,9 +369,9 @@ export function getModalNavbarOptions(title) { export function getOnboardingNavbarOptions() { return { headerStyle: { - shadowColor: 'transparent', + shadowColor: colors.transparent, elevation: 0, - backgroundColor: 'white', + backgroundColor: colors.white, borderBottomWidth: 0 }, headerTitle: ( @@ -272,6 +383,33 @@ export function getOnboardingNavbarOptions() { }; } +/** + * Function that returns the navigation options + * for our metric opt-in screen + * + * @returns {Object} - Corresponding navbar options containing headerLeft + */ +export function getOptinMetricsNavbarOptions() { + return { + headerStyle: { + shadowColor: colors.transparent, + elevation: 0, + backgroundColor: colors.white, + borderBottomWidth: 0, + height: 100 + }, + headerLeft: ( + + + + + + + + + ) + }; +} /** * Function that returns the navigation options * for our closable screens, @@ -310,11 +448,21 @@ export function getWalletNavbarOptions(title, navigation) { const onScanSuccess = data => { if (data.target_address) { navigation.navigate('SendView', { txMeta: data }); + } else if (data.walletConnectURI) { + WalletConnect.newSession(data.walletConnectURI); } }; function openDrawer() { navigation.openDrawer(); + trackEvent(ANALYTICS_EVENT_OPTS.COMMON_TAPS_HAMBURGER_MENU); + } + + function openQRScanner() { + navigation.navigate('QRScanner', { + onScanSuccess + }); + trackEvent(ANALYTICS_EVENT_OPTS.WALLET_QR_SCANNER); } return { @@ -332,11 +480,7 @@ export function getWalletNavbarOptions(title, navigation) { { - navigation.navigate('QRScanner', { - onScanSuccess - }); - }} + onPress={openQRScanner} > @@ -376,6 +520,9 @@ export function getNetworkNavbarOptions(title, translate, navigation) { */ export function getWebviewNavbar(navigation) { const title = navigation.getParam('title', ''); + const share = navigation.getParam('dispatch', () => { + ''; + }); return { title, headerTitleStyle: { @@ -390,16 +537,22 @@ export function getWebviewNavbar(navigation) { ) : ( - + // eslint-disable-next-line react/jsx-no-bind + navigation.pop()} style={styles.backButton}> + + ), headerRight: - Platform.OS === 'ios' ? ( + Platform.OS === 'android' ? ( // eslint-disable-next-line react/jsx-no-bind - navigation.pop()} style={styles.backButton}> - + share()} style={styles.backButton}> + ) : ( - + // eslint-disable-next-line react/jsx-no-bind + share()} style={styles.backButton}> + + ) }; } diff --git a/app/components/UI/NavbarBrowserTitle/index.js b/app/components/UI/NavbarBrowserTitle/index.js index 17b68baf243..32de73f0b8b 100644 --- a/app/components/UI/NavbarBrowserTitle/index.js +++ b/app/components/UI/NavbarBrowserTitle/index.js @@ -6,6 +6,7 @@ import { colors, fontStyles } from '../../../styles/common'; import Networks from '../../../util/networks'; import Icon from 'react-native-vector-icons/FontAwesome'; import { toggleNetworkModal } from '../../../actions/modals'; +import { strings } from '../../../../locales/i18n'; const styles = StyleSheet.create({ wrapper: { @@ -42,8 +43,10 @@ const styles = StyleSheet.create({ currentUrl: { ...fontStyles.normal, fontSize: 14, - textAlign: 'center', - paddingHorizontal: Platform.OS === 'android' ? 30 : 0 + textAlign: 'center' + }, + currentUrlAndroid: { + maxWidth: '60%' } }); @@ -53,6 +56,14 @@ const styles = StyleSheet.create({ */ class NavbarBrowserTitle extends Component { static propTypes = { + /** + * Object representing the navigator + */ + navigation: PropTypes.object, + /** + * String representing the current url + */ + url: PropTypes.string, /** * Object representing the selected the selected network */ @@ -71,19 +82,36 @@ class NavbarBrowserTitle extends Component { toggleNetworkModal: PropTypes.func }; - openNetworkList = () => { - this.props.toggleNetworkModal(); + onTitlePress = () => { + if (this.props.hostname === strings('browser.title')) { + this.props.toggleNetworkModal(); + } else { + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + url: this.props.url, + showUrlModal: true + }); + } }; render = () => { const { https, network, hostname } = this.props; - const { color, name } = Networks[network.provider.type] || { ...Networks.rpc, color: null }; - + let name, color; + if (network.provider.nickname) { + color = Networks[network.provider.type].color || null; + name = network.provider.nickname; + } else { + color = Networks[network.provider.type].color || null; + name = Networks[network.provider.type].name || { ...Networks.rpc, color: null }.name; + } return ( - + {https ? : null} - + {hostname} diff --git a/app/components/UI/NavbarTitle/index.js b/app/components/UI/NavbarTitle/index.js index b2f2a313558..2ec73f273fc 100644 --- a/app/components/UI/NavbarTitle/index.js +++ b/app/components/UI/NavbarTitle/index.js @@ -17,7 +17,7 @@ const styles = StyleSheet.create({ }, networkName: { fontSize: 11, - color: colors.gray, + color: colors.grey400, ...fontStyles.normal }, networkIcon: { @@ -33,7 +33,7 @@ const styles = StyleSheet.create({ }, otherNetworkIcon: { backgroundColor: colors.transparent, - borderColor: colors.borderColor, + borderColor: colors.grey100, borderWidth: 1 } }); @@ -86,7 +86,14 @@ class NavbarTitle extends Component { render = () => { const { network, title, translate } = this.props; - const { color, name } = Networks[network.provider.type] || { ...Networks.rpc, color: null }; + let name, color; + if (network.provider.nickname) { + color = Networks[network.provider.type].color || null; + name = network.provider.nickname; + } else { + color = Networks[network.provider.type].color || null; + name = Networks[network.provider.type].name || { ...Networks.rpc, color: null }.name; + } const realTitle = translate ? strings(title) : title; return ( @@ -95,7 +102,11 @@ class NavbarTitle extends Component { style={styles.wrapper} activeOpacity={this.props.disableNetwork ? 1 : 0.2} > - {title ? {realTitle} : null} + {title ? ( + + {realTitle} + + ) : null} diff --git a/app/components/UI/NetworkList/__snapshots__/index.test.js.snap b/app/components/UI/NetworkList/__snapshots__/index.test.js.snap index db55a86a0ac..d895ce82243 100644 --- a/app/components/UI/NetworkList/__snapshots__/index.test.js.snap +++ b/app/components/UI/NetworkList/__snapshots__/index.test.js.snap @@ -15,7 +15,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", } } > @@ -52,7 +52,7 @@ exports[`NetworkList should render correctly 1`] = ` Array [ Object { "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "flexDirection": "row", "paddingHorizontal": 20, "paddingLeft": 45, @@ -78,7 +78,7 @@ exports[`NetworkList should render correctly 1`] = ` Array [ Object { "marginLeft": 20, - "marginTop": 22, + "marginTop": 20, "position": "absolute", }, Object { @@ -154,7 +154,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "marginTop": 0, } } @@ -182,7 +182,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "flexDirection": "row", "paddingHorizontal": 20, "paddingLeft": 45, @@ -194,7 +194,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "marginLeft": 20, - "marginTop": 22, + "marginTop": 20, "position": "absolute", } } @@ -243,7 +243,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "flexDirection": "row", "paddingHorizontal": 20, "paddingLeft": 45, @@ -255,7 +255,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "marginLeft": 20, - "marginTop": 22, + "marginTop": 20, "position": "absolute", } } @@ -304,7 +304,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "flexDirection": "row", "paddingHorizontal": 20, "paddingLeft": 45, @@ -316,7 +316,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "marginLeft": 20, - "marginTop": 22, + "marginTop": 20, "position": "absolute", } } @@ -365,7 +365,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "flexDirection": "row", "paddingHorizontal": 20, "paddingLeft": 45, @@ -377,7 +377,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "marginLeft": 20, - "marginTop": 22, + "marginTop": 20, "position": "absolute", } } @@ -401,7 +401,7 @@ exports[`NetworkList should render correctly 1`] = ` }, Object { "backgroundColor": "transparent", - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "borderWidth": 2, }, ] @@ -424,9 +424,7 @@ exports[`NetworkList should render correctly 1`] = ` "fontWeight": "400", } } - > - http://10.0.2.2:8545 - + /> @@ -434,7 +432,7 @@ exports[`NetworkList should render correctly 1`] = ` style={ Object { "alignItems": "center", - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "borderTopWidth": 0.5, "flexDirection": "row", "height": 60, @@ -458,7 +456,7 @@ exports[`NetworkList should render correctly 1`] = ` ['mainnet', 'ropsten', 'kovan', 'rinkeby']; - - getOtherNetworks = () => this.getAllNetworks().slice(1); + getOtherNetworks = () => getAllNetworks().slice(1); onNetworkChange = async type => { + const { provider } = this.props; this.props.onClose(false); InteractionManager.runAfterInteractions(() => { - const { NetworkController } = Engine.context; + const { NetworkController, CurrencyRateController } = Engine.context; + CurrencyRateController.configure({ nativeCurrency: 'ETH' }); NetworkController.setProviderType(type); setTimeout(() => { Engine.refreshTransactionHistory(); }, 1000); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.COMMON_SWITCHED_NETWORKS, { + 'From Network': provider.type, + 'To Network': type + }); }); }; @@ -159,8 +165,12 @@ export class NetworkList extends Component { }; onSetRpcTarget = async rpcTarget => { - const { NetworkController } = Engine.context; - NetworkController.setRpcTarget(rpcTarget); + const { frequentRpcList } = this.props; + const { NetworkController, CurrencyRateController } = Engine.context; + const rpc = frequentRpcList.find(({ rpcUrl }) => rpcUrl === rpcTarget); + const { rpcUrl, chainId, ticker, nickname } = rpc; + CurrencyRateController.configure({ nativeCurrency: ticker }); + NetworkController.setRpcTarget(rpcUrl, chainId, ticker, nickname); this.props.onClose(false); }; @@ -195,20 +205,20 @@ export class NetworkList extends Component { renderRpcNetworks = () => { const { frequentRpcList, provider } = this.props; - return frequentRpcList.map((network, i) => { - const { color, name } = { name: network, color: null }; + return frequentRpcList.map(({ rpcUrl }, i) => { + const { color, name } = { name: rpcUrl, color: null }; const selected = - provider.rpcTarget === network && provider.type === 'rpc' ? ( + provider.rpcTarget === rpcUrl && provider.type === 'rpc' ? ( ) : ( this.removeRpcTarget(network)} // eslint-disable-line + onPress={() => this.removeRpcTarget(rpcUrl)} // eslint-disable-line /> ); - return this.networkElement(selected, this.onSetRpcTarget, name, color, i, network); + return this.networkElement(selected, this.onSetRpcTarget, name, color, i, rpcUrl); }); }; diff --git a/app/components/UI/OnboardingWizard/Coachmark/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Coachmark/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..af375ef9f2b --- /dev/null +++ b/app/components/UI/OnboardingWizard/Coachmark/__snapshots__/index.test.js.snap @@ -0,0 +1,227 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Coachmark should render correctly 1`] = ` + + + + + + + + title + + + content + + + Back + + + + + + + + + + Got it! + + + + +`; diff --git a/app/components/UI/OnboardingWizard/Coachmark/index.js b/app/components/UI/OnboardingWizard/Coachmark/index.js new file mode 100644 index 00000000000..be4294c6b44 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Coachmark/index.js @@ -0,0 +1,298 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Animated, View, Text, StyleSheet } from 'react-native'; +import { colors, fontStyles } from '../../../../styles/common'; +import StyledButton from '../../StyledButton'; +import { strings } from '../../../../../locales/i18n'; + +const styles = StyleSheet.create({ + coachmark: { + backgroundColor: colors.blue, + borderRadius: 8, + padding: 18 + }, + progress: { + flexDirection: 'row', + justifyContent: 'space-between' + }, + actions: { + flexDirection: 'column' + }, + actionButton: { + width: '100%', + marginTop: 10 + }, + title: { + ...fontStyles.bold, + color: colors.white, + fontSize: 18, + alignSelf: 'center' + }, + triangle: { + width: 0, + height: 0, + backgroundColor: colors.transparent, + borderStyle: 'solid', + borderLeftWidth: 15, + borderRightWidth: 15, + borderBottomWidth: 10, + borderLeftColor: colors.transparent, + borderRightColor: colors.transparent, + borderBottomColor: colors.blue, + position: 'absolute' + }, + triangleDown: { + width: 0, + height: 0, + backgroundColor: colors.transparent, + borderStyle: 'solid', + borderLeftWidth: 15, + borderRightWidth: 15, + borderTopWidth: 10, + borderLeftColor: colors.transparent, + borderRightColor: colors.transparent, + borderTopColor: colors.blue, + position: 'absolute' + }, + progressButton: { + width: 75, + height: 45, + padding: 5 + }, + leftProgessButton: { + left: 0 + }, + rightProgessButton: { + right: 0 + }, + topCenter: { + marginBottom: 10, + alignItems: 'center' + }, + topLeft: { + marginBottom: 10, + alignItems: 'flex-start', + marginLeft: 30 + }, + topLeftCorner: { + marginBottom: 10, + alignItems: 'flex-start', + marginLeft: 12 + }, + bottomCenter: { + marginBottom: 10, + alignItems: 'center' + }, + bottomLeft: { + marginBottom: 10, + alignItems: 'flex-start', + marginLeft: 30 + }, + circle: { + width: 7, + height: 7, + borderRadius: 7 / 2, + backgroundColor: colors.white, + opacity: 0.4, + margin: 5 + }, + solidCircle: { + opacity: 1 + }, + progessContainer: { + flexDirection: 'row', + alignSelf: 'center' + } +}); + +export default class Coachmark extends Component { + static propTypes = { + /** + * Custom coachmark style to apply + */ + coachmarkStyle: PropTypes.object, + /** + * Custom animated view style to apply + */ + style: PropTypes.object, + /** + * Content text + */ + content: PropTypes.object, + /** + * Title text + */ + title: PropTypes.string, + /** + * Current onboarding wizard step + */ + currentStep: PropTypes.number, + /** + * Callback to be called when next is pressed + */ + onNext: PropTypes.func, + /** + * Callback to be called when back is pressed + */ + onBack: PropTypes.func, + /** + * Whether action buttons have to be rendered + */ + action: PropTypes.bool, + /** + * Top indicator position + */ + topIndicatorPosition: PropTypes.oneOf(['topCenter', 'topLeft', 'topLeftCorner']), + /** + * Bottom indicator position + */ + bottomIndicatorPosition: PropTypes.oneOf(['bottomCenter', 'bottomLeft']) + }; + + state = { + ready: false + }; + + opacity = new Animated.Value(0); + + componentDidMount = () => { + Animated.timing(this.opacity, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + isInteraction: false + }).start(); + }; + + componentWillUnmount = () => { + Animated.timing(this.opacity, { + toValue: 0, + duration: 1000, + useNativeDriver: true, + isInteraction: false + }).start(); + }; + + /** + * Calls props onNext + */ + onNext = () => { + const { onNext } = this.props; + onNext && onNext(); + }; + + /** + * Calls props onBack + */ + onBack = () => { + const { onBack } = this.props; + onBack && onBack(); + }; + + /** + * Gets top indicator style according to 'topIndicatorPosition' + * + * @param {string} topIndicatorPosition - Indicator position + * @returns {Object} - Corresponding style object + */ + getIndicatorStyle = topIndicatorPosition => { + const positions = { + topCenter: styles.topCenter, + topLeft: styles.topLeft, + topLeftCorner: styles.topLeftCorner, + [undefined]: styles.topCenter + }; + return positions[topIndicatorPosition]; + }; + + /** + * Gets top indicator style according to 'bottomIndicatorPosition' + * + * @param {string} bottomIndicatorPosition - Indicator position + * @returns {Object} - Corresponding style object + */ + getBotttomIndicatorStyle = bottomIndicatorPosition => { + const positions = { + bottomCenter: styles.bottomCenter, + bottomLeft: styles.bottomLeft, + [undefined]: styles.bottomCenter + }; + return positions[bottomIndicatorPosition]; + }; + + /** + * Returns progress bar, back and next buttons. According to currentStep + * + * @returns {Object} - Corresponding view object + */ + renderProgressButtons = () => { + const { currentStep } = this.props; + return ( + + + {strings('onboarding_wizard.coachmark.progress_back')} + + + {[1, 2, 3, 4, 5].map(i => ( + + ))} + + + + {strings('onboarding_wizard.coachmark.progress_next')} + + + ); + }; + + /** + * Returns horizontal action buttons + * + * @returns {Object} - Corresponding view object + */ + renderActionButtons = () => ( + + + {strings('onboarding_wizard.coachmark.action_back')} + + + {strings('onboarding_wizard.coachmark.action_next')} + + + ); + + render() { + const { content, title, topIndicatorPosition, bottomIndicatorPosition, action } = this.props; + const style = this.props.style || {}; + const coachmarkStyle = this.props.coachmarkStyle || {}; + return ( + + {topIndicatorPosition && ( + + + + )} + + + {title} + + {content} + {action ? this.renderActionButtons() : this.renderProgressButtons()} + + {bottomIndicatorPosition && ( + + + + )} + + ); + } +} diff --git a/app/components/UI/OnboardingWizard/Coachmark/index.test.js b/app/components/UI/OnboardingWizard/Coachmark/index.test.js new file mode 100644 index 00000000000..9ceeb9cdcda --- /dev/null +++ b/app/components/UI/OnboardingWizard/Coachmark/index.test.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Coachmark from './'; + +describe('Coachmark', () => { + it('should render correctly', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/Step1/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Step1/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..6f517c961eb --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step1/__snapshots__/index.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step1 should render correctly 1`] = ``; diff --git a/app/components/UI/OnboardingWizard/Step1/index.js b/app/components/UI/OnboardingWizard/Step1/index.js new file mode 100644 index 00000000000..57edeaa7aa7 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step1/index.js @@ -0,0 +1,94 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { View, Text, StyleSheet } from 'react-native'; +import Coachmark from '../Coachmark'; +import DeviceSize from '../../../../util/DeviceSize'; +import setOnboardingWizardStep from '../../../../actions/wizard'; +import { strings } from '../../../../../locales/i18n'; +import onboardingStyles from './../styles'; + +const styles = StyleSheet.create({ + main: { + flex: 1 + }, + coachmark: { + marginHorizontal: 16 + }, + coachmarkContainer: { + flex: 1, + position: 'absolute', + left: 0, + right: 0, + bottom: DeviceSize.isIphoneX() ? 36 : 16 + } +}); + +class Step1 extends Component { + static propTypes = { + /** + * Callback called when closing step + */ + onClose: PropTypes.func, + /** + * Dispatch set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func + }; + + /** + * Dispatches 'setOnboardingWizardStep' with next step + */ + onNext = () => { + const { setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(2); + }; + + /** + * Calls props 'onClose' + */ + onClose = () => { + const { onClose } = this.props; + onClose && onClose(); + }; + + /** + * Returns content for this step + */ + content = () => ( + + + {strings('onboarding_wizard.step1.content1')} + + + {strings('onboarding_wizard.step1.content2')} + + + ); + + render() { + return ( + + + + + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + null, + mapDispatchToProps +)(Step1); diff --git a/app/components/UI/OnboardingWizard/Step1/index.test.js b/app/components/UI/OnboardingWizard/Step1/index.test.js new file mode 100644 index 00000000000..e19d094bba4 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step1/index.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Step1 from './'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; + +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('Step1', () => { + it('should render correctly', () => { + const wrapper = shallow( + + + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/Step2/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Step2/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..4e4b5b2f039 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step2/__snapshots__/index.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step2 should render correctly 1`] = ``; diff --git a/app/components/UI/OnboardingWizard/Step2/index.js b/app/components/UI/OnboardingWizard/Step2/index.js new file mode 100644 index 00000000000..7ea47d722ad --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step2/index.js @@ -0,0 +1,86 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Platform, View, Text, StyleSheet } from 'react-native'; +import Coachmark from '../Coachmark'; +import setOnboardingWizardStep from '../../../../actions/wizard'; +import { strings } from '../../../../../locales/i18n'; +import onboardingStyles from './../styles'; + +const styles = StyleSheet.create({ + main: { + flex: 1 + }, + some: { + marginHorizontal: 45 + }, + coachmarkContainer: { + flex: 1, + position: 'absolute', + left: 0, + right: 0, + top: Platform.OS === 'ios' ? 290 : 250 + } +}); + +class Step2 extends Component { + static propTypes = { + /** + * Dispatch set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func + }; + + /** + * Dispatches 'setOnboardingWizardStep' with next step + */ + onNext = () => { + const { setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(3); + }; + + /** + * Dispatches 'setOnboardingWizardStep' with back step + */ + onBack = () => { + const { setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(1); + }; + + /** + * Returns content for this step + */ + content = () => ( + + {strings('onboarding_wizard.step2.content1')} + {strings('onboarding_wizard.step2.content2')} + + ); + + render() { + return ( + + + + + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + null, + mapDispatchToProps +)(Step2); diff --git a/app/components/UI/OnboardingWizard/Step2/index.test.js b/app/components/UI/OnboardingWizard/Step2/index.test.js new file mode 100644 index 00000000000..2d05e67fcc2 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step2/index.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Step2 from './'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; + +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('Step2', () => { + it('should render correctly', () => { + const wrapper = shallow( + + + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/Step3/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Step3/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..5d656a15e64 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step3/__snapshots__/index.test.js.snap @@ -0,0 +1,105 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step3 should render correctly 1`] = ` + + + + + + + + 'Account 1' isn't that catchy. So why not name your account something a little more memorable. + + + + Long tap + + + now to edit account name. + + + } + currentStep={2} + onBack={[Function]} + onNext={[Function]} + style={ + Object { + "marginHorizontal": 45, + } + } + title="Edit Account Name" + topIndicatorPosition="topCenter" + /> + + +`; diff --git a/app/components/UI/OnboardingWizard/Step3/index.js b/app/components/UI/OnboardingWizard/Step3/index.js new file mode 100644 index 00000000000..15726becc64 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step3/index.js @@ -0,0 +1,141 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Platform, Text, View, StyleSheet } from 'react-native'; +import Coachmark from '../Coachmark'; +import setOnboardingWizardStep from '../../../../actions/wizard'; +import { colors, fontStyles } from '../../../../styles/common'; +import { renderAccountName } from '../../../../util/address'; +import AccountOverview from '../../AccountOverview'; +import { strings } from '../../../../../locales/i18n'; +import onboardingStyles from './../styles'; + +const styles = StyleSheet.create({ + main: { + flex: 1 + }, + some: { + marginHorizontal: 45 + }, + coachmarkContainer: { + flex: 1, + position: 'absolute', + left: 0, + right: 0, + top: Platform.OS === 'ios' ? 210 : 180 + }, + accountLabelContainer: { + alignItems: 'center', + marginTop: Platform.OS === 'ios' ? 88 : 57, + backgroundColor: colors.white + } +}); + +class Step3 extends Component { + static propTypes = { + /** + * String that represents the selected address + */ + selectedAddress: PropTypes.string, + /** + /* Identities object required to get account name + */ + identities: PropTypes.object, + /** + * Map of accounts to information objects including balances + */ + accounts: PropTypes.object, + /** + * Currency code of the currently-active currency + */ + currentCurrency: PropTypes.string, + /** + * Dispatch set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func + }; + + state = { + accountLabel: '', + accountLabelEditable: false + }; + + /** + * Sets corresponding account label + */ + componentDidMount = () => { + const { identities, selectedAddress } = this.props; + const accountLabel = renderAccountName(selectedAddress, identities); + this.setState({ accountLabel }); + }; + + /** + * Dispatches 'setOnboardingWizardStep' with next step + */ + onNext = () => { + const { setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(4); + }; + + /** + * Dispatches 'setOnboardingWizardStep' with back step + */ + onBack = () => { + const { setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(2); + }; + + /** + * Returns content for this step + */ + content = () => ( + + {strings('onboarding_wizard.step3.content1')} + + {strings('onboarding_wizard.step3.content2')} + {strings('onboarding_wizard.step3.content3')} + + + ); + + render() { + const { selectedAddress, identities, accounts, currentCurrency } = this.props; + const account = { address: selectedAddress, ...identities[selectedAddress], ...accounts[selectedAddress] }; + + return ( + + + + + + + + + + ); + } +} + +const mapStateToProps = state => ({ + accounts: state.engine.backgroundState.AccountTrackerController.accounts, + currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + identities: state.engine.backgroundState.PreferencesController.identities +}); + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Step3); diff --git a/app/components/UI/OnboardingWizard/Step3/index.test.js b/app/components/UI/OnboardingWizard/Step3/index.test.js new file mode 100644 index 00000000000..283fbe1f85f --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step3/index.test.js @@ -0,0 +1,38 @@ +import React from 'react'; +import Step3 from './'; +import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('Step3', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + PreferencesController: { + selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1', + identities: { '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { name: 'Account 1' } } + }, + AccountTrackerController: { + accounts: { + '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { + name: 'account 1', + address: '0xe7E125654064EEa56229f273dA586F10DF96B0a1', + balance: 0 + } + } + }, + CurrencyRateController: { + currentCurrecy: 'USD' + } + } + } + }; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/Step4/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Step4/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..b6b033d4839 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step4/__snapshots__/index.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step4 should render correctly 1`] = ``; diff --git a/app/components/UI/OnboardingWizard/Step4/index.js b/app/components/UI/OnboardingWizard/Step4/index.js new file mode 100644 index 00000000000..d58bbbe1bb2 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step4/index.js @@ -0,0 +1,96 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Platform, View, Text, StyleSheet } from 'react-native'; +import Coachmark from '../Coachmark'; +import setOnboardingWizardStep from '../../../../actions/wizard'; +import { strings } from '../../../../../locales/i18n'; +import onboardingStyles from './../styles'; +import { fontStyles } from '../../../../styles/common'; + +const styles = StyleSheet.create({ + main: { + flex: 1 + }, + some: { + marginLeft: 10, + marginRight: 85 + }, + coachmarkContainer: { + flex: 1, + position: 'absolute', + left: 0, + right: 0, + top: Platform.OS === 'ios' ? 90 : 60 + } +}); + +class Step4 extends Component { + static propTypes = { + /** + * Object that represents the navigator + */ + navigation: PropTypes.object, + /** + * Dispatch set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func + }; + + /** + * Dispatches 'setOnboardingWizardStep' with next step + */ + onNext = () => { + const { navigation, setOnboardingWizardStep } = this.props; + navigation && navigation.openDrawer(); + setOnboardingWizardStep && setOnboardingWizardStep(5); + }; + + /** + * Dispatches 'setOnboardingWizardStep' with back step + */ + onBack = () => { + const { setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(3); + }; + + /** + * Returns content for this step + */ + content = () => ( + + + {strings('onboarding_wizard.step4.content1')} + {strings('onboarding_wizard.step4.content2')} + + {strings('onboarding_wizard.step4.content3')} + + ); + + render() { + return ( + + + + + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + null, + mapDispatchToProps +)(Step4); diff --git a/app/components/UI/OnboardingWizard/Step4/index.test.js b/app/components/UI/OnboardingWizard/Step4/index.test.js new file mode 100644 index 00000000000..ad07cceb027 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step4/index.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Step4 from './'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; + +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('Step4', () => { + it('should render correctly', () => { + const wrapper = shallow( + + + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/Step5/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Step5/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..db15d71811d --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step5/__snapshots__/index.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step5 should render correctly 1`] = ``; diff --git a/app/components/UI/OnboardingWizard/Step5/index.js b/app/components/UI/OnboardingWizard/Step5/index.js new file mode 100644 index 00000000000..8aacfa0e9da --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step5/index.js @@ -0,0 +1,102 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Platform, View, Text, StyleSheet } from 'react-native'; +import { colors, fontStyles } from '../../../../styles/common'; +import Coachmark from '../Coachmark'; +import setOnboardingWizardStep from '../../../../actions/wizard'; +import { DrawerActions } from 'react-navigation-drawer'; // eslint-disable-line +import { strings } from '../../../../../locales/i18n'; +import onboardingStyles from './../styles'; + +const styles = StyleSheet.create({ + main: { + flex: 1, + backgroundColor: colors.transparent + }, + some: { + marginLeft: 30, + marginRight: 30 + }, + coachmarkContainer: { + flex: 1, + position: 'absolute', + left: 0, + right: 0, + top: Platform.OS === 'ios' ? 400 : 370 + } +}); + +class Step5 extends Component { + static propTypes = { + /** + * Object that represents the navigator + */ + navigation: PropTypes.object, + /** + * Dispatch set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func + }; + + /** + * Dispatches 'setOnboardingWizardStep' with next step + * Closing drawer and navigating to 'BrowserView' + */ + onNext = () => { + const { navigation, setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(6); + navigation && navigation.dispatch(DrawerActions.closeDrawer()); + navigation && navigation.navigate('BrowserView'); + }; + + /** + * Dispatches 'setOnboardingWizardStep' with next step + * Closing drawer and navigating to 'WalletView' + */ + onBack = () => { + const { navigation, setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(4); + navigation && navigation.navigate('WalletView'); + navigation && navigation.dispatch(DrawerActions.closeDrawer()); + }; + + /** + * Returns content for this step + */ + content = () => ( + + + {strings('onboarding_wizard.step5.content1')} + {strings('onboarding_wizard.step5.content2')} + + + ); + + render() { + return ( + + + + + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + null, + mapDispatchToProps +)(Step5); diff --git a/app/components/UI/OnboardingWizard/Step5/index.test.js b/app/components/UI/OnboardingWizard/Step5/index.test.js new file mode 100644 index 00000000000..f7a31ee2113 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step5/index.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Step5 from './'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; + +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('Step5', () => { + it('should render correctly', () => { + const wrapper = shallow( + + + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/Step6/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Step6/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..5210ce98067 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step6/__snapshots__/index.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step6 should render correctly 1`] = ``; diff --git a/app/components/UI/OnboardingWizard/Step6/index.js b/app/components/UI/OnboardingWizard/Step6/index.js new file mode 100644 index 00000000000..cc42eb2cd9b --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step6/index.js @@ -0,0 +1,98 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Platform, View, Text, StyleSheet } from 'react-native'; +import Coachmark from '../Coachmark'; +import setOnboardingWizardStep from '../../../../actions/wizard'; +import { strings } from '../../../../../locales/i18n'; +import onboardingStyles from './../styles'; + +const styles = StyleSheet.create({ + main: { + flex: 1 + }, + coachmarkContainer: { + flex: 1, + position: 'absolute', + left: 0, + right: 0, + top: Platform.OS === 'ios' ? 150 : 120, + marginHorizontal: 45 + } +}); + +class Step6 extends Component { + static propTypes = { + /** + * Object that represents the navigator + */ + navigation: PropTypes.object, + /** + * Dispatch set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func + }; + + state = { + ready: false + }; + + componentDidMount() { + this.setState({ ready: true }); + } + + /** + * Dispatches 'setOnboardingWizardStep' with next step + * Closing drawer and navigating to 'WalletView' + */ + onNext = () => { + const { setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(7); + }; + + /** + * Dispatches 'setOnboardingWizardStep' with back step, opening drawer + */ + onBack = () => { + const { navigation, setOnboardingWizardStep } = this.props; + navigation && navigation.openDrawer(); + setOnboardingWizardStep && setOnboardingWizardStep(5); + }; + + /** + * Returns content for this step + */ + content = () => ( + + {strings('onboarding_wizard.step6.content')} + + ); + + render() { + const { ready } = this.state; + if (!ready) return null; + return ( + + + + + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + null, + mapDispatchToProps +)(Step6); diff --git a/app/components/UI/OnboardingWizard/Step6/index.test.js b/app/components/UI/OnboardingWizard/Step6/index.test.js new file mode 100644 index 00000000000..f018f296038 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step6/index.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Step6 from './'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; + +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('Step6', () => { + it('should render correctly', () => { + const wrapper = shallow( + + + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/Step7/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/Step7/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..5755689e71d --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step7/__snapshots__/index.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step7 should render correctly 1`] = ``; diff --git a/app/components/UI/OnboardingWizard/Step7/index.js b/app/components/UI/OnboardingWizard/Step7/index.js new file mode 100644 index 00000000000..90c9697dc63 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step7/index.js @@ -0,0 +1,87 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { Platform, View, Text, StyleSheet } from 'react-native'; +import Coachmark from '../Coachmark'; +import setOnboardingWizardStep from '../../../../actions/wizard'; +import { strings } from '../../../../../locales/i18n'; +import onboardingStyles from './../styles'; + +const styles = StyleSheet.create({ + main: { + flex: 1 + }, + coachmarkContainer: { + flex: 1, + position: 'absolute', + left: 0, + right: 0, + top: Platform.OS === 'ios' ? 140 : 100, + marginHorizontal: 45 + } +}); + +class Step7 extends Component { + static propTypes = { + /** + * Dispatch set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func, + /** + * Callback to call when closing + */ + onClose: PropTypes.func + }; + + /** + * Dispatches 'setOnboardingWizardStep' with back step + */ + onBack = () => { + const { setOnboardingWizardStep } = this.props; + setOnboardingWizardStep && setOnboardingWizardStep(6); + }; + + /** + * Calls props onClose + */ + onClose = () => { + const { onClose } = this.props; + onClose && onClose(); + }; + + /** + * Returns content for this step + */ + content = () => ( + + {strings('onboarding_wizard.step7.content')} + + ); + + render() { + return ( + + + + + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + null, + mapDispatchToProps +)(Step7); diff --git a/app/components/UI/OnboardingWizard/Step7/index.test.js b/app/components/UI/OnboardingWizard/Step7/index.test.js new file mode 100644 index 00000000000..82f45abf3b9 --- /dev/null +++ b/app/components/UI/OnboardingWizard/Step7/index.test.js @@ -0,0 +1,19 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import Step7 from './'; +import configureMockStore from 'redux-mock-store'; +import { Provider } from 'react-redux'; + +const mockStore = configureMockStore(); +const store = mockStore({}); + +describe('Step7', () => { + it('should render correctly', () => { + const wrapper = shallow( + + + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/__snapshots__/index.test.js.snap b/app/components/UI/OnboardingWizard/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..7923ba49d6f --- /dev/null +++ b/app/components/UI/OnboardingWizard/__snapshots__/index.test.js.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OnboardingWizard should render correctly 1`] = ` + + + + + +`; diff --git a/app/components/UI/OnboardingWizard/index.js b/app/components/UI/OnboardingWizard/index.js new file mode 100644 index 00000000000..3d0fcb584d3 --- /dev/null +++ b/app/components/UI/OnboardingWizard/index.js @@ -0,0 +1,106 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { TouchableOpacity, View, StyleSheet, Text } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import { connect } from 'react-redux'; +import Step1 from './Step1'; +import Step2 from './Step2'; +import Step3 from './Step3'; +import Step4 from './Step4'; +import Step5 from './Step5'; +import Step6 from './Step6'; +import Step7 from './Step7'; +import setOnboardingWizardStep from '../../../actions/wizard'; +import { DrawerActions } from 'react-navigation-drawer'; // eslint-disable-line +import { strings } from '../../../../locales/i18n'; +import AsyncStorage from '@react-native-community/async-storage'; + +const styles = StyleSheet.create({ + root: { + left: 0, + right: 0, + top: 0, + bottom: 0, + position: 'absolute' + }, + main: { + flex: 1, + backgroundColor: colors.transparent + }, + skip: { + height: 30, + bottom: 30 + }, + skipText: { + ...fontStyles.normal, + textAlign: 'center', + fontSize: 18, + color: colors.blue + } +}); + +class OnboardingWizard extends Component { + static propTypes = { + /** + * Object that represents the navigator + */ + navigation: PropTypes.object, + /** + * Wizard state + */ + wizard: PropTypes.object, + /** + * Dispatch set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func + }; + + /** + * Close onboarding wizard setting step to 0 and closing drawer + */ + closeOnboardingWizard = async () => { + const { setOnboardingWizardStep, navigation } = this.props; + await AsyncStorage.setItem('@MetaMask:onboardingWizard', 'explored'); + setOnboardingWizardStep && setOnboardingWizardStep(0); + navigation && navigation.dispatch(DrawerActions.closeDrawer()); + }; + + onboardingWizardNavigator = { + 1: , + 2: , + 3: , + 4: , + 5: , + 6: , + 7: + }; + + render() { + const { + wizard: { step } + } = this.props; + return ( + + {this.onboardingWizardNavigator[step]} + {step !== 1 && ( + + {strings('onboarding_wizard.skip_tutorial')} + + )} + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +const mapStateToProps = state => ({ + wizard: state.wizard +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(OnboardingWizard); diff --git a/app/components/UI/OnboardingWizard/index.test.js b/app/components/UI/OnboardingWizard/index.test.js new file mode 100644 index 00000000000..35af8253919 --- /dev/null +++ b/app/components/UI/OnboardingWizard/index.test.js @@ -0,0 +1,21 @@ +import React from 'react'; +import OnboardingWizard from './'; +import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('OnboardingWizard', () => { + it('should render correctly', () => { + const initialState = { + wizard: { + step: 1 + } + }; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/OnboardingWizard/styles.js b/app/components/UI/OnboardingWizard/styles.js new file mode 100644 index 00000000000..a9a0a9bb0cb --- /dev/null +++ b/app/components/UI/OnboardingWizard/styles.js @@ -0,0 +1,24 @@ +import { StyleSheet } from 'react-native'; +import { fontStyles, colors } from '../../../styles/common'; + +export default StyleSheet.create({ + container: { + flex: 1 + }, + welcome: { + fontSize: 20 + }, + content: { + ...fontStyles.normal, + color: colors.white, + fontSize: 14, + textAlign: 'center', + marginBottom: 20 + }, + leftContent: { + textAlign: 'left' + }, + contentContainer: { + marginTop: 20 + } +}); diff --git a/app/components/UI/OptinMetrics/__snapshots__/index.test.js.snap b/app/components/UI/OptinMetrics/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..86469d229f4 --- /dev/null +++ b/app/components/UI/OptinMetrics/__snapshots__/index.test.js.snap @@ -0,0 +1,392 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OptinMetrics should render correctly 1`] = ` + + + + + Help us improve MetaMask + + + MetaMask would like to gather basic usage data to better understand how our users interact with the mobile app. This data will be used to continually improve the usability and user experience of our product. + + + MetaMask will... + + + + + Always allow you to opt-out via Settings + + + + + + Send anonymied click & pageview events + + + + + + Maintain a public aggregate dashboard to educate the community + + + + + + Never collect keys, addresses, transactions, balances, hashes, or any personal information + + + + + + Never collect your IP address + + + + + + Never sell data for profit. Ever! + + + + + This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our Privacy Policy + + here + + . + + + + + + No Thanks + + + I Agree + + + + +`; diff --git a/app/components/UI/OptinMetrics/index.js b/app/components/UI/OptinMetrics/index.js new file mode 100644 index 00000000000..ab3f20ea2e7 --- /dev/null +++ b/app/components/UI/OptinMetrics/index.js @@ -0,0 +1,244 @@ +import React, { Component } from 'react'; +import { View, SafeAreaView, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native'; +import PropTypes from 'prop-types'; +import { baseStyles, fontStyles, colors } from '../../../styles/common'; +import AsyncStorage from '@react-native-community/async-storage'; +import Entypo from 'react-native-vector-icons/Entypo'; +import { getOptinMetricsNavbarOptions } from '../Navbar'; +import { strings } from '../../../../locales/i18n'; +import setOnboardingWizardStep from '../../../actions/wizard'; +import { connect } from 'react-redux'; +import { NavigationActions } from 'react-navigation'; +import StyledButton from '../StyledButton'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; + +const styles = StyleSheet.create({ + root: { + ...baseStyles.flexGrow + }, + checkIcon: { + color: colors.green500 + }, + crossIcon: { + color: colors.red + }, + icon: { + marginRight: 5 + }, + action: { + flex: 0, + flexDirection: 'row', + paddingVertical: 10, + alignItems: 'center' + }, + title: { + ...fontStyles.bold, + color: colors.black, + fontSize: 22 + }, + description: { + ...fontStyles.normal, + color: colors.black, + flex: 1 + }, + content: { + ...fontStyles.normal, + fontSize: 14, + color: colors.black, + paddingVertical: 10 + }, + wrapper: { + marginHorizontal: 20 + }, + privacyPolicy: { + ...fontStyles.normal, + fontSize: 14, + color: colors.grey400, + marginTop: 10 + }, + link: { + textDecorationLine: 'underline' + }, + actionContainer: { + marginTop: 10, + flex: 0, + flexDirection: 'row', + padding: 16, + bottom: 0 + }, + button: { + flex: 1 + }, + cancel: { + marginRight: 8 + }, + confirm: { + marginLeft: 8 + } +}); + +const PRIVACY_POLICY = 'https://metamask.io/privacy.html'; +/** + * View that is displayed in the flow to agree to metrics + */ +class OptinMetrics extends Component { + static navigationOptions = () => getOptinMetricsNavbarOptions(); + + static propTypes = { + /** + /* navigation object required to push and pop other views + */ + navigation: PropTypes.object, + /** + * Action to set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func + }; + + actionsList = [ + { + action: 0, + description: strings('privacy_policy.action_description_1') + }, + { + action: 0, + description: strings('privacy_policy.action_description_2') + }, + { + action: 0, + description: strings('privacy_policy.action_description_3') + }, + { + action: 1, + description: strings('privacy_policy.action_description_4') + }, + { + action: 1, + description: strings('privacy_policy.action_description_5') + }, + { + action: 1, + description: strings('privacy_policy.action_description_6') + } + ]; + + /** + * Action to be triggered when pressing any button + */ + continue = async () => { + // Get onboarding wizard state + const onboardingWizard = await AsyncStorage.getItem('@MetaMask:onboardingWizard'); + if (onboardingWizard) { + this.props.navigation.navigate('HomeNav'); + } else { + this.props.setOnboardingWizardStep(1); + this.props.navigation.navigate('HomeNav', {}, NavigationActions.navigate({ routeName: 'WalletView' })); + } + }; + + /** + * Render each action with corresponding icon + * + * @param {object} - Object containing action and description to be rendered + * @param {number} i - Index key + */ + renderAction = ({ action, description }, i) => ( + + {action === 0 ? ( + + ) : ( + + )} + {description} + + ); + + /** + * Callback on press cancel + */ + onCancel = async () => { + await AsyncStorage.setItem('@MetaMask:metricsOptIn', 'denied'); + Analytics.disable(); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_METRICS_OPT_OUT); + this.continue(); + }; + + /** + * Callback on press confirm + */ + onConfirm = async () => { + await AsyncStorage.setItem('@MetaMask:metricsOptIn', 'agreed'); + Analytics.enable(); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_METRICS_OPT_IN); + this.continue(); + }; + + /** + * Callback on press policy + */ + onPressPolicy = () => { + const { navigation } = this.props; + navigation.navigate('Webview', { + url: PRIVACY_POLICY, + title: strings('privacy_policy.title') + }); + }; + + /** + * Render privacy policy description + * + * @returns - Touchable opacity object to render with privacy policy information + */ + renderPrivacyPolicy = () => ( + + + {strings('privacy_policy.description') + ' '} + {strings('privacy_policy.here')} + {strings('unit.point')} + + + ); + + render() { + return ( + + + + {strings('privacy_policy.description_title')} + {strings('privacy_policy.description_content_1')} + {strings('privacy_policy.description_content_2')} + {this.actionsList.map((action, i) => this.renderAction(action, i))} + {this.renderPrivacyPolicy()} + + + + + {strings('privacy_policy.decline')} + + + {strings('privacy_policy.agree')} + + + + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + null, + mapDispatchToProps +)(OptinMetrics); diff --git a/app/components/UI/OptinMetrics/index.test.js b/app/components/UI/OptinMetrics/index.test.js new file mode 100644 index 00000000000..c5b170f68b4 --- /dev/null +++ b/app/components/UI/OptinMetrics/index.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import OptinMetrics from './'; +import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('OptinMetrics', () => { + it('should render correctly', () => { + const initialState = {}; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Pager/__snapshots__/index.test.js.snap b/app/components/UI/Pager/__snapshots__/index.test.js.snap index b97feb0714f..1dde9a6864e 100644 --- a/app/components/UI/Pager/__snapshots__/index.test.js.snap +++ b/app/components/UI/Pager/__snapshots__/index.test.js.snap @@ -14,7 +14,7 @@ exports[`Pager should render correctly 1`] = ` style={ Array [ Object { - "backgroundColor": "#DADADA", + "backgroundColor": "#d6d9dc", "height": 7, "marginRight": 4, }, diff --git a/app/components/UI/Pager/index.js b/app/components/UI/Pager/index.js index d96cc653d95..93e7c1ee723 100644 --- a/app/components/UI/Pager/index.js +++ b/app/components/UI/Pager/index.js @@ -12,11 +12,11 @@ const styles = StyleSheet.create({ }, page: { height: 7, - backgroundColor: colors.pager, + backgroundColor: colors.grey100, marginRight: defaultMargin }, selected: { - backgroundColor: colors.primary + backgroundColor: colors.blue } }); diff --git a/app/components/UI/PaymentRequest/AssetList/__snapshots__/index.test.js.snap b/app/components/UI/PaymentRequest/AssetList/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..a6cb2282e0e --- /dev/null +++ b/app/components/UI/PaymentRequest/AssetList/__snapshots__/index.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AssetList should render correctly 1`] = ` + + + +`; diff --git a/app/components/UI/PaymentRequest/AssetList/index.js b/app/components/UI/PaymentRequest/AssetList/index.js new file mode 100644 index 00000000000..c755520089f --- /dev/null +++ b/app/components/UI/PaymentRequest/AssetList/index.js @@ -0,0 +1,109 @@ +import React, { Component } from 'react'; +import { Image, Text, View, StyleSheet } from 'react-native'; +import PropTypes from 'prop-types'; +import StyledButton from '../../StyledButton'; +import AssetIcon from '../../AssetIcon'; +import { colors, fontStyles } from '../../../../styles/common'; +import Identicon from '../../Identicon'; + +const styles = StyleSheet.create({ + item: { + borderWidth: 1, + borderColor: colors.grey100, + padding: 8, + marginBottom: 8, + borderRadius: 8 + }, + assetListElement: { + flex: 1, + flexDirection: 'row', + alignItems: 'flex-start' + }, + text: { + ...fontStyles.normal + }, + textSymbol: { + ...fontStyles.bold, + paddingBottom: 4, + fontSize: 16 + }, + assetInfo: { + flex: 1, + flexDirection: 'column', + alignSelf: 'center', + padding: 4 + }, + assetIcon: { + flexDirection: 'column', + alignSelf: 'center', + marginRight: 12 + }, + ethLogo: { + width: 50, + height: 50 + } +}); + +/** + * Component that provides ability to search assets. + */ +export default class AssetList extends Component { + static propTypes = { + /** + * Array of assets objects returned from the search + */ + searchResults: PropTypes.array, + /** + * Callback triggered when a token is selected + */ + handleSelectAsset: PropTypes.func, + /** + * Message string to display when searchResults is empty + */ + emptyMessage: PropTypes.string + }; + + /** + * Render logo according to asset. Could be ETH, Identicon or contractMap logo + * + * @param {object} asset - Asset to generate the logo to render + */ + renderLogo = asset => { + const { logo, address, isETH } = asset; + if (!logo) { + return ; + } else if (isETH) { + return ; + } + return ; + }; + + render = () => { + const { searchResults, handleSelectAsset } = this.props; + + return ( + + {searchResults.slice(0, 6).map((_, i) => { + const { symbol, name } = searchResults[i] || {}; + return ( + handleSelectAsset(searchResults[i])} // eslint-disable-line + key={i} + > + + {this.renderLogo(searchResults[i])} + + {symbol} + {name && {name}} + + + + ); + })} + {searchResults.length === 0 && {this.props.emptyMessage}} + + ); + }; +} diff --git a/app/components/UI/PaymentRequest/AssetList/index.test.js b/app/components/UI/PaymentRequest/AssetList/index.test.js new file mode 100644 index 00000000000..b4654de87d5 --- /dev/null +++ b/app/components/UI/PaymentRequest/AssetList/index.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import AssetList from './'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('AssetList', () => { + it('should render correctly', () => { + const initialState = {}; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/PaymentRequest/__snapshots__/index.test.js.snap b/app/components/UI/PaymentRequest/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..6c55131f24d --- /dev/null +++ b/app/components/UI/PaymentRequest/__snapshots__/index.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaymentRequest should render correctly 1`] = ``; diff --git a/app/components/UI/PaymentRequest/index.js b/app/components/UI/PaymentRequest/index.js new file mode 100644 index 00000000000..51646252864 --- /dev/null +++ b/app/components/UI/PaymentRequest/index.js @@ -0,0 +1,623 @@ +import React, { Component } from 'react'; +import { Platform, SafeAreaView, TextInput, Text, StyleSheet, View, TouchableOpacity } from 'react-native'; +import { connect } from 'react-redux'; +import { colors, fontStyles, baseStyles } from '../../../styles/common'; +import { getPaymentRequestOptionsTitle } from '../../UI/Navbar'; +import FeatherIcon from 'react-native-vector-icons/Feather'; +import contractMap from 'eth-contract-metadata'; +import Fuse from 'fuse.js'; +import AssetList from './AssetList'; +import PropTypes from 'prop-types'; +import { + weiToFiat, + toWei, + balanceToFiat, + renderFromWei, + fiatNumberToWei, + fromWei, + isDecimal, + fiatNumberToTokenMinimalUnit, + renderFromTokenMinimalUnit, + fromTokenMinimalUnit +} from '../../../util/number'; +import { strings } from '../../../../locales/i18n'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import StyledButton from '../StyledButton'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import { generateETHLink, generateERC20Link } from '../../../util/eip681-link-generator'; +import NetworkList from '../../../util/networks'; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.white, + flex: 1 + }, + contentWrapper: { + paddingTop: 24, + paddingHorizontal: 24 + }, + title: { + ...fontStyles.bold, + fontSize: 16 + }, + searchWrapper: { + marginVertical: 8 + }, + searchInput: { + flex: 1, + marginHorizontal: 0, + paddingTop: Platform.OS === 'android' ? 12 : 2, + borderRadius: 8, + paddingHorizontal: 38, + fontSize: 16, + backgroundColor: colors.white, + height: 40, + color: colors.grey400, + borderColor: colors.grey100, + borderWidth: 1, + ...fontStyles.normal + }, + searchIcon: { + position: 'absolute', + textAlignVertical: 'center', + marginTop: Platform.OS === 'android' ? 9 : 10, + marginLeft: 12 + }, + input: { + ...fontStyles.normal, + backgroundColor: colors.white, + borderWidth: 0, + fontSize: 32, + paddingBottom: 0, + paddingRight: 0, + paddingLeft: 0, + paddingTop: 0 + }, + eth: { + ...fontStyles.normal, + fontSize: 32, + paddingTop: Platform.OS === 'android' ? 3 : 0, + paddingLeft: 10 + }, + fiatValue: { + ...fontStyles.normal, + fontSize: 18 + }, + split: { + flex: 1, + flexDirection: 'row' + }, + ethContainer: { + flex: 1, + flexDirection: 'row', + paddingLeft: 6, + paddingRight: 10 + }, + container: { + flex: 1, + flexDirection: 'row', + paddingRight: 10, + paddingVertical: 10, + paddingLeft: 14, + position: 'relative', + backgroundColor: colors.white, + borderColor: colors.grey100, + borderRadius: 4, + borderWidth: 1 + }, + amounts: { + maxWidth: '70%' + }, + switchContainer: { + flex: 1, + flexDirection: 'column', + alignSelf: 'center', + right: 0 + }, + switchTouchable: { + flexDirection: 'row', + alignSelf: 'flex-end', + right: 0 + }, + enterAmountWrapper: { + flex: 1, + flexDirection: 'column' + }, + button: { + marginBottom: 16 + }, + buttonsWrapper: { + flex: 1, + flexDirection: 'row', + alignSelf: 'center' + }, + buttonsContainer: { + flex: 1, + flexDirection: 'column', + alignSelf: 'flex-end' + }, + scrollViewContainer: { + flexGrow: 1 + }, + errorWrapper: { + backgroundColor: colors.red000, + borderRadius: 4, + marginTop: 8 + }, + errorText: { + color: colors.fontError, + alignSelf: 'center' + }, + assetsWrapper: { + marginTop: 16 + }, + assetsTitle: { + ...fontStyles.normal, + fontSize: 16, + marginBottom: 8 + } +}); + +const contractList = Object.entries(contractMap) + .map(([address, tokenData]) => { + tokenData.address = address; + return tokenData; + }) + .filter(tokenData => Boolean(tokenData.erc20)); + +const fuse = new Fuse(contractList, { + shouldSort: true, + threshold: 0.45, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [{ name: 'name', weight: 0.5 }, { name: 'symbol', weight: 0.5 }] +}); + +const ethLogo = require('../../../images/eth-logo.png'); // eslint-disable-line +const defaultEth = [ + { + symbol: 'ETH', + name: 'Ether', + logo: ethLogo, + isETH: true + } +]; +const defaultAssets = [ + ...defaultEth, + { + address: '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', + decimals: 18, + erc20: true, + logo: 'dai.svg', + name: 'Dai Stablecoin v1.0', + symbol: 'DAI' + } +]; + +const MODE_SELECT = 'select'; +const MODE_AMOUNT = 'amount'; + +/** + * View to generate a payment request link + */ +class PaymentRequest extends Component { + static navigationOptions = ({ navigation }) => + getPaymentRequestOptionsTitle(strings('payment_request.title'), navigation); + + static propTypes = { + /** + * Object that represents the navigator + */ + navigation: PropTypes.object, + /** + * ETH-to-current currency conversion rate from CurrencyRateController + */ + conversionRate: PropTypes.number, + /** + * Currency code for currently-selected currency from CurrencyRateController + */ + currentCurrency: PropTypes.string, + /** + * Object containing token exchange rates in the format address => exchangeRate + */ + contractExchangeRates: PropTypes.object, + /** + * Primary currency, either ETH or Fiat + */ + primaryCurrency: PropTypes.string, + /** + * A string that represents the selected address + */ + selectedAddress: PropTypes.string, + /** + * Array of ERC20 assets + */ + tokens: PropTypes.array, + /** + * A string representing the network name + */ + networkType: PropTypes.string + }; + + state = { + searchInputValue: '', + results: [], + selectedAsset: undefined, + mode: MODE_SELECT, + internalPrimaryCurrency: '', + cryptoAmount: undefined, + amount: undefined, + secondaryAmount: undefined, + symbol: undefined, + showError: false, + chainId: '' + }; + + /** + * Set chainId, internalPrimaryCurrency and receiveAssets, if there is an asset set to this payment request chose it automatically, to state + */ + componentDidMount = () => { + const { primaryCurrency, navigation, networkType } = this.props; + const receiveAsset = navigation && navigation.getParam('receiveAsset', undefined); + const chainId = Object.keys(NetworkList).indexOf(networkType) > -1 && NetworkList[networkType].networkId; + this.setState({ internalPrimaryCurrency: primaryCurrency, chainId }); + if (receiveAsset) { + this.goToAmountInput(receiveAsset); + } + }; + + /** + * Go to asset selection view and modify navbar accordingly + */ + goToAssetSelection = () => { + const { navigation } = this.props; + navigation && navigation.setParams({ mode: MODE_SELECT, dispatch: undefined }); + this.setState({ + mode: MODE_SELECT, + amount: undefined, + cryptoAmount: undefined, + secondaryAmount: undefined, + symbol: undefined + }); + }; + + /** + * Go to enter amount view, with selectedAsset and modify navbar accordingly + * + * @param {object} selectedAsset - Asset selected to build the payment request + */ + goToAmountInput = async selectedAsset => { + const { navigation } = this.props; + navigation && navigation.setParams({ mode: MODE_AMOUNT, dispatch: this.goToAssetSelection }); + await this.setState({ selectedAsset, mode: MODE_AMOUNT }); + this.updateAmount(); + }; + + /** + * Handle search input result + * + * @param {string} searchInputValue - String containing assets query + */ + handleSearch = searchInputValue => { + const fuseSearchResult = fuse.search(searchInputValue); + const addressSearchResult = contractList.filter( + token => token.address.toLowerCase() === searchInputValue.toLowerCase() + ); + const results = [...addressSearchResult, ...fuseSearchResult]; + this.setState({ searchInputValue, results }); + }; + + /** + * Renders a view that allows user to select assets to build the payment request + * Either top picks and user's assets are available to select + */ + renderSelectAssets = () => { + const { tokens } = this.props; + const { chainId } = this.state; + let results; + if (chainId === 1) { + results = this.state.searchInputValue ? this.state.results : defaultAssets; + } else { + results = defaultEth; + } + const userTokens = tokens.map(token => { + const contract = contractList.find(contractToken => contractToken.address === token.address); + if (contract) return contract; + return token; + }); + return ( + + + {strings('payment_request.choose_asset')} + + {chainId === 1 && ( + + + + + )} + + + {this.state.searchInputValue + ? strings('payment_request.search_results') + : strings('payment_request.search_top_picks')} + + + + {userTokens.length > 0 && ( + + {strings('payment_request.your_tokens')} + + + )} + + ); + }; + + /** + * Handles payment request parameters for ETH as primaryCurrency + * + * @param {string} amount - String containing amount number from input, as token value + * @returns {object} - Object containing respective symbol, secondaryAmount and cryptoAmount according to amount and selectedAsset + */ + handleETHPrimaryCurrency = amount => { + const { conversionRate, currentCurrency, contractExchangeRates } = this.props; + const { selectedAsset } = this.state; + let secondaryAmount; + const symbol = selectedAsset.symbol; + const undefAmount = (isDecimal(amount) && amount) || 0; + const cryptoAmount = amount; + const exchangeRate = selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; + + if (selectedAsset.symbol !== 'ETH') { + secondaryAmount = exchangeRate + ? balanceToFiat(undefAmount, conversionRate, exchangeRate, currentCurrency) + : undefined; + } else { + secondaryAmount = weiToFiat(toWei(undefAmount), conversionRate, currentCurrency.toUpperCase()); + } + return { symbol, secondaryAmount, cryptoAmount }; + }; + + /** + * Handles payment request parameters for Fiat as primaryCurrency + * + * @param {string} amount - String containing amount number from input, as fiat value + * @returns {object} - Object containing respective symbol, secondaryAmount and cryptoAmount according to amount and selectedAsset + */ + handleFiatPrimaryCurrency = amount => { + const { conversionRate, currentCurrency, contractExchangeRates } = this.props; + const { selectedAsset } = this.state; + const symbol = currentCurrency.toUpperCase(); + const exchangeRate = selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; + const undefAmount = (isDecimal(amount) && amount) || 0; + let secondaryAmount, cryptoAmount; + if (selectedAsset.symbol !== 'ETH' && (exchangeRate && exchangeRate !== 0)) { + const secondaryMinimalUnit = fiatNumberToTokenMinimalUnit( + undefAmount, + conversionRate, + exchangeRate, + selectedAsset.decimals + ); + secondaryAmount = + renderFromTokenMinimalUnit(secondaryMinimalUnit, selectedAsset.decimals) + ' ' + selectedAsset.symbol; + cryptoAmount = fromTokenMinimalUnit(secondaryMinimalUnit, selectedAsset.decimals); + } else { + secondaryAmount = renderFromWei(fiatNumberToWei(undefAmount, conversionRate)) + ' ' + strings('unit.eth'); + cryptoAmount = fromWei(fiatNumberToWei(undefAmount, conversionRate)); + } + return { symbol, secondaryAmount, cryptoAmount }; + }; + + /** + * Handles amount update, setting amount related state parameters, it handles state according to internalPrimaryCurrency + * + * @param {string} amount - String containing amount number from input + */ + updateAmount = amount => { + const { internalPrimaryCurrency, selectedAsset } = this.state; + const { conversionRate, contractExchangeRates } = this.props; + const exchangeRate = selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; + let res; + // If primary currency is not crypo we need to know if there are conversion and exchange rates to handle + // fiat conversion for the payment request + if (internalPrimaryCurrency !== 'ETH' && conversionRate && (exchangeRate || selectedAsset.isETH)) { + res = this.handleFiatPrimaryCurrency(amount); + } else { + res = this.handleETHPrimaryCurrency(amount); + } + const { cryptoAmount, secondaryAmount, symbol } = res; + this.setState({ amount, cryptoAmount, secondaryAmount, symbol, showError: false }); + }; + + /** + * Updates internalPrimaryCurrency + */ + switchPrimaryCurrency = async () => { + const { internalPrimaryCurrency } = this.state; + const primarycurrencies = { + ETH: 'Fiat', + Fiat: 'ETH' + }; + await this.setState({ internalPrimaryCurrency: primarycurrencies[internalPrimaryCurrency] }); + this.updateAmount(); + }; + + /** + * Resets amount on payment request + */ + onReset = () => { + this.updateAmount(); + }; + + /** + * Generates payment request link and redirects to PaymentRequestSuccess view with it + * If there is an error, an error message will be set to display on the view + */ + onNext = () => { + const { selectedAddress, navigation } = this.props; + const { cryptoAmount, selectedAsset, chainId } = this.state; + try { + let link; + if (selectedAsset.isETH) { + link = generateETHLink(selectedAddress, cryptoAmount, chainId); + } else { + link = generateERC20Link(selectedAddress, selectedAsset.address, cryptoAmount, chainId); + } + navigation && + navigation.replace('PaymentRequestSuccess', { + link, + amount: cryptoAmount, + symbol: selectedAsset.symbol + }); + } catch (e) { + this.setState({ showError: true }); + } + }; + + /** + * Renders a view that allows user to set payment request amount + */ + renderEnterAmount = () => { + const { conversionRate, contractExchangeRates } = this.props; + const { amount, secondaryAmount, symbol, cryptoAmount, showError, selectedAsset } = this.state; + const exchangeRate = selectedAsset && selectedAsset.address && contractExchangeRates[selectedAsset.address]; + let switchable = true; + if (!conversionRate) { + switchable = false; + } else if (selectedAsset.symbol !== 'ETH' && !exchangeRate) { + switchable = false; + } + return ( + + + {strings('payment_request.enter_amount')} + + + + + + + + + {symbol} + + + {secondaryAmount && ( + + {secondaryAmount} + + )} + + {switchable && ( + + + + + + )} + + + {showError && ( + + {strings('payment_request.request_error')} + + )} + + + + + {strings('payment_request.reset')} + + + {strings('payment_request.next')} + + + + + ); + }; + + render() { + const { mode } = this.state; + return ( + + + {mode === MODE_SELECT ? this.renderSelectAssets() : this.renderEnterAmount()} + + + ); + } +} + +const mapStateToProps = state => ({ + conversionRate: state.engine.backgroundState.CurrencyRateController.conversionRate, + currentCurrency: state.engine.backgroundState.CurrencyRateController.currentCurrency, + contractExchangeRates: state.engine.backgroundState.TokenRatesController.contractExchangeRates, + searchEngine: state.settings.searchEngine, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + tokens: state.engine.backgroundState.AssetsController.tokens, + primaryCurrency: state.settings.primaryCurrency, + networkType: state.engine.backgroundState.NetworkController.provider.type +}); + +export default connect(mapStateToProps)(PaymentRequest); diff --git a/app/components/UI/PaymentRequest/index.test.js b/app/components/UI/PaymentRequest/index.test.js new file mode 100644 index 00000000000..148e97928ec --- /dev/null +++ b/app/components/UI/PaymentRequest/index.test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import PaymentRequest from './'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); +const store = mockStore({}); +describe('PaymentRequest', () => { + it('should render correctly', () => { + const wrapper = shallow( + + + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/PaymentRequestSuccess/index.js b/app/components/UI/PaymentRequestSuccess/index.js new file mode 100644 index 00000000000..2a2db02667f --- /dev/null +++ b/app/components/UI/PaymentRequestSuccess/index.js @@ -0,0 +1,324 @@ +import React, { Component } from 'react'; +import { + Dimensions, + Clipboard, + SafeAreaView, + View, + ScrollView, + Text, + StyleSheet, + InteractionManager, + TouchableOpacity, + Platform +} from 'react-native'; +import { connect } from 'react-redux'; +import { colors, fontStyles } from '../../../styles/common'; +import { getPaymentRequestSuccessOptionsTitle } from '../../UI/Navbar'; +import PropTypes from 'prop-types'; +import EvilIcons from 'react-native-vector-icons/EvilIcons'; +import StyledButton from '../StyledButton'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import IonicIcon from 'react-native-vector-icons/Ionicons'; +import { showAlert } from '../../../actions/alert'; +import Logger from '../../../util/Logger'; +import Share from 'react-native-share'; // eslint-disable-line import/default +import Modal from 'react-native-modal'; +import QRCode from 'react-native-qrcode-svg'; +import { renderNumber } from '../../../util/number'; +import { strings } from '../../../../locales/i18n'; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.white, + flex: 1 + }, + contentWrapper: { + padding: 24 + }, + button: { + marginBottom: 16 + }, + titleText: { + ...fontStyles.bold, + fontSize: 24, + marginVertical: 16, + alignSelf: 'center' + }, + descriptionText: { + ...fontStyles.normal, + fontSize: 14, + alignSelf: 'center', + textAlign: 'center', + marginVertical: 8 + }, + linkText: { + ...fontStyles.normal, + fontSize: 14, + color: colors.blue, + alignSelf: 'center', + textAlign: 'center', + marginVertical: 16 + }, + buttonsWrapper: { + flex: 1, + flexDirection: 'row', + alignSelf: 'center' + }, + buttonsContainer: { + flex: 1, + flexDirection: 'column', + alignSelf: 'flex-end' + }, + scrollViewContainer: { + flexGrow: 1 + }, + icon: { + color: colors.blue, + marginBottom: 16 + }, + blueIcon: { + color: colors.white + }, + iconWrapper: { + alignItems: 'center' + }, + buttonText: { + ...fontStyles.bold, + color: colors.blue, + fontSize: 14, + marginLeft: 8 + }, + blueButtonText: { + ...fontStyles.bold, + color: colors.white, + fontSize: 14, + marginLeft: 8 + }, + buttonContent: { + flexDirection: 'row', + alignSelf: 'center' + }, + buttonIconWrapper: { + flexDirection: 'column', + alignSelf: 'center' + }, + buttonTextWrapper: { + flexDirection: 'column', + alignSelf: 'center' + }, + detailsWrapper: { + padding: 10, + alignItems: 'center' + }, + addressTitle: { + fontSize: 16, + marginBottom: 16, + ...fontStyles.normal + }, + informationWrapper: { + paddingHorizontal: 40 + }, + linkWrapper: { + paddingHorizontal: 24 + }, + titleQr: { + flexDirection: 'row' + }, + closeIcon: { + position: 'absolute', + right: Platform.OS === 'ios' ? -30 : -40, + bottom: Platform.OS === 'ios' ? 8 : 10 + }, + qrCode: { + marginBottom: 16, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 36, + paddingBottom: 24, + paddingTop: 16, + backgroundColor: colors.grey000, + borderRadius: 8 + }, + qrCodeWrapper: { + borderColor: colors.grey300, + borderRadius: 8, + borderWidth: 1, + padding: 15 + } +}); + +/** + * View to interact with a previously generated payment request link + */ +class PaymentRequestSuccess extends Component { + static navigationOptions = ({ navigation }) => getPaymentRequestSuccessOptionsTitle(navigation); + + static propTypes = { + /** + * Object that represents the navigator + */ + navigation: PropTypes.object, + /** + /* Triggers global alert + */ + showAlert: PropTypes.func + }; + + state = { + link: '', + amount: '', + symbol: '', + qrModalVisible: false + }; + + /** + * Sets payment request link, amount and symbol of the asset to state + */ + componentDidMount = () => { + const { navigation } = this.props; + const link = navigation && navigation.getParam('link', ''); + const amount = navigation && navigation.getParam('amount', ''); + const symbol = navigation && navigation.getParam('symbol', ''); + this.setState({ link, amount, symbol }); + }; + + /** + * Copies payment request link to clipboard + */ + copyAccountToClipboard = async () => { + const { link } = this.state; + await Clipboard.setString(link); + InteractionManager.runAfterInteractions(() => { + this.props.showAlert({ + isVisible: true, + autodismiss: 1500, + content: 'clipboard-alert', + data: { msg: strings('payment_request.link_copied') } + }); + }); + }; + + /** + * Shows share native UI + */ + onShare = () => { + const { link } = this.state; + Share.open({ + message: link + }).catch(err => { + Logger.log('Error while trying to share payment request', err); + }); + }; + + /** + * Toggles payment request QR code modal on top + */ + showQRModal = () => { + this.setState({ qrModalVisible: true }); + }; + + /** + * Closes payment request QR code modal + */ + closeQRModal = () => { + this.setState({ qrModalVisible: false }); + }; + + render() { + const { link, amount, symbol } = this.state; + return ( + + + + + + + {strings('payment_request.send_link')} + {strings('payment_request.description_1')} + + {strings('payment_request.description_2')} + {' ' + renderNumber(amount) + ' ' + symbol} + + + + {link} + + + + + + + + + + + + {strings('payment_request.copy_to_clipboard')} + + + + + + + + + + + {strings('payment_request.qr_code')} + + + + + + + + + + + {strings('payment_request.send_link')} + + + + + + + + + + + + {strings('payment_request.request_qr_code')} + + + + + + + + + + + + + ); + } +} + +const mapDispatchToProps = dispatch => ({ + showAlert: config => dispatch(showAlert(config)) +}); + +export default connect( + null, + mapDispatchToProps +)(PaymentRequestSuccess); diff --git a/app/components/UI/PersonalSign/__snapshots__/index.test.js.snap b/app/components/UI/PersonalSign/__snapshots__/index.test.js.snap index cd9e6c6e994..15b0d535d6a 100644 --- a/app/components/UI/PersonalSign/__snapshots__/index.test.js.snap +++ b/app/components/UI/PersonalSign/__snapshots__/index.test.js.snap @@ -39,11 +39,12 @@ exports[`PersonalSign should render correctly 1`] = ` } onCancel={[Function]} onConfirm={[Function]} + type="personalSign" > {strings('signature_request.message')} diff --git a/app/components/UI/PhishingModal/__snapshots__/index.test.js.snap b/app/components/UI/PhishingModal/__snapshots__/index.test.js.snap index 2ef231015b8..dc2780a1177 100644 --- a/app/components/UI/PhishingModal/__snapshots__/index.test.js.snap +++ b/app/components/UI/PhishingModal/__snapshots__/index.test.js.snap @@ -52,7 +52,7 @@ exports[`PhishingModal should render correctly 1`] = ` size={15} style={ Object { - "color": "#d95846", + "color": "#D73A49", "marginRight": 10, } } @@ -60,7 +60,7 @@ exports[`PhishingModal should render correctly 1`] = ` + + + + Title + + + + + Description + + + +`; diff --git a/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.js b/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.js new file mode 100644 index 00000000000..89d4e9b0524 --- /dev/null +++ b/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.js @@ -0,0 +1,80 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { TouchableOpacity, StyleSheet, View, Text } from 'react-native'; +import { fontStyles, colors } from '../../../../styles/common'; + +const styles = StyleSheet.create({ + wrapper: { + margin: 8, + borderWidth: 1, + borderColor: colors.blue, + borderRadius: 8, + padding: 10, + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center' + }, + title: { + ...fontStyles.bold, + fontSize: 14, + padding: 5 + }, + description: { + ...fontStyles.normal, + fontSize: 12, + padding: 5, + textAlign: 'center', + color: colors.grey500 + }, + row: { + alignSelf: 'center' + }, + icon: { + marginBottom: 5 + } +}); + +/** + * Component that renders a receive action + */ +export default class ReceiveRequestAction extends Component { + static propTypes = { + /** + * The navigator object + */ + icon: PropTypes.object, + /** + * Action title + */ + actionTitle: PropTypes.string, + /** + * Action description + */ + actionDescription: PropTypes.string, + /** + * Custom style + */ + style: PropTypes.object, + /** + * Callback on press action + */ + onPress: PropTypes.func + }; + + render() { + const { icon, actionTitle, actionDescription, style, onPress } = this.props; + return ( + + {icon} + + {actionTitle} + + + + {actionDescription} + + + + ); + } +} diff --git a/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.test.js b/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.test.js new file mode 100644 index 00000000000..c26149835b8 --- /dev/null +++ b/app/components/UI/ReceiveRequest/ReceiveRequestAction/index.test.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ReceiveRequestAction from './'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('ReceiveRequestAction', () => { + it('should render correctly', () => { + const initialState = {}; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..b6103645a83 --- /dev/null +++ b/app/components/UI/ReceiveRequest/__snapshots__/index.test.js.snap @@ -0,0 +1,400 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReceiveRequest should render correctly 1`] = ` + + + + + + + Receive + + + + + + } + onPress={[Function]} + style={ + Object { + "flex": 1, + "height": 343, + "width": 343, + } + } + /> + + } + onPress={[Function]} + style={ + Object { + "flex": 1, + "height": 343, + "width": 343, + } + } + /> + + + + } + onPress={[Function]} + style={ + Object { + "flex": 1, + "height": 343, + "width": 343, + } + } + /> + + } + onPress={[Function]} + style={ + Object { + "flex": 1, + "height": 343, + "width": 343, + } + } + /> + + + + + + + + Public Address QR Code + + + + + + + + + + + 0x + + + + + + + + + + + + + Coming soon... + + + + +`; diff --git a/app/components/UI/ReceiveRequest/index.js b/app/components/UI/ReceiveRequest/index.js new file mode 100644 index 00000000000..f3111a88aa9 --- /dev/null +++ b/app/components/UI/ReceiveRequest/index.js @@ -0,0 +1,372 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { + InteractionManager, + SafeAreaView, + Platform, + TouchableOpacity, + Dimensions, + StyleSheet, + View, + Text, + Clipboard +} from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import ReceiveRequestAction from './ReceiveRequestAction'; +import Logger from '../../../util/Logger'; +import Share from 'react-native-share'; // eslint-disable-line import/default +import { toChecksumAddress } from 'ethereumjs-util'; +import { connect } from 'react-redux'; +import { toggleReceiveModal } from '../../../actions/modals'; +import Modal from 'react-native-modal'; +import QRCode from 'react-native-qrcode-svg'; +import { strings } from '../../../../locales/i18n'; +import ElevatedView from 'react-native-elevated-view'; +import AntIcon from 'react-native-vector-icons/AntDesign'; +import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons'; +import FontAwesome from 'react-native-vector-icons/FontAwesome'; +import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; +import IonicIcon from 'react-native-vector-icons/Ionicons'; +import DeviceSize from '../../../util/DeviceSize'; +import { showAlert } from '../../../actions/alert'; +import GlobalAlert from '../GlobalAlert'; + +const TOTAL_PADDING = 64; +const ACTION_WIDTH = (Dimensions.get('window').width - TOTAL_PADDING) / 2; + +const styles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.white, + borderTopLeftRadius: 10, + borderTopRightRadius: 10 + }, + draggerWrapper: { + width: '100%', + height: 33, + alignItems: 'center', + justifyContent: 'center', + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: colors.grey100 + }, + dragger: { + width: 48, + height: 5, + borderRadius: 4, + backgroundColor: colors.grey400, + opacity: Platform.OS === 'android' ? 0.6 : 0.5 + }, + actionsWrapper: { + marginHorizontal: 16, + paddingBottom: DeviceSize.isIphoneX() ? 16 : 8 + }, + row: { + flexDirection: 'row', + alignItems: 'center' + }, + detailsWrapper: { + padding: 10, + alignItems: 'center' + }, + qrCode: { + marginBottom: 16, + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 36, + paddingBottom: 24, + paddingTop: 16, + backgroundColor: colors.grey000, + borderRadius: 8 + }, + qrCodeWrapper: { + borderColor: colors.grey300, + borderRadius: 8, + borderWidth: 1, + padding: 15 + }, + addressWrapper: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 16, + paddingTop: 16, + marginTop: 10, + borderRadius: 5, + backgroundColor: colors.grey000 + }, + title: { + ...fontStyles.normal, + fontSize: 18, + flexDirection: 'row', + alignSelf: 'center' + }, + titleQr: { + flexDirection: 'row' + }, + closeIcon: { + position: 'absolute', + right: Platform.OS === 'ios' ? -40 : -50, + bottom: Platform.OS === 'ios' ? 8 : 10 + }, + titleWrapper: { + marginVertical: 8 + }, + addressTitle: { + fontSize: 16, + marginBottom: 16, + ...fontStyles.normal + }, + address: { + ...fontStyles.normal, + fontSize: Platform.OS === 'ios' ? 14 : 20, + textAlign: 'center' + }, + modal: { + margin: 0, + width: '100%' + }, + copyAlert: { + width: 180, + backgroundColor: colors.darkAlert, + padding: 20, + paddingTop: 30, + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 8 + }, + copyAlertIcon: { + marginBottom: 20 + }, + copyAlertText: { + textAlign: 'center', + color: colors.white, + fontSize: 16, + ...fontStyles.normal + }, + receiveAction: { + flex: 1, + width: ACTION_WIDTH, + height: ACTION_WIDTH + } +}); + +/** + * Component that renders receive options + */ +class ReceiveRequest extends Component { + static propTypes = { + /** + * The navigator object + */ + navigation: PropTypes.object, + /** + * Selected address as string + */ + selectedAddress: PropTypes.string, + /** + * Asset to receive, could be not defined + */ + receiveAsset: PropTypes.object, + /** + * Action that toggles the receive modal + */ + toggleReceiveModal: PropTypes.func, + /** + /* Triggers global alert + */ + showAlert: PropTypes.func + }; + + state = { + qrModalVisible: false, + buyModalVisible: false + }; + + /** + * Share current account public address + */ + onShare = () => { + const { selectedAddress } = this.props; + Share.open({ + message: `ethereum:${selectedAddress}` + }).catch(err => { + Logger.log('Error while trying to share address', err); + }); + }; + + /** + * Shows an alert message with a coming soon message + */ + onBuy = () => { + InteractionManager.runAfterInteractions(() => { + this.setState({ buyModalVisible: true }); + setTimeout(() => { + this.setState({ buyModalVisible: false }); + }, 1500); + }); + }; + + /** + * Closes QR code modal + */ + closeQrModal = () => { + this.setState({ qrModalVisible: false }); + }; + + /** + * Opens QR code modal + */ + openQrModal = () => { + this.setState({ qrModalVisible: true }); + }; + + copyAccountToClipboard = async () => { + const { selectedAddress } = this.props; + await Clipboard.setString(selectedAddress); + this.props.showAlert({ + isVisible: true, + autodismiss: 1500, + content: 'clipboard-alert', + data: { msg: strings('account_details.account_copied_to_clipboard') } + }); + }; + + actions = [ + { + icon: , + title: strings('receive_request.share_title'), + description: strings('receive_request.share_description'), + onPress: this.onShare + }, + { + icon: , + title: strings('receive_request.qr_code_title'), + description: strings('receive_request.qr_code_description'), + onPress: this.openQrModal + }, + { + icon: , + title: strings('receive_request.request_title'), + description: strings('receive_request.request_description'), + onPress: () => { + this.props.toggleReceiveModal(); + this.props.navigation.navigate('PaymentRequestView', { receiveAsset: this.props.receiveAsset }); + } + }, + { + icon: , + title: strings('receive_request.buy_title'), + description: strings('receive_request.buy_description'), + onPress: this.onBuy + } + ]; + + render() { + const { qrModalVisible, buyModalVisible } = this.state; + return ( + + + + + + {strings('receive_request.title')} + + + + + + + + + + + + + + + + + + {strings('receive_request.public_address_qr_code')} + + + + + + + + + + {this.props.selectedAddress} + + + + + + + + + + + {strings('receive_request.coming_soon')} + + + + ); + } +} + +const mapStateToProps = state => ({ + selectedAddress: toChecksumAddress(state.engine.backgroundState.PreferencesController.selectedAddress), + receiveAsset: state.modals.receiveAsset +}); + +const mapDispatchToProps = dispatch => ({ + toggleReceiveModal: () => dispatch(toggleReceiveModal()), + showAlert: config => dispatch(showAlert(config)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ReceiveRequest); diff --git a/app/components/UI/ReceiveRequest/index.test.js b/app/components/UI/ReceiveRequest/index.test.js new file mode 100644 index 00000000000..0d70c59336c --- /dev/null +++ b/app/components/UI/ReceiveRequest/index.test.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import ReceiveRequest from './'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('ReceiveRequest', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + PreferencesController: { selectedAddress: '0x' } + } + }, + modals: { + receiveAsset: {} + } + }; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Screen/index.js b/app/components/UI/Screen/index.js index 817c0ebda11..bbf0c9bcabb 100644 --- a/app/components/UI/Screen/index.js +++ b/app/components/UI/Screen/index.js @@ -16,7 +16,7 @@ export default class Screen extends Component { componentDidMount() { StatusBar.setBarStyle('dark-content', true); if (Platform.OS === 'android') { - StatusBar.setBackgroundColor(colors.androidStatusbar); + StatusBar.setBackgroundColor(colors.grey100); } } diff --git a/app/components/UI/SelectComponent/__snapshots__/index.test.js.snap b/app/components/UI/SelectComponent/__snapshots__/index.test.js.snap index 7e31be8e0ee..70ee336a464 100644 --- a/app/components/UI/SelectComponent/__snapshots__/index.test.js.snap +++ b/app/components/UI/SelectComponent/__snapshots__/index.test.js.snap @@ -112,7 +112,7 @@ exports[`SelectComponent should render correctly 1`] = ` { - const el = this.props.options && this.props.options.filter(o => o.value === this.props.selectedValue); + const { options, selectedValue, defaultValue } = this.props; + const el = options && options.filter(o => o.value === selectedValue); if (el.length && el[0].label) { return el[0].label; } + if (defaultValue) { + return defaultValue; + } return ''; }; @@ -191,7 +199,7 @@ export default class SelectComponent extends Component { {option.label} {this.props.selectedValue === option.value ? ( - + ) : null} ))} diff --git a/app/components/UI/SettingsDrawer/index.js b/app/components/UI/SettingsDrawer/index.js index 8e668dba122..17bbec58932 100644 --- a/app/components/UI/SettingsDrawer/index.js +++ b/app/components/UI/SettingsDrawer/index.js @@ -7,7 +7,7 @@ import Icon from 'react-native-vector-icons/FontAwesome'; const styles = StyleSheet.create({ root: { backgroundColor: colors.white, - borderBottomColor: colors.inputBorderColor, + borderBottomColor: colors.grey000, borderBottomWidth: 1, flexDirection: 'row', minHeight: 100, @@ -24,7 +24,7 @@ const styles = StyleSheet.create({ }, description: { ...fontStyles.normal, - color: colors.copy, + color: colors.grey500, fontSize: 14, lineHeight: 20, paddingRight: 8 @@ -35,7 +35,7 @@ const styles = StyleSheet.create({ }, icon: { bottom: 8, - color: colors.gray, + color: colors.grey400, left: 4, position: 'relative' }, diff --git a/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap b/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap index 7399e9d953e..1accc41d5f4 100644 --- a/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap +++ b/app/components/UI/SignatureRequest/__snapshots__/index.test.js.snap @@ -179,13 +179,15 @@ exports[`SignatureRequest should render correctly 1`] = ` confirmTestID="request-signature-confirm-button" confirmText="SIGN" confirmed={false} + onCancelPress={[Function]} + onConfirmPress={[Function]} showCancelButton={true} showConfirmButton={true} > { @@ -150,6 +160,41 @@ class SignatureRequest extends Component { ); }; + /** + * Calls trackCancelSignature and onCancel callback + */ + onCancel = () => { + this.props.onCancel(); + Analytics.trackEventWithParameters( + ANALYTICS_EVENT_OPTS.TRANSACTIONS_CANCEL_SIGNATURE, + this.getTrackingParams() + ); + }; + + /** + * Calls trackConfirmSignature and onConfirm callback + */ + onConfirm = () => { + this.props.onConfirm(); + Analytics.trackEventWithParameters( + ANALYTICS_EVENT_OPTS.TRANSACTIONS_CONFIRM_SIGNATURE, + this.getTrackingParams() + ); + }; + + /** + * Returns corresponding tracking params to send + * + * @return {object} - Object containing network and functionType + */ + getTrackingParams = () => { + const { type, networkType } = this.props; + return { + network: networkType, + functionType: type + }; + }; + render() { const { children, message, accounts, selectedAddress, identities } = this.props; const balance = renderFromWei(accounts[selectedAddress].balance); @@ -190,8 +235,8 @@ class SignatureRequest extends Component { confirmTestID={'request-signature-confirm-button'} cancelText={strings('signature_request.cancel')} confirmText={strings('signature_request.sign')} - onCancelPress={this.props.onCancel} - onConfirmPress={this.props.onConfirm} + onCancelPress={this.onCancel} + onConfirmPress={this.onConfirm} > {children} @@ -205,7 +250,8 @@ class SignatureRequest extends Component { const mapStateToProps = state => ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, - identities: state.engine.backgroundState.PreferencesController.identities + identities: state.engine.backgroundState.PreferencesController.identities, + networkType: state.engine.backgroundState.NetworkController.provider.type }); export default connect(mapStateToProps)(SignatureRequest); diff --git a/app/components/UI/SignatureRequest/index.test.js b/app/components/UI/SignatureRequest/index.test.js index e61ac17b108..8d426007aff 100644 --- a/app/components/UI/SignatureRequest/index.test.js +++ b/app/components/UI/SignatureRequest/index.test.js @@ -16,6 +16,11 @@ describe('SignatureRequest', () => { PreferencesController: { selectedAddress: '0x2', identities: { '0x2': { address: '0x2', name: 'Account 1' } } + }, + NetworkController: { + provider: { + type: 'ropsten' + } } } } diff --git a/app/components/UI/StyledButton/__snapshots__/index.test.js.snap b/app/components/UI/StyledButton/__snapshots__/index.test.js.snap index 02d70c88991..9c0ec1f2be0 100644 --- a/app/components/UI/StyledButton/__snapshots__/index.test.js.snap +++ b/app/components/UI/StyledButton/__snapshots__/index.test.js.snap @@ -12,7 +12,7 @@ exports[`StyledButton should render correctly on Android the button with type ca }, Object { "backgroundColor": "#FFFFFF", - "borderColor": "#9b9b9b", + "borderColor": "#848c96", "borderWidth": 1, }, null, @@ -34,7 +34,7 @@ exports[`StyledButton should render correctly on Android the button with type co "padding": 15, }, Object { - "backgroundColor": "#008edf", + "backgroundColor": "#037dd6", "minHeight": 50, }, null, @@ -57,7 +57,7 @@ exports[`StyledButton should render correctly on Android the button with type no }, Object { "backgroundColor": "#FFFFFF", - "borderColor": "#008edf", + "borderColor": "#037dd6", "borderWidth": 1, }, null, @@ -79,7 +79,7 @@ exports[`StyledButton should render correctly on Android the button with type or "padding": 15, }, Object { - "backgroundColor": "#f7861ce6", + "backgroundColor": "#037dd6", }, null, undefined, @@ -100,7 +100,7 @@ exports[`StyledButton should render correctly on iOS the button with type cancel }, Object { "backgroundColor": "#FFFFFF", - "borderColor": "#9b9b9b", + "borderColor": "#848c96", "borderWidth": 1, }, undefined, @@ -115,7 +115,7 @@ exports[`StyledButton should render correctly on iOS the button with type cancel "textAlign": "center", }, Object { - "color": "#9b9b9b", + "color": "#848c96", }, undefined, ] @@ -134,7 +134,7 @@ exports[`StyledButton should render correctly on iOS the button with type confir "padding": 15, }, Object { - "backgroundColor": "#008edf", + "backgroundColor": "#037dd6", "minHeight": 50, }, undefined, @@ -169,7 +169,7 @@ exports[`StyledButton should render correctly on iOS the button with type normal }, Object { "backgroundColor": "#FFFFFF", - "borderColor": "#008edf", + "borderColor": "#037dd6", "borderWidth": 1, }, undefined, @@ -184,7 +184,7 @@ exports[`StyledButton should render correctly on iOS the button with type normal "textAlign": "center", }, Object { - "color": "#008edf", + "color": "#037dd6", }, undefined, ] @@ -203,7 +203,7 @@ exports[`StyledButton should render correctly on iOS the button with type orange "padding": 15, }, Object { - "backgroundColor": "#f7861ce6", + "backgroundColor": "#037dd6", }, undefined, ] diff --git a/app/components/UI/StyledButton/styledButtonStyles.js b/app/components/UI/StyledButton/styledButtonStyles.js index 5fb77af9ec7..de7eca89ba5 100644 --- a/app/components/UI/StyledButton/styledButtonStyles.js +++ b/app/components/UI/StyledButton/styledButtonStyles.js @@ -14,22 +14,22 @@ const styles = StyleSheet.create({ fontWeight: 'bold' }, blue: { - backgroundColor: colors.primary + backgroundColor: colors.blue }, blueText: { color: colors.white }, orange: { - backgroundColor: colors.primaryFox + backgroundColor: colors.blue }, orangeText: { color: colors.white }, infoText: { - color: colors.primaryFox + color: colors.blue }, confirm: { - backgroundColor: colors.primary, + backgroundColor: colors.blue, minHeight: 50 }, confirmText: { @@ -38,32 +38,32 @@ const styles = StyleSheet.create({ roundedNormal: { backgroundColor: colors.white, borderWidth: 1, - borderColor: colors.primary, + borderColor: colors.blue, padding: 8 }, roundedNormalText: { - color: colors.primary + color: colors.blue }, normal: { backgroundColor: colors.white, borderWidth: 1, - borderColor: colors.primary + borderColor: colors.blue }, normalText: { - color: colors.primary + color: colors.blue }, transparent: { - backgroundColor: colors.white, + backgroundColor: colors.transparent, borderWidth: 0, - borderColor: colors.white + borderColor: colors.transparent }, cancel: { backgroundColor: colors.white, borderWidth: 1, - borderColor: colors.accentGray + borderColor: colors.grey400 }, cancelText: { - color: colors.accentGray + color: colors.grey400 }, warning: { backgroundColor: colors.white, @@ -73,7 +73,7 @@ const styles = StyleSheet.create({ info: { backgroundColor: colors.white, borderWidth: 1, - borderColor: colors.primaryFox + borderColor: colors.blue }, warningText: { color: colors.red @@ -81,15 +81,19 @@ const styles = StyleSheet.create({ neutral: { backgroundColor: colors.white, borderWidth: 1, - borderColor: colors.copy + borderColor: colors.grey500 }, neutralText: { - color: colors.copy + color: colors.grey500 }, danger: { backgroundColor: colors.red, borderColor: colors.red, borderWidth: 1 + }, + whiteText: { + ...fontStyles.bold, + color: colors.white } }); @@ -121,7 +125,7 @@ function getStyles(type) { containerStyle = styles.cancel; break; case 'transparent': - fontStyle = styles.normalText; + fontStyle = styles.whiteText; containerStyle = styles.transparent; break; case 'warning': diff --git a/app/components/UI/Tabs/TabCountIcon/__snapshots__/index.test.js.snap b/app/components/UI/Tabs/TabCountIcon/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..137bb3ead73 --- /dev/null +++ b/app/components/UI/Tabs/TabCountIcon/__snapshots__/index.test.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TabCountIcon should render correctly 1`] = ` + + + 1 + + +`; diff --git a/app/components/UI/Tabs/TabCountIcon/index.js b/app/components/UI/Tabs/TabCountIcon/index.js new file mode 100644 index 00000000000..f3a9847426a --- /dev/null +++ b/app/components/UI/Tabs/TabCountIcon/index.js @@ -0,0 +1,61 @@ +import React, { Component } from 'react'; +import { Platform, TouchableOpacity, StyleSheet, Text } from 'react-native'; +import PropTypes from 'prop-types'; +import { colors, fontStyles } from '../../../../styles/common'; +import { connect } from 'react-redux'; + +const styles = StyleSheet.create({ + tabIcon: { + borderWidth: Platform.OS === 'android' ? 2 : 3, + borderColor: colors.grey500, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center' + }, + tabCount: { + color: colors.grey500, + flex: 0, + lineHeight: Platform.OS === 'android' ? 3 : 15, + fontSize: Platform.OS === 'android' ? 3 : 15, + textAlign: 'center', + alignSelf: 'center', + ...fontStyles.normal + } +}); + +/** + * Component that renders an icon showing + * the current number of open tabs + */ +class TabCountIcon extends Component { + static propTypes = { + /** + * Shows the tabs view + */ + onPress: PropTypes.func, + /** + * Switches to a specific tab + */ + tabCount: PropTypes.number, + /** + * Component styles + */ + style: PropTypes.any + }; + + render() { + const { tabCount, onPress, style } = this.props; + + return ( + + {tabCount} + + ); + } +} + +const mapStateToProps = state => ({ + tabCount: state.browser.tabs.length +}); + +export default connect(mapStateToProps)(TabCountIcon); diff --git a/app/components/UI/Tabs/TabCountIcon/index.test.js b/app/components/UI/Tabs/TabCountIcon/index.test.js new file mode 100644 index 00000000000..e735b04746d --- /dev/null +++ b/app/components/UI/Tabs/TabCountIcon/index.test.js @@ -0,0 +1,24 @@ +import React from 'react'; +import TabCountIcon from './'; +import configureMockStore from 'redux-mock-store'; +import { shallow } from 'enzyme'; + +const mockStore = configureMockStore(); + +describe('TabCountIcon', () => { + it('should render correctly', () => { + const initialState = { + browser: { + tabs: [{ url: 'https://metamask.io' }] + } + }; + + const onPress = () => null; + + // eslint-disable-next-line react/jsx-no-bind + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Tabs/TabThumbnail/__snapshots__/index.test.js.snap b/app/components/UI/Tabs/TabThumbnail/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..d9405534078 --- /dev/null +++ b/app/components/UI/Tabs/TabThumbnail/__snapshots__/index.test.js.snap @@ -0,0 +1,158 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TabThumbnail should render correctly 1`] = ` + + + + + + + New tab + + + + + + + + + + + +`; diff --git a/app/components/UI/Tabs/TabThumbnail/index.js b/app/components/UI/Tabs/TabThumbnail/index.js new file mode 100644 index 00000000000..3dbe88c1a7a --- /dev/null +++ b/app/components/UI/Tabs/TabThumbnail/index.js @@ -0,0 +1,178 @@ +import React, { Component } from 'react'; +import { Platform, View, Image, TouchableOpacity, StyleSheet, Text, Dimensions } from 'react-native'; +import PropTypes from 'prop-types'; +import ElevatedView from 'react-native-elevated-view'; +import WebsiteIcon from '../../WebsiteIcon'; +import { strings } from '../../../../../locales/i18n'; +import IonIcon from 'react-native-vector-icons/Ionicons'; +import { colors, fontStyles } from '../../../../styles/common'; +import URL from 'url-parse'; +import DeviceSize from '../../../../util/DeviceSize'; + +const margin = 16; +const width = Dimensions.get('window').width - margin * 2; +const height = Dimensions.get('window').height / (DeviceSize.isIphone5S() ? 4 : 5); +let paddingTop = Dimensions.get('window').height - 190; +if (DeviceSize.isIphoneX()) { + paddingTop -= 65; +} + +if (Platform.OS === 'android') { + paddingTop -= 10; +} + +const styles = StyleSheet.create({ + tabFavicon: { + alignSelf: 'flex-start', + width: 24, + height: 24, + marginRight: 5, + marginLeft: 2, + marginTop: 1 + }, + tabSiteName: { + color: colors.white, + ...fontStyles.bold, + fontSize: 24, + marginRight: 40, + marginLeft: 5, + marginTop: Platform.OS === 'ios' ? 0 : -5 + }, + tabHeader: { + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'flex-start', + backgroundColor: colors.grey500, + paddingVertical: 15, + paddingHorizontal: 10, + minHeight: 25 + }, + tabWrapper: { + marginBottom: 20, + borderRadius: 10, + elevation: 8, + justifyContent: 'space-evenly', + overflow: 'hidden', + borderColor: colors.grey100, + borderWidth: 1, + width, + height + }, + checkWrapper: { + backgroundColor: colors.transparent, + overflow: 'hidden' + }, + tab: { + backgroundColor: colors.white, + flex: 1, + alignItems: 'flex-start', + justifyContent: 'flex-start' + }, + tabImage: { + ...StyleSheet.absoluteFillObject, + paddingTop, + width: null, + height: null, + resizeMode: 'cover' + }, + activeTab: { + borderWidth: 5, + borderColor: colors.blue + }, + closeTabIcon: { + paddingHorizontal: 10, + paddingTop: 3, + fontSize: 38, + color: colors.white, + right: 0, + marginTop: -7, + position: 'absolute' + }, + titleButton: { + backgroundColor: colors.transparent, + flex: 1, + flexDirection: 'row', + marginRight: 40 + }, + closeTabButton: { + backgroundColor: colors.transparent, + width: 36, + height: 36 + } +}); + +const HOMEPAGE_URL = 'about:blank'; +const METAMASK_FOX = require('../../../../images/fox.png'); // eslint-disable-line import/no-commonjs + +/** + * Component that renders an a thumbnail + * that represents an existing tab + */ +export default class TabThumbnail extends Component { + static propTypes = { + /** + * The tab info + */ + tab: PropTypes.object, + /** + * Flag that determines if this is the active tab + */ + isActiveTab: PropTypes.bool, + /** + * Closes a tab + */ + onClose: PropTypes.func, + /** + * Switches to a specific tab + */ + onSwitch: PropTypes.func + }; + + getHostName = () => { + const urlObj = new URL(this.props.tab.url); + return urlObj.hostname.toLowerCase().replace('www.', ''); + }; + + getContainer = () => (Platform.OS === 'android' ? View : ElevatedView); + + render() { + const { isActiveTab, tab, onClose, onSwitch } = this.props; + const Container = this.getContainer(); + const hostname = this.getHostName(); + + return ( + + + + onSwitch(tab)} // eslint-disable-line react/jsx-no-bind + style={styles.titleButton} + > + {tab.url !== HOMEPAGE_URL ? ( + + ) : ( + + )} + + {tab.url === HOMEPAGE_URL ? strings('browser.new_tab') : hostname} + + + onClose(tab)} // eslint-disable-line react/jsx-no-bind + style={styles.closeTabButton} + > + + + + onSwitch(tab)} + > + + + + + ); + } +} diff --git a/app/components/UI/Tabs/TabThumbnail/index.test.js b/app/components/UI/Tabs/TabThumbnail/index.test.js new file mode 100644 index 00000000000..a142568396c --- /dev/null +++ b/app/components/UI/Tabs/TabThumbnail/index.test.js @@ -0,0 +1,15 @@ +jest.useFakeTimers(); + +import React from 'react'; +import { shallow } from 'enzyme'; +import TabThumbnail from './'; + +describe('TabThumbnail', () => { + it('should render correctly', () => { + const foo = () => null; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/Tabs/__snapshots__/index.test.js.snap b/app/components/UI/Tabs/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..ebe4acc021f --- /dev/null +++ b/app/components/UI/Tabs/__snapshots__/index.test.js.snap @@ -0,0 +1,174 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tabs should render correctly 1`] = ` + + + + + + + + Close All + + + + + + + + + + Done + + + + +`; diff --git a/app/components/UI/Tabs/index.js b/app/components/UI/Tabs/index.js new file mode 100644 index 00000000000..e92e67bd75a --- /dev/null +++ b/app/components/UI/Tabs/index.js @@ -0,0 +1,277 @@ +import React, { PureComponent } from 'react'; +import { + InteractionManager, + Platform, + Dimensions, + View, + Text, + ScrollView, + TouchableOpacity, + StyleSheet +} from 'react-native'; +import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; +import PropTypes from 'prop-types'; +import { strings } from '../../../../locales/i18n'; +import TabThumbnail from './TabThumbnail'; +import { colors, fontStyles } from '../../../styles/common'; +import DeviceSize from '../../../util/DeviceSize'; + +const THUMB_VERTICAL_MARGIN = 20; +const NAVBAR_SIZE = DeviceSize.isIphoneX() ? 88 : 64; +const THUMB_HEIGHT = Dimensions.get('window').height / (DeviceSize.isIphone5S() ? 4 : 5) + THUMB_VERTICAL_MARGIN; +const ROWS_VISIBLE = Math.floor((Dimensions.get('window').height - NAVBAR_SIZE - THUMB_VERTICAL_MARGIN) / THUMB_HEIGHT); +const TABS_VISIBLE = ROWS_VISIBLE; + +const styles = StyleSheet.create({ + noTabs: { + flex: 1, + alignItems: 'center', + justifyContent: 'center' + }, + noTabsTitle: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 18, + marginBottom: 10 + }, + noTabsDesc: { + ...fontStyles.normal, + color: colors.fontSecondary, + fontSize: 14 + }, + tabAction: { + flex: 1, + alignContent: 'center', + alignSelf: 'flex-start', + justifyContent: 'center' + }, + + tabActionleft: { + justifyContent: 'center' + }, + tabActionRight: { + justifyContent: 'center', + alignItems: 'flex-end' + }, + tabActionDone: { + ...fontStyles.bold + }, + tabActionText: { + color: colors.blue, + ...fontStyles.normal, + fontSize: 16 + }, + actionDisabled: { + color: colors.fontSecondary + }, + tabsView: { + flex: 1, + backgroundColor: colors.grey100, + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0 + }, + tabActions: { + paddingHorizontal: 20, + flexDirection: 'row', + marginBottom: DeviceSize.isIphoneX() ? 0 : 0, + paddingTop: 17, + shadowColor: colors.black, + shadowOffset: { + width: 0, + height: 12 + }, + shadowOpacity: 0.58, + shadowRadius: 15.0, + backgroundColor: colors.grey000, + height: DeviceSize.isIphoneX() ? 80 : 50 + }, + tabs: { + flex: 1, + backgroundColor: colors.transparent + }, + tabsContent: { + padding: 15, + backgroundColor: colors.transparent + }, + newTabIcon: { + marginTop: Platform.OS === 'ios' ? 3 : 2.5, + color: colors.white, + fontSize: 24, + textAlign: 'center', + justifyContent: 'center', + alignContent: 'center' + }, + newTabIconButton: { + alignSelf: 'center', + justifyContent: 'flex-start', + alignContent: 'flex-start', + backgroundColor: colors.blue, + borderRadius: 100, + width: 30, + height: 30, + marginTop: -7 + } +}); + +/** + * Component that wraps all the thumbnails + * representing all the open tabs + */ +export default class Tabs extends PureComponent { + static propTypes = { + /** + * Array of tabs + */ + tabs: PropTypes.array, + /** + * ID of the active tab + */ + activeTab: PropTypes.number, + /** + * Opens a new tab + */ + newTab: PropTypes.func, + /** + * Closes a tab + */ + closeTab: PropTypes.func, + /** + * Closes all tabs + */ + closeAllTabs: PropTypes.func, + /** + * Dismiss the entire view + */ + closeTabsView: PropTypes.func, + /** + * Switches to a specific tab + */ + switchToTab: PropTypes.func, + /** + * Sets the current tab used for the animation + */ + animateCurrentTab: PropTypes.func // eslint-disable-line react/no-unused-prop-types + }; + + thumbnails = {}; + + state = { + currentTab: null + }; + + scrollview = React.createRef(); + + constructor(props) { + super(props); + this.createTabsRef(props.tabs); + } + + componentDidMount() { + if (this.props.tabs.length > TABS_VISIBLE) { + // Find the selected index + let index = 0; + this.props.tabs.forEach((tab, i) => { + if (tab.id === this.props.activeTab) { + index = i; + } + }); + + // Calculate the row + + const row = index + 1; + + // Scroll if needed + const pos = (row - 1) * THUMB_HEIGHT; + + InteractionManager.runAfterInteractions(() => { + this.scrollview.current && this.scrollview.current.scrollTo({ x: 0, y: pos, animated: true }); + }); + } + } + + createTabsRef(tabs) { + tabs.forEach(tab => { + this.thumbnails[tab.id] = React.createRef(); + }); + } + + componentDidUpdate(prevProps) { + if (prevProps.tabs.length !== Object.keys(this.thumbnails).length) { + this.createTabsRef(this.props.tabs); + } + } + + onSwitch = async tab => { + this.props.switchToTab(tab); + }; + + renderNoTabs() { + return ( + + {strings('browser.no_tabs_title')} + {strings('browser.no_tabs_desc')} + + ); + } + renderTabs(tabs, activeTab) { + return ( + + {tabs.map(tab => ( + // eslint-disable-next-line react/jsx-key + + ))} + + ); + } + + renderTabActions() { + const { tabs, closeAllTabs, newTab, closeTabsView } = this.props; + return ( + + + + {strings('browser.tabs_close_all')} + + + + + + + + + + + {strings('browser.tabs_done')} + + + + ); + } + + render() { + const { tabs, activeTab } = this.props; + + return ( + + {tabs.length === 0 ? this.renderNoTabs() : this.renderTabs(tabs, activeTab)} + {this.renderTabActions()} + + ); + } +} diff --git a/app/components/UI/Tabs/index.test.js b/app/components/UI/Tabs/index.test.js new file mode 100644 index 00000000000..3adfce98d35 --- /dev/null +++ b/app/components/UI/Tabs/index.test.js @@ -0,0 +1,12 @@ +jest.useFakeTimers(); + +import React from 'react'; +import { shallow } from 'enzyme'; +import Tabs from './'; + +describe('Tabs', () => { + it('should render correctly', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/TokenImage/index.js b/app/components/UI/TokenImage/index.js index 8ae5aa8f8a4..b72868660e6 100644 --- a/app/components/UI/TokenImage/index.js +++ b/app/components/UI/TokenImage/index.js @@ -50,11 +50,12 @@ export default class TokenElement extends Component { asset.logo = contractMap[checksumAddress].logo; } } - + // When image is defined, is coming from a token added by watchAsset, so it has to be handled alone + const watchedAsset = asset.image !== undefined; return ( - {asset.logo ? ( - + {asset.logo || asset.image ? ( + ) : ( )} diff --git a/app/components/UI/Tokens/index.js b/app/components/UI/Tokens/index.js index f299e15ebf0..434a075fec0 100644 --- a/app/components/UI/Tokens/index.js +++ b/app/components/UI/Tokens/index.js @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { Image, TouchableOpacity, StyleSheet, Text, View } from 'react-native'; +import { Alert, Image, TouchableOpacity, StyleSheet, Text, View } from 'react-native'; import TokenImage from '../TokenImage'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import { colors, fontStyles } from '../../../styles/common'; @@ -11,6 +11,7 @@ import { renderFromTokenMinimalUnit, balanceToFiat } from '../../../util/number' import Engine from '../../../core/Engine'; import AssetElement from '../AssetElement'; import FadeIn from 'react-native-fade-in-image'; +import { toChecksumAddress } from 'ethereumjs-util'; const styles = StyleSheet.create({ wrapper: { @@ -37,7 +38,7 @@ const styles = StyleSheet.create({ }, addText: { fontSize: 15, - color: colors.primary, + color: colors.blue, ...fontStyles.normal }, footer: { @@ -101,7 +102,11 @@ export default class Tokens extends PureComponent { /** * Array of transactions */ - transactions: PropTypes.array + transactions: PropTypes.array, + /** + * Primary currency, either ETH or Fiat + */ + primaryCurrency: PropTypes.string }; actionSheet = null; @@ -121,42 +126,52 @@ export default class Tokens extends PureComponent { renderFooter = () => ( - + {strings('wallet.add_tokens').toUpperCase()} ); - renderItem = item => { - const { conversionRate, currentCurrency, tokenBalances, tokenExchangeRates } = this.props; - const logo = item.logo || ((contractMap[item.address] && contractMap[item.address].logo) || undefined); - const exchangeRate = item.address in tokenExchangeRates ? tokenExchangeRates[item.address] : undefined; + renderItem = asset => { + const { conversionRate, currentCurrency, tokenBalances, tokenExchangeRates, primaryCurrency } = this.props; + const itemAddress = (asset.address && toChecksumAddress(asset.address)) || undefined; + const logo = asset.logo || ((contractMap[itemAddress] && contractMap[itemAddress].logo) || undefined); + const exchangeRate = itemAddress in tokenExchangeRates ? tokenExchangeRates[itemAddress] : undefined; const balance = - item.balance || - (item.address in tokenBalances - ? renderFromTokenMinimalUnit(tokenBalances[item.address], item.decimals) - : 0); - const balanceFiat = item.balanceFiat || balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency); - item = { ...item, ...{ logo, balance, balanceFiat } }; + asset.balance || + (itemAddress in tokenBalances ? renderFromTokenMinimalUnit(tokenBalances[itemAddress], asset.decimals) : 0); + const balanceFiat = asset.balanceFiat || balanceToFiat(balance, conversionRate, exchangeRate, currentCurrency); + const balanceValue = balance + ' ' + asset.symbol; + + // render balances according to primary currency + let mainBalance, secondaryBalance; + if (primaryCurrency === 'ETH') { + mainBalance = balanceValue; + secondaryBalance = balanceFiat; + } else { + mainBalance = !balanceFiat ? balanceValue : balanceFiat; + secondaryBalance = !balanceFiat ? balanceFiat : balanceValue; + } + + asset = { ...asset, ...{ logo, balance, balanceFiat } }; return ( - {item.symbol === 'ETH' ? ( + {asset.isETH ? ( ) : ( - + )} + - - {balance} {item.symbol} - - {balanceFiat ? {balanceFiat} : null} + {mainBalance} + {secondaryBalance ? {secondaryBalance} : null} ); @@ -184,7 +199,8 @@ export default class Tokens extends PureComponent { removeToken = () => { const { AssetsController } = Engine.context; - AssetsController.removeToken(this.tokenToRemove.address); + AssetsController.removeAndIgnoreToken(this.tokenToRemove.address); + Alert.alert(strings('wallet.token_removed_title'), strings('wallet.token_removed_desc')); }; createActionSheetRef = ref => { diff --git a/app/components/UI/TransactionEdit/__snapshots__/index.test.js.snap b/app/components/UI/TransactionEdit/__snapshots__/index.test.js.snap index c609a83033b..32a81f9e4ee 100644 --- a/app/components/UI/TransactionEdit/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionEdit/__snapshots__/index.test.js.snap @@ -16,7 +16,9 @@ exports[`TransactionEdit should render correctly 1`] = ` confirmTestID="" confirmText="Next" confirmed={false} + keyboardShouldPersistTaps="handled" onConfirmPress={[Function]} + onTouchablePress={[Function]} showCancelButton={true} showConfirmButton={true} > @@ -25,6 +27,7 @@ exports[`TransactionEdit should render correctly 1`] = ` Object { "flex": 1, "flexDirection": "column", + "minHeight": "100%", "padding": 16, } } @@ -58,7 +61,7 @@ exports[`TransactionEdit should render correctly 1`] = ` @@ -171,7 +176,7 @@ exports[`TransactionEdit should render correctly 1`] = ` @@ -226,7 +234,7 @@ exports[`TransactionEdit should render correctly 1`] = ` @@ -269,7 +278,7 @@ exports[`TransactionEdit should render correctly 1`] = ` getTransactionOptionsTitle('send.title', 'Cancel', navigation); + static propTypes = { /** * List of accounts from the AccountTrackerController */ accounts: PropTypes.object, + /** + * Callback to warn if transaction to is a known contract address + */ + checkForAssetAddress: PropTypes.func, /** * react-navigation object used for switching between screens */ @@ -180,10 +186,25 @@ class TransactionEdit extends Component { amountError: '', addressError: '', toAddressError: '', + toAddressWarning: '', gasError: '', fillMax: false, ensRecipient: undefined, - data: undefined + data: undefined, + accountSelectIsOpen: false, + ethInputIsOpen: false + }; + + openAccountSelect = isOpen => { + this.setState({ accountSelectIsOpen: isOpen, ethInputIsOpen: false }); + }; + + openEthInputIsOpen = isOpen => { + this.setState({ ethInputIsOpen: isOpen, accountSelectIsOpen: false }); + }; + + closeDropdowns = () => { + this.setState({ accountSelectIsOpen: false, ethInputIsOpen: false }); }; componentDidMount = () => { @@ -261,16 +282,9 @@ class TransactionEdit extends Component { return amountError || gasError || toAddressError; }; - updateAmount = async amount => { - const { selectedAsset, assetType } = this.props.transaction; - let processedAmount; - if (assetType !== 'ETH') { - processedAmount = isDecimal(amount) ? toTokenMinimalUnit(amount, selectedAsset.decimals) : undefined; - } else { - processedAmount = isDecimal(amount) ? toWei(amount) : undefined; - } - await this.props.handleUpdateAmount(processedAmount); - this.props.handleUpdateReadableValue(amount); + updateAmount = async (amount, renderValue) => { + await this.props.handleUpdateAmount(amount); + this.props.handleUpdateReadableValue(renderValue); const amountError = await this.props.validateAmount(true); this.setState({ amountError }); }; @@ -297,9 +311,10 @@ class TransactionEdit extends Component { updateAndValidateToAddress = async (to, ensRecipient) => { await this.props.handleUpdateToAddress(to, ensRecipient); - let { toAddressError } = this.state; + let { toAddressError, toAddressWarning } = this.state; toAddressError = toAddressError || this.props.validateToAddress(); - this.setState({ toAddressError, ensRecipient }); + toAddressWarning = toAddressWarning || this.props.checkForAssetAddress(); + this.setState({ toAddressError, toAddressWarning, ensRecipient }); }; renderAmountLabel = () => { @@ -327,17 +342,23 @@ class TransactionEdit extends Component { ); }; - render = () => { + render() { const { navigation, transaction: { value, gas, gasPrice, from, to, selectedAsset, readableValue, ensRecipient }, showHexData } = this.props; - const { gasError, toAddressError, data } = this.state; + const { gasError, toAddressError, toAddressWarning, data, accountSelectIsOpen, ethInputIsOpen } = this.state; const totalGas = isBN(gas) && isBN(gasPrice) ? gas.mul(gasPrice) : toBN('0x0'); return ( - + @@ -355,12 +376,17 @@ class TransactionEdit extends Component { readableValue={readableValue} fillMax={this.state.fillMax} updateFillMax={this.updateFillMax} + openEthInput={this.openEthInputIsOpen} + isOpen={ethInputIsOpen} /> {strings('transaction.to')}: {toAddressError ? {toAddressError} : null} + {!toAddressError && toAddressWarning ? ( + {toAddressWarning} + ) : null} @@ -384,6 +413,7 @@ class TransactionEdit extends Component { totalGas={totalGas} gas={gas} gasPrice={gasPrice} + onPress={this.closeDropdowns} /> @@ -406,7 +436,7 @@ class TransactionEdit extends Component { ); - }; + } } const mapStateToProps = state => ({ diff --git a/app/components/UI/TransactionEditor/index.js b/app/components/UI/TransactionEditor/index.js index 9ee2111bfac..4c973f15910 100644 --- a/app/components/UI/TransactionEditor/index.js +++ b/app/components/UI/TransactionEditor/index.js @@ -12,6 +12,7 @@ import { generateTransferData } from '../../../util/transactions'; import { setTransactionObject } from '../../../actions/transaction'; import Engine from '../../../core/Engine'; import collectiblesTransferInformation from '../../../util/collectibles-transfer'; +import contractMap from 'eth-contract-metadata'; const styles = StyleSheet.create({ root: { @@ -33,6 +34,10 @@ class TransactionEditor extends Component { * react-navigation object used for switching between screens */ navigation: PropTypes.object, + /** + * A string representing the network name + */ + networkType: PropTypes.string, /** * Current mode this transaction editor is in */ @@ -49,6 +54,14 @@ class TransactionEditor extends Component { * Called when a user changes modes */ onModeChange: PropTypes.func, + /** + * Array of ERC20 assets + */ + tokens: PropTypes.array, + /** + * Array of ERC721 assets + */ + collectibles: PropTypes.array, /** * Transaction object associated with this transaction */ @@ -210,7 +223,7 @@ class TransactionEditor extends Component { */ handleUpdateAsset = async asset => { const { transaction } = this.props; - if (asset.symbol === 'ETH') { + if (asset.isETH) { const { gas } = await this.estimateGas({ to: transaction.to }); this.props.setTransactionObject({ value: undefined, @@ -297,7 +310,7 @@ class TransactionEditor extends Component { } = this.props; const validations = { ETH: () => this.validateEtherAmount(allowEmpty), - ERC20: () => this.validateTokenAmount(allowEmpty), + ERC20: async () => await this.validateTokenAmount(allowEmpty), ERC721: async () => await this.validateCollectibleOwnership() }; return await validations[assetType](); @@ -358,7 +371,7 @@ class TransactionEditor extends Component { * @param {bool} allowEmpty - Whether the validation allows empty amount or not * @returns {string} - String containing error message whether the Ether transaction amount is valid or not */ - validateTokenAmount = (allowEmpty = true) => { + validateTokenAmount = async (allowEmpty = true) => { let error; if (!allowEmpty) { const { @@ -370,9 +383,24 @@ class TransactionEditor extends Component { if (!value || !gas || !gasPrice || !from) { return strings('transaction.invalid_amount'); } - const contractBalanceForAddress = hexToBN(contractBalances[selectedAsset.address].toString(16)); + // If user trying to send a token that doesn't own, validate balance querying contract + // If it fails, skip validation + let contractBalanceForAddress; + if (contractBalances[selectedAsset.address]) { + contractBalanceForAddress = hexToBN(contractBalances[selectedAsset.address].toString(16)); + } else { + const { AssetsContractController } = Engine.context; + try { + contractBalanceForAddress = await AssetsContractController.getBalanceOf( + selectedAsset.address, + checksummedFrom + ); + } catch (e) { + // Don't validate balance if error + } + } if (value && !isBN(value)) return strings('transaction.invalid_amount'); - const validateAssetAmount = contractBalanceForAddress.lt(value); + const validateAssetAmount = contractBalanceForAddress && contractBalanceForAddress.lt(value); const ethTotalAmount = gas.mul(gasPrice); if ( value && @@ -425,6 +453,34 @@ class TransactionEditor extends Component { return error; }; + /** + * Checks if current transaction to is a known contract address + * If that's the case returns a warning message + * + * @returns {string} - Warning message if defined + */ + checkForAssetAddress = () => { + const { + tokens, + collectibles, + transaction: { to }, + networkType + } = this.props; + if (!to) { + return undefined; + } + const address = toChecksumAddress(to); + if (networkType === 'mainnet') { + const contractMapToken = contractMap[address]; + if (contractMapToken) return strings('transaction.known_asset_contract'); + } + const tokenAddress = tokens.find(token => token.address === address); + if (tokenAddress) return strings('transaction.known_asset_contract'); + const collectibleAddress = collectibles.find(collectible => collectible.address === address); + if (collectibleAddress) return strings('transaction.known_asset_contract'); + return undefined; + }; + handleNewTxMeta = async ({ target_address, chain_id = null, // eslint-disable-line no-unused-vars @@ -472,6 +528,7 @@ class TransactionEditor extends Component { validateGas={this.validateGas} validateToAddress={this.validateToAddress} handleUpdateAsset={this.handleUpdateAsset} + checkForAssetAddress={this.checkForAssetAddress} handleUpdateReadableValue={this.handleUpdateReadableValue} /> )} @@ -491,8 +548,11 @@ class TransactionEditor extends Component { const mapStateToProps = state => ({ accounts: state.engine.backgroundState.AccountTrackerController.accounts, + collectibles: state.engine.backgroundState.AssetsController.collectibles, contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, + networkType: state.engine.backgroundState.NetworkController.provider.type, selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + tokens: state.engine.backgroundState.AssetsController.tokens, transaction: state.transaction }); diff --git a/app/components/UI/TransactionEditor/index.test.js b/app/components/UI/TransactionEditor/index.test.js index b02715ee0fa..7f3e4eb880b 100644 --- a/app/components/UI/TransactionEditor/index.test.js +++ b/app/components/UI/TransactionEditor/index.test.js @@ -19,6 +19,15 @@ describe('TransactionEditor', () => { }, PreferencesController: { selectedAddress: '0x0' + }, + AssetsController: { + tokens: [], + collectibles: [] + }, + NetworkController: { + provider: { + type: 'mainnet' + } } } } diff --git a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap index 1bc6d70e847..e8d846ca714 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/TransactionDetails/__snapshots__/index.test.js.snap @@ -1,7 +1,542 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TransactionDetails should render correctly 1`] = ` - - - + + + + + Attempt to cancel? + + + Gas Cancellation Fee + + + + 0 + + ETH + + + + Submitting this attempt does not guarantee your original transaction will be cancelled. If the canellation attempt is successful, you will be charged the transaction fee above. + + + + + + Hash + + + + 0x2 ... 0x2 + + + + + + + + From + + + + 0x0 + + + + + + + To + + + + 0x1 + + + + + + + Details + + + + + Amount + + + 2 TKN + + + + + Gas Limit (Units) + + + 21000 + + + + + Gas Price (GWEI) + + + 2 + + + + + Total + + + 2 TKN / 0.001 ETH + + + + `; diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.js b/app/components/UI/TransactionElement/TransactionDetails/index.js index 0d008c9716c..3954c373738 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.js @@ -4,11 +4,21 @@ import { Clipboard, TouchableOpacity, StyleSheet, Text, View } from 'react-nativ import { colors, fontStyles } from '../../../../styles/common'; import { strings } from '../../../../../locales/i18n'; import Icon from 'react-native-vector-icons/FontAwesome'; +import Button from '../../Button'; +import ActionModal from '../../../UI/ActionModal'; +import Engine from '../../../../core/Engine'; +import { renderFromWei } from '../../../../util/number'; +import { CANCEL_RATE } from 'gaba/TransactionController'; +import { getNetworkTypeById, findBlockExplorerForRpc, getBlockExplorerName } from '../../../../util/networks'; +import { getEtherscanTransactionUrl, getEtherscanBaseUrl } from '../../../../util/etherscan'; +import Logger from '../../../../util/Logger'; +import { connect } from 'react-redux'; +import URL from 'url-parse'; const styles = StyleSheet.create({ detailRowWrapper: { flex: 1, - backgroundColor: colors.concrete, + backgroundColor: colors.grey000, paddingVertical: 10, paddingHorizontal: 15, marginTop: 10 @@ -22,7 +32,7 @@ const styles = StyleSheet.create({ }, detailRowInfo: { borderRadius: 5, - shadowColor: colors.accentGray, + shadowColor: colors.grey400, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.5, shadowRadius: 3, @@ -34,7 +44,7 @@ const styles = StyleSheet.create({ flex: 1, flexDirection: 'row', borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderColor, + borderColor: colors.grey100, marginBottom: 10, paddingBottom: 5 }, @@ -57,7 +67,7 @@ const styles = StyleSheet.create({ }, viewOnEtherscan: { fontSize: 14, - color: colors.primary, + color: colors.blue, ...fontStyles.normal, textAlign: 'center', marginTop: 15, @@ -71,14 +81,72 @@ const styles = StyleSheet.create({ }, copyIcon: { paddingRight: 5 + }, + cancelButton: { + backgroundColor: colors.red, + height: 22, + paddingHorizontal: 0, + paddingVertical: 0, + position: 'absolute', + right: 15, + top: 15, + width: 54, + zIndex: 1337 + }, + cancelText: { + color: colors.white, + fontSize: 10, + textTransform: 'uppercase' + }, + modalView: { + alignItems: 'stretch', + flex: 1, + flexDirection: 'column', + justifyContent: 'space-between', + padding: 20 + }, + modalText: { + ...fontStyles.normal, + fontSize: 14, + textAlign: 'center' + }, + modalTitle: { + ...fontStyles.bold, + fontSize: 22, + textAlign: 'center' + }, + gasTitle: { + ...fontStyles.bold, + fontSize: 16, + textAlign: 'center' + }, + cancelFeeWrapper: { + backgroundColor: colors.grey000, + textAlign: 'center', + padding: 15 + }, + cancelFee: { + ...fontStyles.bold, + fontSize: 24, + textAlign: 'center' } }); +const NO_RPC_BLOCK_EXPLORER = 'NO_BLOCK_EXPLORER'; + /** * View that renders a transaction details as part of transactions list */ -export default class TransactionDetails extends PureComponent { +class TransactionDetails extends PureComponent { static propTypes = { + /** + /* navigation object required to push new views + */ + navigation: PropTypes.object, + /** + * Object representing the selected the selected network + */ + network: PropTypes.object, /** * Object corresponding to a transaction, containing transaction object, networkId and transaction hash string */ @@ -92,13 +160,32 @@ export default class TransactionDetails extends PureComponent { */ showAlert: PropTypes.func, /** - * Action that shows the global alert + * Object with information to render */ - viewOnEtherscan: PropTypes.func, + transactionDetails: PropTypes.object, /** - * Object with information to render + * Frequent RPC list from PreferencesController */ - transactionDetails: PropTypes.object + frequentRpcList: PropTypes.array + }; + + state = { + cancelIsOpen: false, + rpcBlockExplorer: undefined + }; + + componentDidMount = () => { + const { + network: { + provider: { rpcTarget, type } + }, + frequentRpcList + } = this.props; + let blockExplorer; + if (type === 'rpc') { + blockExplorer = findBlockExplorerForRpc(rpcTarget, frequentRpcList) || NO_RPC_BLOCK_EXPLORER; + } + this.setState({ rpcBlockExplorer: blockExplorer }); }; renderTxHash = transactionHash => { @@ -149,34 +236,105 @@ export default class TransactionDetails extends PureComponent { renderCopyIcon = () => ( - + ); renderCopyToIcon = () => ( - + ); renderCopyFromIcon = () => ( - + ); viewOnEtherscan = () => { const { - transactionObject: { networkID } + transactionObject: { networkID }, + transactionDetails: { transactionHash }, + network: { + provider: { type } + } } = this.props; - this.props.viewOnEtherscan(networkID, this.props.transactionDetails.transactionHash); + const { rpcBlockExplorer } = this.state; + try { + if (type === 'rpc') { + const url = `${rpcBlockExplorer}/tx/${transactionHash}`; + const title = new URL(rpcBlockExplorer).hostname; + this.props.navigation.push('Webview', { + url, + title + }); + } else { + const network = getNetworkTypeById(networkID); + const url = getEtherscanTransactionUrl(network, transactionHash); + const etherscan_url = getEtherscanBaseUrl(network).replace('https://', ''); + this.props.navigation.push('Webview', { + url, + title: etherscan_url + }); + } + } catch (e) { + // eslint-disable-next-line no-console + Logger.error(`can't get a block explorer link for network `, networkID, e); + } }; - render = () => { - const { blockExplorer } = this.props; + showCancelModal = () => { + this.setState({ cancelIsOpen: true }); + }; + hideCancelModal = () => { + this.setState({ cancelIsOpen: false }); + }; + + cancelTransaction = () => { + Engine.context.TransactionController.stopTransaction(this.props.transactionObject.id); + this.hideCancelModal(); + }; + + renderCancelButton = () => { + const { transactionObject } = this.props; + if (transactionObject.status === 'submitted' || transactionObject.status === 'approved') { + return ( + + ); + } + }; + + render = () => { + const { blockExplorer, transactionObject } = this.props; + const { rpcBlockExplorer } = this.state; + const existingGasPrice = transactionObject.transaction ? transactionObject.transaction.gasPrice : '0x0'; + const existingGasPriceDecimal = parseInt(existingGasPrice === undefined ? '0x0' : existingGasPrice, 16); return ( + {this.renderCancelButton()} + + + {strings('transaction.cancel_tx_title')} + {strings('transaction.gasCancelFee')} + + + {renderFromWei(existingGasPriceDecimal * CANCEL_RATE)} {strings('unit.eth')} + + + {strings('transaction.cancel_tx_message')} + + {this.renderTxHash(this.props.transactionDetails.transactionHash)} {strings('transactions.from')} @@ -220,22 +378,37 @@ export default class TransactionDetails extends PureComponent { {this.props.transactionDetails.renderTotalValue} - {this.props.transactionDetails.renderTotalValueFiat && ( + {this.props.transactionDetails.renderTotalValueFiat ? ( {this.props.transactionDetails.renderTotalValueFiat} - )} + ) : null} - {this.props.transactionDetails.transactionHash && blockExplorer && ( - - {strings('transactions.view_on_etherscan')} - - )} + {this.props.transactionDetails.transactionHash && + transactionObject.status !== 'cancelled' && + blockExplorer && + rpcBlockExplorer !== NO_RPC_BLOCK_EXPLORER && ( + + + {(rpcBlockExplorer && + `${strings('transactions.view_on')} ${getBlockExplorerName( + rpcBlockExplorer + ).toUpperCase()}`) || + strings('transactions.view_on_etherscan')} + + + )} ); }; } + +const mapStateToProps = state => ({ + network: state.engine.backgroundState.NetworkController, + frequentRpcList: state.engine.backgroundState.PreferencesController.frequentRpcList +}); +export default connect(mapStateToProps)(TransactionDetails); diff --git a/app/components/UI/TransactionElement/TransactionDetails/index.test.js b/app/components/UI/TransactionElement/TransactionDetails/index.test.js index 9adb426bbdc..250c4770bb4 100644 --- a/app/components/UI/TransactionElement/TransactionDetails/index.test.js +++ b/app/components/UI/TransactionElement/TransactionDetails/index.test.js @@ -13,6 +13,15 @@ describe('TransactionDetails', () => { CurrencyRateController: { conversionRate: 2, currentCurrency: 'USD' + }, + PreferencesController: { + frequentRpcList: [] + }, + NetworkController: { + provider: { + rpcTarget: '', + type: '' + } } } } diff --git a/app/components/UI/TransactionElement/TransferElement/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/TransferElement/__snapshots__/index.test.js.snap index d895b30ac96..43ce3299ffd 100644 --- a/app/components/UI/TransactionElement/TransferElement/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/TransferElement/__snapshots__/index.test.js.snap @@ -15,7 +15,7 @@ exports[`TransferElement should render correctly 1`] = ` Object { "backgroundColor": "#FFFFFF", "borderBottomWidth": 0.5, - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "flex": 1, }, Object { diff --git a/app/components/UI/TransactionElement/TransferElement/index.js b/app/components/UI/TransactionElement/TransferElement/index.js index ac09abe951b..eebff126b1c 100644 --- a/app/components/UI/TransactionElement/TransferElement/index.js +++ b/app/components/UI/TransactionElement/TransferElement/index.js @@ -24,7 +24,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.white, flex: 1, borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderColor + borderColor: colors.grey100 }, rowContent: { padding: 0 diff --git a/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap b/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap index b0662a7a1de..5a39b6cd463 100644 --- a/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionElement/__snapshots__/index.test.js.snap @@ -1,23 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`TransactionElement should render correctly 1`] = ` - - + `; diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js index 6e6de525525..39877de7433 100644 --- a/app/components/UI/TransactionElement/index.js +++ b/app/components/UI/TransactionElement/index.js @@ -7,20 +7,21 @@ import { toLocaleDateTime } from '../../../util/date'; import { renderFromWei, weiToFiat, hexToBN, toBN, isBN, renderToGwei } from '../../../util/number'; import { toChecksumAddress } from 'ethereumjs-util'; import Identicon from '../Identicon'; -import { getActionKey, decodeTransferData } from '../../../util/transactions'; +import { getActionKey, decodeTransferData, getTicker } from '../../../util/transactions'; import TransactionDetails from './TransactionDetails'; import { renderFullAddress } from '../../../util/address'; import FadeIn from 'react-native-fade-in-image'; import TokenImage from '../TokenImage'; import contractMap from 'eth-contract-metadata'; import TransferElement from './TransferElement'; +import { connect } from 'react-redux'; const styles = StyleSheet.create({ row: { backgroundColor: colors.white, flex: 1, borderBottomWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderColor + borderColor: colors.grey100 }, rowContent: { padding: 0 @@ -48,8 +49,8 @@ const styles = StyleSheet.create({ paddingVertical: 3, paddingHorizontal: 5, textAlign: 'center', - backgroundColor: colors.concrete, - color: colors.gray, + backgroundColor: colors.grey000, + color: colors.grey400, fontSize: 9, letterSpacing: 0.5, width: 75, @@ -73,16 +74,16 @@ const styles = StyleSheet.create({ flexDirection: 'row' }, statusConfirmed: { - backgroundColor: colors.lightSuccess, - color: colors.success + backgroundColor: colors.green100, + color: colors.green500 }, statusSubmitted: { - backgroundColor: colors.lightWarning, - color: colors.warning + backgroundColor: colors.orange000, + color: colors.orange300 }, statusFailed: { - backgroundColor: colors.lightRed, - color: colors.error + backgroundColor: colors.red000, + color: colors.red }, ethLogo: { width: 24, @@ -100,8 +101,12 @@ const ethLogo = require('../../../images/eth-logo.png'); // eslint-disable-line /** * View that renders a transaction item part of transactions list */ -export default class TransactionElement extends PureComponent { +class TransactionElement extends PureComponent { static propTypes = { + /** + /* navigation object required to push new views + */ + navigation: PropTypes.object, /** * Asset object (in this case ERC721 token) */ @@ -152,9 +157,9 @@ export default class TransactionElement extends PureComponent { */ showAlert: PropTypes.func, /** - * Action that shows the global alert + * Current provider ticker */ - viewOnEtherscan: PropTypes.func + ticker: PropTypes.string }; state = { @@ -165,8 +170,8 @@ export default class TransactionElement extends PureComponent { componentDidMount = async () => { this.mounted = true; - const { tx, selectedAddress } = this.props; - const actionKey = await getActionKey(tx, selectedAddress); + const { tx, selectedAddress, ticker } = this.props; + const actionKey = await getActionKey(tx, selectedAddress, ticker); this.mounted && this.setState({ actionKey }); }; @@ -209,8 +214,8 @@ export default class TransactionElement extends PureComponent { transactionObject={tx} blockExplorer={blockExplorer} showAlert={showAlert} - viewOnEtherscan={this.props.viewOnEtherscan} transactionDetails={transactionDetails} + navigation={this.props.navigation} /> ) : null; }; @@ -325,10 +330,11 @@ export default class TransactionElement extends PureComponent { conversionRate, currentCurrency } = this.props; + const ticker = getTicker(this.props.ticker); const { actionKey } = this.state; const totalEth = hexToBN(value); - const renderTotalEth = renderFromWei(totalEth) + ' ' + strings('unit.eth'); - const renderTotalEthFiat = weiToFiat(totalEth, conversionRate, currentCurrency).toUpperCase(); + const renderTotalEth = renderFromWei(totalEth) + ' ' + ticker; + const renderTotalEthFiat = weiToFiat(totalEth, conversionRate, currentCurrency.toUpperCase()); const gasBN = hexToBN(gas); const gasPriceBN = hexToBN(gasPrice); @@ -339,11 +345,11 @@ export default class TransactionElement extends PureComponent { renderFrom: renderFullAddress(from), renderTo: renderFullAddress(to), transactionHash, - renderValue: renderFromWei(value) + ' ' + strings('unit.eth'), + renderValue: renderFromWei(value) + ' ' + ticker, renderGas: parseInt(gas, 16).toString(), renderGasPrice: renderToGwei(gasPrice), - renderTotalValue: renderFromWei(totalValue) + ' ' + strings('unit.eth'), - renderTotalValueFiat: weiToFiat(totalValue, conversionRate, currentCurrency).toUpperCase() + renderTotalValue: renderFromWei(totalValue) + ' ' + ticker, + renderTotalValueFiat: weiToFiat(totalValue, conversionRate, currentCurrency.toUpperCase()) }; const transactionElement = { @@ -365,13 +371,14 @@ export default class TransactionElement extends PureComponent { conversionRate, currentCurrency } = this.props; + const ticker = getTicker(this.props.ticker); const { actionKey } = this.state; const gasBN = hexToBN(gas); const gasPriceBN = hexToBN(gasPrice); const totalGas = isBN(gasBN) && isBN(gasPriceBN) ? gasBN.mul(gasPriceBN) : toBN('0x0'); - const renderTotalEth = renderFromWei(totalGas) + ' ' + strings('unit.eth'); - const renderTotalEthFiat = weiToFiat(totalGas, conversionRate, currentCurrency).toUpperCase(); + const renderTotalEth = renderFromWei(totalGas) + ' ' + ticker; + const renderTotalEthFiat = weiToFiat(totalGas, conversionRate, currentCurrency.toUpperCase()); const totalEth = isBN(value) ? value.add(totalGas) : totalGas; const transactionElement = { @@ -385,11 +392,11 @@ export default class TransactionElement extends PureComponent { renderFrom: renderFullAddress(from), renderTo: strings('transactions.to_contract'), transactionHash, - renderValue: renderFromWei(value) + ' ' + strings('unit.eth'), + renderValue: renderFromWei(value) + ' ' + ticker, renderGas: parseInt(gas, 16).toString(), renderGasPrice: renderToGwei(gasPrice), - renderTotalValue: renderFromWei(totalEth) + ' ' + strings('unit.eth'), - renderTotalValueFiat: weiToFiat(totalEth, conversionRate, currentCurrency).toUpperCase() + renderTotalValue: renderFromWei(totalEth) + ' ' + ticker, + renderTotalValueFiat: weiToFiat(totalEth, conversionRate, currentCurrency.toUpperCase()) }; return [transactionElement, transactionDetails]; @@ -440,7 +447,7 @@ export default class TransactionElement extends PureComponent { @@ -451,3 +458,8 @@ export default class TransactionElement extends PureComponent { ); } } + +const mapStateToProps = state => ({ + ticker: state.engine.backgroundState.NetworkController.provider.ticker +}); +export default connect(mapStateToProps)(TransactionElement); diff --git a/app/components/UI/TransactionElement/index.test.js b/app/components/UI/TransactionElement/index.test.js index 30869783245..15cbd4ee5f3 100644 --- a/app/components/UI/TransactionElement/index.test.js +++ b/app/components/UI/TransactionElement/index.test.js @@ -13,6 +13,11 @@ describe('TransactionElement', () => { CurrencyRateController: { currentCurrency: 'usd', conversionRate: 0.1 + }, + NetworkController: { + provider: { + ticker: 'ETH' + } } } } diff --git a/app/components/UI/TransactionNotification/index.js b/app/components/UI/TransactionNotification/index.js index 4d08c77b25d..6144f88908e 100644 --- a/app/components/UI/TransactionNotification/index.js +++ b/app/components/UI/TransactionNotification/index.js @@ -1,5 +1,5 @@ import React, { Fragment } from 'react'; -import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; +import { TouchableOpacity, StyleSheet, View, Text } from 'react-native'; import PropTypes from 'prop-types'; import { colors, baseStyles, fontStyles } from '../../../styles/common'; import ElevatedView from 'react-native-elevated-view'; @@ -9,6 +9,7 @@ import DeviceSize from '../../../util/DeviceSize'; import AnimatedSpinner from '../AnimatedSpinner'; import { hideMessage } from 'react-native-flash-message'; import { strings } from '../../../../locales/i18n'; +import GestureRecognizer from 'react-native-swipe-gestures'; const styles = StyleSheet.create({ defaultFlashFloating: { @@ -60,17 +61,12 @@ export const TransactionNotification = props => { case 'pending': return ; case 'success': - return ; case 'received': - return ; + return ; + case 'cancelled': case 'error': return ( - + ); } }; @@ -87,6 +83,8 @@ export const TransactionNotification = props => { amount: transaction.amount, assetType: transaction.assetType }); + case 'cancelled': + return strings('notifications.cancelled_title'); case 'error': return strings('notifications.error_title'); } @@ -118,9 +116,23 @@ export const TransactionNotification = props => { return ( - - {this._getContent()} - + hideMessage()} + config={{ + velocityThreshold: 0.2, + directionalOffsetThreshold: 50 + }} + style={baseStyles.flex} + > + + {this._getContent()} + + ); }; diff --git a/app/components/UI/TransactionReview/TransactionReviewData/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewData/__snapshots__/index.test.js.snap index cbd64a4f342..464eaa10ff3 100644 --- a/app/components/UI/TransactionReview/TransactionReviewData/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewData/__snapshots__/index.test.js.snap @@ -16,7 +16,7 @@ exports[`TransactionReviewData should render correctly 1`] = ` }, Object { "borderBottomWidth": 1, - "borderColor": "#d2d8dd", + "borderColor": "#bbc0c5", }, ] } @@ -32,7 +32,7 @@ exports[`TransactionReviewData should render correctly 1`] = ` - 0 ETH + < 0.00001 ETH @@ -95,7 +95,7 @@ exports[`TransactionReviewInformation should render correctly 1`] = ` { const totalEth = isBN(value) ? value.add(totalGas) : totalGas; - const totalFiat = weiToFiat(totalEth, conversionRate, currentCurrency).toUpperCase(); + const totalFiat = weiToFiat(totalEth, conversionRate, currentCurrency.toUpperCase()); const totalValue = ( {' '} @@ -217,7 +218,7 @@ class TransactionReviewInformation extends Component { } = this.props; const { amountError } = this.state; const totalGas = isBN(gas) && isBN(gasPrice) ? gas.mul(gasPrice) : toBN('0x0'); - const totalGasFiat = weiToFiat(totalGas, conversionRate, currentCurrency).toUpperCase(); + const totalGasFiat = weiToFiat(totalGas, conversionRate, currentCurrency.toUpperCase()); const totalGasEth = renderFromWei(totalGas).toString() + ' ' + strings('unit.eth'); const [totalFiat, totalValue] = this.getRenderTotals()(); diff --git a/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap index eaa61175e48..b912d5f673c 100644 --- a/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/TransactionReviewSummary/__snapshots__/index.test.js.snap @@ -4,7 +4,7 @@ exports[`TransactionReviewSummary should render correctly 1`] = ` - - - - Edit - - `; diff --git a/app/components/UI/TransactionReview/TransactionReviewSummary/index.js b/app/components/UI/TransactionReview/TransactionReviewSummary/index.js index b03d9ff9ed2..40b14a8d487 100644 --- a/app/components/UI/TransactionReview/TransactionReviewSummary/index.js +++ b/app/components/UI/TransactionReview/TransactionReviewSummary/index.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { StyleSheet, Text, View, TouchableOpacity } from 'react-native'; +import { StyleSheet, Text, View } from 'react-native'; import { weiToFiat, balanceToFiat, @@ -10,17 +10,16 @@ import { } from '../../../../util/number'; import { colors, fontStyles } from '../../../../styles/common'; import { strings } from '../../../../../locales/i18n'; -import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import { connect } from 'react-redux'; const styles = StyleSheet.create({ confirmBadge: { ...fontStyles.normal, alignItems: 'center', - borderColor: colors.subtleGray, + borderColor: colors.grey400, borderRadius: 4, borderWidth: 1, - color: colors.subtleGray, + color: colors.grey400, fontSize: 12, lineHeight: 22, textAlign: 'center', @@ -32,31 +31,14 @@ const styles = StyleSheet.create({ }, summaryFiat: { ...fontStyles.normal, - color: colors.copy, + color: colors.fontPrimary, fontSize: 44, paddingVertical: 4 }, summaryEth: { ...fontStyles.normal, - color: colors.subtleGray, + color: colors.grey400, fontSize: 24 - }, - goBack: { - alignItems: 'center', - flexDirection: 'row', - marginLeft: -8, - marginTop: 8, - position: 'relative', - width: 150 - }, - goBackText: { - ...fontStyles.bold, - color: colors.primary, - fontSize: 22 - }, - goBackIcon: { - color: colors.primary, - flex: 0 } }); @@ -84,16 +66,7 @@ class TransactionReviewSummary extends Component { /** * Transaction corresponding action key */ - actionKey: PropTypes.string, - /** - * Callback for transaction edition - */ - edit: PropTypes.func - }; - - edit = () => { - const { edit } = this.props; - edit && edit(); + actionKey: PropTypes.string }; getRenderValues = () => { @@ -149,11 +122,6 @@ class TransactionReviewSummary extends Component { {assetAmount} )} - - - - {strings('transaction.edit')} - ); }; diff --git a/app/components/UI/TransactionReview/__snapshots__/index.test.js.snap b/app/components/UI/TransactionReview/__snapshots__/index.test.js.snap index ff079a7cacf..5422b8d5d04 100644 --- a/app/components/UI/TransactionReview/__snapshots__/index.test.js.snap +++ b/app/components/UI/TransactionReview/__snapshots__/index.test.js.snap @@ -13,7 +13,7 @@ exports[`TransactionReview should render correctly 1`] = ` style={ Object { "borderBottomWidth": 1, - "borderColor": "#dedede", + "borderColor": "#d6d9dc", "borderTopWidth": 1, "flexDirection": "row", "flexGrow": 0, @@ -32,7 +32,7 @@ exports[`TransactionReview should render correctly 1`] = ` "width": "50%", }, Object { - "borderColor": "#dedede", + "borderColor": "#d6d9dc", "borderRightWidth": 1, "paddingLeft": 20, "paddingRight": 35, @@ -63,7 +63,7 @@ exports[`TransactionReview should render correctly 1`] = ` Object { "alignSelf": "center", "backgroundColor": "#FFFFFF", - "borderColor": "#d2d8dd", + "borderColor": "#bbc0c5", "borderRadius": 15, "borderWidth": 1, "height": 30, @@ -82,7 +82,7 @@ exports[`TransactionReview should render correctly 1`] = ` size={22} style={ Object { - "color": "#7c7e84", + "color": "#848c96", "marginLeft": 3, "marginTop": 3, } @@ -144,7 +144,6 @@ exports[`TransactionReview should render correctly 1`] = ` { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.TRANSACTIONS_CONFIRM_STARTED); + }); }; edit = () => { const { onModeChange } = this.props; + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.TRANSACTIONS_EDIT_TRANSACTION); onModeChange && onModeChange('edit'); }; @@ -227,7 +233,7 @@ class TransactionReview extends Component { return ( - + {this.renderTransactionDetails()} {error && {error}} diff --git a/app/components/UI/Transactions/index.js b/app/components/UI/Transactions/index.js index f1c3673a18d..b7036663df6 100644 --- a/app/components/UI/Transactions/index.js +++ b/app/components/UI/Transactions/index.js @@ -17,10 +17,8 @@ import { colors, fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import TransactionElement from '../TransactionElement'; import Engine from '../../../core/Engine'; -import { hasBlockExplorer, getNetworkTypeById } from '../../../util/networks'; +import { hasBlockExplorer } from '../../../util/networks'; import { showAlert } from '../../../actions/alert'; -import { getEtherscanTransactionUrl, getEtherscanBaseUrl } from '../../../util/etherscan'; -import Logger from '../../../util/Logger'; import TransactionsNotificationManager from '../../../core/TransactionsNotificationManager'; const styles = StyleSheet.create({ @@ -215,21 +213,6 @@ class Transactions extends PureComponent { keyExtractor = item => item.id; - viewOnEtherscan = (networkID, transactionHash) => { - try { - const network = getNetworkTypeById(networkID); - const url = getEtherscanTransactionUrl(network, transactionHash); - const etherscan_url = getEtherscanBaseUrl(network).replace('https://', ''); - this.props.navigation.push('Webview', { - url, - title: etherscan_url - }); - } catch (e) { - // eslint-disable-next-line no-console - Logger.error(`can't get a block explorer link for network `, networkID, e); - } - }; - blockExplorer = () => hasBlockExplorer(this.props.networkType); renderItem = ({ item, index }) => ( @@ -246,7 +229,7 @@ class Transactions extends PureComponent { conversionRate={this.props.conversionRate} currentCurrency={this.props.currentCurrency} showAlert={this.props.showAlert} - viewOnEtherscan={this.viewOnEtherscan} + navigation={this.props.navigation} /> ); diff --git a/app/components/UI/TypedSign/__snapshots__/index.test.js.snap b/app/components/UI/TypedSign/__snapshots__/index.test.js.snap index bb1ca8ebb5b..a56c8794799 100644 --- a/app/components/UI/TypedSign/__snapshots__/index.test.js.snap +++ b/app/components/UI/TypedSign/__snapshots__/index.test.js.snap @@ -39,11 +39,12 @@ exports[`TypedSign should render correctly 1`] = ` } onCancel={[Function]} onConfirm={[Function]} + type="typedSign" > {strings('signature_request.message')} diff --git a/app/components/UI/WalletConnectSessionApproval/__snapshots__/index.test.js.snap b/app/components/UI/WalletConnectSessionApproval/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..bf4849675a0 --- /dev/null +++ b/app/components/UI/WalletConnectSessionApproval/__snapshots__/index.test.js.snap @@ -0,0 +1,290 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WalletConnectSessionApproval should render correctly 1`] = ` + + + + + WALLETCONNECT REQUEST + + + + + + + + + + + + + + + + + + + + + + + Account 1 + + + + + + + + would like to + : + + + + View your + + + public address + + + + + + By clicking connect, you allow this dapp to view your public address. This is an important security step to protect your data from potential phishing risks. + + + + +`; diff --git a/app/components/UI/WalletConnectSessionApproval/index.js b/app/components/UI/WalletConnectSessionApproval/index.js new file mode 100644 index 00000000000..79b5b18adde --- /dev/null +++ b/app/components/UI/WalletConnectSessionApproval/index.js @@ -0,0 +1,246 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { StyleSheet, Text, View } from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import ActionView from '../ActionView'; +import ElevatedView from 'react-native-elevated-view'; +import Identicon from '../Identicon'; +import { strings } from '../../../../locales/i18n'; +import { colors, fontStyles } from '../../../styles/common'; +import DeviceSize from '../../../util/DeviceSize'; +import WebsiteIcon from '../WebsiteIcon'; +import { renderAccountName } from '../../../util/address'; + +const styles = StyleSheet.create({ + root: { + backgroundColor: colors.white, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + minHeight: '70%', + paddingBottom: DeviceSize.isIphoneX() ? 20 : 0 + }, + wrapper: { + paddingHorizontal: 25 + }, + title: { + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 14, + marginVertical: 24, + textAlign: 'center' + }, + intro: { + ...fontStyles.normal, + textAlign: 'center', + color: colors.fontPrimary, + fontSize: 20, + marginVertical: 24 + }, + dappTitle: { + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 20 + }, + permissions: { + alignItems: 'center', + borderBottomWidth: 1, + borderColor: colors.grey100, + borderTopWidth: 1, + display: 'flex', + flexDirection: 'row', + paddingHorizontal: 8, + paddingVertical: 16 + }, + permissionText: { + ...fontStyles.normal, + color: colors.fontPrimary, + flexGrow: 1, + flexShrink: 1, + fontSize: 14 + }, + permission: { + ...fontStyles.bold, + color: colors.fontPrimary, + fontSize: 14 + }, + warning: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 14, + marginTop: 24 + }, + header: { + alignItems: 'flex-start', + display: 'flex', + flexDirection: 'row', + marginBottom: 12 + }, + headerTitle: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 16, + textAlign: 'center' + }, + selectedAddress: { + ...fontStyles.normal, + color: colors.fontPrimary, + fontSize: 16, + marginTop: 12, + textAlign: 'center' + }, + headerUrl: { + ...fontStyles.normal, + color: colors.fontSecondary, + fontSize: 12, + textAlign: 'center' + }, + dapp: { + alignItems: 'center', + paddingHorizontal: 14, + width: '50%' + }, + graphic: { + alignItems: 'center', + position: 'absolute', + top: 12, + width: '100%' + }, + check: { + alignItems: 'center', + height: 2, + width: '33%' + }, + border: { + borderColor: colors.grey400, + borderStyle: 'dashed', + borderWidth: 1, + left: 0, + overflow: 'hidden', + position: 'absolute', + top: 12, + width: '100%', + zIndex: 1 + }, + checkWrapper: { + alignItems: 'center', + backgroundColor: colors.green500, + borderRadius: 12, + height: 24, + position: 'relative', + width: 24, + zIndex: 2 + }, + checkIcon: { + color: colors.white, + fontSize: 14, + lineHeight: 24 + }, + icon: { + borderRadius: 27, + marginBottom: 12, + height: 54, + width: 54 + } +}); + +/** + * WalletConnect request approval component + */ +class WalletConnectSessionApproval extends Component { + static propTypes = { + /** + * Object containing current page title, url, and icon href + */ + currentPageInformation: PropTypes.object, + /** + * Callback triggered on account access approval + */ + onConfirm: PropTypes.func, + /** + * Callback triggered on account access rejection + */ + onCancel: PropTypes.func, + /** + /* Identities object required to get account name + */ + identities: PropTypes.object, + /** + * A string that represents the selected address + */ + selectedAddress: PropTypes.string + }; + + render = () => { + const { + currentPageInformation: { title, url }, + onConfirm, + onCancel, + selectedAddress, + identities + } = this.props; + return ( + + + + {strings('accountApproval.walletconnect_title')} + + + + + + + + + {title} + + + {url} + + + + + + + + + + + + + + {renderAccountName(selectedAddress, identities)} + + + + + {title} + {strings('accountApproval.action')}: + + + + {strings('accountApproval.permission')} + {strings('accountApproval.address')} + + + + {strings('accountApproval.warning')} + + + + ); + }; +} + +const mapStateToProps = state => ({ + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + identities: state.engine.backgroundState.PreferencesController.identities +}); + +export default connect(mapStateToProps)(WalletConnectSessionApproval); diff --git a/app/components/UI/WalletConnectSessionApproval/index.test.js b/app/components/UI/WalletConnectSessionApproval/index.test.js new file mode 100644 index 00000000000..e1feeefedac --- /dev/null +++ b/app/components/UI/WalletConnectSessionApproval/index.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import WalletConnectSessionApproval from './'; +import { shallow } from 'enzyme'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); + +describe('WalletConnectSessionApproval', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + PreferencesController: { + selectedAddress: '0xe7E125654064EEa56229f273dA586F10DF96B0a1', + identities: { '0xe7E125654064EEa56229f273dA586F10DF96B0a1': { name: 'Account 1' } } + } + } + } + }; + + const wrapper = shallow( + , + { + context: { store: mockStore(initialState) } + } + ); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/WatchAssetRequest/__snapshots__/index.test.js.snap b/app/components/UI/WatchAssetRequest/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..4e6b0fee289 --- /dev/null +++ b/app/components/UI/WatchAssetRequest/__snapshots__/index.test.js.snap @@ -0,0 +1,212 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WatchAssetRequest should render correctly 1`] = ` + + + + Add Suggested Token + + + + + + + Would you like to add this token? + + + + + + + Token + + + + + + + + + TKN + + + + + + + + Balance + + + + + 0 + + TKN + + + + + + + +`; diff --git a/app/components/UI/WatchAssetRequest/index.js b/app/components/UI/WatchAssetRequest/index.js new file mode 100644 index 00000000000..790bdb7bf51 --- /dev/null +++ b/app/components/UI/WatchAssetRequest/index.js @@ -0,0 +1,189 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Platform, StyleSheet, View, Text } from 'react-native'; +import { colors, fontStyles } from '../../../styles/common'; +import { strings } from '../../../../locales/i18n'; +import { connect } from 'react-redux'; +import ActionView from '../ActionView'; +import { renderFromTokenMinimalUnit } from '../../../util/number'; +import TokenImage from '../../UI/TokenImage'; +import DeviceSize from '../../../util/DeviceSize'; +import Engine from '../../../core/Engine'; + +const styles = StyleSheet.create({ + root: { + backgroundColor: colors.white, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + paddingBottom: DeviceSize.isIphoneX() ? 20 : 0, + minHeight: Platform.OS === 'ios' ? '50%' : '60%' + }, + title: { + textAlign: 'center', + fontSize: 18, + marginVertical: 12, + marginHorizontal: 20, + color: colors.fontPrimary, + ...fontStyles.bold + }, + text: { + ...fontStyles.normal, + fontSize: 16, + paddingTop: 25, + paddingHorizontal: 10 + }, + tokenInformation: { + flexDirection: 'row', + marginHorizontal: 40, + flex: 1, + alignItems: 'flex-start', + marginVertical: 30 + }, + tokenInfo: { + flex: 1, + flexDirection: 'column' + }, + infoTitleWrapper: { + alignItems: 'center' + }, + infoTitle: { + ...fontStyles.bold + }, + infoBalance: { + alignItems: 'center' + }, + infoToken: { + alignItems: 'center' + }, + token: { + flexDirection: 'row' + }, + identicon: { + paddingVertical: 10 + }, + signText: { + ...fontStyles.normal, + fontSize: 16 + }, + addMessage: { + flexDirection: 'row', + margin: 20 + }, + children: { + alignItems: 'center', + borderTopColor: colors.grey200, + borderTopWidth: 1 + } +}); + +/** + * Component that renders watch asset content + */ +class WatchAssetRequest extends Component { + static propTypes = { + /** + * Callback triggered when this message signature is rejected + */ + onCancel: PropTypes.func, + /** + * Callback triggered when this message signature is approved + */ + onConfirm: PropTypes.func, + /** + * Token object + */ + suggestedAssetMeta: PropTypes.object, + /** + * Object containing token balances in the format address => balance + */ + contractBalances: PropTypes.object + }; + + componentWillUnmount = async () => { + const { AssetsController } = Engine.context; + const { suggestedAssetMeta } = this.props; + await AssetsController.rejectWatchAsset(suggestedAssetMeta.id); + }; + + onConfirm = async () => { + const { onConfirm, suggestedAssetMeta } = this.props; + const { AssetsController } = Engine.context; + await AssetsController.acceptWatchAsset(suggestedAssetMeta.id); + onConfirm && onConfirm(); + }; + + render() { + const { + suggestedAssetMeta: { asset }, + contractBalances + } = this.props; + const balance = + asset.address in contractBalances + ? renderFromTokenMinimalUnit(contractBalances[asset.address], asset.decimals) + : '0'; + return ( + + + + {strings('watch_asset_request.title')} + + + + + + {strings('watch_asset_request.message')} + + + + + + {strings('watch_asset_request.token')} + + + + + + + + {asset.symbol} + + + + + + + {strings('watch_asset_request.balance')} + + + + + {balance} {asset.symbol} + + + + + + + + ); + } +} + +const mapStateToProps = state => ({ + contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances +}); + +export default connect(mapStateToProps)(WatchAssetRequest); diff --git a/app/components/UI/WatchAssetRequest/index.test.js b/app/components/UI/WatchAssetRequest/index.test.js new file mode 100644 index 00000000000..9b8c2545128 --- /dev/null +++ b/app/components/UI/WatchAssetRequest/index.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import WatchAssetRequest from './'; +import configureMockStore from 'redux-mock-store'; +import { BN } from 'ethereumjs-util'; + +const mockStore = configureMockStore(); + +describe('WatchAssetRequest', () => { + it('should render correctly', () => { + const initialState = { + engine: { + backgroundState: { + TokenBalancesController: { + contractBalances: { '0x2': new BN(0) } + } + } + } + }; + + const wrapper = shallow( + , + { + context: { store: mockStore(initialState) } + } + ); + expect(wrapper.dive()).toMatchSnapshot(); + }); +}); diff --git a/app/components/UI/WebsiteIcon/__snapshots__/index.test.js.snap b/app/components/UI/WebsiteIcon/__snapshots__/index.test.js.snap index 008a28b0565..c48a03fffc1 100644 --- a/app/components/UI/WebsiteIcon/__snapshots__/index.test.js.snap +++ b/app/components/UI/WebsiteIcon/__snapshots__/index.test.js.snap @@ -2,24 +2,22 @@ exports[`WebsiteIcon should render correctly 1`] = ` - - + - - - + /> + `; diff --git a/app/components/UI/WebsiteIcon/index.js b/app/components/UI/WebsiteIcon/index.js index 505630885f0..7becf8020dd 100644 --- a/app/components/UI/WebsiteIcon/index.js +++ b/app/components/UI/WebsiteIcon/index.js @@ -8,7 +8,7 @@ import { getHost } from '../../../util/browser'; const styles = StyleSheet.create({ fallback: { alignContent: 'center', - backgroundColor: colors.gray, + backgroundColor: colors.grey400, borderRadius: 27, height: 54, justifyContent: 'center', @@ -46,31 +46,39 @@ export default class WebsiteIcon extends Component { /** * String corresponding to website url */ - url: PropTypes.string + url: PropTypes.string, + /** + * Flag that determines if the background + * should be transaparent or not + */ + transparent: PropTypes.bool }; state = { renderIconUrlError: false }; - componentDidMount = () => { - this.getIconUrl(this.props.url); - }; - + /** + * Get image url from favicon api + */ getIconUrl = url => { const iconUrl = `https://api.faviconkit.com/${getHost(url)}/64`; - this.setState({ apiLogoUrl: { uri: iconUrl } }); + return iconUrl; }; + /** + * Sets component state to renderIconUrlError to render placeholder image + */ onRenderIconUrlError = async () => { await this.setState({ renderIconUrlError: true }); }; - renderIconWithFallback = error => { - const { viewStyle, style, title, textStyle } = this.props; - const { apiLogoUrl } = this.state; + render = () => { + const { renderIconUrlError } = this.state; + const { url, viewStyle, style, title, textStyle, transparent } = this.props; + const apiLogoUrl = { uri: this.getIconUrl(url) }; - if (error && title) { + if (renderIconUrlError && title) { return ( @@ -82,15 +90,10 @@ export default class WebsiteIcon extends Component { return ( - + ); }; - - render = () => { - const { renderIconUrlError } = this.state; - return {this.renderIconWithFallback(renderIconUrlError)}; - }; } diff --git a/app/components/UI/WebviewProgressBar/__snapshots__/index.test.js.snap b/app/components/UI/WebviewProgressBar/__snapshots__/index.test.js.snap index 33d7cec1491..9cc23f64456 100644 --- a/app/components/UI/WebviewProgressBar/__snapshots__/index.test.js.snap +++ b/app/components/UI/WebviewProgressBar/__snapshots__/index.test.js.snap @@ -14,7 +14,7 @@ exports[`WebviewProgressBar should render correctly 1`] = ` animationType="spring" borderRadius={0} borderWidth={0} - color="#008edf" + color="#037dd6" height={3} indeterminate={false} progress={0} diff --git a/app/components/UI/WebviewProgressBar/index.js b/app/components/UI/WebviewProgressBar/index.js index 1bd6d4aa208..c89f6d2a853 100644 --- a/app/components/UI/WebviewProgressBar/index.js +++ b/app/components/UI/WebviewProgressBar/index.js @@ -52,7 +52,7 @@ export default class WebviewProgressBar extends Component { } titleText={strings('account_backup_step_5.modal_title')} buttonText={strings('account_backup_step_5.modal_button')} diff --git a/app/components/Views/AccountBackupStep6/index.js b/app/components/Views/AccountBackupStep6/index.js index 92ddf69077e..2ddcf216ee2 100644 --- a/app/components/Views/AccountBackupStep6/index.js +++ b/app/components/Views/AccountBackupStep6/index.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Clipboard, ScrollView, Text, Image, View, SafeAreaView, StyleSheet } from 'react-native'; +import { Clipboard, ScrollView, Text, Image, View, SafeAreaView, StyleSheet, TouchableOpacity } from 'react-native'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Emoji from 'react-native-emoji'; @@ -8,7 +8,6 @@ import { colors, fontStyles } from '../../../styles/common'; import StyledButton from '../../UI/StyledButton'; import { strings } from '../../../../locales/i18n'; import CustomAlert from '../../UI/CustomAlert'; -import { TouchableOpacity } from 'react-native-gesture-handler'; import { showAlert } from '../../../actions/alert'; const styles = StyleSheet.create({ @@ -155,7 +154,7 @@ class AccountBackupStep6 extends Component { {strings('account_backup_step_6.tip_2')} - + {strings('account_backup_step_6.copy_seed_phrase')} @@ -185,7 +184,7 @@ class AccountBackupStep6 extends Component { } titleText={strings('account_backup_step_6.modal_title')} buttonText={strings('account_backup_step_6.modal_button')} diff --git a/app/components/Views/AddAsset/index.js b/app/components/Views/AddAsset/index.js index 3012924a4c3..cdfa1fefb16 100644 --- a/app/components/Views/AddAsset/index.js +++ b/app/components/Views/AddAsset/index.js @@ -17,7 +17,7 @@ const styles = StyleSheet.create({ }, tabUnderlineStyle: { height: 2, - backgroundColor: colors.primary + backgroundColor: colors.blue }, tabStyle: { paddingBottom: 0 @@ -52,7 +52,7 @@ export default class AddAsset extends Component { return ( ) : ( - + )} ); diff --git a/app/components/Views/AddBookmark/__snapshots__/index.test.js.snap b/app/components/Views/AddBookmark/__snapshots__/index.test.js.snap index 3d27ef67c68..77e6461ae25 100644 --- a/app/components/Views/AddBookmark/__snapshots__/index.test.js.snap +++ b/app/components/Views/AddBookmark/__snapshots__/index.test.js.snap @@ -49,7 +49,7 @@ exports[`AddBookmark should render correctly 1`] = ` returnKeyType="next" style={ Object { - "borderColor": "#CCCCCC", + "borderColor": "#d6d9dc", "borderRadius": 4, "borderWidth": 1, "fontFamily": "Roboto", @@ -64,7 +64,7 @@ exports[`AddBookmark should render correctly 1`] = ` - getTransactionOptionsTitle('approval.title', strings('navigation.cancel'), navigation); + static navigationOptions = ({ navigation }) => getTransactionOptionsTitle('approval.title', navigation); static propTypes = { /** @@ -42,11 +47,19 @@ class Approval extends Component { /** * List of transactions */ - transactions: PropTypes.array + transactions: PropTypes.array, + /** + * Map representing the address book + */ + addressBook: PropTypes.array, + /** + * A string representing the network name + */ + networkType: PropTypes.string }; state = { - mode: 'review', + mode: REVIEW, transactionHandled: false }; @@ -56,9 +69,73 @@ class Approval extends Component { if (!transactionHandled) { Engine.context.TransactionController.cancelTransaction(transaction.id); } + Engine.context.TransactionController.hub.removeAllListeners(`${transaction.id}:finished`); this.clear(); }; + componentDidMount = () => { + const { navigation } = this.props; + navigation && navigation.setParams({ mode: REVIEW, dispatch: this.onModeChange }); + this.trackConfirmScreen(); + }; + + /** + * Call Analytics to track confirm started event for approval screen + */ + trackConfirmScreen = () => { + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.TRANSACTIONS_CONFIRM_STARTED, this.getTrackingParams()); + }; + + /** + * Call Analytics to track confirm started event for approval screen + */ + trackEditScreen = async () => { + const { transaction } = this.props; + const actionKey = await getTransactionReviewActionKey(transaction); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.TRANSACTIONS_EDIT_TRANSACTION, { + ...this.getTrackingParams(), + actionKey + }); + }; + + /** + * Call Analytics to track cancel pressed + */ + trackOnCancel = () => { + Analytics.trackEventWithParameters( + ANALYTICS_EVENT_OPTS.TRANSACTIONS_CANCEL_TRANSACTION, + this.getTrackingParams() + ); + }; + + /** + * Call Analytics to track confirm pressed + */ + trackOnConfirm = () => { + Analytics.trackEventWithParameters( + ANALYTICS_EVENT_OPTS.TRANSACTIONS_COMPLETED_TRANSACTION, + this.getTrackingParams() + ); + }; + + /** + * Returns corresponding tracking params to send + * + * @return {object} - Object containing view, network, activeCurrency and assetType + */ + getTrackingParams = () => { + const { + networkType, + transaction: { selectedAsset, assetType } + } = this.props; + return { + view: APPROVAL, + network: networkType, + activeCurrency: selectedAsset.symbol || selectedAsset.contractName, + assetType + }; + }; + /** * Transaction state is erased, ready to create a new clean transaction */ @@ -68,23 +145,36 @@ class Approval extends Component { onCancel = () => { this.props.navigation.pop(); + this.state.mode === REVIEW && this.trackOnCancel(); }; /** * Callback on confirm transaction */ onConfirm = async () => { - const { TransactionController } = Engine.context; - const { transactions } = this.props; + const { TransactionController, AddressBookController } = Engine.context; + const { transactions, addressBook } = this.props; let { transaction } = this.props; try { transaction = this.prepareTransaction(transaction); TransactionController.hub.once(`${transaction.id}:finished`, transactionMeta => { + // Add to the AddressBook if it's an unkonwn address + const checksummedAddress = toChecksumAddress(transactionMeta.transaction.to); + const existingContact = addressBook.find( + ({ address }) => toChecksumAddress(address) === checksummedAddress + ); + if (!existingContact) { + AddressBookController.set(checksummedAddress, ''); + } + if (transactionMeta.status === 'submitted') { this.setState({ transactionHandled: true }); this.props.navigation.pop(); - TransactionsNotificationManager.watchSubmittedTransaction(transactionMeta); + TransactionsNotificationManager.watchSubmittedTransaction({ + ...transactionMeta, + assetType: transaction.assetType + }); } else { throw transactionMeta.error; } @@ -95,13 +185,26 @@ class Approval extends Component { await TransactionController.updateTransaction(updatedTx); await TransactionController.approveTransaction(transaction.id); } catch (error) { - Alert.alert('Transaction error', JSON.stringify(error), [{ text: 'OK' }]); + Alert.alert('Transaction error', error && error.message, [{ text: 'OK' }]); this.setState({ transactionHandled: false }); } + this.trackOnConfirm(); }; + /** + * Handle approval mode change + * If changed to 'review' sends an Analytics track event + * + * @param mode - Transaction mode, review or edit + */ onModeChange = mode => { + const { navigation } = this.props; + navigation && navigation.setParams({ mode }); this.setState({ mode }); + InteractionManager.runAfterInteractions(() => { + mode === REVIEW && this.trackConfirmScreen(); + mode === EDIT && this.trackEditScreen(); + }); }; /** @@ -141,10 +244,11 @@ class Approval extends Component { render = () => { const { transaction } = this.props; + const { mode } = this.state; return ( ({ transaction: state.transaction, - transactions: state.engine.backgroundState.TransactionController.transactions + transactions: state.engine.backgroundState.TransactionController.transactions, + addressBook: state.engine.backgroundState.AddressBookController.addressBook, + networkType: state.engine.backgroundState.NetworkController.provider.type }); const mapDispatchToProps = dispatch => ({ diff --git a/app/components/Views/Approval/index.test.js b/app/components/Views/Approval/index.test.js index 3cafcb54b07..6f4b0004d98 100644 --- a/app/components/Views/Approval/index.test.js +++ b/app/components/Views/Approval/index.test.js @@ -15,13 +15,21 @@ describe('Approval', () => { gas: '', gasPrice: '', to: '0x2', - selectedAsset: undefined, + selectedAsset: { symbol: 'ETH' }, assetType: undefined }, engine: { backgroundState: { TransactionController: { transactions: [] + }, + AddressBookController: { + addressBook: [] + }, + NetworkController: { + provider: { + type: 'ropsten' + } } } } diff --git a/app/components/Views/Asset/index.js b/app/components/Views/Asset/index.js index b66775e5b07..972d39837d5 100644 --- a/app/components/Views/Asset/index.js +++ b/app/components/Views/Asset/index.js @@ -69,6 +69,7 @@ class Asset extends Component { txs = []; txsPending = []; isNormalizing = false; + networkType = ''; static navigationOptions = ({ navigation }) => getNetworkNavbarOptions(navigation.getParam('symbol', ''), false, navigation); @@ -136,6 +137,7 @@ class Asset extends Component { if ( (this.txs.length === 0 && !this.state.transactionsUpdated) || this.txs.length !== txs.length || + this.networkType !== networkType || this.didTxStatusesChange(newPendingTxs) ) { this.txs = txs; @@ -146,6 +148,7 @@ class Asset extends Component { this.setState({ transactionsUpdated: true, loading: false }); } this.isNormalizing = false; + this.networkType = networkType; } renderLoader = () => ( diff --git a/app/components/Views/Browser/index.js b/app/components/Views/Browser/index.js index 0ef270041f2..04ae0db83cb 100644 --- a/app/components/Views/Browser/index.js +++ b/app/components/Views/Browser/index.js @@ -1,1634 +1,278 @@ -import React, { Component } from 'react'; -import { - Dimensions, - Text, - ActivityIndicator, - Platform, - StyleSheet, - TextInput, - View, - TouchableWithoutFeedback, - Alert, - Animated, - TouchableOpacity, - Linking, - Keyboard, - BackHandler -} from 'react-native'; -import Web3Webview from 'react-native-web3-webview'; -import Icon from 'react-native-vector-icons/FontAwesome'; -import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; -import IonIcon from 'react-native-vector-icons/Ionicons'; -import PropTypes from 'prop-types'; -import RNFS from 'react-native-fs'; -import Share from 'react-native-share'; // eslint-disable-line import/default +import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; -import BackgroundBridge from '../../../core/BackgroundBridge'; -import Engine from '../../../core/Engine'; +import { Dimensions, Platform } from 'react-native'; +import PropTypes from 'prop-types'; +import { createNewTab, closeAllTabs, closeTab, setActiveTab, updateTab } from '../../../actions/browser'; +import Tabs from '../../UI/Tabs'; import { getBrowserViewNavbarOptions } from '../../UI/Navbar'; -import PhishingModal from '../../UI/PhishingModal'; -import WebviewProgressBar from '../../UI/WebviewProgressBar'; -import BrowserHome from '../../Views/BrowserHome'; -import { colors, baseStyles, fontStyles } from '../../../styles/common'; -import Networks from '../../../util/networks'; +import { captureScreen } from 'react-native-view-shot'; import Logger from '../../../util/Logger'; -import onUrlSubmit, { getHost } from '../../../util/browser'; -import resolveEnsToIpfsContentId from '../../../lib/ens-ipfs/resolver'; -import Button from '../../UI/Button'; -import { strings } from '../../../../locales/i18n'; -import URL from 'url-parse'; -import Modal from 'react-native-modal'; -import PersonalSign from '../../UI/PersonalSign'; -import TypedSign from '../../UI/TypedSign'; -import UrlAutocomplete from '../../UI/UrlAutocomplete'; -import AccountApproval from '../../UI/AccountApproval'; -import { approveHost } from '../../../actions/privacy'; -import { addBookmark } from '../../../actions/bookmarks'; -import { addToHistory, addToWhitelist } from '../../../actions/browser'; -import { setTransactionObject } from '../../../actions/transaction'; -import { hexToBN } from '../../../util/number'; -import DeviceSize from '../../../util/DeviceSize'; -import AppConstants from '../../../core/AppConstants'; -import SearchApi from 'react-native-search-api'; -import DeeplinkManager from '../../../core/DeeplinkManager'; -import Branch from 'react-native-branch'; - -const HOMEPAGE_URL = 'about:blank'; -const SUPPORTED_TOP_LEVEL_DOMAINS = ['eth', 'test']; -const BOTTOM_NAVBAR_HEIGHT = Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 86 : 60; +import BrowserTab from '../BrowserTab'; -const styles = StyleSheet.create({ - wrapper: { - ...baseStyles.flexGrow, - backgroundColor: colors.white - }, - icon: { - color: colors.copy, - height: 28, - lineHeight: 28, - textAlign: 'center', - width: 36, - alignSelf: 'center' - }, - disabledIcon: { - color: colors.ash - }, - progressBarWrapper: { - height: 3, - width: '100%', - left: 0, - right: 0, - top: 0, - position: 'absolute' - }, - loader: { - backgroundColor: colors.white, - flex: 1, - justifyContent: 'center', - alignItems: 'center' - }, - optionsOverlay: { - position: 'absolute', - zIndex: 99999998, - top: 0, - bottom: 0, - left: 0, - right: 0 - }, - optionsWrapper: { - position: 'absolute', - zIndex: 99999999, - width: 200, - borderWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderColor, - backgroundColor: colors.concrete - }, - optionsWrapperAndroid: { - top: 0, - right: 0, - elevation: 5 - }, - optionsWrapperIos: { - shadowColor: colors.gray, - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.5, - shadowRadius: 3, - bottom: 70, - right: 3 - }, - option: { - backgroundColor: colors.concrete, - flexDirection: 'row', - alignItems: 'flex-start', - justifyContent: 'flex-start', - marginTop: Platform.OS === 'android' ? 0 : -5 - }, - optionText: { - fontSize: 14, - color: colors.fontPrimary, - ...fontStyles.normal - }, - optionIcon: { - width: 18, - color: colors.copy, - flex: 0, - height: 15, - lineHeight: 15, - marginRight: 10, - textAlign: 'center', - alignSelf: 'center' - }, - webview: { - ...baseStyles.flexGrow - }, - bottomBar: { - backgroundColor: colors.concrete, - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - paddingTop: Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 15 : 12, - paddingBottom: Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 32 : 8, - alignItems: 'center', - flexDirection: 'row', - paddingHorizontal: 10 - }, - iconSearch: { - alignSelf: 'flex-end', - alignContent: 'flex-end' - }, - iconMore: { - alignSelf: 'flex-end', - alignContent: 'flex-end' - }, - iconsLeft: { - flex: 1, - alignContent: 'flex-start', - flexDirection: 'row' - }, - iconsRight: { - flexDirection: 'row', - alignItems: 'flex-end' - }, - urlModalContent: { - flexDirection: 'row', - paddingTop: Platform.OS === 'android' ? 10 : DeviceSize.isIphoneX() ? 50 : 27, - paddingHorizontal: 10, - backgroundColor: colors.white, - height: Platform.OS === 'android' ? 59 : DeviceSize.isIphoneX() ? 87 : 65 - }, - urlModal: { - justifyContent: 'flex-start', - margin: 0 - }, - urlInput: { - ...fontStyles.normal, - backgroundColor: Platform.OS === 'android' ? colors.white : colors.slate, - borderRadius: 30, - fontSize: Platform.OS === 'android' ? 16 : 14, - padding: 8, - paddingLeft: 15, - textAlign: 'left', - flex: 1, - height: Platform.OS === 'android' ? 40 : 30 - }, - cancelButton: { - marginTop: 7, - marginLeft: 10 - }, - cancelButtonText: { - fontSize: 14, - color: colors.primary, - ...fontStyles.normal - }, - iconCloseButton: { - borderRadius: 300, - backgroundColor: colors.fontSecondary, - color: colors.white, - fontSize: 18, - padding: 0, - height: 20, - width: 20, - paddingBottom: 0, - alignItems: 'center', - justifyContent: 'center', - marginTop: 10, - marginRight: 5 - }, - iconClose: { - color: colors.white, - fontSize: 18 - }, - bottomModal: { - justifyContent: 'flex-end', - margin: 0 - }, - fullScreenModal: { - flex: 1 - }, - homepage: { - flex: 1, - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0 - } -}); +const margin = 16; +const THUMB_WIDTH = Dimensions.get('window').width / 2 - margin * 2; +const THUMB_HEIGHT = Platform.OS === 'ios' ? THUMB_WIDTH * 1.81 : THUMB_WIDTH * 1.48; /** - * Complete Web browser component with URL entry and history management + * Component that wraps all the browser + * individual tabs and the tabs view */ -export class Browser extends Component { - static navigationOptions = ({ navigation }) => getBrowserViewNavbarOptions(navigation); - - static defaultProps = { - defaultProtocol: 'https://' - }; - +class Browser extends PureComponent { static propTypes = { - /** - * Called to approve account access for a given hostname - */ - approveHost: PropTypes.func, - /** - * Map of hostnames with approved account access - */ - approvedHosts: PropTypes.object, - /** - * Protocol string to append to URLs that have none - */ - defaultProtocol: PropTypes.string, - /** - * Object containing the information for the current transaction - */ - transaction: PropTypes.object, /** * react-navigation object used to switch between screens */ navigation: PropTypes.object, /** - * A string representing the network type + * Function to create a new tab */ - networkType: PropTypes.string, + createNewTab: PropTypes.func, /** - * A string representing the network id + * Function to close all the existing tabs */ - network: PropTypes.string, + closeAllTabs: PropTypes.func, /** - * Indicates whether privacy mode is enabled + * Function to close a specific tab */ - privacyMode: PropTypes.bool, + closeTab: PropTypes.func, /** - * A string that represents the selected address + * Function to set the active tab */ - selectedAddress: PropTypes.string, + setActiveTab: PropTypes.func, /** - * whitelisted url to bypass the phishing detection + * Function to set the update the url of a tab */ - whitelist: PropTypes.array, + updateTab: PropTypes.func, /** - * Url coming from an external source - * For ex. deeplinks + * Array of tabs */ - url: PropTypes.string, + tabs: PropTypes.array, /** - * Function to store bookmarks + * ID of the active tab */ - addBookmark: PropTypes.func, - /** - * Array of bookmarks - */ - bookmarks: PropTypes.array, - /** - * String representing the current search engine - */ - searchEngine: PropTypes.string, - /** - * Action that sets a transaction - */ - setTransactionObject: PropTypes.func, - /** - * Function to store the a page in the browser history - */ - addToBrowserHistory: PropTypes.func, - /** - * Function to store the a website in the browser whitelist - */ - addToWhitelist: PropTypes.func + activeTab: PropTypes.number }; + static navigationOptions = ({ navigation }) => getBrowserViewNavbarOptions(navigation); + tabs = {}; constructor(props) { super(props); - - const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); - - this.state = { - approvedOrigin: false, - currentEnsName: null, - currentPageTitle: '', - currentPageUrl: '', - currentPageIcon: undefined, - entryScriptWeb3: null, - fullHostname: '', - hostname: '', - inputValue: '', - autocompleteInputValue: '', - ipfsGateway: 'https://ipfs.io/ipfs/', - ipfsHash: null, - ipfsWebsite: false, - showApprovalDialog: false, - showPhishingModal: false, - signMessage: false, - signMessageParams: { data: '' }, - signType: '', - timeout: false, - url: HOMEPAGE_URL, - scrollAnim, - offsetAnim, - clampedScroll, - contentHeight: 0, - forwardEnabled: false, - forceReload: false - }; - } - - webview = React.createRef(); - inputRef = React.createRef(); - timeoutHandler = null; - prevScrollOffset = 0; - goingBack = false; - forwardHistoryStack = []; - approvalRequest; - accountsRequest; - - clampedScrollValue = 0; - offsetValue = 0; - scrollValue = 0; - scrollStopTimer = null; - - initScrollVariables() { - const scrollAnim = Platform.OS === 'ios' ? new Animated.Value(0) : null; - const offsetAnim = Platform.OS === 'ios' ? new Animated.Value(0) : null; - let clampedScroll = null; - if (Platform.OS === 'ios') { - clampedScroll = Animated.diffClamp( - Animated.add( - scrollAnim.interpolate({ - inputRange: [0, 1], - outputRange: [0, 1], - extrapolateLeft: 'clamp' - }), - offsetAnim - ), - 0, - BOTTOM_NAVBAR_HEIGHT - ); + if (!props.tabs.length) { + this.newTab(); } - - return { scrollAnim, offsetAnim, clampedScroll }; + this.createBrowserTabs(props.tabs); } - async componentDidMount() { - this.mounted = true; - this.backgroundBridge = new BackgroundBridge(Engine, this.webview, { - eth_requestAccounts: ({ hostname, params }) => { - const { approvedHosts, privacyMode, selectedAddress } = this.props; - const promise = new Promise((resolve, reject) => { - this.approvalRequest = { resolve, reject }; - }); - if (!privacyMode || ((!params || !params.force) && approvedHosts[hostname])) { - this.approvalRequest.resolve([selectedAddress]); - this.backgroundBridge.enableAccounts(); - } else { - // Let the damn website load first! - // Otherwise we don't get enough time to load the metadata - // (title, icon, etc) - - setTimeout(() => { - this.setState({ showApprovalDialog: true }); - }, 1000); - } - return promise; - }, - eth_accounts: ({ id, jsonrpc, hostname }) => { - const { approvedHosts, privacyMode, selectedAddress } = this.props; - const isEnabled = !privacyMode || approvedHosts[hostname]; - const promise = new Promise((resolve, reject) => { - this.accountsRequest = { resolve, reject }; - }); - if (isEnabled) { - this.accountsRequest.resolve({ id, jsonrpc, result: [selectedAddress] }); - } else { - this.accountsRequest.resolve({ id, jsonrpc, result: [] }); - } - return promise; - }, - web3_clientVersion: payload => - Promise.resolve({ result: 'MetaMask/0.1.0/Alpha/Mobile', jsonrpc: payload.jsonrpc, id: payload.id }), - wallet_scanQRCode: payload => { - const promise = new Promise((resolve, reject) => { - this.props.navigation.navigate('QRScanner', { - onScanSuccess: data => { - let result = data; - if (data.target_address) { - result = data.target_address; - } else if (data.scheme) { - result = JSON.stringify(data); - } - resolve({ result, jsonrpc: payload.jsonrpc, id: payload.id }); - }, - onScanError: e => { - reject({ errir: e.toString(), jsonrpc: payload.jsonrpc, id: payload.id }); - } - }); - }); - return promise; - } - }); - - const entryScriptWeb3 = - Platform.OS === 'ios' - ? await RNFS.readFile(`${RNFS.MainBundlePath}/InpageBridgeWeb3.js`, 'utf8') - : await RNFS.readFileAssets(`InpageBridgeWeb3.js`); - - const updatedentryScriptWeb3 = entryScriptWeb3.replace( - 'undefined; // INITIAL_NETWORK', - this.props.networkType === 'rpc' - ? `'${this.props.network}'` - : `'${Networks[this.props.networkType].networkId}'` - ); - - const SPA_urlChangeListener = `(function () { - var __mmHistory = window.history; - var __mmPushState = __mmHistory.pushState; - var __mmReplaceState = __mmHistory.replaceState; - function __mm__updateUrl(){ - const siteName = document.querySelector('head > meta[property="og:site_name"]'); - const title = siteName || document.querySelector('head > meta[name="title"]') || document.title; - const height = Math.max(document.documentElement.clientHeight, document.documentElement.scrollHeight, document.body.clientHeight, document.body.scrollHeight); - - window.postMessageToNative( - { - type: 'NAV_CHANGE', - payload: { - url: location.href, - title: title, - } - } - ); + componentDidMount() { + const activeTab = this.props.tabs.find(tab => tab.id === this.props.activeTab); + activeTab && this.switchToTab(activeTab); + } - setTimeout(() => { - const height = Math.max(document.documentElement.clientHeight, document.documentElement.scrollHeight, document.body.clientHeight, document.body.scrollHeight); - window.postMessageToNative( - { - type: 'GET_HEIGHT', - payload: { - height: height - } - }) - }, 500); + createBrowserTabs(tabs) { + // Delete closed tabs + Object.keys(this.tabs).forEach(tabID => { + const existingTab = tabs.find(tab => tab.id === tabID); + if (!existingTab) { + delete this.tabs[tabID]; } - - __mmHistory.pushState = function(state) { - setTimeout(function () { - __mm__updateUrl(); - }, 100); - return __mmPushState.apply(history, arguments); - }; - - __mmHistory.replaceState = function(state) { - setTimeout(function () { - __mm__updateUrl(); - }, 100); - return __mmReplaceState.apply(history, arguments); - }; - - window.onpopstate = function(event) { - __mm__updateUrl(); - }; - })(); - `; - - await this.setState({ entryScriptWeb3: updatedentryScriptWeb3 + SPA_urlChangeListener }); - - Engine.context.TransactionController.hub.on('unapprovedTransaction', this.onUnapprovedTransaction); - - Engine.context.PersonalMessageManager.hub.on('unapprovedMessage', messageParams => { - this.setState({ signMessage: true, signMessageParams: messageParams, signType: 'personal' }); - }); - Engine.context.TypedMessageManager.hub.on('unapprovedMessage', messageParams => { - this.setState({ signMessage: true, signMessageParams: messageParams, signType: 'typed' }); }); - this.loadUrl(); - - Branch.subscribe(this.handleDeeplinks); - - if (Platform.OS === 'ios') { - this.state.scrollAnim.addListener(({ value }) => { - const diff = value - this.scrollValue; - this.scrollValue = value; - this.clampedScrollValue = Math.min(Math.max(this.clampedScrollValue + diff, 0), BOTTOM_NAVBAR_HEIGHT); - }); - - this.state.offsetAnim.addListener(({ value }) => { - this.offsetValue = value; - }); - } else { - this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide); - } - - // Listen to network changes - Engine.context.TransactionController.hub.on('networkChange', this.reload); - BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBackPress); - } - - handleDeeplinks = async ({ error, params }) => { - if (error) { - Logger.error('Error from Branch: ', error); - return; - } - if (params['+non_branch_link']) { - const dm = new DeeplinkManager(this.props.navigation); - dm.parse(params['+non_branch_link']); - } else if (params.spotlight_identifier) { - setTimeout(() => { - this.props.navigation.setParams({ - url: params.spotlight_identifier, - silent: false, - showUrlModal: false + // Add new tabs + tabs.forEach(tab => { + if (!this.tabs[tab.id]) { + this.tabs[tab.id] = React.createElement(BrowserTab, { + id: tab.id, + key: `tab_${tab.id}`, + initialUrl: tab.url || 'about:blank', + updateTabInfo: (url, tabID) => this.updateTabInfo(url, tabID), + showTabs: () => this.showTabs(), + newTab: () => this.newTab() }); - }, 1000); - } - }; - - handleAndroidBackPress = () => { - if (this.state.url === HOMEPAGE_URL && this.props.navigation.getParam('url', null) === null) { - return false; - } - this.goBack(); - return true; - }; - - onUnapprovedTransaction = transactionMeta => { - if (this.props.transaction.value || this.props.transaction.to) { - return; - } - const { - transaction: { value, gas, gasPrice } - } = transactionMeta; - transactionMeta.transaction.value = hexToBN(value); - transactionMeta.transaction.gas = hexToBN(gas); - transactionMeta.transaction.gasPrice = hexToBN(gasPrice); - this.props.setTransactionObject({ - ...{ symbol: 'ETH', assetType: 'ETH', id: transactionMeta.id }, - ...transactionMeta.transaction - }); - this.props.navigation.push('ApprovalView'); - }; - - async loadUrl() { - const { navigation } = this.props; - if (navigation) { - const url = navigation.getParam('url', null); - const silent = navigation.getParam('silent', false); - if (url && !silent) { - await this.go(url); } - } - } - - componentDidUpdate(prevProps) { - const prevNavigation = prevProps.navigation; - const { navigation } = this.props; - - if (prevNavigation && navigation) { - const prevUrl = prevNavigation.getParam('url', null); - const currentUrl = navigation.getParam('url', null); - - if (currentUrl && prevUrl !== currentUrl && currentUrl !== this.state.url) { - this.loadUrl(); - } - } + }); } - componentWillUnmount() { - this.mounted = false; - // Remove all Engine listeners - Engine.context.PersonalMessageManager.hub.removeAllListeners(); - Engine.context.TypedMessageManager.hub.removeAllListeners(); - Engine.context.TransactionController.hub.removeListener('unapprovedTransaction', this.onUnapprovedTransaction); - Engine.context.TransactionController.hub.removeListener('networkChange', this.reload); - if (Platform.OS === 'ios') { - this.state.scrollAnim.removeAllListeners(); - this.state.offsetAnim.removeAllListeners(); - } else { - this.keyboardDidHideListener.remove(); - BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBackPress); + componentDidUpdate() { + if (this.props.tabs.length !== Object.keys(this.tabs).length) { + this.createBrowserTabs(this.props.tabs); } } - keyboardDidHide = () => { - const showUrlModal = (this.props.navigation && this.props.navigation.getParam('showUrlModal', false)) || false; - if (showUrlModal) { - this.hideUrlModal(); + showTabs = async () => { + try { + const activeTab = this.props.tabs.find(tab => tab.id === this.props.activeTab); + await this.takeScreenshot(activeTab.url, activeTab.id); + } catch (e) { + Logger.error(e); } - }; - isENSUrl(url) { - const urlObj = new URL(url); - const { hostname } = urlObj; - const tld = hostname.split('.').pop(); - if (SUPPORTED_TOP_LEVEL_DOMAINS.indexOf(tld.toLowerCase()) !== -1) { - return true; - } - return false; - } - - isAllowedUrl = url => { - const urlObj = new URL(url); - const { PhishingController } = Engine.context; - return ( - (this.props.whitelist && this.props.whitelist.includes(urlObj.hostname)) || - (PhishingController && !PhishingController.test(urlObj.hostname)) - ); + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + showTabs: true + }); }; - handleNotAllowedUrl = (urlToGo, hostname) => { - let host = hostname; - if (!host) { - const urlObj = new URL(urlToGo); - host = urlObj.hostname; - } - this.blockedUrl = urlToGo; - setTimeout(() => { - this.setState({ showPhishingModal: true }); - }, 500); + hideTabsAndUpdateUrl = url => { + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + showTabs: false, + url, + silent: true + }); }; - go = async url => { - const hasProtocol = url.match(/^[a-z]*:\/\//) || url === HOMEPAGE_URL; - const sanitizedURL = hasProtocol ? url : `${this.props.defaultProtocol}${url}`; - const urlObj = new URL(sanitizedURL); - const { hostname, query, pathname } = urlObj; - - let ipfsContent = null; - let currentEnsName = null; - let ipfsHash = null; - - if (this.isENSUrl(sanitizedURL)) { - ipfsContent = await this.handleIpfsContent(sanitizedURL, { hostname, query, pathname }); - - if (ipfsContent) { - const urlObj = new URL(sanitizedURL); - currentEnsName = urlObj.hostname; - ipfsHash = ipfsContent - .replace(this.state.ipfsGateway, '') - .split('/') - .shift(); - } - // Needed for the navbar to mask the URL + closeAllTabs = () => { + if (this.props.tabs.length) { + this.props.closeAllTabs(); this.props.navigation.setParams({ ...this.props.navigation.state.params, - currentEnsName: urlObj.hostname + url: null, + silent: true }); } - const urlToGo = ipfsContent || sanitizedURL; - - if (this.isAllowedUrl(urlToGo)) { - this.setState({ - url: urlToGo, - progress: 0, - ipfsWebsite: !!ipfsContent, - inputValue: sanitizedURL, - currentEnsName, - ipfsHash, - hostname: this.formatHostname(hostname) - }); - - this.timeoutHandler && clearTimeout(this.timeoutHandler); - if (urlToGo !== HOMEPAGE_URL) { - this.timeoutHandler = setTimeout(() => { - this.urlTimedOut(urlToGo); - }, 60000); - } - - return sanitizedURL; - } - this.handleNotAllowedUrl(urlToGo, hostname); - return null; - }; - - urlTimedOut(url) { - Logger.log('Browser::url::Timeout!', url); - } - - urlNotFound(url) { - Logger.log('Browser::url::Not found!', url); - } - - urlNotSupported(url) { - Logger.log('Browser::url::Not supported!', url); - } - - urlErrored(url) { - Logger.log('Browser::url::Unknown error!', url); - } - - async handleIpfsContent(fullUrl, { hostname, pathname, query }) { - const { provider } = Engine.context.NetworkController; - let ipfsHash; - try { - ipfsHash = await resolveEnsToIpfsContentId({ provider, name: hostname }); - } catch (err) { - this.timeoutHandler && clearTimeout(this.timeoutHandler); - Logger.error('Failed to resolve ENS name', err); - err === 'unsupport' ? this.urlNotSupported(fullUrl) : this.urlErrored(fullUrl); - return null; - } - - const gatewayUrl = `${this.state.ipfsGateway}${ipfsHash}${pathname || '/'}${query || ''}`; - - try { - const response = await fetch(gatewayUrl, { method: 'HEAD' }); - const statusCode = response.status; - if (statusCode !== 200) { - this.urlNotFound(gatewayUrl); - return null; - } - return gatewayUrl; - } catch (err) { - // If there's an error our fallback mechanism is - // to point straight to the ipfs gateway - Logger.error('Failed to fetch ipfs website via ens', err); - return `https://ipfs.io/ipfs/${ipfsHash}/`; - } - } - - onUrlInputSubmit = async (input = null) => { - this.toggleOptionsIfNeeded(); - const inputValue = (typeof input === 'string' && input) || this.state.autocompleteInputValue; - const { defaultProtocol, searchEngine } = this.props; - if (inputValue !== '') { - const sanitizedInput = onUrlSubmit(inputValue, searchEngine, defaultProtocol); - const url = await this.go(sanitizedInput); - this.hideUrlModal(url); - } else { - this.hideUrlModal(); - } }; - goBack = () => { - this.toggleOptionsIfNeeded(); - this.goingBack = true; + newTab = () => { + this.props.createNewTab('about:blank'); setTimeout(() => { - this.goingBack = false; - }, 500); - - if (this.initialUrl && this.state.inputValue !== this.initialUrl) { - const { current } = this.webview; - current && current.goBack(); - setTimeout(() => { - this.setState({ forwardEnabled: true }); - this.props.navigation.setParams({ - ...this.props.navigation.state.params, - url: this.state.inputValue + const { tabs } = this.props; + this.switchToTab(tabs[tabs.length - 1]); + }, 100); + }; + + closeTab = tab => { + const { activeTab, tabs } = this.props; + + // If the tab was selected we have to select + // the next one, and if there's no next one, + // we select the previous one. + if (tab.id === activeTab) { + if (tabs.length > 1) { + tabs.forEach((t, i) => { + if (t.id === tab.id) { + let newTab = tabs[i - 1]; + if (tabs[i + 1]) { + newTab = tabs[i + 1]; + } + this.props.setActiveTab(newTab.id); + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + url: newTab.url, + silent: true + }); + } }); - }, 100); - } else { - this.goBackToHomepage(); - } - }; - - goBackToHomepage = () => { - this.toggleOptionsIfNeeded(); - this.props.navigation.setParams({ - url: null - }); - - const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); - - this.setState({ - approvedOrigin: false, - currentEnsName: null, - currentPageTitle: '', - currentPageUrl: '', - currentPageIcon: undefined, - fullHostname: '', - hostname: '', - inputValue: '', - autocompleteInputValue: '', - ipfsHash: null, - ipfsWebsite: false, - showApprovalDialog: false, - showPhishingModal: false, - signMessage: false, - signMessageParams: { data: '' }, - signType: '', - timeout: false, - url: HOMEPAGE_URL, - scrollAnim, - offsetAnim, - clampedScroll, - contentHeight: 0, - forwardEnabled: false - }); - - this.initialUrl = null; - }; - - close = () => { - this.toggleOptionsIfNeeded(); - this.props.navigation.pop(); - }; - - goForward = () => { - if (this.canGoForward()) { - this.toggleOptionsIfNeeded(); - const { current } = this.webview; - this.setState({ forwardEnabled: false }); - current && current.goForward(); - setTimeout(() => { + } else { this.props.navigation.setParams({ ...this.props.navigation.state.params, - url: this.state.inputValue + url: null, + silent: true }); - }, 100); - } - }; - - reload = () => { - this.toggleOptionsIfNeeded(); - if (Platform.OS === 'ios') { - const { current } = this.webview; - current && current.reload(); - } else { - // Force unmount the webview to avoid caching problems - this.setState({ forceReload: true }, () => { - setTimeout(() => { - this.setState({ forceReload: false }, () => { - setTimeout(() => { - this.go(this.state.inputValue); - }, 300); - }); - }, 300); - }); - } - }; - - bookmark = () => { - this.toggleOptionsIfNeeded(); - // Check it doesn't exist already - if (this.props.bookmarks.filter(i => i.url === this.state.inputValue).length) { - Alert.alert(strings('browser.error'), strings('browser.bookmark_already_exists')); - return false; - } - - this.props.navigation.push('AddBookmarkView', { - title: this.state.currentPageTitle || '', - url: this.state.inputValue, - onAddBookmark: async ({ name, url }) => { - this.props.addBookmark({ name, url }); - if (Platform.OS === 'ios') { - const item = { - uniqueIdentifier: url, - title: name || url, - contentDescription: `Launch ${name || url} on MetaMask`, - keywords: [name.split(' '), url, 'dapp'], - thumbnail: { uri: `https://api.faviconkit.com/${getHost(url)}/256` } - }; - try { - SearchApi.indexSpotlightItem(item); - } catch (e) { - Logger.error('Error adding to spotlight', e); - } - } } - }); - }; - - share = () => { - this.toggleOptionsIfNeeded(); - Share.open({ - url: this.state.inputValue - }).catch(err => { - Logger.log('Error while trying to share address', err); - }); - }; - - changeUrl = () => { - this.toggleOptionsIfNeeded(); - setTimeout(() => { - this.showUrlModal(); - }, 300); - }; + } - openInBrowser = () => { - this.toggleOptionsIfNeeded(); - Linking.openURL(this.state.inputValue).catch(error => - Logger.log('Error while trying to open external link: ${url}', error) - ); + this.props.closeTab(tab.id); }; - toggleOptionsIfNeeded() { - if ( - this.props.navigation && - this.props.navigation.state.params && - this.props.navigation.state.params.showOptions - ) { - this.toggleOptions(); - } - } - - toggleOptions = () => { - this.props.navigation && + closeTabsView = () => { + if (this.props.tabs.length) { this.props.navigation.setParams({ ...this.props.navigation.state.params, - showOptions: !this.props.navigation.state.params.showOptions - }); - }; - - onMessage = ({ nativeEvent: { data } }) => { - try { - data = typeof data === 'string' ? JSON.parse(data) : data; - if (!data || !data.type) { - return; - } - switch (data.type) { - case 'GET_HEIGHT': - this.setState({ contentHeight: data.payload.height }); - // Reset the navbar every time we change the page - if (Platform.OS === 'ios') { - setTimeout(() => { - this.state.scrollAnim.setValue(0); - this.state.offsetAnim.setValue(0); - }, 100); - } - break; - case 'NAV_CHANGE': { - const { url, title } = data.payload; - this.setState({ inputValue: url, currentPageTitle: title, forwardEnabled: false }); - this.props.navigation.setParams({ url: data.payload.url, silent: true, showUrlModal: false }); - if (Platform.OS === 'ios') { - setTimeout(() => { - this.resetBottomBarPosition(); - }, 100); - } - break; - } - case 'INPAGE_REQUEST': - this.backgroundBridge.onMessage(data); - break; - case 'GET_TITLE_FOR_BOOKMARK': - if (data.payload.title) { - this.setState({ - currentPageTitle: data.payload.title, - currentPageUrl: data.payload.url, - currentPageIcon: data.payload.icon - }); - } - break; - } - } catch (e) { - Logger.error(`Browser::onMessage on ${this.state.inputValue}`, e.toString()); - } - }; - - resetBottomBarPosition() { - const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); - - this.mounted && - this.setState({ - scrollAnim, - offsetAnim, - clampedScroll + showTabs: false, + silent: true }); - } - - onPageChange = ({ url }) => { - if ((this.goingBack && url === 'about:blank') || (this.initialUrl === url && url === 'about:blank')) { - this.goBackToHomepage(); - return; - } - - // Reset the navbar every time we change the page - if (Platform.OS === 'ios') { - this.resetBottomBarPosition(); - } - - this.forwardHistoryStack = []; - const data = {}; - const urlObj = new URL(url); - - data.fullHostname = urlObj.hostname; - if (!this.state.ipfsWebsite) { - data.inputValue = url; - } else if (url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) === -1) { - data.inputValue = url.replace( - `${this.state.ipfsGateway}${this.state.ipfsHash}/`, - `https://${this.state.currentEnsName}/` - ); - } else if (this.isENSUrl(url)) { - this.go(url); - return; - } else { - data.inputValue = url; - data.hostname = this.formatHostname(urlObj.hostname); - } - - const { fullHostname, inputValue, hostname } = data; - if ( - fullHostname !== this.state.fullHostname || - url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) !== -1 - ) { - this.props.navigation.setParams({ url, silent: true, showUrlModal: false }); } - - this.setState({ fullHostname, inputValue, hostname }); - }; - - formatHostname(hostname) { - return hostname.toLowerCase().replace('www.', ''); - } - - onURLChange = inputValue => { - this.setState({ autocompleteInputValue: inputValue }); }; - sendStateUpdate = () => { - this.backgroundBridge.sendStateUpdate(); - }; - - onLoadProgress = progress => { - this.setState({ progress }); - }; - - onLoadEnd = () => { - if (Platform.OS === 'ios') { - setTimeout(() => { - this.state.scrollAnim.setValue(0); - }, 100); - } - - const { approvedHosts, privacyMode } = this.props; - if (!privacyMode || approvedHosts[this.state.fullHostname]) { - this.backgroundBridge.enableAccounts(); - } - - // Wait for the title, then store the visit - setTimeout(() => { - this.props.addToBrowserHistory({ - name: this.state.currentPageTitle, - url: this.state.inputValue - }); - }, 500); - - // Let's wait for potential redirects that might break things - if (!this.initialUrl || this.initialUrl === HOMEPAGE_URL) { - setTimeout(() => { - this.initialUrl = this.state.inputValue; - }, 1000); - } - - // We need to get the title of the page and the height - const { current } = this.webview; - const js = ` - (function () { - const shortcutIcon = window.document.querySelector('head > link[rel="shortcut icon"]'); - const icon = shortcutIcon || Array.from(window.document.querySelectorAll('head > link[rel="icon"]')).find((icon) => Boolean(icon.href)); - - const siteName = document.querySelector('head > meta[property="og:site_name"]'); - const title = siteName || document.querySelector('head > meta[name="title"]'); - - window.postMessageToNative( - { - type: 'GET_TITLE_FOR_BOOKMARK', - payload: { - title: title ? title.content : document.title, - url: location.href, - icon: icon && icon.href - } - } - ) - ${ - Platform.OS === 'ios' - ? `setTimeout(() => { - const height = Math.max(document.documentElement.clientHeight, document.documentElement.scrollHeight, document.body.clientHeight, document.body.scrollHeight); - window.postMessageToNative( - { - type: 'GET_HEIGHT', - payload: { - height: height - } - }) - }, 500)` - : '' - } - })(); - `; - Platform.OS === 'ios' ? current.evaluateJavaScript(js) : current.injectJavaScript(js); - clearTimeout(this.timeoutHandler); + switchToTab = tab => { + this.props.setActiveTab(tab.id); + this.hideTabsAndUpdateUrl(tab.url); + this.updateTabInfo(tab.url, tab.id); }; - renderLoader = () => ( - - - - ); - - renderOptions = () => { - const showOptions = (this.props.navigation && this.props.navigation.getParam('showOptions', false)) || false; - if (showOptions) { + renderTabsView() { + const { tabs, activeTab } = this.props; + const showTabs = this.props.navigation.getParam('showTabs', false); + if (showTabs) { return ( - - - - {Platform.OS === 'android' && this.canGoBack() ? ( - - ) : null} - {Platform.OS === 'android' && this.canGoForward() ? ( - - ) : null} - - - - - - {Platform.OS === 'android' ? ( - - ) : null} - - - + ); } - }; - - handleScroll = e => { - if (Platform.OS === 'android') return; - - if (e.contentSize.height < Dimensions.get('window').height) { - return; - } - - if (this.state.progress < 1) { - return; - } - - const newOffset = e.contentOffset.y; - - // Avoid wrong position at the beginning - if ((this.state.scrollAnim._value === 0 && newOffset > BOTTOM_NAVBAR_HEIGHT) || newOffset <= 0) { - return; - } - - if (newOffset > this.state.contentHeight - BOTTOM_NAVBAR_HEIGHT) { - return; - } - - this.state.scrollAnim.setValue(newOffset); - - this.scrollStopTimer = setTimeout(() => { - if (Math.abs(this.scrollValue - newOffset) > 1) { - this.onScrollStop(); - } - }, 200); - }; - - onMomentumScrollBegin = () => { - if (Platform.OS === 'android') return; - clearTimeout(this.scrollStopTimer); - }; - - onScrollStop = () => { - if (Platform.OS === 'android') return; - const toValue = - this.clampedScrollValue > BOTTOM_NAVBAR_HEIGHT / 2 - ? this.offsetValue + BOTTOM_NAVBAR_HEIGHT - : this.offsetValue - BOTTOM_NAVBAR_HEIGHT; - - this.animateBottomNavbar(toValue); - }; - - animateBottomNavbar(toValue) { - Animated.timing(this.state.offsetAnim, { - toValue, - duration: 300, - useNativeDriver: true - }).start(); + return null; } - renderBottomBar = (canGoBack, canGoForward) => { - if (this.state.url === HOMEPAGE_URL) { - return false; + updateTabInfo = (url, tabID) => { + if (this.snapshotTimer) { + clearTimeout(this.snapshotTimer); } - const { clampedScroll } = this.state; - - const bottomBarPosition = clampedScroll.interpolate({ - inputRange: [0, BOTTOM_NAVBAR_HEIGHT], - outputRange: [0, BOTTOM_NAVBAR_HEIGHT], - extrapolate: 'clamp' - }); - - return ( - - - - - - - - - - - ); - }; - - isHttps() { - return this.state.inputValue.toLowerCase().substr(0, 6) === 'https:'; - } - - showUrlModal = () => { - this.setState({ autocompleteInputValue: this.state.inputValue }); - this.props.navigation.setParams({ - ...this.props.navigation.state.params, - url: this.state.inputValue, - showUrlModal: true - }); - }; - - hideUrlModal = url => { - const urlParam = typeof url === 'string' && url ? url : this.props.navigation.state.params.url; - this.props.navigation.setParams({ - ...this.props.navigation.state.params, - url: urlParam, - showUrlModal: false - }); - }; - - clearInputText = () => { - const { current } = this.inputRef; - current.clear(); - }; - - onAutocomplete = link => { - this.setState({ inputValue: link }, () => { - this.onUrlInputSubmit(link); - }); + this.snapshotTimer = setTimeout(() => { + const showTabs = this.props.navigation.getParam('showTabs', false); + if (showTabs) { + this.updateTabInfo(url, tabID); + return false; + } + this.takeScreenshot(url, tabID); + }, 500); }; - renderProgressBar = () => ( - - - - ); - - renderUrlModal = () => { - const showUrlModal = (this.props.navigation && this.props.navigation.getParam('showUrlModal', false)) || false; - - if (showUrlModal && this.inputRef) { - setTimeout(() => { - const { current } = this.inputRef; - if (current && !current.isFocused()) { - current.focus(); + takeScreenshot = (url, tabID) => + new Promise((resolve, reject) => { + captureScreen({ + format: 'jpg', + quality: 0.2, + THUMB_WIDTH, + THUMB_HEIGHT + }).then( + uri => { + const { updateTab } = this.props; + + updateTab(tabID, { + url, + image: uri + }); + resolve(true); + }, + error => { + Logger.error(`Error saving tab ${url}`, error); + reject(error); } - }, 300); - } - - return ( - - - - - {Platform.OS === 'android' ? ( - - - - ) : ( - - {strings('browser.cancel')} - - )} - - - - ); - }; - - onSignAction = () => { - this.setState({ signMessage: false }); - }; - - renderSigningModal = () => { - const { signMessage, signMessageParams, signType, currentPageTitle, currentPageUrl } = this.state; - return ( - - {signType === 'personal' && ( - - )} - {signType === 'typed' && ( - - )} - - ); - }; - - onAccountsConfirm = () => { - const { approveHost, selectedAddress } = this.props; - this.setState({ showApprovalDialog: false }); - approveHost(this.state.fullHostname); - this.backgroundBridge.enableAccounts(); - this.approvalRequest.resolve([selectedAddress]); - }; - - onAccountsReject = () => { - this.setState({ showApprovalDialog: false }); - this.approvalRequest.reject('User rejected account access'); - }; - - renderApprovalModal = () => { - const { showApprovalDialog, currentPageTitle, currentPageUrl, currentPageIcon } = this.state; - return ( - - - - ); - }; - - goToETHPhishingDetector = () => { - this.setState({ showPhishingModal: false }); - this.go(`https://github.com/metamask/eth-phishing-detect`); - }; - - continueToPhishingSite = () => { - const urlObj = new URL(this.blockedUrl); - this.props.addToWhitelist(urlObj.hostname); - this.setState({ showPhishingModal: false }); - setTimeout(() => { - this.go(this.blockedUrl); - }, 1000); - }; - - goToEtherscam = () => { - this.setState({ showPhishingModal: false }); - this.go(`https://etherscamdb.info/domain/meta-mask.com`); - }; - - goToFilePhishingIssue = () => { - this.setState({ showPhishingModal: false }); - this.go(`https://github.com/metamask/eth-phishing-detect/issues/new`); - }; - - goBackToSafety = () => { - if (this.canGoBack()) { - this.mounted && this.setState({ showPhishingModal: false }); - } else { - this.close(); - } - }; - - renderPhishingModal() { - const { showPhishingModal } = this.state; - return ( - - - - ); - } - - onBrowserHomeGoToUrl = url => { - this.props.navigation.setParams({ - url, - silent: false, - showUrlModal: false + ); }); - }; - getUserAgent() { - if (Platform.OS === 'android') { - return 'Mozilla/5.0 (Linux; Android 8.1.0; Android SDK built for x86 Build/OSM1.180201.023) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36'; - } - return 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; + renderBrowserTabs() { + const tabs = Object.keys(this.tabs).map(tabID => this.tabs[tabID]); + return tabs; } - onLoadStart = () => { - this.backgroundBridge.disableAccounts(); - }; - - canGoBack = () => true; - - canGoForward = () => this.state.forwardEnabled; - render() { - const { entryScriptWeb3, url } = this.state; - const canGoBack = this.canGoBack(); - const canGoForward = this.canGoForward(); - return ( - - {!this.state.forceReload && ( - - )} - {this.renderProgressBar()} - {this.state.url === HOMEPAGE_URL ? ( - - - - ) : null} - {this.renderUrlModal()} - {this.renderSigningModal()} - {this.renderApprovalModal()} - {this.renderPhishingModal()} - {this.renderOptions()} - {Platform.OS === 'ios' ? this.renderBottomBar(canGoBack, canGoForward) : null} - + + {this.renderBrowserTabs()} + {this.renderTabsView()} + ); } } const mapStateToProps = state => ({ - approvedHosts: state.privacy.approvedHosts, - bookmarks: state.bookmarks, - networkType: state.engine.backgroundState.NetworkController.provider.type, - network: state.engine.backgroundState.NetworkController.network, - selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, - privacyMode: state.privacy.privacyMode, - searchEngine: state.settings.searchEngine, - whitelist: state.browser.whitelist, - transaction: state.transaction + tabs: state.browser.tabs, + activeTab: state.browser.activeTab }); const mapDispatchToProps = dispatch => ({ - approveHost: hostname => dispatch(approveHost(hostname)), - addBookmark: bookmark => dispatch(addBookmark(bookmark)), - addToBrowserHistory: ({ url, name }) => dispatch(addToHistory({ url, name })), - addToWhitelist: url => dispatch(addToWhitelist(url)), - setTransactionObject: asset => dispatch(setTransactionObject(asset)) + createNewTab: url => dispatch(createNewTab(url)), + closeAllTabs: () => dispatch(closeAllTabs()), + closeTab: id => dispatch(closeTab(id)), + setActiveTab: id => dispatch(setActiveTab(id)), + updateTab: (id, url) => dispatch(updateTab(id, url)) }); export default connect( diff --git a/app/components/Views/Browser/__snapshots__/index.test.js.snap b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap similarity index 67% rename from app/components/Views/Browser/__snapshots__/index.test.js.snap rename to app/components/Views/BrowserTab/__snapshots__/index.test.js.snap index d07f5b4a215..60fd15309b9 100644 --- a/app/components/Views/Browser/__snapshots__/index.test.js.snap +++ b/app/components/Views/BrowserTab/__snapshots__/index.test.js.snap @@ -3,10 +3,13 @@ exports[`Browser should render correctly 1`] = ` - - - + > + + - - + + + + + + + + + + + + + `; diff --git a/app/components/Views/BrowserTab/index.js b/app/components/Views/BrowserTab/index.js new file mode 100644 index 00000000000..68479d3bf42 --- /dev/null +++ b/app/components/Views/BrowserTab/index.js @@ -0,0 +1,1739 @@ +import React, { PureComponent } from 'react'; +import { + Dimensions, + Text, + ActivityIndicator, + Platform, + StyleSheet, + TextInput, + View, + TouchableWithoutFeedback, + Alert, + Animated, + TouchableOpacity, + Linking, + Keyboard, + BackHandler, + InteractionManager +} from 'react-native'; +import { withNavigation } from 'react-navigation'; +import Web3Webview from 'react-native-web3-webview'; +import Icon from 'react-native-vector-icons/FontAwesome'; +import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; +import IonIcon from 'react-native-vector-icons/Ionicons'; +import PropTypes from 'prop-types'; +import RNFS from 'react-native-fs'; +import Share from 'react-native-share'; // eslint-disable-line import/default +import { connect } from 'react-redux'; +import BackgroundBridge from '../../../core/BackgroundBridge'; +import Engine from '../../../core/Engine'; +import PhishingModal from '../../UI/PhishingModal'; +import WebviewProgressBar from '../../UI/WebviewProgressBar'; +import BrowserHome from '../../Views/BrowserHome'; +import { colors, baseStyles, fontStyles } from '../../../styles/common'; +import Networks from '../../../util/networks'; +import Logger from '../../../util/Logger'; +import onUrlSubmit, { getHost } from '../../../util/browser'; +import { + SPA_urlChangeListener, + JS_WINDOW_INFORMATION, + JS_WINDOW_INFORMATION_HEIGHT +} from '../../../util/browserSripts'; +import resolveEnsToIpfsContentId from '../../../lib/ens-ipfs/resolver'; +import Button from '../../UI/Button'; +import { strings } from '../../../../locales/i18n'; +import URL from 'url-parse'; +import Modal from 'react-native-modal'; +import UrlAutocomplete from '../../UI/UrlAutocomplete'; +import AccountApproval from '../../UI/AccountApproval'; +import { approveHost } from '../../../actions/privacy'; +import { addBookmark } from '../../../actions/bookmarks'; +import { addToHistory, addToWhitelist } from '../../../actions/browser'; +import { setTransactionObject } from '../../../actions/transaction'; +import DeviceSize from '../../../util/DeviceSize'; +import AppConstants from '../../../core/AppConstants'; +import SearchApi from 'react-native-search-api'; +import DeeplinkManager from '../../../core/DeeplinkManager'; +import Branch from 'react-native-branch'; +import WatchAssetRequest from '../../UI/WatchAssetRequest'; +import TabCountIcon from '../../UI/Tabs/TabCountIcon'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; +import { toggleNetworkModal } from '../../../actions/modals'; + +const HOMEPAGE_URL = 'about:blank'; +const SUPPORTED_TOP_LEVEL_DOMAINS = ['eth', 'test']; +const BOTTOM_NAVBAR_HEIGHT = Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 86 : 60; + +const styles = StyleSheet.create({ + wrapper: { + ...baseStyles.flexGrow, + backgroundColor: colors.white + }, + hide: { + flex: 0, + opacity: 0, + display: 'none', + width: 0, + height: 0 + }, + icon: { + color: colors.grey500, + height: 28, + lineHeight: 28, + textAlign: 'center', + width: 36, + alignSelf: 'center' + }, + disabledIcon: { + color: colors.grey100 + }, + progressBarWrapper: { + height: 3, + width: '100%', + left: 0, + right: 0, + top: 0, + position: 'absolute' + }, + loader: { + backgroundColor: colors.white, + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + optionsOverlay: { + position: 'absolute', + zIndex: 99999998, + top: 0, + bottom: 0, + left: 0, + right: 0 + }, + optionsWrapper: { + position: 'absolute', + zIndex: 99999999, + width: 200, + borderWidth: StyleSheet.hairlineWidth, + borderColor: colors.grey100, + backgroundColor: colors.grey000 + }, + optionsWrapperAndroid: { + top: 0, + right: 0, + elevation: 5 + }, + optionsWrapperIos: { + shadowColor: colors.grey400, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.5, + shadowRadius: 3, + bottom: 75, + right: 3 + }, + option: { + backgroundColor: colors.grey000, + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'flex-start', + marginTop: Platform.OS === 'android' ? 0 : -5 + }, + optionText: { + fontSize: 14, + color: colors.fontPrimary, + ...fontStyles.normal + }, + optionIcon: { + width: 18, + color: colors.grey500, + flex: 0, + height: 15, + lineHeight: 15, + marginRight: 10, + textAlign: 'center', + alignSelf: 'center' + }, + webview: { + ...baseStyles.flexGrow + }, + bottomBar: { + backgroundColor: colors.grey000, + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + paddingTop: Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 15 : 12, + paddingBottom: Platform.OS === 'ios' && DeviceSize.isIphoneX() ? 32 : 8, + flexDirection: 'row', + paddingHorizontal: 10, + flex: 1 + }, + iconSearch: { + alignSelf: 'flex-end', + alignContent: 'flex-end' + }, + iconMore: { + alignSelf: 'flex-end', + alignContent: 'flex-end' + }, + iconsLeft: { + flex: 1, + alignContent: 'flex-start', + flexDirection: 'row' + }, + iconsMiddle: { + flex: 1, + alignContent: 'center', + flexDirection: 'row', + justifyContent: 'center' + }, + iconsRight: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end' + }, + tabIcon: { + width: 30, + height: 30 + }, + urlModalContent: { + flexDirection: 'row', + paddingTop: Platform.OS === 'android' ? 10 : DeviceSize.isIphoneX() ? 50 : 27, + paddingHorizontal: 10, + backgroundColor: colors.white, + height: Platform.OS === 'android' ? 59 : DeviceSize.isIphoneX() ? 87 : 65 + }, + urlModal: { + justifyContent: 'flex-start', + margin: 0 + }, + urlInput: { + ...fontStyles.normal, + backgroundColor: Platform.OS === 'android' ? colors.white : colors.grey000, + borderRadius: 30, + fontSize: Platform.OS === 'android' ? 16 : 14, + padding: 8, + paddingLeft: 15, + textAlign: 'left', + flex: 1, + height: Platform.OS === 'android' ? 40 : 30 + }, + cancelButton: { + marginTop: 7, + marginLeft: 10 + }, + cancelButtonText: { + fontSize: 14, + color: colors.blue, + ...fontStyles.normal + }, + iconCloseButton: { + borderRadius: 300, + backgroundColor: colors.fontSecondary, + color: colors.white, + fontSize: 18, + padding: 0, + height: 20, + width: 20, + paddingBottom: 0, + alignItems: 'center', + justifyContent: 'center', + marginTop: 10, + marginRight: 5 + }, + iconClose: { + color: colors.white, + fontSize: 18 + }, + bottomModal: { + justifyContent: 'flex-end', + margin: 0 + }, + fullScreenModal: { + flex: 1 + }, + homepage: { + flex: 1, + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0 + } +}); + +/** + * Complete Web browser component with URL entry and history management + * which represents an individual tab inside the component + */ +export class BrowserTab extends PureComponent { + static defaultProps = { + defaultProtocol: 'https://' + }; + + static propTypes = { + /** + * The ID of the current tab + */ + id: PropTypes.number, + /** + * The ID of the active tab + */ + activeTab: PropTypes.number, + /** + * InitialUrl + */ + initialUrl: PropTypes.string, + /** + * Called to approve account access for a given hostname + */ + approveHost: PropTypes.func, + /** + * Map of hostnames with approved account access + */ + approvedHosts: PropTypes.object, + /** + * Protocol string to append to URLs that have none + */ + defaultProtocol: PropTypes.string, + /** + * A string that of the chosen ipfs gateway + */ + ipfsGateway: PropTypes.string, + /** + * Object containing the information for the current transaction + */ + transaction: PropTypes.object, + /** + * react-navigation object used to switch between screens + */ + navigation: PropTypes.object, + /** + * A string representing the network type + */ + networkType: PropTypes.string, + /** + * A string representing the network id + */ + network: PropTypes.string, + /** + * Indicates whether privacy mode is enabled + */ + privacyMode: PropTypes.bool, + /** + * A string that represents the selected address + */ + selectedAddress: PropTypes.string, + /** + * whitelisted url to bypass the phishing detection + */ + whitelist: PropTypes.array, + /** + * Url coming from an external source + * For ex. deeplinks + */ + url: PropTypes.string, + /** + * Function to toggle the network switcher modal + */ + toggleNetworkModal: PropTypes.func, + /** + * Function to open a new tab + */ + newTab: PropTypes.func, + /** + * Function to store bookmarks + */ + addBookmark: PropTypes.func, + /** + * Array of bookmarks + */ + bookmarks: PropTypes.array, + /** + * String representing the current search engine + */ + searchEngine: PropTypes.string, + /** + * Action that sets a transaction + */ + setTransactionObject: PropTypes.func, + /** + * Function to store the a page in the browser history + */ + addToBrowserHistory: PropTypes.func, + /** + * Function to store the a website in the browser whitelist + */ + addToWhitelist: PropTypes.func, + /** + * Function to update the tab information + */ + updateTabInfo: PropTypes.func, + /** + * Function to update the tab information + */ + showTabs: PropTypes.func + }; + + constructor(props) { + super(props); + + const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); + + this.state = { + approvedOrigin: false, + currentEnsName: null, + currentPageTitle: '', + currentPageUrl: '', + currentPageIcon: undefined, + entryScriptWeb3: null, + fullHostname: '', + hostname: '', + inputValue: '', + autocompleteInputValue: '', + ipfsGateway: 'https://ipfs.io/ipfs/', + ipfsHash: null, + ipfsWebsite: false, + showApprovalDialog: false, + showPhishingModal: false, + timeout: false, + url: props.initialUrl || HOMEPAGE_URL, + scrollAnim, + offsetAnim, + clampedScroll, + contentHeight: 0, + forwardEnabled: false, + forceReload: false, + suggestedAssetMeta: undefined, + watchAsset: false, + activated: props.id === props.activeTab + }; + } + + webview = React.createRef(); + inputRef = React.createRef(); + timeoutHandler = null; + snapshotTimer = null; + prevScrollOffset = 0; + goingBack = false; + forwardHistoryStack = []; + approvalRequest; + accountsRequest; + + clampedScrollValue = 0; + offsetValue = 0; + scrollValue = 0; + scrollStopTimer = null; + + initScrollVariables() { + const scrollAnim = Platform.OS === 'ios' ? new Animated.Value(0) : null; + const offsetAnim = Platform.OS === 'ios' ? new Animated.Value(0) : null; + let clampedScroll = null; + if (Platform.OS === 'ios') { + clampedScroll = Animated.diffClamp( + Animated.add( + scrollAnim.interpolate({ + inputRange: [0, 1], + outputRange: [0, 1], + extrapolateLeft: 'clamp' + }), + offsetAnim + ), + 0, + BOTTOM_NAVBAR_HEIGHT + ); + } + + return { scrollAnim, offsetAnim, clampedScroll }; + } + + getPageMeta() { + return { + meta: { + title: this.state.currentPageTitle, + url: this.state.currentPageUrl + } + }; + } + + async componentDidMount() { + if (this.state.url !== HOMEPAGE_URL && Platform.OS === 'android' && this.isTabActive()) { + this.reload(); + } + this.mounted = true; + this.backgroundBridge = new BackgroundBridge(Engine, this.webview, { + eth_sign: async payload => { + const { PersonalMessageManager } = Engine.context; + try { + const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ + data: payload.params[1], + from: payload.params[0], + ...this.getPageMeta() + }); + return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); + } catch (error) { + return Promise.reject({ error: error.message, jsonrpc: payload.jsonrpc, id: payload.id }); + } + }, + personal_sign: async payload => { + const { PersonalMessageManager } = Engine.context; + try { + const rawSig = await PersonalMessageManager.addUnapprovedMessageAsync({ + data: payload.params[0], + from: payload.params[1], + ...this.getPageMeta() + }); + return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); + } catch (error) { + return Promise.reject({ error: error.message, jsonrpc: payload.jsonrpc, id: payload.id }); + } + }, + eth_signTypedData: async payload => { + const { TypedMessageManager } = Engine.context; + try { + const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( + { + data: payload.params[0], + from: payload.params[1], + ...this.getPageMeta() + }, + 'V1' + ); + return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); + } catch (error) { + return Promise.reject({ error: error.message, jsonrpc: payload.jsonrpc, id: payload.id }); + } + }, + eth_signTypedData_v3: async payload => { + const { TypedMessageManager } = Engine.context; + try { + const rawSig = await TypedMessageManager.addUnapprovedMessageAsync( + { + data: payload.params[1], + from: payload.params[0], + ...this.getPageMeta() + }, + 'V3' + ); + return Promise.resolve({ result: rawSig, jsonrpc: payload.jsonrpc, id: payload.id }); + } catch (error) { + return Promise.reject({ error: error.message, jsonrpc: payload.jsonrpc, id: payload.id }); + } + }, + eth_requestAccounts: ({ hostname, params }) => { + const { approvedHosts, privacyMode, selectedAddress } = this.props; + const promise = new Promise((resolve, reject) => { + this.approvalRequest = { resolve, reject }; + }); + if (!privacyMode || ((!params || !params.force) && approvedHosts[hostname])) { + this.approvalRequest.resolve([selectedAddress]); + this.backgroundBridge.enableAccounts(); + } else { + // Let the damn website load first! + // Otherwise we don't get enough time to load the metadata + // (title, icon, etc) + + setTimeout(() => { + this.setState({ showApprovalDialog: true }); + }, 1000); + } + return promise; + }, + eth_accounts: ({ id, jsonrpc, hostname }) => { + const { approvedHosts, privacyMode, selectedAddress } = this.props; + const isEnabled = !privacyMode || approvedHosts[hostname]; + const promise = new Promise((resolve, reject) => { + this.accountsRequest = { resolve, reject }; + }); + if (isEnabled) { + this.accountsRequest.resolve({ id, jsonrpc, result: [selectedAddress] }); + } else { + this.accountsRequest.resolve({ id, jsonrpc, result: [] }); + } + return promise; + }, + web3_clientVersion: payload => + Promise.resolve({ result: 'MetaMask/0.1.0/Alpha/Mobile', jsonrpc: payload.jsonrpc, id: payload.id }), + wallet_scanQRCode: payload => { + const promise = new Promise((resolve, reject) => { + this.props.navigation.navigate('QRScanner', { + onScanSuccess: data => { + let result = data; + if (data.target_address) { + result = data.target_address; + } else if (data.scheme) { + result = JSON.stringify(data); + } + resolve({ result, jsonrpc: payload.jsonrpc, id: payload.id }); + }, + onScanError: e => { + reject({ errir: e.toString(), jsonrpc: payload.jsonrpc, id: payload.id }); + } + }); + }); + return promise; + }, + wallet_watchAsset: async ({ params }) => { + const { + options: { address, decimals, image, symbol }, + type + } = params; + const { AssetsController } = Engine.context; + const suggestionResult = await AssetsController.watchAsset({ address, symbol, decimals, image }, type); + return suggestionResult.result; + }, + metamask_isApproved: async ({ hostname }) => ({ + isApproved: !!this.props.approvedHosts[hostname] + }) + }); + + const entryScriptWeb3 = + Platform.OS === 'ios' + ? await RNFS.readFile(`${RNFS.MainBundlePath}/InpageBridgeWeb3.js`, 'utf8') + : await RNFS.readFileAssets(`InpageBridgeWeb3.js`); + + const updatedentryScriptWeb3 = entryScriptWeb3.replace( + 'undefined; // INITIAL_NETWORK', + this.props.networkType === 'rpc' + ? `'${this.props.network}'` + : `'${Networks[this.props.networkType].networkId}'` + ); + await this.setState({ entryScriptWeb3: updatedentryScriptWeb3 + SPA_urlChangeListener }); + Engine.context.AssetsController.hub.on('pendingSuggestedAsset', suggestedAssetMeta => { + if (!this.isTabActive()) return false; + this.setState({ watchAsset: true, suggestedAssetMeta }); + }); + + Branch.subscribe(this.handleDeeplinks); + + if (Platform.OS === 'ios') { + this.state.scrollAnim.addListener(({ value }) => { + const diff = value - this.scrollValue; + this.scrollValue = value; + this.clampedScrollValue = Math.min(Math.max(this.clampedScrollValue + diff, 0), BOTTOM_NAVBAR_HEIGHT); + }); + + this.state.offsetAnim.addListener(({ value }) => { + this.offsetValue = value; + }); + } else { + this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide); + } + + // Listen to network changes + Engine.context.TransactionController.hub.on('networkChange', this.reload); + + BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBackPress); + } + + handleDeeplinks = async ({ error, params }) => { + if (!this.isTabActive()) return false; + if (error) { + Logger.error('Error from Branch: ', error); + return; + } + if (params['+non_branch_link']) { + const dm = new DeeplinkManager(this.props.navigation); + dm.parse(params['+non_branch_link']); + } else if (params.spotlight_identifier) { + setTimeout(() => { + this.props.navigation.setParams({ + url: params.spotlight_identifier, + silent: false, + showUrlModal: false + }); + }, 1000); + } + }; + + handleAndroidBackPress = () => { + if (!this.isTabActive()) return false; + + if (this.state.url === HOMEPAGE_URL && this.props.navigation.getParam('url', null) === null) { + return false; + } + this.goBack(); + return true; + }; + + async loadUrl() { + if (!this.isTabActive()) return; + const { navigation } = this.props; + if (navigation) { + const url = navigation.getParam('url', null); + const silent = navigation.getParam('silent', false); + if (url && !silent) { + await this.go(url); + } + } + } + + setTabActive() { + this.setState({ activated: true }); + } + + componentDidUpdate(prevProps) { + const prevNavigation = prevProps.navigation; + const { navigation } = this.props; + + // If tab wasn't activated and we detect an tab change + // we need to check if it's time to activate the tab + if (!this.state.activated && prevProps.activeTab !== this.props.activeTab) { + if (this.props.id === this.props.activeTab) { + this.setTabActive(); + } + } + + if (prevNavigation && navigation) { + const prevUrl = prevNavigation.getParam('url', null); + const currentUrl = navigation.getParam('url', null); + + if (currentUrl && prevUrl !== currentUrl && currentUrl !== this.state.url) { + this.loadUrl(); + } + } + } + + componentWillUnmount() { + this.mounted = false; + // Remove all Engine listeners + Engine.context.AssetsController.hub.removeAllListeners(); + Engine.context.TransactionController.hub.removeListener('networkChange', this.reload); + if (Platform.OS === 'ios') { + this.state.scrollAnim && this.state.scrollAnim.removeAllListeners(); + this.state.offsetAnim && this.state.offsetAnim.removeAllListeners(); + } else { + this.keyboardDidHideListener && this.keyboardDidHideListener.remove(); + BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBackPress); + } + } + + keyboardDidHide = () => { + if (!this.isTabActive()) return false; + const showUrlModal = (this.props.navigation && this.props.navigation.getParam('showUrlModal', false)) || false; + if (showUrlModal) { + this.hideUrlModal(); + } + }; + + isENSUrl(url) { + const urlObj = new URL(url); + const { hostname } = urlObj; + const tld = hostname.split('.').pop(); + if (SUPPORTED_TOP_LEVEL_DOMAINS.indexOf(tld.toLowerCase()) !== -1) { + return true; + } + return false; + } + + isAllowedUrl = url => { + const urlObj = new URL(url); + const { PhishingController } = Engine.context; + return ( + (this.props.whitelist && this.props.whitelist.includes(urlObj.hostname)) || + (PhishingController && !PhishingController.test(urlObj.hostname)) + ); + }; + + handleNotAllowedUrl = (urlToGo, hostname) => { + let host = hostname; + if (!host) { + const urlObj = new URL(urlToGo); + host = urlObj.hostname; + } + this.blockedUrl = urlToGo; + setTimeout(() => { + this.setState({ showPhishingModal: true }); + }, 500); + }; + + updateTabInfo(url) { + this.isTabActive() && this.props.updateTabInfo(url, this.props.id); + } + + go = async url => { + const hasProtocol = url.match(/^[a-z]*:\/\//) || url === HOMEPAGE_URL; + const sanitizedURL = hasProtocol ? url : `${this.props.defaultProtocol}${url}`; + const urlObj = new URL(sanitizedURL); + const { hostname, query, pathname } = urlObj; + const { ipfsGateway } = this.props; + + let ipfsContent = null; + let currentEnsName = null; + let ipfsHash = null; + + if (this.isENSUrl(sanitizedURL)) { + ipfsContent = await this.handleIpfsContent(sanitizedURL, { hostname, query, pathname }); + + if (ipfsContent) { + const urlObj = new URL(sanitizedURL); + currentEnsName = urlObj.hostname; + ipfsHash = ipfsContent + .replace(ipfsGateway, '') + .split('/') + .shift(); + } + // Needed for the navbar to mask the URL + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + currentEnsName: urlObj.hostname + }); + } + const urlToGo = ipfsContent || sanitizedURL; + + if (this.isAllowedUrl(urlToGo)) { + this.setState({ + url: urlToGo, + progress: 0, + ipfsWebsite: !!ipfsContent, + inputValue: sanitizedURL, + currentEnsName, + ipfsHash, + hostname: this.formatHostname(hostname) + }); + this.updateTabInfo(sanitizedURL); + + this.timeoutHandler && clearTimeout(this.timeoutHandler); + if (urlToGo !== HOMEPAGE_URL) { + this.timeoutHandler = setTimeout(() => { + this.urlTimedOut(urlToGo); + }, 60000); + } + + return sanitizedURL; + } + this.handleNotAllowedUrl(urlToGo, hostname); + return null; + }; + + urlTimedOut(url) { + Logger.log('Browser::url::Timeout!', url); + } + + urlNotFound(url) { + Logger.log('Browser::url::Not found!', url); + } + + urlNotSupported(url) { + Logger.log('Browser::url::Not supported!', url); + } + + urlErrored(url) { + Logger.log('Browser::url::Unknown error!', url); + } + + async handleIpfsContent(fullUrl, { hostname, pathname, query }) { + const { provider } = Engine.context.NetworkController; + const { ipfsGateway } = this.props; + + let ipfsHash; + try { + ipfsHash = await resolveEnsToIpfsContentId({ provider, name: hostname }); + } catch (err) { + this.timeoutHandler && clearTimeout(this.timeoutHandler); + Logger.error('Failed to resolve ENS name', err); + err === 'unsupport' ? this.urlNotSupported(fullUrl) : this.urlErrored(fullUrl); + return null; + } + + const gatewayUrl = `${ipfsGateway}${ipfsHash}${pathname || '/'}${query || ''}`; + + try { + const response = await fetch(gatewayUrl, { method: 'HEAD' }); + const statusCode = response.status; + if (statusCode !== 200) { + this.urlNotFound(gatewayUrl); + return null; + } + return gatewayUrl; + } catch (err) { + // If there's an error our fallback mechanism is + // to point straight to the ipfs gateway + Logger.error('Failed to fetch ipfs website via ens', err); + return `https://ipfs.io/ipfs/${ipfsHash}/`; + } + } + + onUrlInputSubmit = async (input = null) => { + this.toggleOptionsIfNeeded(); + const inputValue = (typeof input === 'string' && input) || this.state.autocompleteInputValue; + const { defaultProtocol, searchEngine } = this.props; + if (inputValue !== '') { + const sanitizedInput = onUrlSubmit(inputValue, searchEngine, defaultProtocol); + const url = await this.go(sanitizedInput); + this.hideUrlModal(url); + } else { + this.hideUrlModal(); + } + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.BROWSER_SEARCH); + }; + + goBack = () => { + this.toggleOptionsIfNeeded(); + this.goingBack = true; + setTimeout(() => { + this.goingBack = false; + }, 500); + + if (this.initialUrl && this.state.inputValue !== this.initialUrl) { + const { current } = this.webview; + current && current.goBack(); + setTimeout(() => { + this.setState({ forwardEnabled: true }); + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + url: this.state.inputValue + }); + }, 100); + } else { + this.goBackToHomepage(); + } + }; + + goBackToHomepage = () => { + this.toggleOptionsIfNeeded(); + this.props.navigation.setParams({ + url: null + }); + + const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); + + this.setState({ + approvedOrigin: false, + currentEnsName: null, + currentPageTitle: '', + currentPageUrl: '', + currentPageIcon: undefined, + fullHostname: '', + hostname: '', + inputValue: '', + autocompleteInputValue: '', + ipfsHash: null, + ipfsWebsite: false, + showApprovalDialog: false, + showPhishingModal: false, + timeout: false, + url: HOMEPAGE_URL, + scrollAnim, + offsetAnim, + clampedScroll, + contentHeight: 0, + forwardEnabled: false + }); + + this.initialUrl = null; + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_HOME); + }; + + close = () => { + this.toggleOptionsIfNeeded(); + this.props.navigation.pop(); + }; + + goForward = () => { + if (this.canGoForward()) { + this.toggleOptionsIfNeeded(); + const { current } = this.webview; + this.setState({ forwardEnabled: false }); + current && current.goForward(); + setTimeout(() => { + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + url: this.state.inputValue + }); + }, 100); + } + }; + + reload = () => { + this.toggleOptionsIfNeeded(); + if (Platform.OS === 'ios') { + const { current } = this.webview; + current && current.reload(); + } else { + // Force unmount the webview to avoid caching problems + this.setState({ forceReload: true }, () => { + setTimeout(() => { + this.setState({ forceReload: false }, () => { + setTimeout(() => { + this.go(this.state.inputValue); + }, 300); + }); + }, 300); + }); + } + }; + + addBookmark = () => { + this.toggleOptionsIfNeeded(); + // Check it doesn't exist already + if (this.props.bookmarks.filter(i => i.url === this.state.inputValue).length) { + Alert.alert(strings('browser.error'), strings('browser.bookmark_already_exists')); + return false; + } + if (!this.state.currentPageTitle) { + // We need to get the title to add bookmark + const { current } = this.webview; + Platform.OS === 'ios' + ? current.evaluateJavaScript(JS_WINDOW_INFORMATION) + : current.injectJavaScript(JS_WINDOW_INFORMATION); + } + setTimeout(() => { + this.props.navigation.push('AddBookmarkView', { + title: this.state.currentPageTitle || '', + url: this.state.inputValue, + onAddBookmark: async ({ name, url }) => { + this.props.addBookmark({ name, url }); + if (Platform.OS === 'ios') { + const item = { + uniqueIdentifier: url, + title: name || url, + contentDescription: `Launch ${name || url} on MetaMask`, + keywords: [name.split(' '), url, 'dapp'], + thumbnail: { uri: `https://api.faviconkit.com/${getHost(url)}/256` } + }; + try { + SearchApi.indexSpotlightItem(item); + } catch (e) { + Logger.error('Error adding to spotlight', e); + } + } + } + }); + }, 500); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_ADD_TO_FAVORITE); + }; + + share = () => { + this.toggleOptionsIfNeeded(); + Share.open({ + url: this.state.inputValue + }).catch(err => { + Logger.log('Error while trying to share address', err); + }); + }; + + switchNetwork = () => { + this.toggleOptionsIfNeeded(); + setTimeout(() => { + this.props.toggleNetworkModal(); + }, 300); + }; + + openNewTab = () => { + this.toggleOptionsIfNeeded(); + setTimeout(() => { + this.props.newTab(); + }, 300); + }; + + openInBrowser = () => { + this.toggleOptionsIfNeeded(); + Linking.openURL(this.state.inputValue).catch(error => + Logger.log('Error while trying to open external link: ${url}', error) + ); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_OPEN_IN_BROWSER); + }; + + toggleOptionsIfNeeded() { + if ( + this.props.navigation && + this.props.navigation.state.params && + this.props.navigation.state.params.showOptions + ) { + this.toggleOptions(); + } + } + + toggleOptions = () => { + this.props.navigation && + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + showOptions: !this.props.navigation.state.params.showOptions + }); + + !this.props.navigation.state.params.showOptions && + InteractionManager.runAfterInteractions(() => { + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.DAPP_BROWSER_OPTIONS); + }); + }; + + onMessage = ({ nativeEvent: { data } }) => { + try { + data = typeof data === 'string' ? JSON.parse(data) : data; + if (!data || !data.type) { + return; + } + switch (data.type) { + case 'GET_HEIGHT': + this.setState({ contentHeight: data.payload.height }); + // Reset the navbar every time we change the page + if (Platform.OS === 'ios') { + setTimeout(() => { + this.state.scrollAnim.setValue(0); + this.state.offsetAnim.setValue(0); + }, 100); + } + break; + case 'NAV_CHANGE': { + const { url, title } = data.payload; + this.setState({ + inputValue: url, + autocompletInputValue: url, + currentPageTitle: title, + forwardEnabled: false + }); + this.props.navigation.setParams({ url: data.payload.url, silent: true, showUrlModal: false }); + this.updateTabInfo(data.payload.url); + if (Platform.OS === 'ios') { + setTimeout(() => { + this.resetBottomBarPosition(); + }, 100); + } + break; + } + case 'INPAGE_REQUEST': + this.backgroundBridge.onMessage(data); + break; + case 'GET_TITLE_FOR_BOOKMARK': + if (data.payload.title) { + this.setState({ + currentPageTitle: data.payload.title, + currentPageUrl: data.payload.url, + currentPageIcon: data.payload.icon + }); + } + break; + } + } catch (e) { + Logger.error(`Browser::onMessage on ${this.state.inputValue}`, e.toString()); + } + }; + + resetBottomBarPosition() { + const { scrollAnim, offsetAnim, clampedScroll } = this.initScrollVariables(); + + this.mounted && + this.setState({ + scrollAnim, + offsetAnim, + clampedScroll + }); + } + + onPageChange = ({ url }) => { + const { ipfsGateway } = this.props; + if ((this.goingBack && url === 'about:blank') || (this.initialUrl === url && url === 'about:blank')) { + this.goBackToHomepage(); + return; + } + + // Reset the navbar every time we change the page + if (Platform.OS === 'ios') { + this.resetBottomBarPosition(); + } + + this.forwardHistoryStack = []; + const data = {}; + const urlObj = new URL(url); + + data.fullHostname = urlObj.hostname; + if (!this.state.ipfsWebsite) { + data.inputValue = url; + } else if (url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) === -1) { + data.inputValue = url.replace( + `${ipfsGateway}${this.state.ipfsHash}/`, + `https://${this.state.currentEnsName}/` + ); + } else if (this.isENSUrl(url)) { + this.go(url); + return; + } else { + data.inputValue = url; + data.hostname = this.formatHostname(urlObj.hostname); + } + + const { fullHostname, inputValue, hostname } = data; + if ( + fullHostname !== this.state.fullHostname || + url.search(`${AppConstants.IPFS_OVERRIDE_PARAM}=false`) !== -1 + ) { + if (this.isTabActive()) { + this.props.navigation.setParams({ url, silent: true, showUrlModal: false }); + } + } + this.updateTabInfo(inputValue); + this.setState({ fullHostname, inputValue, autocompleteInputValue: inputValue, hostname }); + }; + + formatHostname(hostname) { + return hostname.toLowerCase().replace('www.', ''); + } + + onURLChange = inputValue => { + this.setState({ autocompleteInputValue: inputValue }); + }; + + sendStateUpdate = () => { + this.backgroundBridge.sendStateUpdate(); + }; + + onLoadProgress = progress => { + this.setState({ progress }); + }; + + onLoadEnd = () => { + if (Platform.OS === 'ios') { + setTimeout(() => { + this.state.scrollAnim.setValue(0); + }, 100); + } + + const { approvedHosts, privacyMode } = this.props; + if (!privacyMode || approvedHosts[this.state.fullHostname]) { + this.backgroundBridge.enableAccounts(); + } + + // Wait for the title, then store the visit + setTimeout(() => { + this.props.addToBrowserHistory({ + name: this.state.currentPageTitle, + url: this.state.inputValue + }); + }, 500); + + // Let's wait for potential redirects that might break things + if (!this.initialUrl || this.initialUrl === HOMEPAGE_URL) { + setTimeout(() => { + this.initialUrl = this.state.inputValue; + }, 1000); + } + + // We need to get the title of the page and the height + const { current } = this.webview; + + Platform.OS === 'ios' + ? current.evaluateJavaScript(JS_WINDOW_INFORMATION_HEIGHT) + : current.injectJavaScript(JS_WINDOW_INFORMATION_HEIGHT); + clearTimeout(this.timeoutHandler); + }; + + renderLoader = () => ( + + + + ); + + renderOptions = () => { + const showOptions = (this.props.navigation && this.props.navigation.getParam('showOptions', false)) || false; + if (showOptions) { + return ( + + + + + {this.renderNonHomeOptions()} + + + + + ); + } + }; + + renderNonHomeOptions = () => { + if (this.state.url === HOMEPAGE_URL) return null; + + return ( + + {Platform.OS === 'android' && this.canGoBack() ? ( + + ) : null} + {Platform.OS === 'android' && this.canGoForward() ? ( + + ) : null} + + + + + + + ); + }; + + handleScroll = e => { + if (Platform.OS === 'android') return; + + if (e.contentSize.height < Dimensions.get('window').height) { + return; + } + + if (this.state.progress < 1) { + return; + } + + const newOffset = e.contentOffset.y; + + // Avoid wrong position at the beginning + if ((this.state.scrollAnim._value === 0 && newOffset > BOTTOM_NAVBAR_HEIGHT) || newOffset <= 0) { + return; + } + + if (newOffset > this.state.contentHeight - BOTTOM_NAVBAR_HEIGHT) { + return; + } + + this.state.scrollAnim.setValue(newOffset); + + this.scrollStopTimer = setTimeout(() => { + if (Math.abs(this.scrollValue - newOffset) > 1) { + this.onScrollStop(); + } + }, 200); + }; + + onMomentumScrollBegin = () => { + if (Platform.OS === 'android') return; + clearTimeout(this.scrollStopTimer); + }; + + onScrollStop = () => { + if (Platform.OS === 'android') return; + const toValue = + this.clampedScrollValue > BOTTOM_NAVBAR_HEIGHT / 2 + ? this.offsetValue + BOTTOM_NAVBAR_HEIGHT + : this.offsetValue - BOTTOM_NAVBAR_HEIGHT; + + this.animateBottomNavbar(toValue); + }; + + animateBottomNavbar(toValue) { + Animated.timing(this.state.offsetAnim, { + toValue, + duration: 300, + useNativeDriver: true + }).start(); + } + + showTabs = () => { + this.props.showTabs(); + }; + + renderBottomBar = (canGoBack, canGoForward) => { + const { clampedScroll } = this.state; + + const bottomBarPosition = clampedScroll.interpolate({ + inputRange: [0, BOTTOM_NAVBAR_HEIGHT], + outputRange: [0, BOTTOM_NAVBAR_HEIGHT], + extrapolate: 'clamp' + }); + + return ( + + + + + + + + + + + + + + ); + }; + + isHttps() { + return this.state.inputValue.toLowerCase().substr(0, 6) === 'https:'; + } + + showUrlModal = () => { + if (!this.isTabActive()) return false; + this.setState({ autocompleteInputValue: this.state.inputValue }); + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + url: this.state.inputValue, + showUrlModal: true + }); + }; + + hideUrlModal = url => { + const urlParam = typeof url === 'string' && url ? url : this.props.navigation.state.params.url; + this.props.navigation.setParams({ + ...this.props.navigation.state.params, + url: urlParam, + showUrlModal: false + }); + }; + + clearInputText = () => { + const { current } = this.inputRef; + current.clear(); + }; + + onAutocomplete = link => { + this.setState({ inputValue: link, autocompleteInputValue: link }, () => { + this.onUrlInputSubmit(link); + this.updateTabInfo(link); + }); + }; + + renderProgressBar = () => ( + + + + ); + + renderUrlModal = () => { + const showUrlModal = (this.props.navigation && this.props.navigation.getParam('showUrlModal', false)) || false; + + if (showUrlModal && this.inputRef) { + setTimeout(() => { + const { current } = this.inputRef; + if (current && !current.isFocused()) { + current.focus(); + } + }, 300); + } + + return ( + + + + + {Platform.OS === 'android' ? ( + + + + ) : ( + + {strings('browser.cancel')} + + )} + + + + ); + }; + + onCancelWatchAsset = () => { + this.setState({ watchAsset: false }); + }; + + renderWatchAssetModal = () => { + const { watchAsset, suggestedAssetMeta } = this.state; + return ( + + + + ); + }; + + onAccountsConfirm = () => { + const { approveHost, selectedAddress } = this.props; + this.setState({ showApprovalDialog: false }); + approveHost(this.state.fullHostname); + this.backgroundBridge.enableAccounts(); + this.approvalRequest.resolve([selectedAddress]); + }; + + onAccountsReject = () => { + this.setState({ showApprovalDialog: false }); + this.approvalRequest.reject('User rejected account access'); + }; + + renderApprovalModal = () => { + const { showApprovalDialog, currentPageTitle, currentPageUrl, currentPageIcon } = this.state; + return ( + + + + ); + }; + + goToETHPhishingDetector = () => { + this.setState({ showPhishingModal: false }); + this.go(`https://github.com/metamask/eth-phishing-detect`); + }; + + continueToPhishingSite = () => { + const urlObj = new URL(this.blockedUrl); + this.props.addToWhitelist(urlObj.hostname); + this.setState({ showPhishingModal: false }); + setTimeout(() => { + this.go(this.blockedUrl); + }, 1000); + }; + + goToEtherscam = () => { + this.setState({ showPhishingModal: false }); + this.go(`https://etherscamdb.info/domain/meta-mask.com`); + }; + + goToFilePhishingIssue = () => { + this.setState({ showPhishingModal: false }); + this.go(`https://github.com/metamask/eth-phishing-detect/issues/new`); + }; + + goBackToSafety = () => { + if (this.canGoBack()) { + this.mounted && this.setState({ showPhishingModal: false }); + } else { + this.close(); + } + }; + + renderPhishingModal() { + const { showPhishingModal } = this.state; + return ( + + + + ); + } + + getUserAgent() { + if (Platform.OS === 'android') { + return 'Mozilla/5.0 (Linux; Android 8.1.0; Android SDK built for x86 Build/OSM1.180201.023) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.98 Mobile Safari/537.36'; + } + return 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; + } + + onLoadStart = () => { + this.backgroundBridge.disableAccounts(); + }; + + canGoBack = () => true; + + canGoForward = () => this.state.forwardEnabled; + + isTabActive = () => { + const { activeTab, id } = this.props; + return activeTab === id; + }; + + render() { + const { entryScriptWeb3, url, forceReload, activated } = this.state; + + const canGoBackIOS = Platform.OS === 'ios' && url === HOMEPAGE_URL ? false : this.canGoBack(); + const canGoForward = this.canGoForward(); + + const isHidden = !this.isTabActive(); + + return ( + + {activated && !forceReload && ( + + )} + {this.renderProgressBar()} + {!isHidden && url === HOMEPAGE_URL ? ( + + + + ) : null} + {!isHidden && this.renderUrlModal()} + {!isHidden && this.renderApprovalModal()} + {!isHidden && this.renderPhishingModal()} + {!isHidden && this.renderWatchAssetModal()} + {!isHidden && this.renderOptions()} + {!isHidden && Platform.OS === 'ios' ? this.renderBottomBar(canGoBackIOS, canGoForward) : null} + + ); + } +} + +const mapStateToProps = state => ({ + approvedHosts: state.privacy.approvedHosts, + bookmarks: state.bookmarks, + ipfsGateway: state.engine.backgroundState.PreferencesController.ipfsGateway, + networkType: state.engine.backgroundState.NetworkController.provider.type, + network: state.engine.backgroundState.NetworkController.network, + selectedAddress: state.engine.backgroundState.PreferencesController.selectedAddress, + privacyMode: state.privacy.privacyMode, + searchEngine: state.settings.searchEngine, + whitelist: state.browser.whitelist, + activeTab: state.browser.activeTab +}); + +const mapDispatchToProps = dispatch => ({ + approveHost: hostname => dispatch(approveHost(hostname)), + addBookmark: bookmark => dispatch(addBookmark(bookmark)), + addToBrowserHistory: ({ url, name }) => dispatch(addToHistory({ url, name })), + addToWhitelist: url => dispatch(addToWhitelist(url)), + setTransactionObject: asset => dispatch(setTransactionObject(asset)), + toggleNetworkModal: () => dispatch(toggleNetworkModal()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withNavigation(BrowserTab)); diff --git a/app/components/Views/Browser/index.test.js b/app/components/Views/BrowserTab/index.test.js similarity index 64% rename from app/components/Views/Browser/index.test.js rename to app/components/Views/BrowserTab/index.test.js index 4d9d955fc61..c292a61a1f6 100644 --- a/app/components/Views/Browser/index.test.js +++ b/app/components/Views/BrowserTab/index.test.js @@ -2,11 +2,11 @@ jest.useFakeTimers(); import React from 'react'; import { shallow } from 'enzyme'; -import { Browser } from './'; +import { BrowserTab } from './'; describe('Browser', () => { it('should render correctly', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/Views/ChoosePassword/__snapshots__/index.test.js.snap b/app/components/Views/ChoosePassword/__snapshots__/index.test.js.snap index 42dcb410fd5..9ce16188906 100644 --- a/app/components/Views/ChoosePassword/__snapshots__/index.test.js.snap +++ b/app/components/Views/ChoosePassword/__snapshots__/index.test.js.snap @@ -141,7 +141,7 @@ exports[`ChoosePassword should render correctly 1`] = ` secureTextEntry={true} style={ Object { - "borderBottomColor": "#CCCCCC", + "borderBottomColor": "#d6d9dc", "borderBottomWidth": 1, "borderRadius": 4, "fontFamily": "Roboto", @@ -152,7 +152,7 @@ exports[`ChoosePassword should render correctly 1`] = ` } } testID="input-password" - underlineColorAndroid="#CCCCCC" + underlineColorAndroid="#d6d9dc" value="" /> this.setState({ biometryChoice })} // eslint-disable-line react/jsx-no-bind value={this.state.biometryChoice} style={styles.biometrySwitch} - trackColor={ - Platform.OS === 'ios' ? { true: colors.switchOnColor, false: colors.switchOffColor } : null - } - ios_backgroundColor={colors.switchOffColor} + trackColor={Platform.OS === 'ios' ? { true: colors.green300, false: colors.grey300 } : null} + ios_backgroundColor={colors.grey300} /> ); @@ -320,32 +332,17 @@ class ChoosePassword extends Component { onValueChange={rememberMe => this.setState({ rememberMe })} // eslint-disable-line react/jsx-no-bind value={this.state.rememberMe} style={styles.biometrySwitch} - trackColor={ - Platform.OS === 'ios' ? { true: colors.switchOnColor, false: colors.switchOffColor } : null - } - ios_backgroundColor={colors.switchOffColor} + trackColor={Platform.OS === 'ios' ? { true: colors.green300, false: colors.grey300 } : null} + ios_backgroundColor={colors.grey300} /> ); }; onPasswordChange = val => { - let strength = 1; - - // If the password length is greater than 6 and contain alphabet,number,special character respectively - if ( - val.length > 6 && - ((val.match(/[a-z]/) && val.match(/\d+/)) || - (val.match(/\d+/) && val.match(/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/)) || - (val.match(/[a-z]/) && val.match(/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/))) - ) - strength = 2; - - // If the password length is greater than 6 and must contain alphabets,numbers and special characters - if (val.length > 6 && val.match(/[a-z]/) && val.match(/\d+/) && val.match(/.[!,@,#,$,%,^,&,*,?,_,~,-,(,)]/)) - strength = 3; + const passInfo = zxcvbn(val); - this.setState({ password: val, passwordStrength: strength }); + this.setState({ password: val, passwordStrength: passInfo.score }); }; toggleShowHide = () => { @@ -411,7 +408,7 @@ class ChoosePassword extends Component { onChangeText={this.onPasswordChange} // eslint-disable-line react/jsx-no-bind secureTextEntry={this.state.secureTextEntry} placeholder={''} - underlineColorAndroid={colors.borderColor} + underlineColorAndroid={colors.grey100} testID={'input-password'} onSubmitEditing={this.jumpToConfirmPassword} returnKeyType={'next'} @@ -474,7 +471,7 @@ class ChoosePassword extends Component { onChangeText={val => this.setState({ confirmPassword: val })} // eslint-disable-line react/jsx-no-bind secureTextEntry={this.state.secureTextEntry} placeholder={''} - underlineColorAndroid={colors.borderColor} + underlineColorAndroid={colors.grey100} testID={'input-password-confirm'} onSubmitEditing={this.onPressCreate} returnKeyType={'done'} @@ -485,7 +482,7 @@ class ChoosePassword extends Component { {this.state.password !== '' && this.state.password === this.state.confirmPassword ? ( - + ) : null} diff --git a/app/components/Views/Collectible/index.js b/app/components/Views/Collectible/index.js index 8433b2d4e27..dcea0d4bce4 100644 --- a/app/components/Views/Collectible/index.js +++ b/app/components/Views/Collectible/index.js @@ -81,6 +81,9 @@ class Collectible extends Component { if (!collectible.name || collectible.name === '') { collectible.name = collectibleContract.name; } + if (!collectible.image && collectibleContract.logo) { + collectible.image = collectibleContract.logo; + } return collectible; }); diff --git a/app/components/Views/CreateWallet/index.js b/app/components/Views/CreateWallet/index.js index 3e2fca7f894..a74aa9a6111 100644 --- a/app/components/Views/CreateWallet/index.js +++ b/app/components/Views/CreateWallet/index.js @@ -20,6 +20,8 @@ import SecureKeychain from '../../../core/SecureKeychain'; import { passwordUnset, seedphraseNotBackedUp } from '../../../actions/user'; import { setLockTime } from '../../../actions/settings'; import { connect } from 'react-redux'; +import setOnboardingWizardStep from '../../../actions/wizard'; +import { NavigationActions } from 'react-navigation'; const styles = StyleSheet.create({ flex: { @@ -48,7 +50,7 @@ const styles = StyleSheet.create({ fontSize: 16, lineHeight: 23, marginBottom: 20, - color: colors.copy, + color: colors.grey500, textAlign: 'center', ...fontStyles.normal }, @@ -98,7 +100,11 @@ class CreateWallet extends Component { /** * Action to reset the flag seedphraseBackedUp in redux */ - seedphraseNotBackedUp: PropTypes.func + seedphraseNotBackedUp: PropTypes.func, + /** + * Action to set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func }; // Temporary disabling the back button so users can't go back @@ -115,13 +121,28 @@ class CreateWallet extends Component { await SecureKeychain.setGenericPassword('metamask-user', ''); await AsyncStorage.removeItem('@MetaMask:biometryChoice'); await AsyncStorage.setItem('@MetaMask:existingUser', 'true'); + // Get onboarding wizard state + const onboardingWizard = await AsyncStorage.getItem('@MetaMask:onboardingWizard'); + // Check if user passed through metrics opt-in screen + const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn'); // Making sure we reset the flag while going to // the first time flow this.props.passwordUnset(); this.props.setLockTime(-1); this.props.seedphraseNotBackedUp(); setTimeout(() => { - this.props.navigation.navigate('HomeNav'); + if (!metricsOptIn) { + this.props.navigation.navigate('OptinMetrics'); + } else if (onboardingWizard) { + this.props.navigation.navigate('HomeNav'); + } else { + this.props.setOnboardingWizardStep(1); + this.props.navigation.navigate( + 'HomeNav', + {}, + NavigationActions.navigate({ routeName: 'WalletView' }) + ); + } }, 1000); }); } @@ -146,10 +167,7 @@ class CreateWallet extends Component { )} - + {strings('create_wallet.title')} {strings('create_wallet.subtitle')} @@ -161,6 +179,7 @@ class CreateWallet extends Component { const mapDispatchToProps = dispatch => ({ setLockTime: time => dispatch(setLockTime(time)), + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)), passwordUnset: () => dispatch(passwordUnset()), seedphraseNotBackedUp: () => dispatch(seedphraseNotBackedUp()) }); diff --git a/app/components/Views/CreateWallet/index.js.rej b/app/components/Views/CreateWallet/index.js.rej new file mode 100644 index 00000000000..da5511c6cfe --- /dev/null +++ b/app/components/Views/CreateWallet/index.js.rej @@ -0,0 +1,14 @@ +diff a/app/components/Views/CreateWallet/index.js b/app/components/Views/CreateWallet/index.js (rejected hunks) +@@ -23 +23 @@ import { connect } from 'react-redux'; +-import setOnboardingWizardStep from '../../../actions/wizard' ++import setOnboardingWizardStep from '../../../actions/wizard'; +@@ -132 +132 @@ class CreateWallet extends Component { +- this.props.setOnboardingWizardStep(1) ++ this.props.setOnboardingWizardStep(1); +@@ -139 +139,5 @@ class CreateWallet extends Component { +- this.props.navigation.navigate('HomeNav', {}, NavigationActions.navigate({ routeName: 'WalletView'})); ++ this.props.navigation.navigate( ++ 'HomeNav', ++ {}, ++ NavigationActions.navigate({ routeName: 'WalletView' }) ++ ); diff --git a/app/components/Views/Entry/__snapshots__/index.test.js.snap b/app/components/Views/Entry/__snapshots__/index.test.js.snap index 53f3bf3fb78..35d1a849156 100644 --- a/app/components/Views/Entry/__snapshots__/index.test.js.snap +++ b/app/components/Views/Entry/__snapshots__/index.test.js.snap @@ -1,3 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Entry should render correctly 1`] = ``; +exports[`Entry should render correctly 1`] = ` + +`; diff --git a/app/components/Views/Entry/index.js b/app/components/Views/Entry/index.js index 9589f673ba0..bb0cde71a08 100644 --- a/app/components/Views/Entry/index.js +++ b/app/components/Views/Entry/index.js @@ -1,32 +1,131 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { Platform, Animated, Dimensions, StyleSheet, View } from 'react-native'; import AsyncStorage from '@react-native-community/async-storage'; import Engine from '../../../core/Engine'; -import FoxScreen from '../../UI/FoxScreen'; +import LottieView from 'lottie-react-native'; import SecureKeychain from '../../../core/SecureKeychain'; +import setOnboardingWizardStep from '../../../actions/wizard'; +import { NavigationActions } from 'react-navigation'; +import { connect } from 'react-redux'; +import { colors } from '../../../styles/common'; + /** * Entry Screen that decides which screen to show * depending on the state of the user * new, existing , logged in or not * while showing the fox */ -export default class Entry extends Component { +const LOGO_SIZE = 194; +const LOGO_PADDING = 25; +const styles = StyleSheet.create({ + main: { + flex: 1, + backgroundColor: colors.white + }, + metamaskName: { + marginTop: 10, + height: 30, + width: 190, + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center' + }, + logoWrapper: { + backgroundColor: colors.white, + paddingTop: 50, + marginTop: Dimensions.get('window').height / 2 - LOGO_SIZE / 2 - LOGO_PADDING, + height: LOGO_SIZE + LOGO_PADDING * 2 + }, + foxAndName: { + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center' + }, + animation: { + width: 125, + height: 125, + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center' + }, + fox: { + width: 125, + height: 125, + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center' + } +}); + +class Entry extends Component { static propTypes = { /** * The navigator object */ - navigation: PropTypes.object + navigation: PropTypes.object, + /** + * Action to set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func }; - async componentDidMount() { - const existingUser = await AsyncStorage.getItem('@MetaMask:existingUser'); - if (existingUser !== null) { - await this.unlockKeychain(); - } else { - this.goToOnboarding(); - } + state = { + viewToGo: null + }; + + animation = React.createRef(); + animationName = React.createRef(); + opacity = new Animated.Value(1); + + componentDidMount() { + setTimeout(async () => { + const existingUser = await AsyncStorage.getItem('@MetaMask:existingUser'); + if (existingUser !== null) { + await this.unlockKeychain(); + } else { + this.animateAndGoTo('OnboardingRootNav'); + } + }, 100); } + animateAndGoTo(view) { + this.setState({ viewToGo: view }, () => { + if (Platform.OS === 'android') { + setTimeout(() => { + this.animation.play(0, 25); + setTimeout(() => { + this.animationName.play(); + }, 1); + }, 50); + } else { + this.animation.play(); + this.animationName.play(); + } + }); + } + + onAnimationFinished = () => { + setTimeout(() => { + Animated.timing(this.opacity, { + toValue: 0, + duration: 300, + useNativeDriver: true, + isInteraction: false + }).start(() => { + if (this.state.viewToGo !== 'WalletView') { + this.props.navigation.navigate(this.state.viewToGo); + } else { + this.props.navigation.navigate( + 'HomeNav', + {}, + NavigationActions.navigate({ routeName: 'WalletView' }) + ); + } + }); + }, 100); + }; + async unlockKeychain() { try { // Retreive the credentials @@ -35,27 +134,80 @@ export default class Entry extends Component { // Restore vault with existing credentials const { KeyringController } = Engine.context; await KeyringController.submitPassword(credentials.password); - this.goToWallet(); + // Get onboarding wizard state + const onboardingWizard = await AsyncStorage.getItem('@MetaMask:onboardingWizard'); + // Check if user passed through metrics opt-in screen + const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn'); + if (!metricsOptIn) { + this.animateAndGoTo('OptinMetrics'); + } else if (onboardingWizard) { + this.animateAndGoTo('HomeNav'); + } else { + this.props.setOnboardingWizardStep(1); + this.animateAndGoTo('WalletView'); + } } else { - this.goToLogin(); + this.animateAndGoTo('Login'); } } catch (error) { console.log(`Keychain couldn't be accessed`, error); // eslint-disable-line - this.goToLogin(); + this.animateAndGoTo('Login'); } } - goToOnboarding() { - this.props.navigation.navigate('OnboardingRootNav'); - } + renderAnimations() { + if (!this.state.viewToGo) { + return ( + + ); + } - goToWallet() { - this.props.navigation.navigate('HomeNav'); + return ( + + { + this.animation = animation; + }} + style={styles.animation} + loop={false} + source={require('../../../animations/fox-in.json')} + onAnimationFinish={this.onAnimationFinished} + /> + { + this.animationName = animation; + }} + style={styles.metamaskName} + loop={false} + source={require('../../../animations/wordmark.json')} + /> + + ); } - goToLogin() { - this.props.navigation.navigate('Login'); + render() { + return ( + + + {this.renderAnimations()} + + + ); } - - render = () => ; } + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + null, + mapDispatchToProps +)(Entry); diff --git a/app/components/Views/Entry/index.test.js b/app/components/Views/Entry/index.test.js index a928efcd1e8..fc39e57bd75 100644 --- a/app/components/Views/Entry/index.test.js +++ b/app/components/Views/Entry/index.test.js @@ -1,10 +1,17 @@ import React from 'react'; import { shallow } from 'enzyme'; import Entry from './'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); describe('Entry', () => { it('should render correctly', () => { - const wrapper = shallow(); + const initialState = {}; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/Views/ImportFromSeed/__snapshots__/index.test.js.snap b/app/components/Views/ImportFromSeed/__snapshots__/index.test.js.snap index 54c638da846..2402ad0253e 100644 --- a/app/components/Views/ImportFromSeed/__snapshots__/index.test.js.snap +++ b/app/components/Views/ImportFromSeed/__snapshots__/index.test.js.snap @@ -25,7 +25,7 @@ exports[`ImportFromSeed should render correctly 1`] = ` style={ Object { "flex": 1, - "padding": 20, + "paddingHorizontal": 20, } } viewIsInsideTabBar={false} @@ -36,7 +36,7 @@ exports[`ImportFromSeed should render correctly 1`] = ` - - New Password (min. 8 characters) - + New Password + + + + Show + + + - Confirm Password - + + + + Must be at least 8 characters + @@ -195,17 +308,18 @@ exports[`ImportFromSeed should render correctly 1`] = ` Remember me + + + diff --git a/app/components/Views/ImportFromSeed/index.js b/app/components/Views/ImportFromSeed/index.js index 94e8d3ef780..56be60940ef 100644 --- a/app/components/Views/ImportFromSeed/index.js +++ b/app/components/Views/ImportFromSeed/index.js @@ -1,9 +1,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { + Animated, Switch, ActivityIndicator, Alert, + TouchableOpacity, Text, View, TextInput, @@ -19,11 +21,15 @@ import { passwordSet, seedphraseBackedUp } from '../../../actions/user'; import { setLockTime } from '../../../actions/settings'; import StyledButton from '../../UI/StyledButton'; import Engine from '../../../core/Engine'; - import { colors, fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import SecureKeychain from '../../../core/SecureKeychain'; import AppConstants from '../../../core/AppConstants'; +import setOnboardingWizardStep from '../../../actions/wizard'; +import { NavigationActions } from 'react-navigation'; +import TermsAndConditions from '../TermsAndConditions'; +import zxcvbn from 'zxcvbn'; +import Icon from 'react-native-vector-icons/FontAwesome'; const styles = StyleSheet.create({ mainWrapper: { @@ -32,38 +38,44 @@ const styles = StyleSheet.create({ }, wrapper: { flex: 1, - padding: 20 + paddingHorizontal: 20 }, title: { fontSize: Platform.OS === 'android' ? 20 : 25, marginTop: 20, marginBottom: 20, - color: colors.title, + color: colors.fontPrimary, justifyContent: 'center', textAlign: 'center', ...fontStyles.bold }, field: { - marginBottom: Platform.OS === 'android' ? 0 : 10 + marginTop: 20, + marginBottom: 10 }, label: { + position: 'absolute', + marginTop: -35, + marginLeft: 5, fontSize: 16, - marginBottom: Platform.OS === 'android' ? 0 : 10, - marginTop: 10 + color: colors.fontSecondary, + textAlign: 'left', + ...fontStyles.normal }, input: { - borderWidth: Platform.OS === 'android' ? 0 : 1, - borderColor: colors.borderColor, - padding: 10, + borderBottomWidth: Platform.OS === 'android' ? 0 : 1, + borderBottomColor: colors.grey100, + paddingLeft: 0, + paddingVertical: 10, borderRadius: 4, - fontSize: Platform.OS === 'android' ? 15 : 20, + fontSize: Platform.OS === 'android' ? 14 : 20, ...fontStyles.normal }, ctaWrapper: { marginTop: 20 }, errorMsg: { - color: colors.error, + color: colors.red, textAlign: 'center', ...fontStyles.normal }, @@ -80,13 +92,12 @@ const styles = StyleSheet.create({ minHeight: 110, height: 'auto', borderWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderColor, + borderColor: colors.grey100, ...fontStyles.normal }, biometrics: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 20, + alignItems: 'flex-start', + marginTop: 30, marginBottom: 30 }, biometryLabel: { @@ -95,7 +106,43 @@ const styles = StyleSheet.create({ ...fontStyles.normal }, biometrySwitch: { + marginTop: 10, flex: 0 + }, + termsAndConditions: { + paddingVertical: 30 + }, + passwordStrengthLabel: { + height: 20, + marginLeft: 5, + marginTop: 10, + fontSize: 12, + color: colors.fontSecondary, + textAlign: 'left', + ...fontStyles.normal + }, + // eslint-disable-next-line react-native/no-unused-styles + strength_weak: { + color: colors.red + }, + // eslint-disable-next-line react-native/no-unused-styles + strength_good: { + color: colors.blue + }, + // eslint-disable-next-line react-native/no-unused-styles + strength_strong: { + color: colors.green300 + }, + showHideToggle: { + backgroundColor: colors.white, + position: 'absolute', + marginTop: 8, + alignSelf: 'flex-end' + }, + showMatchingPasswords: { + position: 'absolute', + marginTop: 8, + alignSelf: 'flex-end' } }); @@ -127,7 +174,11 @@ class ImportFromSeed extends Component { * The action to update the seedphrase backed up flag * in the redux store */ - seedphraseBackedUp: PropTypes.func + seedphraseBackedUp: PropTypes.func, + /** + * Action to set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func }; state = { @@ -136,6 +187,9 @@ class ImportFromSeed extends Component { seed: '', biometryType: null, rememberMe: false, + labelsScaleNew: new Animated.Value(1), + labelsScaleConfirm: new Animated.Value(1), + secureTextEntry: true, biometryChoice: false, loading: false, error: null, @@ -148,7 +202,12 @@ class ImportFromSeed extends Component { async componentDidMount() { const biometryType = await SecureKeychain.getSupportedBiometryType(); if (biometryType) { - this.setState({ biometryType, biometryChoice: true }); + let enabled = true; + const previouslyDisabled = await AsyncStorage.removeItem('@MetaMask:biometryChoiceDisabled'); + if (previouslyDisabled && previouslyDisabled === 'true') { + enabled = false; + } + this.setState({ biometryType, biometryChoice: enabled }); } this.mounted = true; // Workaround https://github.com/facebook/react-native/issues/9958 @@ -196,6 +255,11 @@ class ImportFromSeed extends Component { if (!this.state.biometryChoice) { await AsyncStorage.removeItem('@MetaMask:biometryChoice'); } else { + // If the user enables biometrics, we're trying to read the password + // immediately so we get the permission prompt + if (Platform.OS === 'ios') { + await SecureKeychain.getGenericPassword(); + } await AsyncStorage.setItem('@MetaMask:biometryChoice', this.state.biometryType); } } else { @@ -208,14 +272,28 @@ class ImportFromSeed extends Component { } await AsyncStorage.removeItem('@MetaMask:biometryChoice'); } - + // Get onboarding wizard state + const onboardingWizard = await AsyncStorage.getItem('@MetaMask:onboardingWizard'); + // Check if user passed through metrics opt-in screen + const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn'); // mark the user as existing so it doesn't see the create password screen again await AsyncStorage.setItem('@MetaMask:existingUser', 'true'); this.setState({ loading: false }); this.props.passwordSet(); this.props.setLockTime(AppConstants.DEFAULT_LOCK_TIMEOUT); this.props.seedphraseBackedUp(); - this.props.navigation.navigate('HomeNav'); + if (!metricsOptIn) { + this.props.navigation.navigate('OptinMetrics'); + } else if (onboardingWizard) { + this.props.navigation.navigate('HomeNav'); + } else { + this.props.setOnboardingWizardStep(1); + this.props.navigation.navigate( + 'HomeNav', + {}, + NavigationActions.navigate({ routeName: 'WalletView' }) + ); + } } catch (error) { // Should we force people to enable passcode / biometrics? if (error.toString() === PASSCODE_NOT_SET_ERROR) { @@ -240,7 +318,9 @@ class ImportFromSeed extends Component { }; onPasswordChange = val => { - this.setState({ password: val }); + const passInfo = zxcvbn(val); + + this.setState({ password: val, passwordStrength: passInfo.score }); }; onPasswordConfirmChange = val => { @@ -257,6 +337,15 @@ class ImportFromSeed extends Component { current && current.focus(); }; + updateBiometryChoice = async biometryChoice => { + if (!biometryChoice) { + await AsyncStorage.setItem('@MetaMask:biometryChoiceDisabled', 'true'); + } else { + await AsyncStorage.removeItem('@MetaMask:biometryChoiceDisabled'); + } + this.setState({ biometryChoice }); + }; + renderSwitch = () => { if (this.state.biometryType) { return ( @@ -268,10 +357,8 @@ class ImportFromSeed extends Component { onValueChange={biometryChoice => this.setState({ biometryChoice })} // eslint-disable-line react/jsx-no-bind value={this.state.biometryChoice} style={styles.biometrySwitch} - trackColor={ - Platform.OS === 'ios' ? { true: colors.switchOnColor, false: colors.switchOffColor } : null - } - ios_backgroundColor={colors.switchOffColor} + trackColor={Platform.OS === 'ios' ? { true: colors.green300, false: colors.grey300 } : null} + ios_backgroundColor={colors.grey300} /> ); @@ -284,88 +371,227 @@ class ImportFromSeed extends Component { onValueChange={rememberMe => this.setState({ rememberMe })} // eslint-disable-line react/jsx-no-bind value={this.state.rememberMe} style={styles.biometrySwitch} - trackColor={ - Platform.OS === 'ios' ? { true: colors.switchOnColor, false: colors.switchOffColor } : null - } - ios_backgroundColor={colors.switchOffColor} + trackColor={Platform.OS === 'ios' ? { true: colors.green300, false: colors.grey300 } : null} + ios_backgroundColor={colors.grey300} /> ); }; - render = () => ( - - - - {strings('import_from_seed.title')} - - - {strings('import_from_seed.new_password')} + toggleShowHide = () => { + this.setState({ secureTextEntry: !this.state.secureTextEntry }); + }; + + animateInLabel = label => { + if ( + (label === 'new' && this.state.password !== '') || + (label === 'confirm' && this.state.confirmPassword !== '') + ) { + return; + } + Animated.timing(label === 'new' ? this.state.labelsScaleNew : this.state.labelsScaleConfirm, { + toValue: 1, + duration: 200, + useNativeDriver: true, + isInteraction: false + }).start(); + }; + + animateOutLabel = label => { + Animated.timing(label === 'new' ? this.state.labelsScaleNew : this.state.labelsScaleConfirm, { + toValue: 0.66, + duration: 200, + useNativeDriver: true, + isInteraction: false + }).start(); + }; + + getPasswordStrengthWord() { + // this.state.passwordStrength is calculated by zxcvbn + // which returns a score based on "entropy to crack time" + // 0 is the weakest, 4 the strongest + switch (this.state.passwordStrength) { + case 0: + return 'weak'; + case 1: + return 'weak'; + case 2: + return 'weak'; + case 3: + return 'good'; + case 4: + return 'strong'; + } + } + + render() { + const startX = 0; + const startY = 0; + const width = 100; + const height = 24; + const initialScale = 1; + const endX = 0; + const endY = 50; + + const labelsScaleNewX = this.state.labelsScaleNew.interpolate({ + inputRange: [0, 1], + outputRange: [startX - width / 2 - (width * initialScale) / 2, endX] + }); + const labelsScaleNewY = this.state.labelsScaleNew.interpolate({ + inputRange: [0, 1], + outputRange: [startY - height / 2 - (height * initialScale) / 2, endY] + }); + + const labelsScaleNewXConfirm = this.state.labelsScaleConfirm.interpolate({ + inputRange: [0, 1], + outputRange: [startX - width / 2 - (width * initialScale) / 2, endX] + }); + const labelsScaleNewYConfirm = this.state.labelsScaleConfirm.interpolate({ + inputRange: [0, 1], + outputRange: [startY - height / 2 - (height * initialScale) / 2, endY] + }); + + const { password, confirmPassword, seed } = this.state; + + return ( + + + + {strings('import_from_seed.title')} - - - {strings('import_from_seed.confirm_password')} - - + + + {strings('import_from_seed.new_password')} + + this.animateOutLabel('new')} // eslint-disable-line react/jsx-no-bind + onBlur={() => this.animateInLabel('new')} // eslint-disable-line react/jsx-no-bind + autoCapitalize="none" + /> + + + {strings(`choose_password.${this.state.secureTextEntry ? 'show' : 'hide'}`)} + + + {(this.state.password !== '' && ( + + {strings('choose_password.password_strength')} + + {' '} + {strings(`choose_password.strength_${this.getPasswordStrengthWord()}`)} + + + )) || } + + + + + {strings('import_from_seed.confirm_password')} + + this.animateOutLabel('confirm')} // eslint-disable-line react/jsx-no-bind + onBlur={() => this.animateInLabel('confirm')} // eslint-disable-line react/jsx-no-bind + autoCapitalize="none" + /> + + {this.state.password !== '' && this.state.password === this.state.confirmPassword ? ( + + ) : null} + + + {strings('choose_password.must_be_at_least', { number: 8 })} + + - {this.renderSwitch()} + {this.renderSwitch()} - {this.state.error && {this.state.error}} + {this.state.error && {this.state.error}} - - - {this.state.loading ? ( - - ) : ( - strings('import_from_seed.import_button') - )} - + + + {this.state.loading ? ( + + ) : ( + strings('import_from_seed.import_button') + )} + + + + + - - - - ); + + + ); + } } const mapDispatchToProps = dispatch => ({ setLockTime: time => dispatch(setLockTime(time)), + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)), passwordSet: () => dispatch(passwordSet()), seedphraseBackedUp: () => dispatch(seedphraseBackedUp()) }); diff --git a/app/components/Views/ImportFromSeed/index.js.rej b/app/components/Views/ImportFromSeed/index.js.rej new file mode 100644 index 00000000000..dbb970d2aff --- /dev/null +++ b/app/components/Views/ImportFromSeed/index.js.rej @@ -0,0 +1,14 @@ +diff a/app/components/Views/ImportFromSeed/index.js b/app/components/Views/ImportFromSeed/index.js (rejected hunks) +@@ -27 +27 @@ import AppConstants from '../../../core/AppConstants'; +-import setOnboardingWizardStep from '../../../actions/wizard' ++import setOnboardingWizardStep from '../../../actions/wizard'; +@@ -233 +233 @@ class ImportFromSeed extends Component { +- this.props.setOnboardingWizardStep(1) ++ this.props.setOnboardingWizardStep(1); +@@ -235 +235,5 @@ class ImportFromSeed extends Component { +- this.props.navigation.navigate('HomeNav', {}, NavigationActions.navigate({ routeName: 'WalletView'})); ++ this.props.navigation.navigate( ++ 'HomeNav', ++ {}, ++ NavigationActions.navigate({ routeName: 'WalletView' }) ++ ); diff --git a/app/components/Views/ImportPrivateKey/__snapshots__/index.test.js.snap b/app/components/Views/ImportPrivateKey/__snapshots__/index.test.js.snap index 3e6e0b1d1ef..2ce7891883c 100644 --- a/app/components/Views/ImportPrivateKey/__snapshots__/index.test.js.snap +++ b/app/components/Views/ImportPrivateKey/__snapshots__/index.test.js.snap @@ -4,7 +4,7 @@ exports[`ImportPrivateKey should render correctly 1`] = ` - + {strings('import_private_key_success.title')} diff --git a/app/components/Views/LockScreen/__snapshots__/index.test.js.snap b/app/components/Views/LockScreen/__snapshots__/index.test.js.snap index 189b38730ad..1e4a44358fb 100644 --- a/app/components/Views/LockScreen/__snapshots__/index.test.js.snap +++ b/app/components/Views/LockScreen/__snapshots__/index.test.js.snap @@ -1,3 +1,1070 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LockScreen should render correctly 1`] = ``; +exports[`LockScreen should render correctly 1`] = ` + + + + + + + +`; diff --git a/app/components/Views/LockScreen/index.js b/app/components/Views/LockScreen/index.js index 3cc729a2f33..8e54eb32094 100644 --- a/app/components/Views/LockScreen/index.js +++ b/app/components/Views/LockScreen/index.js @@ -1,11 +1,45 @@ import React, { Component } from 'react'; -import { AppState } from 'react-native'; +import { StyleSheet, Dimensions, Animated, View, AppState, Platform } from 'react-native'; import PropTypes from 'prop-types'; - -import FoxScreen from '../../UI/FoxScreen'; +import LottieView from 'lottie-react-native'; import Engine from '../../../core/Engine'; import SecureKeychain from '../../../core/SecureKeychain'; +import { baseStyles } from '../../../styles/common'; +const LOGO_SIZE = 194; +const styles = StyleSheet.create({ + metamaskName: { + marginTop: 10, + height: 30, + width: 190, + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center' + }, + logoWrapper: { + marginTop: Dimensions.get('window').height / 2 - LOGO_SIZE / 2, + height: LOGO_SIZE + }, + foxAndName: { + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center' + }, + animation: { + width: 125, + height: 125, + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center' + }, + fox: { + width: 125, + height: 125, + alignSelf: 'center', + alignItems: 'center', + justifyContent: 'center' + } +}); /** * Main view component for the Lock screen */ @@ -17,23 +51,20 @@ export default class LockScreen extends Component { navigation: PropTypes.object }; + state = { + ready: false + }; + appState = 'active'; locked = true; + firstAnimation = React.createRef(); + secondAnimation = React.createRef(); + animationName = React.createRef(); + opacity = new Animated.Value(1); componentDidMount() { // Check if is the app is launching or it went to background mode - const isBackgroundMode = this.props.navigation.getParam('backgroundMode', false); - if (!isBackgroundMode) { - // Because this is also the first screen - // We need to wait for the engine to bootstrap before we can continue - if (!Engine.context) { - this.waitForEngine(); - } else { - this.unlockKeychain(); - } - } else { - this.appState = 'background'; - } + this.appState = 'background'; AppState.addEventListener('change', this.handleAppStateChange); this.mounted = true; } @@ -47,6 +78,7 @@ export default class LockScreen extends Component { handleAppStateChange = async nextAppState => { // Try to unlock when coming from the background if (this.locked && this.appState !== 'active' && nextAppState === 'active') { + this.firstAnimation.play(); this.appState = nextAppState; this.unlockKeychain(); } @@ -66,12 +98,84 @@ export default class LockScreen extends Component { const { KeyringController } = Engine.context; await KeyringController.submitPassword(credentials.password); this.locked = false; - this.props.navigation.goBack(); + this.setState({ ready: true }, () => { + if (Platform.OS === 'android') { + setTimeout(() => { + this.secondAnimation.play(0, 25); + setTimeout(() => { + this.animationName.play(); + }, 1); + }, 50); + } else { + this.secondAnimation.play(); + this.animationName.play(); + } + }); } } catch (error) { console.log(`Keychain couldn't be accessed`, error); // eslint-disable-line } } - render = () => ; + onAnimationFinished = () => { + setTimeout(() => { + Animated.timing(this.opacity, { + toValue: 0, + duration: 300, + useNativeDriver: true, + isInteraction: false + }).start(() => { + this.props.navigation.goBack(); + }); + }, 100); + }; + + renderAnimations() { + if (!this.state.ready) { + return ( + { + this.firstAnimation = animation; + }} + style={styles.animation} + source={require('../../../animations/bounce.json')} + /> + ); + } + + return ( + + { + this.secondAnimation = animation; + }} + style={styles.animation} + loop={false} + source={require('../../../animations/fox-in.json')} + onAnimationFinish={this.onAnimationFinished} + /> + { + this.animationName = animation; + }} + style={styles.metamaskName} + loop={false} + source={require('../../../animations/wordmark.json')} + /> + + ); + } + + render() { + return ( + + + {this.renderAnimations()} + + + ); + } } diff --git a/app/components/Views/Login/__snapshots__/index.test.js.snap b/app/components/Views/Login/__snapshots__/index.test.js.snap index 9d8e90cd8fc..39e9c50719d 100644 --- a/app/components/Views/Login/__snapshots__/index.test.js.snap +++ b/app/components/Views/Login/__snapshots__/index.test.js.snap @@ -1,195 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Login should render correctly 1`] = ` - - - - - - - - Welcome Back! - - - - Password - - - - - - Remember me - - - - - - LOG IN - - - - - - - - + `; diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index d59e325dd67..f911182ce6e 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -21,6 +21,12 @@ import AnimatedFox from 'react-native-animated-fox'; import { colors, fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import SecureKeychain from '../../../core/SecureKeychain'; +import FadeOutOverlay from '../../UI/FadeOutOverlay'; +import setOnboardingWizardStep from '../../../actions/wizard'; +import { NavigationActions } from 'react-navigation'; +import { connect } from 'react-redux'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; const styles = StyleSheet.create({ mainWrapper: { @@ -47,7 +53,7 @@ const styles = StyleSheet.create({ fontSize: Platform.OS === 'android' ? 30 : 35, marginTop: 20, marginBottom: 20, - color: colors.title, + color: colors.fontPrimary, justifyContent: 'center', textAlign: 'center', ...fontStyles.bold @@ -62,7 +68,7 @@ const styles = StyleSheet.create({ }, input: { borderWidth: Platform.OS === 'android' ? 0 : 1, - borderColor: colors.borderColor, + borderColor: colors.grey100, padding: 10, borderRadius: 4, fontSize: Platform.OS === 'android' ? 15 : 20, @@ -75,7 +81,7 @@ const styles = StyleSheet.create({ marginVertical: 40 }, errorMsg: { - color: colors.error, + color: colors.red, ...fontStyles.normal }, goBack: { @@ -104,12 +110,28 @@ const WRONG_PASSWORD_ERROR = 'Error: Decrypt failed'; /** * View where returning users can authenticate */ -export default class Login extends Component { +class Login extends Component { static propTypes = { /** * The navigator object */ - navigation: PropTypes.object + navigation: PropTypes.object, + /** + * Action to set onboarding wizard step + */ + setOnboardingWizardStep: PropTypes.func, + /** + * Number of tokens + */ + tokensLength: PropTypes.number, + /** + * Number of accounts + */ + accountsLength: PropTypes.number, + /** + * A string representing the network name + */ + networkType: PropTypes.string }; state = { @@ -126,7 +148,12 @@ export default class Login extends Component { async componentDidMount() { const biometryType = await SecureKeychain.getSupportedBiometryType(); if (biometryType) { - this.setState({ biometryType, biometryChoice: true }); + let enabled = true; + const previouslyDisabled = await AsyncStorage.removeItem('@MetaMask:biometryChoiceDisabled'); + if (previouslyDisabled && previouslyDisabled === 'true') { + enabled = false; + } + this.setState({ biometryType, biometryChoice: enabled }); } } @@ -167,9 +194,20 @@ export default class Login extends Component { } await AsyncStorage.removeItem('@MetaMask:biometryChoice'); } - this.setState({ loading: false }); - this.props.navigation.navigate('HomeNav'); + + // Get onboarding wizard state + const onboardingWizard = await AsyncStorage.getItem('@MetaMask:onboardingWizard'); + // Check if user passed through metrics opt-in screen + const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn'); + if (!metricsOptIn) { + this.props.navigation.navigate('OptinMetrics'); + } else if (onboardingWizard) { + this.props.navigation.navigate('HomeNav'); + } else { + this.props.setOnboardingWizardStep(1); + this.props.navigation.navigate('HomeNav', {}, NavigationActions.navigate({ routeName: 'WalletView' })); + } } catch (error) { // Should we force people to enable passcode / biometrics? if (error.toString() === WRONG_PASSWORD_ERROR) { @@ -183,6 +221,12 @@ export default class Login extends Component { } else { this.setState({ loading: false, error: error.toString() }); } + const { tokensLength, accountsLength, networkType } = this.props; + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.AUTHENTICATION_INCORRECT_PASSWORD, { + numberOfTokens: tokensLength, + numberOfAccounts: accountsLength, + network: networkType + }); } }; @@ -190,6 +234,15 @@ export default class Login extends Component { this.props.navigation.navigate('OnboardingRootNav'); }; + updateBiometryChoice = async biometryChoice => { + if (!biometryChoice) { + await AsyncStorage.setItem('@MetaMask:biometryChoiceDisabled', 'true'); + } else { + await AsyncStorage.removeItem('@MetaMask:biometryChoiceDisabled'); + } + this.setState({ biometryChoice }); + }; + renderSwitch = () => { if (this.state.biometryType) { return ( @@ -198,13 +251,11 @@ export default class Login extends Component { {strings(`biometrics.enable_${this.state.biometryType.toLowerCase()}`)} this.setState({ biometryChoice })} // eslint-disable-line react/jsx-no-bind + onValueChange={biometryChoice => this.updateBiometryChoice(biometryChoice)} // eslint-disable-line react/jsx-no-bind value={this.state.biometryChoice} style={styles.biometrySwitch} - trackColor={ - Platform.OS === 'ios' ? { true: colors.switchOnColor, false: colors.switchOffColor } : null - } - ios_backgroundColor={colors.switchOffColor} + trackColor={Platform.OS === 'ios' ? { true: colors.green300, false: colors.grey300 } : null} + ios_backgroundColor={colors.grey300} /> ); @@ -217,10 +268,8 @@ export default class Login extends Component { onValueChange={rememberMe => this.setState({ rememberMe })} // eslint-disable-line react/jsx-no-bind value={this.state.rememberMe} style={styles.biometrySwitch} - trackColor={ - Platform.OS === 'ios' ? { true: colors.switchOnColor, false: colors.switchOffColor } : null - } - ios_backgroundColor={colors.switchOffColor} + trackColor={Platform.OS === 'ios' ? { true: colors.green300, false: colors.grey300 } : null} + ios_backgroundColor={colors.grey300} /> ); @@ -252,7 +301,7 @@ export default class Login extends Component { onChangeText={this.setPassword} secureTextEntry placeholder={''} - underlineColorAndroid={colors.borderColor} + underlineColorAndroid={colors.grey100} onSubmitEditing={this.onLogin} returnKeyType={'done'} autoCapitalize="none" @@ -280,6 +329,22 @@ export default class Login extends Component { + ); } + +const mapStateToProps = state => ({ + accountsLength: Object.keys(state.engine.backgroundState.AccountTrackerController.accounts).length, + tokensLength: state.engine.backgroundState.AssetsController.tokens.length, + networkType: state.engine.backgroundState.NetworkController.provider.type +}); + +const mapDispatchToProps = dispatch => ({ + setOnboardingWizardStep: step => dispatch(setOnboardingWizardStep(step)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(Login); diff --git a/app/components/Views/Login/index.test.js b/app/components/Views/Login/index.test.js index 8d0147efb48..e1e77a4dc3e 100644 --- a/app/components/Views/Login/index.test.js +++ b/app/components/Views/Login/index.test.js @@ -1,10 +1,33 @@ import React from 'react'; import { shallow } from 'enzyme'; import Login from './'; +import configureMockStore from 'redux-mock-store'; + +const mockStore = configureMockStore(); describe('Login', () => { it('should render correctly', () => { - const wrapper = shallow(); + const initialState = { + engine: { + backgroundState: { + AccountTrackerController: { + accounts: { '0x2': { balance: '0' } } + }, + NetworkController: { + provider: { + type: 'ropsten' + } + }, + AssetsController: { + tokens: [] + } + } + } + }; + + const wrapper = shallow(, { + context: { store: mockStore(initialState) } + }); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js index b1ca86c6e04..b12eea7eb1c 100644 --- a/app/components/Views/Onboarding/index.js +++ b/app/components/Views/Onboarding/index.js @@ -4,13 +4,17 @@ import { Platform, Text, View, ScrollView, StyleSheet, Image, Alert } from 'reac import AsyncStorage from '@react-native-community/async-storage'; import StyledButton from '../../UI/StyledButton'; import AnimatedFox from 'react-native-animated-fox'; -import { colors, fontStyles } from '../../../styles/common'; +import { colors, fontStyles, baseStyles } from '../../../styles/common'; import OnboardingScreenWithBg from '../../UI/OnboardingScreenWithBg'; import { strings } from '../../../../locales/i18n'; import Button from 'react-native-button'; import { connect } from 'react-redux'; import SecureKeychain from '../../../core/SecureKeychain'; import Engine from '../../../core/Engine'; +import FadeOutOverlay from '../../UI/FadeOutOverlay'; +import TermsAndConditions from '../TermsAndConditions'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; const styles = StyleSheet.create({ flex: { @@ -28,9 +32,12 @@ const styles = StyleSheet.create({ }, ctas: { justifyContent: 'flex-end', - height: 190, + height: 250, paddingBottom: 40 }, + termsAndConditions: { + paddingTop: 30 + }, foxWrapper: { width: Platform.OS === 'ios' ? 100 : 66, height: Platform.OS === 'ios' ? 100 : 66, @@ -55,7 +62,7 @@ const styles = StyleSheet.create({ fontSize: 14, lineHeight: 19, marginBottom: 20, - color: colors.copy, + color: colors.grey500, justifyContent: 'flex-start', textAlign: 'left', ...fontStyles.normal @@ -130,6 +137,7 @@ class Onboarding extends Component { onPressCreate = () => { const { existingUser } = this.state; + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_CREATE_NEW_WALLET); const action = () => this.props.navigation.push('CreateWallet'); if (existingUser) { this.alertExistingUser(action); @@ -140,6 +148,7 @@ class Onboarding extends Component { onPressImport = () => { this.props.navigation.push('ImportWallet'); + Analytics.trackEvent(ANALYTICS_EVENT_OPTS.ONBOARDING_SELECTED_IMPORT_WALLET); }; alertExistingUser = callback => { @@ -156,54 +165,63 @@ class Onboarding extends Component { render() { return ( - - - - - - {Platform.OS === 'android' ? ( - - ) : ( - - )} - - {strings('onboarding.title')} - {strings('onboarding.subtitle')} - - - - - {strings('onboarding.start_exploring_now')} - + + + + + + + {Platform.OS === 'android' ? ( + + ) : ( + + )} + + {strings('onboarding.title')} + {strings('onboarding.subtitle')} - - - {strings('onboarding.import_wallet_button')} - + + + + {strings('onboarding.start_exploring_now')} + + + + + {strings('onboarding.import_wallet_button')} + + + + + + {this.state.existingUser && ( + + + + )} - {this.state.existingUser && ( - - - - )} - - - + + + + ); } } diff --git a/app/components/Views/PickComponent/__snapshots__/index.test.js.snap b/app/components/Views/PickComponent/__snapshots__/index.test.js.snap new file mode 100644 index 00000000000..a1cdcbc9ff7 --- /dev/null +++ b/app/components/Views/PickComponent/__snapshots__/index.test.js.snap @@ -0,0 +1,99 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PickComponent should render correctly 1`] = ` + + + + + + Text First + + + + + + + + Text Second + + + + +`; diff --git a/app/components/Views/PickComponent/index.js b/app/components/Views/PickComponent/index.js new file mode 100644 index 00000000000..6bf1607b5e7 --- /dev/null +++ b/app/components/Views/PickComponent/index.js @@ -0,0 +1,103 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; +import { baseStyles, colors, fontStyles } from '../../../styles/common'; + +const styles = StyleSheet.create({ + root: { + ...baseStyles.flexGrow, + flexDirection: 'row' + }, + circle: { + width: 12, + height: 12, + borderRadius: 12 / 2, + backgroundColor: colors.white, + opacity: 1, + margin: 2, + borderWidth: 2, + borderColor: colors.grey300, + marginRight: 6 + }, + option: { + flex: 1 + }, + touchableOption: { + flex: 1, + flexDirection: 'row' + }, + optionText: { + ...fontStyles.normal + }, + selectedCircle: { + width: 12, + height: 12, + borderRadius: 12 / 2, + backgroundColor: colors.blue, + opacity: 1, + margin: 2, + marginRight: 6 + } +}); + +/** + * Componets that allows to select clicking two options + */ +export default class PickComponent extends Component { + static propTypes = { + /** + * Callback to pick an option + */ + pick: PropTypes.func, + /** + * Text to first option + */ + textFirst: PropTypes.string, + /** + * Value of first option + */ + valueFirst: PropTypes.string, + /** + * Text to second option + */ + textSecond: PropTypes.string, + /** + * Value of second option + */ + valueSecond: PropTypes.string, + /** + * Current selected value + */ + selectedValue: PropTypes.string + }; + + pickFirst = () => { + const { pick, valueFirst } = this.props; + pick && pick(valueFirst); + }; + + pickSecond = () => { + const { pick, valueSecond } = this.props; + pick && pick(valueSecond); + }; + + render = () => { + const { selectedValue, valueFirst, valueSecond, textFirst, textSecond } = this.props; + return ( + + + + + {textFirst} + + + + + + {textSecond} + + + + ); + }; +} diff --git a/app/components/Views/PickComponent/index.test.js b/app/components/Views/PickComponent/index.test.js new file mode 100644 index 00000000000..ae5520d586a --- /dev/null +++ b/app/components/Views/PickComponent/index.test.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import PickComponent from './'; + +describe('PickComponent', () => { + it('should render correctly', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/QRScanner/index.js b/app/components/Views/QRScanner/index.js index 4e737e43d0b..4009b314342 100644 --- a/app/components/Views/QRScanner/index.js +++ b/app/components/Views/QRScanner/index.js @@ -79,12 +79,21 @@ export default class QrScanner extends Component { if (content.split('ethereum:').length > 1) { this.shouldReadBarCode = false; data = parse(content); + let action = 'send-eth'; + if (data.function_name === 'transfer') { + // Send erc20 token + action = 'send-token'; + } + data = { ...data, action }; } else if (content.substring(0, 2).toLowerCase() === '0x') { this.shouldReadBarCode = false; data = { target_address: content }; } else if (content.split('metamask-sync:').length > 1) { this.shouldReadBarCode = false; data = { content }; + } else if (content.split('wc:').length > 1) { + this.shouldReadBarCode = false; + data = { walletConnectURI: content }; } else { // EIP-945 allows scanning arbitrary data data = content; diff --git a/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.js.snap b/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.js.snap index 3e31edc2e90..ea53517e381 100644 --- a/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.js.snap +++ b/app/components/Views/RevealPrivateCredential/__snapshots__/index.test.js.snap @@ -30,7 +30,7 @@ exports[`RevealPrivateCredential should render correctly 1`] = ` "padding": 20, }, Object { - "borderBottomColor": "#9b9b9b", + "borderBottomColor": "#848c96", "borderBottomWidth": 1, "fontFamily": "Roboto", "fontWeight": "400", @@ -45,7 +45,7 @@ exports[`RevealPrivateCredential should render correctly 1`] = ` @@ -70,7 +70,7 @@ exports[`RevealPrivateCredential should render correctly 1`] = ` size={22} style={ Object { - "color": "#f00", + "color": "#D73A49", "margin": 10, } } @@ -115,7 +115,7 @@ exports[`RevealPrivateCredential should render correctly 1`] = ` secureTextEntry={true} style={ Object { - "borderColor": "#f1f2f6", + "borderColor": "#f2f3f4", "borderRadius": 5, "borderWidth": 2, "padding": 10, @@ -127,7 +127,7 @@ exports[`RevealPrivateCredential should render correctly 1`] = ` } + loading={null} persistor={ Object { "dispatch": [Function], diff --git a/app/components/Views/Root/index.js b/app/components/Views/Root/index.js index a3e419a5630..c31d2a59c81 100644 --- a/app/components/Views/Root/index.js +++ b/app/components/Views/Root/index.js @@ -5,7 +5,6 @@ import { PersistGate } from 'redux-persist/lib/integration/react'; import { store, persistor } from '../../../store/'; import App from '../../Nav/App'; -import FoxScreen from '../../UI/FoxScreen'; import SecureKeychain from '../../../core/SecureKeychain'; /** @@ -20,7 +19,7 @@ export default class Root extends Component { render = () => ( - } persistor={persistor}> + diff --git a/app/components/Views/Send/index.js b/app/components/Views/Send/index.js index cbd6047bfb6..8f64315b51d 100644 --- a/app/components/Views/Send/index.js +++ b/app/components/Views/Send/index.js @@ -4,13 +4,23 @@ import { InteractionManager, SafeAreaView, ActivityIndicator, Alert, StyleSheet, import { colors } from '../../../styles/common'; import Engine from '../../../core/Engine'; import TransactionEditor from '../../UI/TransactionEditor'; -import { toBN, BNToHex, hexToBN, fromWei } from '../../../util/number'; +import { toBN, BNToHex, hexToBN, fromWei, toTokenMinimalUnit, renderFromTokenMinimalUnit } from '../../../util/number'; import { toChecksumAddress } from 'ethereumjs-util'; import { strings } from '../../../../locales/i18n'; import { getTransactionOptionsTitle } from '../../UI/Navbar'; import { connect } from 'react-redux'; import { newTransaction, setTransactionObject } from '../../../actions/transaction'; import TransactionsNotificationManager from '../../../core/TransactionsNotificationManager'; +import NetworkList, { getNetworkTypeById } from '../../../util/networks'; +import contractMap from 'eth-contract-metadata'; +import { showAlert } from '../../../actions/alert'; +import Analytics from '../../../core/Analytics'; +import ANALYTICS_EVENT_OPTS from '../../../util/analytics'; +import { getTransactionReviewActionKey } from '../../../util/transactions'; + +const REVIEW = 'review'; +const EDIT = 'edit'; +const SEND = 'Send'; const styles = StyleSheet.create({ wrapper: { @@ -29,8 +39,7 @@ const styles = StyleSheet.create({ * View that wraps the wraps the "Send" screen */ class Send extends Component { - static navigationOptions = ({ navigation }) => - getTransactionOptionsTitle('send.title', strings('navigation.cancel'), navigation); + static navigationOptions = ({ navigation }) => getTransactionOptionsTitle('send.title', navigation); static propTypes = { /** @@ -41,18 +50,34 @@ class Send extends Component { * Action that cleans transaction state */ newTransaction: PropTypes.func.isRequired, + /** + * A string representing the network name + */ + networkType: PropTypes.string, /** * Action that sets transaction attributes from object to a transaction */ setTransactionObject: PropTypes.func.isRequired, + /** + * Array of ERC20 assets + */ + tokens: PropTypes.array, /** * Transaction state */ - transaction: PropTypes.object.isRequired + transaction: PropTypes.object.isRequired, + /** + /* Triggers global alert + */ + showAlert: PropTypes.func, + /** + * Map representing the address book + */ + addressBook: PropTypes.array }; state = { - mode: 'edit', + mode: EDIT, transactionKey: undefined, ready: false, transactionConfirmed: false, @@ -66,13 +91,14 @@ class Send extends Component { * Resets gas and gasPrice of transaction, passing state to 'edit' */ async reset() { - const { transaction } = this.props; + const { transaction, navigation } = this.props; const { gas, gasPrice } = await Engine.context.TransactionController.estimateGas(transaction); this.props.setTransactionObject({ gas: hexToBN(gas), gasPrice: hexToBN(gasPrice) }); - return this.mounted && this.setState({ mode: 'edit', transactionKey: Date.now() }); + navigation && navigation.setParams({ mode: EDIT }); + return this.mounted && this.setState({ mode: EDIT, transactionKey: Date.now() }); } /** @@ -93,6 +119,9 @@ class Send extends Component { this.props.newTransaction(); }; + /** + * Check if view is called with txMeta object for a deeplink + */ checkForDeeplinks() { const { navigation } = this.props; const txMeta = navigation && navigation.getParam('txMeta', null); @@ -107,6 +136,8 @@ class Send extends Component { * Sets state mounted to true, resets transaction and check for deeplinks */ async componentDidMount() { + const { navigation } = this.props; + navigation && navigation.setParams({ mode: EDIT, dispatch: this.onModeChange }); this.mounted = true; await this.reset(); this.checkForDeeplinks(); @@ -141,21 +172,49 @@ class Send extends Component { } } + /** + * Handle txMeta object, setting neccesary state to make a transaction + */ handleNewTxMeta = async ({ target_address, - chain_id = null, // eslint-disable-line no-unused-vars + action, + chain_id = null, function_name = null, // eslint-disable-line no-unused-vars parameters = null }) => { - const newTxMeta = { symbol: 'ETH', assetType: 'ETH', type: 'ETHER_TRANSACTION' }; - newTxMeta.to = toChecksumAddress(target_address); + if (chain_id) { + this.handleNetworkSwitch(chain_id); + } + let newTxMeta; + switch (action) { + case 'send-eth': + newTxMeta = { symbol: 'ETH', assetType: 'ETH', type: 'ETHER_TRANSACTION' }; + newTxMeta.to = toChecksumAddress(target_address); + if (parameters && parameters.value) { + newTxMeta.value = toBN(parameters.value); + newTxMeta.readableValue = fromWei(newTxMeta.value); + } + break; + case 'send-token': { + const selectedAsset = await this.handleTokenDeeplink(target_address); + newTxMeta = { + assetType: 'ERC20', + type: 'INDIVIDUAL_TOKEN_TRANSACTION', + selectedAsset + }; + newTxMeta.to = toChecksumAddress(parameters.address); + if (parameters && parameters.uint256) { + newTxMeta.value = toTokenMinimalUnit(parameters.uint256, selectedAsset.decimals); + newTxMeta.readableValue = String( + renderFromTokenMinimalUnit(newTxMeta.value, selectedAsset.decimals) + ); + } + break; + } + } if (parameters) { - const { value, gas, gasPrice } = parameters; - if (value) { - newTxMeta.value = toBN(value); - newTxMeta.readableValue = fromWei(newTxMeta.value); - } + const { gas, gasPrice } = parameters; if (gas) { newTxMeta.gas = toBN(gas); } @@ -177,7 +236,71 @@ class Send extends Component { } this.props.setTransactionObject(newTxMeta); - this.mounted && this.setState({ ready: true, mode: 'edit', transactionKey: Date.now() }); + this.mounted && this.setState({ ready: true, mode: EDIT, transactionKey: Date.now() }); + }; + + /** + * Method in charge of changing network if is needed + * + * @param chainId - Corresponding id for network + */ + handleNetworkSwitch = chainId => { + const { networkType } = this.props; + const newNetworkType = getNetworkTypeById(chainId); + if (newNetworkType && networkType !== newNetworkType) { + const { NetworkController, CurrencyRateController } = Engine.context; + CurrencyRateController.configure({ nativeCurrency: 'ETH' }); + NetworkController.setProviderType(newNetworkType); + this.props.showAlert({ + isVisible: true, + autodismiss: 5000, + content: 'clipboard-alert', + data: { msg: strings('send.warn_network_change') + NetworkList[newNetworkType].name } + }); + } + }; + + /** + * Retrieves ERC20 asset information (symbol and decimals) to be used with deeplinks + * + * @param address - Corresponding ERC20 asset address + * + * @returns ERC20 asset, containing address, symbol and decimals + */ + handleTokenDeeplink = async address => { + const { tokens } = this.props; + address = toChecksumAddress(address); + // First check if we have token information in contractMap + if (address in contractMap) { + return contractMap[address]; + } + // Then check if the token is already in state + const stateToken = tokens.find(token => token.address === address); + if (stateToken) { + return stateToken; + } + // Finally try to query the contract + const { AssetsContractController } = Engine.context; + const token = { address }; + try { + const decimals = await AssetsContractController.getTokenDecimals(address); + token.decimals = parseInt(String(decimals)); + } catch (e) { + // Drop tx since we don't have any form to get decimals and send the correct tx + this.props.showAlert({ + isVisible: true, + autodismiss: 2000, + content: 'clipboard-alert', + data: { msg: strings(`send.deeplink_failure`) } + }); + this.onCancel(); + } + try { + token.symbol = await AssetsContractController.getAssetSymbol(address); + } catch (e) { + token.symbol = 'ERC20'; + } + return token; }; /** @@ -227,6 +350,7 @@ class Send extends Component { Engine.context.TransactionController.cancelTransaction(id); this.props.navigation.pop(); this.unmountHandled = true; + this.state.mode === REVIEW && this.trackOnCancel(); }; /** @@ -236,10 +360,11 @@ class Send extends Component { * and returns to edit transaction */ onConfirm = async () => { - const { TransactionController } = Engine.context; + const { TransactionController, AddressBookController } = Engine.context; this.setState({ transactionConfirmed: true }); const { - transaction: { selectedAsset, assetType } + transaction: { selectedAsset, assetType }, + addressBook } = this.props; let { transaction } = this.props; try { @@ -249,23 +374,114 @@ class Send extends Component { transaction = this.prepareAssetTransaction(transaction, selectedAsset); } const { result, transactionMeta } = await TransactionController.addTransaction(transaction); + await TransactionController.approveTransaction(transactionMeta.id); - await result; + + // Add to the AddressBook if it's an unkonwn address + const checksummedAddress = toChecksumAddress(transactionMeta.transaction.to); + const existingContact = addressBook.find( + ({ address }) => toChecksumAddress(address) === checksummedAddress + ); + if (!existingContact) { + AddressBookController.set(checksummedAddress, ''); + } + + await new Promise(resolve => { + resolve(result); + }); + if (transactionMeta.error) { + throw transactionMeta.error; + } this.removeCollectible(); this.setState({ transactionConfirmed: false, transactionSubmitted: true }); this.props.navigation.pop(); InteractionManager.runAfterInteractions(() => { - TransactionsNotificationManager.watchSubmittedTransaction(transactionMeta); + TransactionsNotificationManager.watchSubmittedTransaction({ + ...transactionMeta, + assetType: transaction.assetType + }); }); } catch (error) { - Alert.alert('Transaction error', JSON.stringify(error), [{ text: 'OK' }]); + Alert.alert('Transaction error', error && error.message, [{ text: 'OK' }]); this.setState({ transactionConfirmed: false }); await this.reset(); } + InteractionManager.runAfterInteractions(() => { + this.trackOnConfirm(); + }); + }; + + /** + * Call Analytics to track confirm started event for send screen + */ + trackConfirmScreen = () => { + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.TRANSACTIONS_CONFIRM_STARTED, this.getTrackingParams()); }; + /** + * Call Analytics to track confirm started event for send screen + */ + trackEditScreen = async () => { + const { transaction } = this.props; + const actionKey = await getTransactionReviewActionKey(transaction); + Analytics.trackEventWithParameters(ANALYTICS_EVENT_OPTS.TRANSACTIONS_EDIT_TRANSACTION, { + ...this.getTrackingParams(), + actionKey + }); + }; + + /** + * Call Analytics to track cancel pressed + */ + trackOnCancel = () => { + Analytics.trackEventWithParameters( + ANALYTICS_EVENT_OPTS.TRANSACTIONS_CANCEL_TRANSACTION, + this.getTrackingParams() + ); + }; + + /** + * Call Analytics to track confirm pressed + */ + trackOnConfirm = () => { + Analytics.trackEventWithParameters( + ANALYTICS_EVENT_OPTS.TRANSACTIONS_COMPLETED_TRANSACTION, + this.getTrackingParams() + ); + }; + + /** + * Returns corresponding tracking params to send + * + * @return {object} - Object containing view, network, activeCurrency and assetType + */ + getTrackingParams = () => { + const { + networkType, + transaction: { selectedAsset, assetType } + } = this.props; + return { + view: SEND, + network: networkType, + activeCurrency: selectedAsset.symbol || selectedAsset.contractName, + assetType + }; + }; + + /** + * Change transaction mode + * If changed to 'review' sends an Analytics track event + * + * @param mode - Transaction mode, review or edit + */ onModeChange = mode => { + const { navigation } = this.props; + navigation && navigation.setParams({ mode }); this.mounted && this.setState({ mode }); + InteractionManager.runAfterInteractions(() => { + mode === REVIEW && this.trackConfirmScreen(); + mode === EDIT && this.trackEditScreen(); + }); }; renderLoader() { @@ -295,14 +511,18 @@ class Send extends Component { } const mapStateToProps = state => ({ + addressBook: state.engine.backgroundState.AddressBookController.addressBook, accounts: state.engine.backgroundState.AccountTrackerController.accounts, contractBalances: state.engine.backgroundState.TokenBalancesController.contractBalances, - transaction: state.transaction + transaction: state.transaction, + networkType: state.engine.backgroundState.NetworkController.provider.type, + tokens: state.engine.backgroundState.AssetsController.tokens }); const mapDispatchToProps = dispatch => ({ newTransaction: () => dispatch(newTransaction()), - setTransactionObject: transaction => dispatch(setTransactionObject(transaction)) + setTransactionObject: transaction => dispatch(setTransactionObject(transaction)), + showAlert: config => dispatch(showAlert(config)) }); export default connect( diff --git a/app/components/Views/AdvancedSettings/__snapshots__/index.test.js.snap b/app/components/Views/Settings/AdvancedSettings/__snapshots__/index.test.js.snap similarity index 77% rename from app/components/Views/AdvancedSettings/__snapshots__/index.test.js.snap rename to app/components/Views/Settings/AdvancedSettings/__snapshots__/index.test.js.snap index fa92f98e0ca..d04d06dc80f 100644 --- a/app/components/Views/AdvancedSettings/__snapshots__/index.test.js.snap +++ b/app/components/Views/Settings/AdvancedSettings/__snapshots__/index.test.js.snap @@ -116,7 +116,7 @@ exports[`AdvancedSettings should render correctly 1`] = ` - New RPC Network + IPFS Gateway - Use a custom RPC-capable network via URL instead of one of the provided networks. + Choose your preferred IPFS gateway. - - - -