diff options
Diffstat (limited to 'ui')
-rw-r--r-- | ui/classic/.gitignore (renamed from ui/.gitignore) | 0 | ||||
-rw-r--r-- | ui/classic/app/account-detail.js (renamed from ui/app/account-detail.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/accounts/account-list-item.js (renamed from ui/app/accounts/account-list-item.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/accounts/import/index.js (renamed from ui/app/accounts/import/index.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/accounts/import/json.js (renamed from ui/app/accounts/import/json.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/accounts/import/private-key.js (renamed from ui/app/accounts/import/private-key.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/accounts/import/seed.js (renamed from ui/app/accounts/import/seed.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/accounts/index.js (renamed from ui/app/accounts/index.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/actions.js (renamed from ui/app/actions.js) | 2 | ||||
-rw-r--r-- | ui/classic/app/add-token.js (renamed from ui/app/add-token.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/app.js (renamed from ui/app/app.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/account-export.js (renamed from ui/app/components/account-export.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/account-info-link.js (renamed from ui/app/components/account-info-link.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/account-panel.js (renamed from ui/app/components/account-panel.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/balance.js (renamed from ui/app/components/balance.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/binary-renderer.js (renamed from ui/app/components/binary-renderer.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/bn-as-decimal-input.js (renamed from ui/app/components/bn-as-decimal-input.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/buy-button-subview.js (renamed from ui/app/components/buy-button-subview.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/coinbase-form.js (renamed from ui/app/components/coinbase-form.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/copyButton.js (renamed from ui/app/components/copyButton.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/copyable.js (renamed from ui/app/components/copyable.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/custom-radio-list.js (renamed from ui/app/components/custom-radio-list.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/drop-menu-item.js (renamed from ui/app/components/drop-menu-item.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/editable-label.js (renamed from ui/app/components/editable-label.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/ens-input.js (renamed from ui/app/components/ens-input.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/eth-balance.js (renamed from ui/app/components/eth-balance.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/fiat-value.js (renamed from ui/app/components/fiat-value.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/hex-as-decimal-input.js (renamed from ui/app/components/hex-as-decimal-input.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/identicon.js (renamed from ui/app/components/identicon.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/loading.js (renamed from ui/app/components/loading.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/mascot.js (renamed from ui/app/components/mascot.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/mini-account-panel.js (renamed from ui/app/components/mini-account-panel.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/network.js (renamed from ui/app/components/network.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/notice.js (renamed from ui/app/components/notice.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/pending-msg-details.js (renamed from ui/app/components/pending-msg-details.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/pending-msg.js (renamed from ui/app/components/pending-msg.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/pending-personal-msg-details.js (renamed from ui/app/components/pending-personal-msg-details.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/pending-personal-msg.js (renamed from ui/app/components/pending-personal-msg.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/pending-tx.js (renamed from ui/app/components/pending-tx.js) | 2 | ||||
-rw-r--r-- | ui/classic/app/components/qr-code.js (renamed from ui/app/components/qr-code.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/range-slider.js (renamed from ui/app/components/range-slider.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/shapeshift-form.js (renamed from ui/app/components/shapeshift-form.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/shift-list-item.js (renamed from ui/app/components/shift-list-item.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/tab-bar.js (renamed from ui/app/components/tab-bar.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/template.js (renamed from ui/app/components/template.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/token-cell.js (renamed from ui/app/components/token-cell.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/token-list.js (renamed from ui/app/components/token-list.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/tooltip.js (renamed from ui/app/components/tooltip.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/transaction-list-item-icon.js (renamed from ui/app/components/transaction-list-item-icon.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/transaction-list-item.js (renamed from ui/app/components/transaction-list-item.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/components/transaction-list.js (renamed from ui/app/components/transaction-list.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/conf-tx.js (renamed from ui/app/conf-tx.js) | 2 | ||||
-rw-r--r-- | ui/classic/app/config.js (renamed from ui/app/config.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/conversion.json (renamed from ui/app/conversion.json) | 0 | ||||
-rw-r--r-- | ui/classic/app/css/debug.css (renamed from ui/app/css/debug.css) | 0 | ||||
-rw-r--r-- | ui/classic/app/css/fonts.css (renamed from ui/app/css/fonts.css) | 0 | ||||
-rw-r--r-- | ui/classic/app/css/index.css (renamed from ui/app/css/index.css) | 0 | ||||
-rw-r--r-- | ui/classic/app/css/lib.css (renamed from ui/app/css/lib.css) | 0 | ||||
-rw-r--r-- | ui/classic/app/css/reset.css (renamed from ui/app/css/reset.css) | 0 | ||||
-rw-r--r-- | ui/classic/app/css/transitions.css (renamed from ui/app/css/transitions.css) | 0 | ||||
-rw-r--r-- | ui/classic/app/first-time/init-menu.js (renamed from ui/app/first-time/init-menu.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/img/identicon-tardigrade.png (renamed from ui/app/img/identicon-tardigrade.png) | bin | 141119 -> 141119 bytes | |||
-rw-r--r-- | ui/classic/app/img/identicon-walrus.png (renamed from ui/app/img/identicon-walrus.png) | bin | 388973 -> 388973 bytes | |||
-rw-r--r-- | ui/classic/app/info.js (renamed from ui/app/info.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/keychains/hd/create-vault-complete.js (renamed from ui/app/keychains/hd/create-vault-complete.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/keychains/hd/recover-seed/confirmation.js (renamed from ui/app/keychains/hd/recover-seed/confirmation.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/keychains/hd/restore-vault.js (renamed from ui/app/keychains/hd/restore-vault.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/new-keychain.js (renamed from ui/app/new-keychain.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/reducers.js (renamed from ui/app/reducers.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/reducers/app.js (renamed from ui/app/reducers/app.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/reducers/identities.js (renamed from ui/app/reducers/identities.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/reducers/metamask.js (renamed from ui/app/reducers/metamask.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/root.js (renamed from ui/app/root.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/send.js (renamed from ui/app/send.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/settings.js (renamed from ui/app/settings.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/store.js (renamed from ui/app/store.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/template.js (renamed from ui/app/template.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/unlock.js (renamed from ui/app/unlock.js) | 0 | ||||
-rw-r--r-- | ui/classic/app/util.js (renamed from ui/app/util.js) | 0 | ||||
-rw-r--r-- | ui/classic/css.js (renamed from ui/css.js) | 4 | ||||
-rw-r--r-- | ui/classic/design/00-metamask-SignIn.jpg (renamed from ui/design/00-metamask-SignIn.jpg) | bin | 57848 -> 57848 bytes | |||
-rw-r--r-- | ui/classic/design/01-metamask-SelectAcc.jpg (renamed from ui/design/01-metamask-SelectAcc.jpg) | bin | 76063 -> 76063 bytes | |||
-rw-r--r-- | ui/classic/design/02-metamask-AccDetails.jpg (renamed from ui/design/02-metamask-AccDetails.jpg) | bin | 75780 -> 75780 bytes | |||
-rw-r--r-- | ui/classic/design/02a-metamask-AccDetails-OverToken.jpg (renamed from ui/design/02a-metamask-AccDetails-OverToken.jpg) | bin | 121847 -> 121847 bytes | |||
-rw-r--r-- | ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg (renamed from ui/design/02a-metamask-AccDetails-OverTransaction.jpg) | bin | 122075 -> 122075 bytes | |||
-rw-r--r-- | ui/classic/design/02a-metamask-AccDetails.jpg (renamed from ui/design/02a-metamask-AccDetails.jpg) | bin | 117570 -> 117570 bytes | |||
-rw-r--r-- | ui/classic/design/02b-metamask-AccDetails-Send.jpg (renamed from ui/design/02b-metamask-AccDetails-Send.jpg) | bin | 110143 -> 110143 bytes | |||
-rw-r--r-- | ui/classic/design/03-metamask-Qr.jpg (renamed from ui/design/03-metamask-Qr.jpg) | bin | 66052 -> 66052 bytes | |||
-rw-r--r-- | ui/classic/design/05-metamask-Menu.jpg (renamed from ui/design/05-metamask-Menu.jpg) | bin | 130264 -> 130264 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/final_screen_dao_accounts.png (renamed from ui/design/chromeStorePics/final_screen_dao_accounts.png) | bin | 249708 -> 249708 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/final_screen_dao_locked.png (renamed from ui/design/chromeStorePics/final_screen_dao_locked.png) | bin | 220295 -> 220295 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/final_screen_dao_notification.png (renamed from ui/design/chromeStorePics/final_screen_dao_notification.png) | bin | 214405 -> 214405 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/final_screen_wei_account.png (renamed from ui/design/chromeStorePics/final_screen_wei_account.png) | bin | 253382 -> 253382 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/final_screen_wei_notification.png (renamed from ui/design/chromeStorePics/final_screen_wei_notification.png) | bin | 193865 -> 193865 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/icon-128.png (renamed from ui/design/chromeStorePics/icon-128.png) | bin | 5770 -> 5770 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/icon-64.png (renamed from ui/design/chromeStorePics/icon-64.png) | bin | 3573 -> 3573 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/metamask_icon.ai (renamed from ui/design/chromeStorePics/metamask_icon.ai) | 0 | ||||
-rw-r--r-- | ui/classic/design/chromeStorePics/promo1400560.png (renamed from ui/design/chromeStorePics/promo1400560.png) | bin | 261644 -> 261644 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/promo440280.png (renamed from ui/design/chromeStorePics/promo440280.png) | bin | 57471 -> 57471 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/promo920680.png (renamed from ui/design/chromeStorePics/promo920680.png) | bin | 206713 -> 206713 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/screen_dao_accounts.png (renamed from ui/design/chromeStorePics/screen_dao_accounts.png) | bin | 517598 -> 517598 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/screen_dao_locked.png (renamed from ui/design/chromeStorePics/screen_dao_locked.png) | bin | 287108 -> 287108 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/screen_dao_notification.png (renamed from ui/design/chromeStorePics/screen_dao_notification.png) | bin | 296498 -> 296498 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/screen_wei_account.png (renamed from ui/design/chromeStorePics/screen_wei_account.png) | bin | 653633 -> 653633 bytes | |||
-rw-r--r-- | ui/classic/design/chromeStorePics/screen_wei_notification.png (renamed from ui/design/chromeStorePics/screen_wei_notification.png) | bin | 402486 -> 402486 bytes | |||
-rw-r--r-- | ui/classic/design/metamask-logo-eyes.png (renamed from ui/design/metamask-logo-eyes.png) | bin | 146076 -> 146076 bytes | |||
-rw-r--r-- | ui/classic/design/wireframes/1st_time_use.png (renamed from ui/design/wireframes/1st_time_use.png) | bin | 937556 -> 937556 bytes | |||
-rw-r--r-- | ui/classic/design/wireframes/metamask_wfs_jan_13.pdf (renamed from ui/design/wireframes/metamask_wfs_jan_13.pdf) | bin | 452413 -> 452413 bytes | |||
-rw-r--r-- | ui/classic/design/wireframes/metamask_wfs_jan_13.png (renamed from ui/design/wireframes/metamask_wfs_jan_13.png) | bin | 419066 -> 419066 bytes | |||
-rw-r--r-- | ui/classic/design/wireframes/metamask_wfs_jan_18.pdf (renamed from ui/design/wireframes/metamask_wfs_jan_18.pdf) | bin | 612778 -> 612778 bytes | |||
-rw-r--r-- | ui/classic/example.js (renamed from ui/example.js) | 0 | ||||
-rw-r--r-- | ui/classic/index.html (renamed from ui/index.html) | 0 | ||||
-rw-r--r-- | ui/classic/index.js (renamed from ui/index.js) | 0 | ||||
-rw-r--r-- | ui/classic/lib/account-link.js (renamed from ui/lib/account-link.js) | 0 | ||||
-rw-r--r-- | ui/classic/lib/contract-namer.js (renamed from ui/lib/contract-namer.js) | 0 | ||||
-rw-r--r-- | ui/classic/lib/etherscan-prefix-for-network.js (renamed from ui/lib/etherscan-prefix-for-network.js) | 0 | ||||
-rw-r--r-- | ui/classic/lib/explorer-link.js (renamed from ui/lib/explorer-link.js) | 0 | ||||
-rw-r--r-- | ui/classic/lib/icon-factory.js (renamed from ui/lib/icon-factory.js) | 0 | ||||
-rw-r--r-- | ui/classic/lib/lost-accounts-notice.js (renamed from ui/lib/lost-accounts-notice.js) | 0 | ||||
-rw-r--r-- | ui/classic/lib/persistent-form.js (renamed from ui/lib/persistent-form.js) | 0 | ||||
-rw-r--r-- | ui/classic/lib/tx-helper.js (renamed from ui/lib/tx-helper.js) | 0 | ||||
-rw-r--r-- | ui/responsive/.gitignore | 66 | ||||
-rw-r--r-- | ui/responsive/app/account-detail.js | 312 | ||||
-rw-r--r-- | ui/responsive/app/accounts/account-list-item.js | 91 | ||||
-rw-r--r-- | ui/responsive/app/accounts/import/index.js | 100 | ||||
-rw-r--r-- | ui/responsive/app/accounts/import/json.js | 100 | ||||
-rw-r--r-- | ui/responsive/app/accounts/import/private-key.js | 67 | ||||
-rw-r--r-- | ui/responsive/app/accounts/import/seed.js | 30 | ||||
-rw-r--r-- | ui/responsive/app/accounts/index.js | 163 | ||||
-rw-r--r-- | ui/responsive/app/actions.js | 1031 | ||||
-rw-r--r-- | ui/responsive/app/add-token.js | 219 | ||||
-rw-r--r-- | ui/responsive/app/app.js | 586 | ||||
-rw-r--r-- | ui/responsive/app/components/account-export.js | 122 | ||||
-rw-r--r-- | ui/responsive/app/components/account-info-link.js | 41 | ||||
-rw-r--r-- | ui/responsive/app/components/account-panel.js | 86 | ||||
-rw-r--r-- | ui/responsive/app/components/balance.js | 89 | ||||
-rw-r--r-- | ui/responsive/app/components/binary-renderer.js | 46 | ||||
-rw-r--r-- | ui/responsive/app/components/bn-as-decimal-input.js | 174 | ||||
-rw-r--r-- | ui/responsive/app/components/buy-button-subview.js | 197 | ||||
-rw-r--r-- | ui/responsive/app/components/coinbase-form.js | 63 | ||||
-rw-r--r-- | ui/responsive/app/components/copyButton.js | 59 | ||||
-rw-r--r-- | ui/responsive/app/components/copyable.js | 46 | ||||
-rw-r--r-- | ui/responsive/app/components/custom-radio-list.js | 60 | ||||
-rw-r--r-- | ui/responsive/app/components/drop-menu-item.js | 59 | ||||
-rw-r--r-- | ui/responsive/app/components/editable-label.js | 51 | ||||
-rw-r--r-- | ui/responsive/app/components/ens-input.js | 170 | ||||
-rw-r--r-- | ui/responsive/app/components/eth-balance.js | 89 | ||||
-rw-r--r-- | ui/responsive/app/components/fiat-value.js | 63 | ||||
-rw-r--r-- | ui/responsive/app/components/hex-as-decimal-input.js | 154 | ||||
-rw-r--r-- | ui/responsive/app/components/identicon.js | 72 | ||||
-rw-r--r-- | ui/responsive/app/components/loading.js | 53 | ||||
-rw-r--r-- | ui/responsive/app/components/mascot.js | 59 | ||||
-rw-r--r-- | ui/responsive/app/components/mini-account-panel.js | 74 | ||||
-rw-r--r-- | ui/responsive/app/components/network.js | 125 | ||||
-rw-r--r-- | ui/responsive/app/components/notice.js | 126 | ||||
-rw-r--r-- | ui/responsive/app/components/pending-msg-details.js | 50 | ||||
-rw-r--r-- | ui/responsive/app/components/pending-msg.js | 56 | ||||
-rw-r--r-- | ui/responsive/app/components/pending-personal-msg-details.js | 60 | ||||
-rw-r--r-- | ui/responsive/app/components/pending-personal-msg.js | 47 | ||||
-rw-r--r-- | ui/responsive/app/components/pending-tx.js | 480 | ||||
-rw-r--r-- | ui/responsive/app/components/qr-code.js | 79 | ||||
-rw-r--r-- | ui/responsive/app/components/range-slider.js | 58 | ||||
-rw-r--r-- | ui/responsive/app/components/shapeshift-form.js | 306 | ||||
-rw-r--r-- | ui/responsive/app/components/shift-list-item.js | 204 | ||||
-rw-r--r-- | ui/responsive/app/components/tab-bar.js | 36 | ||||
-rw-r--r-- | ui/responsive/app/components/template.js | 18 | ||||
-rw-r--r-- | ui/responsive/app/components/token-cell.js | 72 | ||||
-rw-r--r-- | ui/responsive/app/components/token-list.js | 192 | ||||
-rw-r--r-- | ui/responsive/app/components/tooltip.js | 22 | ||||
-rw-r--r-- | ui/responsive/app/components/transaction-list-item-icon.js | 68 | ||||
-rw-r--r-- | ui/responsive/app/components/transaction-list-item.js | 165 | ||||
-rw-r--r-- | ui/responsive/app/components/transaction-list.js | 79 | ||||
-rw-r--r-- | ui/responsive/app/conf-tx.js | 213 | ||||
-rw-r--r-- | ui/responsive/app/config.js | 211 | ||||
-rw-r--r-- | ui/responsive/app/conversion.json | 207 | ||||
-rw-r--r-- | ui/responsive/app/css/debug.css | 21 | ||||
-rw-r--r-- | ui/responsive/app/css/fonts.css | 36 | ||||
-rw-r--r-- | ui/responsive/app/css/index.css | 674 | ||||
-rw-r--r-- | ui/responsive/app/css/lib.css | 268 | ||||
-rw-r--r-- | ui/responsive/app/css/reset.css | 48 | ||||
-rw-r--r-- | ui/responsive/app/css/transitions.css | 42 | ||||
-rw-r--r-- | ui/responsive/app/first-time/init-menu.js | 179 | ||||
-rw-r--r-- | ui/responsive/app/img/identicon-tardigrade.png | bin | 0 -> 141119 bytes | |||
-rw-r--r-- | ui/responsive/app/img/identicon-walrus.png | bin | 0 -> 388973 bytes | |||
-rw-r--r-- | ui/responsive/app/info.js | 154 | ||||
-rw-r--r-- | ui/responsive/app/keychains/hd/create-vault-complete.js | 76 | ||||
-rw-r--r-- | ui/responsive/app/keychains/hd/recover-seed/confirmation.js | 118 | ||||
-rw-r--r-- | ui/responsive/app/keychains/hd/restore-vault.js | 152 | ||||
-rw-r--r-- | ui/responsive/app/new-keychain.js | 29 | ||||
-rw-r--r-- | ui/responsive/app/reducers.js | 52 | ||||
-rw-r--r-- | ui/responsive/app/reducers/app.js | 585 | ||||
-rw-r--r-- | ui/responsive/app/reducers/identities.js | 15 | ||||
-rw-r--r-- | ui/responsive/app/reducers/metamask.js | 137 | ||||
-rw-r--r-- | ui/responsive/app/root.js | 22 | ||||
-rw-r--r-- | ui/responsive/app/send.js | 288 | ||||
-rw-r--r-- | ui/responsive/app/settings.js | 59 | ||||
-rw-r--r-- | ui/responsive/app/store.js | 21 | ||||
-rw-r--r-- | ui/responsive/app/template.js | 30 | ||||
-rw-r--r-- | ui/responsive/app/unlock.js | 118 | ||||
-rw-r--r-- | ui/responsive/app/util.js | 217 | ||||
-rw-r--r-- | ui/responsive/css.js | 29 | ||||
-rw-r--r-- | ui/responsive/design/00-metamask-SignIn.jpg | bin | 0 -> 57848 bytes | |||
-rw-r--r-- | ui/responsive/design/01-metamask-SelectAcc.jpg | bin | 0 -> 76063 bytes | |||
-rw-r--r-- | ui/responsive/design/02-metamask-AccDetails.jpg | bin | 0 -> 75780 bytes | |||
-rw-r--r-- | ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg | bin | 0 -> 121847 bytes | |||
-rw-r--r-- | ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg | bin | 0 -> 122075 bytes | |||
-rw-r--r-- | ui/responsive/design/02a-metamask-AccDetails.jpg | bin | 0 -> 117570 bytes | |||
-rw-r--r-- | ui/responsive/design/02b-metamask-AccDetails-Send.jpg | bin | 0 -> 110143 bytes | |||
-rw-r--r-- | ui/responsive/design/03-metamask-Qr.jpg | bin | 0 -> 66052 bytes | |||
-rw-r--r-- | ui/responsive/design/05-metamask-Menu.jpg | bin | 0 -> 130264 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png | bin | 0 -> 249708 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/final_screen_dao_locked.png | bin | 0 -> 220295 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/final_screen_dao_notification.png | bin | 0 -> 214405 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/final_screen_wei_account.png | bin | 0 -> 253382 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/final_screen_wei_notification.png | bin | 0 -> 193865 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/icon-128.png | bin | 0 -> 5770 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/icon-64.png | bin | 0 -> 3573 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/metamask_icon.ai | 2383 | ||||
-rw-r--r-- | ui/responsive/design/chromeStorePics/promo1400560.png | bin | 0 -> 261644 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/promo440280.png | bin | 0 -> 57471 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/promo920680.png | bin | 0 -> 206713 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/screen_dao_accounts.png | bin | 0 -> 517598 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/screen_dao_locked.png | bin | 0 -> 287108 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/screen_dao_notification.png | bin | 0 -> 296498 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/screen_wei_account.png | bin | 0 -> 653633 bytes | |||
-rw-r--r-- | ui/responsive/design/chromeStorePics/screen_wei_notification.png | bin | 0 -> 402486 bytes | |||
-rw-r--r-- | ui/responsive/design/metamask-logo-eyes.png | bin | 0 -> 146076 bytes | |||
-rw-r--r-- | ui/responsive/design/wireframes/1st_time_use.png | bin | 0 -> 937556 bytes | |||
-rw-r--r-- | ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf | bin | 0 -> 452413 bytes | |||
-rw-r--r-- | ui/responsive/design/wireframes/metamask_wfs_jan_13.png | bin | 0 -> 419066 bytes | |||
-rw-r--r-- | ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf | bin | 0 -> 612778 bytes | |||
-rw-r--r-- | ui/responsive/example.js | 123 | ||||
-rw-r--r-- | ui/responsive/index.html | 20 | ||||
-rw-r--r-- | ui/responsive/index.js | 58 | ||||
-rw-r--r-- | ui/responsive/lib/account-link.js | 26 | ||||
-rw-r--r-- | ui/responsive/lib/contract-namer.js | 33 | ||||
-rw-r--r-- | ui/responsive/lib/etherscan-prefix-for-network.js | 21 | ||||
-rw-r--r-- | ui/responsive/lib/explorer-link.js | 6 | ||||
-rw-r--r-- | ui/responsive/lib/icon-factory.js | 65 | ||||
-rw-r--r-- | ui/responsive/lib/lost-accounts-notice.js | 23 | ||||
-rw-r--r-- | ui/responsive/lib/persistent-form.js | 61 | ||||
-rw-r--r-- | ui/responsive/lib/tx-helper.js | 17 |
242 files changed, 13677 insertions, 5 deletions
diff --git a/ui/.gitignore b/ui/classic/.gitignore index c6b1254b5..c6b1254b5 100644 --- a/ui/.gitignore +++ b/ui/classic/.gitignore diff --git a/ui/app/account-detail.js b/ui/classic/app/account-detail.js index bed05a7fb..bed05a7fb 100644 --- a/ui/app/account-detail.js +++ b/ui/classic/app/account-detail.js diff --git a/ui/app/accounts/account-list-item.js b/ui/classic/app/accounts/account-list-item.js index 10a0b6cc7..10a0b6cc7 100644 --- a/ui/app/accounts/account-list-item.js +++ b/ui/classic/app/accounts/account-list-item.js diff --git a/ui/app/accounts/import/index.js b/ui/classic/app/accounts/import/index.js index 97b387229..97b387229 100644 --- a/ui/app/accounts/import/index.js +++ b/ui/classic/app/accounts/import/index.js diff --git a/ui/app/accounts/import/json.js b/ui/classic/app/accounts/import/json.js index 158a3c923..158a3c923 100644 --- a/ui/app/accounts/import/json.js +++ b/ui/classic/app/accounts/import/json.js diff --git a/ui/app/accounts/import/private-key.js b/ui/classic/app/accounts/import/private-key.js index 68ccee58e..68ccee58e 100644 --- a/ui/app/accounts/import/private-key.js +++ b/ui/classic/app/accounts/import/private-key.js diff --git a/ui/app/accounts/import/seed.js b/ui/classic/app/accounts/import/seed.js index b4a7c0afa..b4a7c0afa 100644 --- a/ui/app/accounts/import/seed.js +++ b/ui/classic/app/accounts/import/seed.js diff --git a/ui/app/accounts/index.js b/ui/classic/app/accounts/index.js index ac2615cd7..ac2615cd7 100644 --- a/ui/app/accounts/index.js +++ b/ui/classic/app/accounts/index.js diff --git a/ui/app/actions.js b/ui/classic/app/actions.js index d99291e46..2c60448dd 100644 --- a/ui/app/actions.js +++ b/ui/classic/app/actions.js @@ -1,4 +1,4 @@ -const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') +const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') var actions = { _setBackgroundConnection: _setBackgroundConnection, diff --git a/ui/app/add-token.js b/ui/classic/app/add-token.js index b303b5c0d..b303b5c0d 100644 --- a/ui/app/add-token.js +++ b/ui/classic/app/add-token.js diff --git a/ui/app/app.js b/ui/classic/app/app.js index 1a63002e1..1a63002e1 100644 --- a/ui/app/app.js +++ b/ui/classic/app/app.js diff --git a/ui/app/components/account-export.js b/ui/classic/app/components/account-export.js index 394d878f7..394d878f7 100644 --- a/ui/app/components/account-export.js +++ b/ui/classic/app/components/account-export.js diff --git a/ui/app/components/account-info-link.js b/ui/classic/app/components/account-info-link.js index 6526ab502..6526ab502 100644 --- a/ui/app/components/account-info-link.js +++ b/ui/classic/app/components/account-info-link.js diff --git a/ui/app/components/account-panel.js b/ui/classic/app/components/account-panel.js index abaaf8163..abaaf8163 100644 --- a/ui/app/components/account-panel.js +++ b/ui/classic/app/components/account-panel.js diff --git a/ui/app/components/balance.js b/ui/classic/app/components/balance.js index 57ca84564..57ca84564 100644 --- a/ui/app/components/balance.js +++ b/ui/classic/app/components/balance.js diff --git a/ui/app/components/binary-renderer.js b/ui/classic/app/components/binary-renderer.js index 0b6a1f5c2..0b6a1f5c2 100644 --- a/ui/app/components/binary-renderer.js +++ b/ui/classic/app/components/binary-renderer.js diff --git a/ui/app/components/bn-as-decimal-input.js b/ui/classic/app/components/bn-as-decimal-input.js index f3ace4720..f3ace4720 100644 --- a/ui/app/components/bn-as-decimal-input.js +++ b/ui/classic/app/components/bn-as-decimal-input.js diff --git a/ui/app/components/buy-button-subview.js b/ui/classic/app/components/buy-button-subview.js index 87084f92d..87084f92d 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/classic/app/components/buy-button-subview.js diff --git a/ui/app/components/coinbase-form.js b/ui/classic/app/components/coinbase-form.js index f44d86045..f44d86045 100644 --- a/ui/app/components/coinbase-form.js +++ b/ui/classic/app/components/coinbase-form.js diff --git a/ui/app/components/copyButton.js b/ui/classic/app/components/copyButton.js index a25d0719c..a25d0719c 100644 --- a/ui/app/components/copyButton.js +++ b/ui/classic/app/components/copyButton.js diff --git a/ui/app/components/copyable.js b/ui/classic/app/components/copyable.js index a4f6f4bc6..a4f6f4bc6 100644 --- a/ui/app/components/copyable.js +++ b/ui/classic/app/components/copyable.js diff --git a/ui/app/components/custom-radio-list.js b/ui/classic/app/components/custom-radio-list.js index a4c525396..a4c525396 100644 --- a/ui/app/components/custom-radio-list.js +++ b/ui/classic/app/components/custom-radio-list.js diff --git a/ui/app/components/drop-menu-item.js b/ui/classic/app/components/drop-menu-item.js index e42948209..e42948209 100644 --- a/ui/app/components/drop-menu-item.js +++ b/ui/classic/app/components/drop-menu-item.js diff --git a/ui/app/components/editable-label.js b/ui/classic/app/components/editable-label.js index 41936f5e0..41936f5e0 100644 --- a/ui/app/components/editable-label.js +++ b/ui/classic/app/components/editable-label.js diff --git a/ui/app/components/ens-input.js b/ui/classic/app/components/ens-input.js index 3a33ebf74..3a33ebf74 100644 --- a/ui/app/components/ens-input.js +++ b/ui/classic/app/components/ens-input.js diff --git a/ui/app/components/eth-balance.js b/ui/classic/app/components/eth-balance.js index 4f538fd31..4f538fd31 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/classic/app/components/eth-balance.js diff --git a/ui/app/components/fiat-value.js b/ui/classic/app/components/fiat-value.js index 8a64a1cfc..8a64a1cfc 100644 --- a/ui/app/components/fiat-value.js +++ b/ui/classic/app/components/fiat-value.js diff --git a/ui/app/components/hex-as-decimal-input.js b/ui/classic/app/components/hex-as-decimal-input.js index 4a71e9585..4a71e9585 100644 --- a/ui/app/components/hex-as-decimal-input.js +++ b/ui/classic/app/components/hex-as-decimal-input.js diff --git a/ui/app/components/identicon.js b/ui/classic/app/components/identicon.js index c754bc6ba..c754bc6ba 100644 --- a/ui/app/components/identicon.js +++ b/ui/classic/app/components/identicon.js diff --git a/ui/app/components/loading.js b/ui/classic/app/components/loading.js index 87d6f5d20..87d6f5d20 100644 --- a/ui/app/components/loading.js +++ b/ui/classic/app/components/loading.js diff --git a/ui/app/components/mascot.js b/ui/classic/app/components/mascot.js index 973ec2cad..973ec2cad 100644 --- a/ui/app/components/mascot.js +++ b/ui/classic/app/components/mascot.js diff --git a/ui/app/components/mini-account-panel.js b/ui/classic/app/components/mini-account-panel.js index c09cf5b7a..c09cf5b7a 100644 --- a/ui/app/components/mini-account-panel.js +++ b/ui/classic/app/components/mini-account-panel.js diff --git a/ui/app/components/network.js b/ui/classic/app/components/network.js index d5d3e18cd..d5d3e18cd 100644 --- a/ui/app/components/network.js +++ b/ui/classic/app/components/network.js diff --git a/ui/app/components/notice.js b/ui/classic/app/components/notice.js index d9f0067cd..d9f0067cd 100644 --- a/ui/app/components/notice.js +++ b/ui/classic/app/components/notice.js diff --git a/ui/app/components/pending-msg-details.js b/ui/classic/app/components/pending-msg-details.js index 16308d121..16308d121 100644 --- a/ui/app/components/pending-msg-details.js +++ b/ui/classic/app/components/pending-msg-details.js diff --git a/ui/app/components/pending-msg.js b/ui/classic/app/components/pending-msg.js index b2cac164a..b2cac164a 100644 --- a/ui/app/components/pending-msg.js +++ b/ui/classic/app/components/pending-msg.js diff --git a/ui/app/components/pending-personal-msg-details.js b/ui/classic/app/components/pending-personal-msg-details.js index 1050513f2..1050513f2 100644 --- a/ui/app/components/pending-personal-msg-details.js +++ b/ui/classic/app/components/pending-personal-msg-details.js diff --git a/ui/app/components/pending-personal-msg.js b/ui/classic/app/components/pending-personal-msg.js index 4542adb28..4542adb28 100644 --- a/ui/app/components/pending-personal-msg.js +++ b/ui/classic/app/components/pending-personal-msg.js diff --git a/ui/app/components/pending-tx.js b/ui/classic/app/components/pending-tx.js index d7d602f31..962680d30 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/classic/app/components/pending-tx.js @@ -6,7 +6,7 @@ const clone = require('clone') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN -const hexToBn = require('../../../app/scripts/lib/hex-to-bn') +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') const util = require('../util') const MiniAccountPanel = require('./mini-account-panel') const Copyable = require('./copyable') diff --git a/ui/app/components/qr-code.js b/ui/classic/app/components/qr-code.js index 06b9aed9b..06b9aed9b 100644 --- a/ui/app/components/qr-code.js +++ b/ui/classic/app/components/qr-code.js diff --git a/ui/app/components/range-slider.js b/ui/classic/app/components/range-slider.js index 823f5eb01..823f5eb01 100644 --- a/ui/app/components/range-slider.js +++ b/ui/classic/app/components/range-slider.js diff --git a/ui/app/components/shapeshift-form.js b/ui/classic/app/components/shapeshift-form.js index e0a720426..e0a720426 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/classic/app/components/shapeshift-form.js diff --git a/ui/app/components/shift-list-item.js b/ui/classic/app/components/shift-list-item.js index 32bfbeda4..32bfbeda4 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/classic/app/components/shift-list-item.js diff --git a/ui/app/components/tab-bar.js b/ui/classic/app/components/tab-bar.js index 6295e7dd9..6295e7dd9 100644 --- a/ui/app/components/tab-bar.js +++ b/ui/classic/app/components/tab-bar.js diff --git a/ui/app/components/template.js b/ui/classic/app/components/template.js index b6ed8eaa0..b6ed8eaa0 100644 --- a/ui/app/components/template.js +++ b/ui/classic/app/components/template.js diff --git a/ui/app/components/token-cell.js b/ui/classic/app/components/token-cell.js index 19d7139bb..19d7139bb 100644 --- a/ui/app/components/token-cell.js +++ b/ui/classic/app/components/token-cell.js diff --git a/ui/app/components/token-list.js b/ui/classic/app/components/token-list.js index 20cfa897e..20cfa897e 100644 --- a/ui/app/components/token-list.js +++ b/ui/classic/app/components/token-list.js diff --git a/ui/app/components/tooltip.js b/ui/classic/app/components/tooltip.js index edbc074bb..edbc074bb 100644 --- a/ui/app/components/tooltip.js +++ b/ui/classic/app/components/tooltip.js diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/classic/app/components/transaction-list-item-icon.js index 431054340..431054340 100644 --- a/ui/app/components/transaction-list-item-icon.js +++ b/ui/classic/app/components/transaction-list-item-icon.js diff --git a/ui/app/components/transaction-list-item.js b/ui/classic/app/components/transaction-list-item.js index dbda66a31..dbda66a31 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/classic/app/components/transaction-list-item.js diff --git a/ui/app/components/transaction-list.js b/ui/classic/app/components/transaction-list.js index 3b4ba741e..3b4ba741e 100644 --- a/ui/app/components/transaction-list.js +++ b/ui/classic/app/components/transaction-list.js diff --git a/ui/app/conf-tx.js b/ui/classic/app/conf-tx.js index 747d3ce2b..63b77ef7f 100644 --- a/ui/app/conf-tx.js +++ b/ui/classic/app/conf-tx.js @@ -6,7 +6,7 @@ const connect = require('react-redux').connect const actions = require('./actions') const NetworkIndicator = require('./components/network') const txHelper = require('../lib/tx-helper') -const isPopupOrNotification = require('../../app/scripts/lib/is-popup-or-notification') +const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') const PendingTx = require('./components/pending-tx') const PendingMsg = require('./components/pending-msg') diff --git a/ui/app/config.js b/ui/classic/app/config.js index 62785c49b..62785c49b 100644 --- a/ui/app/config.js +++ b/ui/classic/app/config.js diff --git a/ui/app/conversion.json b/ui/classic/app/conversion.json index 155ffc4fc..155ffc4fc 100644 --- a/ui/app/conversion.json +++ b/ui/classic/app/conversion.json diff --git a/ui/app/css/debug.css b/ui/classic/app/css/debug.css index 3e125bcd4..3e125bcd4 100644 --- a/ui/app/css/debug.css +++ b/ui/classic/app/css/debug.css diff --git a/ui/app/css/fonts.css b/ui/classic/app/css/fonts.css index 3b9f581b9..3b9f581b9 100644 --- a/ui/app/css/fonts.css +++ b/ui/classic/app/css/fonts.css diff --git a/ui/app/css/index.css b/ui/classic/app/css/index.css index 808aafb4c..808aafb4c 100644 --- a/ui/app/css/index.css +++ b/ui/classic/app/css/index.css diff --git a/ui/app/css/lib.css b/ui/classic/app/css/lib.css index 910a24ee2..910a24ee2 100644 --- a/ui/app/css/lib.css +++ b/ui/classic/app/css/lib.css diff --git a/ui/app/css/reset.css b/ui/classic/app/css/reset.css index 9ce89e8bc..9ce89e8bc 100644 --- a/ui/app/css/reset.css +++ b/ui/classic/app/css/reset.css diff --git a/ui/app/css/transitions.css b/ui/classic/app/css/transitions.css index 393a944f9..393a944f9 100644 --- a/ui/app/css/transitions.css +++ b/ui/classic/app/css/transitions.css diff --git a/ui/app/first-time/init-menu.js b/ui/classic/app/first-time/init-menu.js index cc7c51bd3..cc7c51bd3 100644 --- a/ui/app/first-time/init-menu.js +++ b/ui/classic/app/first-time/init-menu.js diff --git a/ui/app/img/identicon-tardigrade.png b/ui/classic/app/img/identicon-tardigrade.png Binary files differindex 1742a32b8..1742a32b8 100644 --- a/ui/app/img/identicon-tardigrade.png +++ b/ui/classic/app/img/identicon-tardigrade.png diff --git a/ui/app/img/identicon-walrus.png b/ui/classic/app/img/identicon-walrus.png Binary files differindex d58fae912..d58fae912 100644 --- a/ui/app/img/identicon-walrus.png +++ b/ui/classic/app/img/identicon-walrus.png diff --git a/ui/app/info.js b/ui/classic/app/info.js index e8470de97..e8470de97 100644 --- a/ui/app/info.js +++ b/ui/classic/app/info.js diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/classic/app/keychains/hd/create-vault-complete.js index a318a9b50..a318a9b50 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/classic/app/keychains/hd/create-vault-complete.js diff --git a/ui/app/keychains/hd/recover-seed/confirmation.js b/ui/classic/app/keychains/hd/recover-seed/confirmation.js index 4ccbec9fc..4ccbec9fc 100644 --- a/ui/app/keychains/hd/recover-seed/confirmation.js +++ b/ui/classic/app/keychains/hd/recover-seed/confirmation.js diff --git a/ui/app/keychains/hd/restore-vault.js b/ui/classic/app/keychains/hd/restore-vault.js index 06e51d9b3..06e51d9b3 100644 --- a/ui/app/keychains/hd/restore-vault.js +++ b/ui/classic/app/keychains/hd/restore-vault.js diff --git a/ui/app/new-keychain.js b/ui/classic/app/new-keychain.js index cc9633166..cc9633166 100644 --- a/ui/app/new-keychain.js +++ b/ui/classic/app/new-keychain.js diff --git a/ui/app/reducers.js b/ui/classic/app/reducers.js index 11efca529..11efca529 100644 --- a/ui/app/reducers.js +++ b/ui/classic/app/reducers.js diff --git a/ui/app/reducers/app.js b/ui/classic/app/reducers/app.js index 2fcc9bfe0..2fcc9bfe0 100644 --- a/ui/app/reducers/app.js +++ b/ui/classic/app/reducers/app.js diff --git a/ui/app/reducers/identities.js b/ui/classic/app/reducers/identities.js index 341a404e7..341a404e7 100644 --- a/ui/app/reducers/identities.js +++ b/ui/classic/app/reducers/identities.js diff --git a/ui/app/reducers/metamask.js b/ui/classic/app/reducers/metamask.js index e0c416c2d..e0c416c2d 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/classic/app/reducers/metamask.js diff --git a/ui/app/root.js b/ui/classic/app/root.js index 9e7314b20..9e7314b20 100644 --- a/ui/app/root.js +++ b/ui/classic/app/root.js diff --git a/ui/app/send.js b/ui/classic/app/send.js index a21a219eb..a21a219eb 100644 --- a/ui/app/send.js +++ b/ui/classic/app/send.js diff --git a/ui/app/settings.js b/ui/classic/app/settings.js index 454cc95e0..454cc95e0 100644 --- a/ui/app/settings.js +++ b/ui/classic/app/settings.js diff --git a/ui/app/store.js b/ui/classic/app/store.js index ba9e58b49..ba9e58b49 100644 --- a/ui/app/store.js +++ b/ui/classic/app/store.js diff --git a/ui/app/template.js b/ui/classic/app/template.js index d15b30fd2..d15b30fd2 100644 --- a/ui/app/template.js +++ b/ui/classic/app/template.js diff --git a/ui/app/unlock.js b/ui/classic/app/unlock.js index 1aee3c5d0..1aee3c5d0 100644 --- a/ui/app/unlock.js +++ b/ui/classic/app/unlock.js diff --git a/ui/app/util.js b/ui/classic/app/util.js index ac3f42c6b..ac3f42c6b 100644 --- a/ui/app/util.js +++ b/ui/classic/app/util.js diff --git a/ui/css.js b/ui/classic/css.js index 043363cd7..7c394a87b 100644 --- a/ui/css.js +++ b/ui/classic/css.js @@ -9,8 +9,8 @@ var cssFiles = { 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), - 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), - 'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), } function bundleCss () { diff --git a/ui/design/00-metamask-SignIn.jpg b/ui/classic/design/00-metamask-SignIn.jpg Binary files differindex 2becdb032..2becdb032 100644 --- a/ui/design/00-metamask-SignIn.jpg +++ b/ui/classic/design/00-metamask-SignIn.jpg diff --git a/ui/design/01-metamask-SelectAcc.jpg b/ui/classic/design/01-metamask-SelectAcc.jpg Binary files differindex 239091a98..239091a98 100644 --- a/ui/design/01-metamask-SelectAcc.jpg +++ b/ui/classic/design/01-metamask-SelectAcc.jpg diff --git a/ui/design/02-metamask-AccDetails.jpg b/ui/classic/design/02-metamask-AccDetails.jpg Binary files differindex d7d408ffc..d7d408ffc 100644 --- a/ui/design/02-metamask-AccDetails.jpg +++ b/ui/classic/design/02-metamask-AccDetails.jpg diff --git a/ui/design/02a-metamask-AccDetails-OverToken.jpg b/ui/classic/design/02a-metamask-AccDetails-OverToken.jpg Binary files differindex f26ff31e8..f26ff31e8 100644 --- a/ui/design/02a-metamask-AccDetails-OverToken.jpg +++ b/ui/classic/design/02a-metamask-AccDetails-OverToken.jpg diff --git a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg Binary files differindex 8a06be6b9..8a06be6b9 100644 --- a/ui/design/02a-metamask-AccDetails-OverTransaction.jpg +++ b/ui/classic/design/02a-metamask-AccDetails-OverTransaction.jpg diff --git a/ui/design/02a-metamask-AccDetails.jpg b/ui/classic/design/02a-metamask-AccDetails.jpg Binary files differindex c37e0f539..c37e0f539 100644 --- a/ui/design/02a-metamask-AccDetails.jpg +++ b/ui/classic/design/02a-metamask-AccDetails.jpg diff --git a/ui/design/02b-metamask-AccDetails-Send.jpg b/ui/classic/design/02b-metamask-AccDetails-Send.jpg Binary files differindex 10f2d27fd..10f2d27fd 100644 --- a/ui/design/02b-metamask-AccDetails-Send.jpg +++ b/ui/classic/design/02b-metamask-AccDetails-Send.jpg diff --git a/ui/design/03-metamask-Qr.jpg b/ui/classic/design/03-metamask-Qr.jpg Binary files differindex 9c09de42f..9c09de42f 100644 --- a/ui/design/03-metamask-Qr.jpg +++ b/ui/classic/design/03-metamask-Qr.jpg diff --git a/ui/design/05-metamask-Menu.jpg b/ui/classic/design/05-metamask-Menu.jpg Binary files differindex 0a43d7b2a..0a43d7b2a 100644 --- a/ui/design/05-metamask-Menu.jpg +++ b/ui/classic/design/05-metamask-Menu.jpg diff --git a/ui/design/chromeStorePics/final_screen_dao_accounts.png b/ui/classic/design/chromeStorePics/final_screen_dao_accounts.png Binary files differindex 805cc96b6..805cc96b6 100644 --- a/ui/design/chromeStorePics/final_screen_dao_accounts.png +++ b/ui/classic/design/chromeStorePics/final_screen_dao_accounts.png diff --git a/ui/design/chromeStorePics/final_screen_dao_locked.png b/ui/classic/design/chromeStorePics/final_screen_dao_locked.png Binary files differindex 9d9e33930..9d9e33930 100644 --- a/ui/design/chromeStorePics/final_screen_dao_locked.png +++ b/ui/classic/design/chromeStorePics/final_screen_dao_locked.png diff --git a/ui/design/chromeStorePics/final_screen_dao_notification.png b/ui/classic/design/chromeStorePics/final_screen_dao_notification.png Binary files differindex d56a5ce62..d56a5ce62 100644 --- a/ui/design/chromeStorePics/final_screen_dao_notification.png +++ b/ui/classic/design/chromeStorePics/final_screen_dao_notification.png diff --git a/ui/design/chromeStorePics/final_screen_wei_account.png b/ui/classic/design/chromeStorePics/final_screen_wei_account.png Binary files differindex d503ff301..d503ff301 100644 --- a/ui/design/chromeStorePics/final_screen_wei_account.png +++ b/ui/classic/design/chromeStorePics/final_screen_wei_account.png diff --git a/ui/design/chromeStorePics/final_screen_wei_notification.png b/ui/classic/design/chromeStorePics/final_screen_wei_notification.png Binary files differindex 3560c51ff..3560c51ff 100644 --- a/ui/design/chromeStorePics/final_screen_wei_notification.png +++ b/ui/classic/design/chromeStorePics/final_screen_wei_notification.png diff --git a/ui/design/chromeStorePics/icon-128.png b/ui/classic/design/chromeStorePics/icon-128.png Binary files differindex ae687147d..ae687147d 100644 --- a/ui/design/chromeStorePics/icon-128.png +++ b/ui/classic/design/chromeStorePics/icon-128.png diff --git a/ui/design/chromeStorePics/icon-64.png b/ui/classic/design/chromeStorePics/icon-64.png Binary files differindex 7062cf4f1..7062cf4f1 100644 --- a/ui/design/chromeStorePics/icon-64.png +++ b/ui/classic/design/chromeStorePics/icon-64.png diff --git a/ui/design/chromeStorePics/metamask_icon.ai b/ui/classic/design/chromeStorePics/metamask_icon.ai index 27400c5a4..27400c5a4 100644 --- a/ui/design/chromeStorePics/metamask_icon.ai +++ b/ui/classic/design/chromeStorePics/metamask_icon.ai diff --git a/ui/design/chromeStorePics/promo1400560.png b/ui/classic/design/chromeStorePics/promo1400560.png Binary files differindex d3637ecc8..d3637ecc8 100644 --- a/ui/design/chromeStorePics/promo1400560.png +++ b/ui/classic/design/chromeStorePics/promo1400560.png diff --git a/ui/design/chromeStorePics/promo440280.png b/ui/classic/design/chromeStorePics/promo440280.png Binary files differindex c1f92b1c0..c1f92b1c0 100644 --- a/ui/design/chromeStorePics/promo440280.png +++ b/ui/classic/design/chromeStorePics/promo440280.png diff --git a/ui/design/chromeStorePics/promo920680.png b/ui/classic/design/chromeStorePics/promo920680.png Binary files differindex 726bd810a..726bd810a 100644 --- a/ui/design/chromeStorePics/promo920680.png +++ b/ui/classic/design/chromeStorePics/promo920680.png diff --git a/ui/design/chromeStorePics/screen_dao_accounts.png b/ui/classic/design/chromeStorePics/screen_dao_accounts.png Binary files differindex 1a2e8052c..1a2e8052c 100644 --- a/ui/design/chromeStorePics/screen_dao_accounts.png +++ b/ui/classic/design/chromeStorePics/screen_dao_accounts.png diff --git a/ui/design/chromeStorePics/screen_dao_locked.png b/ui/classic/design/chromeStorePics/screen_dao_locked.png Binary files differindex 6592c17e4..6592c17e4 100644 --- a/ui/design/chromeStorePics/screen_dao_locked.png +++ b/ui/classic/design/chromeStorePics/screen_dao_locked.png diff --git a/ui/design/chromeStorePics/screen_dao_notification.png b/ui/classic/design/chromeStorePics/screen_dao_notification.png Binary files differindex baeb2ec39..baeb2ec39 100644 --- a/ui/design/chromeStorePics/screen_dao_notification.png +++ b/ui/classic/design/chromeStorePics/screen_dao_notification.png diff --git a/ui/design/chromeStorePics/screen_wei_account.png b/ui/classic/design/chromeStorePics/screen_wei_account.png Binary files differindex 23301e4bf..23301e4bf 100644 --- a/ui/design/chromeStorePics/screen_wei_account.png +++ b/ui/classic/design/chromeStorePics/screen_wei_account.png diff --git a/ui/design/chromeStorePics/screen_wei_notification.png b/ui/classic/design/chromeStorePics/screen_wei_notification.png Binary files differindex 7a763e5df..7a763e5df 100644 --- a/ui/design/chromeStorePics/screen_wei_notification.png +++ b/ui/classic/design/chromeStorePics/screen_wei_notification.png diff --git a/ui/design/metamask-logo-eyes.png b/ui/classic/design/metamask-logo-eyes.png Binary files differindex c29331b28..c29331b28 100644 --- a/ui/design/metamask-logo-eyes.png +++ b/ui/classic/design/metamask-logo-eyes.png diff --git a/ui/design/wireframes/1st_time_use.png b/ui/classic/design/wireframes/1st_time_use.png Binary files differindex c18ced5e2..c18ced5e2 100644 --- a/ui/design/wireframes/1st_time_use.png +++ b/ui/classic/design/wireframes/1st_time_use.png diff --git a/ui/design/wireframes/metamask_wfs_jan_13.pdf b/ui/classic/design/wireframes/metamask_wfs_jan_13.pdf Binary files differindex c77c9274a..c77c9274a 100644 --- a/ui/design/wireframes/metamask_wfs_jan_13.pdf +++ b/ui/classic/design/wireframes/metamask_wfs_jan_13.pdf diff --git a/ui/design/wireframes/metamask_wfs_jan_13.png b/ui/classic/design/wireframes/metamask_wfs_jan_13.png Binary files differindex d71d7bdb4..d71d7bdb4 100644 --- a/ui/design/wireframes/metamask_wfs_jan_13.png +++ b/ui/classic/design/wireframes/metamask_wfs_jan_13.png diff --git a/ui/design/wireframes/metamask_wfs_jan_18.pdf b/ui/classic/design/wireframes/metamask_wfs_jan_18.pdf Binary files differindex 592ba8532..592ba8532 100644 --- a/ui/design/wireframes/metamask_wfs_jan_18.pdf +++ b/ui/classic/design/wireframes/metamask_wfs_jan_18.pdf diff --git a/ui/example.js b/ui/classic/example.js index 4627c0e9c..4627c0e9c 100644 --- a/ui/example.js +++ b/ui/classic/example.js diff --git a/ui/index.html b/ui/classic/index.html index 9dfaefbb3..9dfaefbb3 100644 --- a/ui/index.html +++ b/ui/classic/index.html diff --git a/ui/index.js b/ui/classic/index.js index a729138d3..a729138d3 100644 --- a/ui/index.js +++ b/ui/classic/index.js diff --git a/ui/lib/account-link.js b/ui/classic/lib/account-link.js index d061d0ad1..d061d0ad1 100644 --- a/ui/lib/account-link.js +++ b/ui/classic/lib/account-link.js diff --git a/ui/lib/contract-namer.js b/ui/classic/lib/contract-namer.js index f05e770cc..f05e770cc 100644 --- a/ui/lib/contract-namer.js +++ b/ui/classic/lib/contract-namer.js diff --git a/ui/lib/etherscan-prefix-for-network.js b/ui/classic/lib/etherscan-prefix-for-network.js index 2c1904f1c..2c1904f1c 100644 --- a/ui/lib/etherscan-prefix-for-network.js +++ b/ui/classic/lib/etherscan-prefix-for-network.js diff --git a/ui/lib/explorer-link.js b/ui/classic/lib/explorer-link.js index 3b82ecd5f..3b82ecd5f 100644 --- a/ui/lib/explorer-link.js +++ b/ui/classic/lib/explorer-link.js diff --git a/ui/lib/icon-factory.js b/ui/classic/lib/icon-factory.js index 27a74de66..27a74de66 100644 --- a/ui/lib/icon-factory.js +++ b/ui/classic/lib/icon-factory.js diff --git a/ui/lib/lost-accounts-notice.js b/ui/classic/lib/lost-accounts-notice.js index 948b13db6..948b13db6 100644 --- a/ui/lib/lost-accounts-notice.js +++ b/ui/classic/lib/lost-accounts-notice.js diff --git a/ui/lib/persistent-form.js b/ui/classic/lib/persistent-form.js index d4dc20b03..d4dc20b03 100644 --- a/ui/lib/persistent-form.js +++ b/ui/classic/lib/persistent-form.js diff --git a/ui/lib/tx-helper.js b/ui/classic/lib/tx-helper.js index ec19daf64..ec19daf64 100644 --- a/ui/lib/tx-helper.js +++ b/ui/classic/lib/tx-helper.js diff --git a/ui/responsive/.gitignore b/ui/responsive/.gitignore new file mode 100644 index 000000000..c6b1254b5 --- /dev/null +++ b/ui/responsive/.gitignore @@ -0,0 +1,66 @@ + +# Created by https://www.gitignore.io/api/osx,node + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon
+ +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + diff --git a/ui/responsive/app/account-detail.js b/ui/responsive/app/account-detail.js new file mode 100644 index 000000000..ff5c2aadb --- /dev/null +++ b/ui/responsive/app/account-detail.js @@ -0,0 +1,312 @@ +const inherits = require('util').inherits +const extend = require('xtend') +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const CopyButton = require('./components/copyButton') +const AccountInfoLink = require('./components/account-info-link') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const valuesFor = require('./util').valuesFor + +const Identicon = require('./components/identicon') +const EthBalance = require('./components/eth-balance') +const TransactionList = require('./components/transaction-list') +const ExportAccountView = require('./components/account-export') +const ethUtil = require('ethereumjs-util') +const EditableLabel = require('./components/editable-label') +const Tooltip = require('./components/tooltip') +const TabBar = require('./components/tab-bar') +const TokenList = require('./components/token-list') + +module.exports = connect(mapStateToProps)(AccountDetailScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + identities: state.metamask.identities, + accounts: state.metamask.accounts, + address: state.metamask.selectedAddress, + accountDetail: state.appState.accountDetail, + network: state.metamask.network, + unapprovedMsgs: valuesFor(state.metamask.unapprovedMsgs), + shapeShiftTxList: state.metamask.shapeShiftTxList, + transactions: state.metamask.selectedAddressTxList || [], + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, + } +} + +inherits(AccountDetailScreen, Component) +function AccountDetailScreen () { + Component.call(this) +} + +AccountDetailScreen.prototype.render = function () { + var props = this.props + var selected = props.address || Object.keys(props.accounts)[0] + var checksumAddress = selected && ethUtil.toChecksumAddress(selected) + var identity = props.identities[selected] + var account = props.accounts[selected] + const { network, conversionRate, currentCurrency } = props + + return ( + + h('.account-detail-section', [ + + // identicon, label, balance, etc + h('.account-data-subsection', { + style: { + margin: '0 20px', + maxWidth: '320px', + }, + }, [ + + // header - identicon + nav + h('div', { + style: { + paddingTop: '20px', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + }, [ + + // large identicon and addresses + h('.identicon-wrapper.select-none', [ + h(Identicon, { + diameter: 62, + address: selected, + }), + ]), + h('flex-column', { + style: { + lineHeight: '10px', + marginLeft: '15px', + }, + }, [ + h(EditableLabel, { + textValue: identity ? identity.name : '', + state: { + isEditingLabel: false, + }, + saveText: (text) => { + props.dispatch(actions.saveAccountLabel(selected, text)) + }, + }, [ + + // What is shown when not editing + edit text: + h('label.editing-label', [h('.edit-text', 'edit')]), + h('h2.font-medium.color-forest', {name: 'edit'}, identity && identity.name), + ]), + h('.flex-row', { + style: { + width: '15em', + justifyContent: 'space-between', + alignItems: 'baseline', + }, + }, [ + + // address + + h('div', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingTop: '3px', + width: '5em', + fontSize: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + marginTop: '10px', + marginBottom: '15px', + color: '#AEAEAE', + }, + }, checksumAddress), + + // copy and export + + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + + h(AccountInfoLink, { selected, network }), + + h(CopyButton, { + value: checksumAddress, + }), + + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.showQrView(selected, identity ? identity.name : '')), + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '5px', + marginLeft: '3px', + marginRight: '3px', + }, + }), + ]), + + h(Tooltip, { + title: 'Export Private Key', + }, [ + h('div', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/key-32.png', + onClick: () => this.requestAccountExport(selected), + style: { + height: '19px', + }, + }), + ]), + ]), + ]), + ]), + + // account ballence + + ]), + ]), + h('.flex-row', { + style: { + justifyContent: 'space-between', + alignItems: 'flex-start', + }, + }, [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + + h('button', { + onClick: () => props.dispatch(actions.buyEthView(selected)), + style: { + marginBottom: '20px', + marginRight: '8px', + position: 'absolute', + left: '219px', + }, + }, 'BUY'), + + h('button', { + onClick: () => props.dispatch(actions.showSendPage()), + style: { + marginBottom: '20px', + marginRight: '8px', + }, + }, 'SEND'), + + ]), + ]), + + // subview (tx history, pk export confirm, buy eth warning) + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.subview(), + ]), + + ]) + ) +} + +AccountDetailScreen.prototype.subview = function () { + var subview + try { + subview = this.props.accountDetail.subview + } catch (e) { + subview = null + } + + switch (subview) { + case 'transactions': + return this.tabSections() + case 'export': + var state = extend({key: 'export'}, this.props) + return h(ExportAccountView, state) + default: + return this.tabSections() + } +} + +AccountDetailScreen.prototype.tabSections = function () { + const { currentAccountTab } = this.props + + return h('section.tabSection', [ + + h(TabBar, { + tabs: [ + { content: 'Sent', key: 'history' }, + { content: 'Tokens', key: 'tokens' }, + ], + defaultTab: currentAccountTab || 'history', + tabSelected: (key) => { + this.props.dispatch(actions.setCurrentAccountTab(key)) + }, + }), + + this.tabSwitchView(), + ]) +} + +AccountDetailScreen.prototype.tabSwitchView = function () { + const props = this.props + const { address, network } = props + const { currentAccountTab, tokens } = this.props + + switch (currentAccountTab) { + case 'tokens': + return h(TokenList, { + userAddress: address, + network, + tokens, + addToken: () => this.props.dispatch(actions.showAddTokenPage()), + }) + default: + return this.transactionList() + } +} + +AccountDetailScreen.prototype.transactionList = function () { + const {transactions, unapprovedMsgs, address, + network, shapeShiftTxList, conversionRate } = this.props + + return h(TransactionList, { + transactions: transactions.sort((a, b) => b.time - a.time), + network, + unapprovedMsgs, + conversionRate, + address, + shapeShiftTxList, + viewPendingTx: (txId) => { + this.props.dispatch(actions.viewPendingTx(txId)) + }, + }) +} + +AccountDetailScreen.prototype.requestAccountExport = function () { + this.props.dispatch(actions.requestExportAccount()) +} diff --git a/ui/responsive/app/accounts/account-list-item.js b/ui/responsive/app/accounts/account-list-item.js new file mode 100644 index 000000000..10a0b6cc7 --- /dev/null +++ b/ui/responsive/app/accounts/account-list-item.js @@ -0,0 +1,91 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') + +const EthBalance = require('../components/eth-balance') +const CopyButton = require('../components/copyButton') +const Identicon = require('../components/identicon') + +module.exports = AccountListItem + +inherits(AccountListItem, Component) +function AccountListItem () { + Component.call(this) +} + +AccountListItem.prototype.render = function () { + const { identity, selectedAddress, accounts, onShowDetail, + conversionRate, currentCurrency } = this.props + + const checksumAddress = identity && identity.address && ethUtil.toChecksumAddress(identity.address) + const isSelected = selectedAddress === identity.address + const account = accounts[identity.address] + const selectedClass = isSelected ? '.selected' : '' + + return ( + h(`.accounts-list-option.flex-row.flex-space-between.pointer.hover-white${selectedClass}`, { + key: `account-panel-${identity.address}`, + onClick: (event) => onShowDetail(identity.address, event), + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + this.pendingOrNot(), + this.indicateIfLoose(), + h(Identicon, { + address: identity.address, + imageify: true, + }), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', { + style: { + width: '200px', + }, + }, [ + h('span', identity.name), + h('span.font-small', { + style: { + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + }, checksumAddress), + h(EthBalance, { + value: account && account.balance, + currentCurrency, + conversionRate, + style: { + lineHeight: '7px', + marginTop: '10px', + }, + }), + ]), + + // copy button + h('.identity-copy.flex-column', { + style: { + margin: '0 20px', + }, + }, [ + h(CopyButton, { + value: checksumAddress, + }), + ]), + ]) + ) +} + +AccountListItem.prototype.indicateIfLoose = function () { + try { // Sometimes keyrings aren't loaded yet: + const type = this.props.keyring.type + const isLoose = type !== 'HD Key Tree' + return isLoose ? h('.keyring-label', 'LOOSE') : null + } catch (e) { return } +} + +AccountListItem.prototype.pendingOrNot = function () { + const pending = this.props.pending + if (pending.length === 0) return null + return h('.pending-dot', pending.length) +} diff --git a/ui/responsive/app/accounts/import/index.js b/ui/responsive/app/accounts/import/index.js new file mode 100644 index 000000000..97b387229 --- /dev/null +++ b/ui/responsive/app/accounts/import/index.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +import Select from 'react-select' + +// Subviews +const JsonImportView = require('./json.js') +const PrivateKeyImportView = require('./private-key.js') + +const menuItems = [ + 'Private Key', + 'JSON File', +] + +module.exports = connect(mapStateToProps)(AccountImportSubview) + +function mapStateToProps (state) { + return { + menuItems, + } +} + +inherits(AccountImportSubview, Component) +function AccountImportSubview () { + Component.call(this) +} + +AccountImportSubview.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { menuItems } = props + const { type } = state + + return ( + h('div', { + style: { + }, + }, [ + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Import Accounts'), + ]), + h('div', { + style: { + padding: '10px', + color: 'rgb(174, 174, 174)', + }, + }, [ + + h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'), + + h('style', ` + .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label { + color: rgb(174,174,174); + } + `), + + h(Select, { + name: 'import-type-select', + clearable: false, + value: type || menuItems[0], + options: menuItems.map((type) => { + return { + value: type, + label: type, + } + }), + onChange: (opt) => { + this.setState({ type: opt.value }) + }, + }), + ]), + + this.renderImportView(), + ]) + ) +} + +AccountImportSubview.prototype.renderImportView = function () { + const props = this.props + const state = this.state || {} + const { type } = state + const { menuItems } = props + const current = type || menuItems[0] + + switch (current) { + case 'Private Key': + return h(PrivateKeyImportView) + case 'JSON File': + return h(JsonImportView) + default: + return h(JsonImportView) + } +} diff --git a/ui/responsive/app/accounts/import/json.js b/ui/responsive/app/accounts/import/json.js new file mode 100644 index 000000000..158a3c923 --- /dev/null +++ b/ui/responsive/app/accounts/import/json.js @@ -0,0 +1,100 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') +const FileInput = require('react-simple-file-input').default + +const HELP_LINK = 'https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file' + +module.exports = connect(mapStateToProps)(JsonImportSubview) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(JsonImportSubview, Component) +function JsonImportSubview () { + Component.call(this) +} + +JsonImportSubview.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + + h('p', 'Used by a variety of different clients'), + h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'), + + h(FileInput, { + readAs: 'text', + onLoad: this.onLoad.bind(this), + style: { + margin: '20px 0px 12px 20px', + fontSize: '15px', + }, + }), + + h('input.large-input.letter-spacey', { + type: 'password', + placeholder: 'Enter password', + id: 'json-password-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +JsonImportSubview.prototype.onLoad = function (event, file) { + this.setState({file: file, fileContents: event.target.result}) +} + +JsonImportSubview.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +JsonImportSubview.prototype.createNewKeychain = function () { + const state = this.state + const { fileContents } = state + + if (!fileContents) { + const message = 'You must select a file to import.' + return this.props.dispatch(actions.displayWarning(message)) + } + + const passwordInput = document.getElementById('json-password-box') + const password = passwordInput.value + + if (!password) { + const message = 'You must enter a password for the selected file.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ])) +} diff --git a/ui/responsive/app/accounts/import/private-key.js b/ui/responsive/app/accounts/import/private-key.js new file mode 100644 index 000000000..68ccee58e --- /dev/null +++ b/ui/responsive/app/accounts/import/private-key.js @@ -0,0 +1,67 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(PrivateKeyImportView) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(PrivateKeyImportView, Component) +function PrivateKeyImportView () { + Component.call(this) +} + +PrivateKeyImportView.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 15px 0px 15px', + }, + }, [ + h('span', 'Paste your private key string here'), + + h('input.large-input.letter-spacey', { + type: 'password', + id: 'private-key-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + h('button.primary', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +PrivateKeyImportView.prototype.createNewKeychain = function () { + const input = document.getElementById('private-key-box') + const privateKey = input.value + this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ])) +} diff --git a/ui/responsive/app/accounts/import/seed.js b/ui/responsive/app/accounts/import/seed.js new file mode 100644 index 000000000..b4a7c0afa --- /dev/null +++ b/ui/responsive/app/accounts/import/seed.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(SeedImportSubview) + +function mapStateToProps (state) { + return {} +} + +inherits(SeedImportSubview, Component) +function SeedImportSubview () { + Component.call(this) +} + +SeedImportSubview.prototype.render = function () { + return ( + h('div', { + style: { + }, + }, [ + `Paste your seed phrase here!`, + h('textarea'), + h('br'), + h('button', 'Submit'), + ]) + ) +} + diff --git a/ui/responsive/app/accounts/index.js b/ui/responsive/app/accounts/index.js new file mode 100644 index 000000000..3e0830b63 --- /dev/null +++ b/ui/responsive/app/accounts/index.js @@ -0,0 +1,163 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../actions') +const valuesFor = require('../util').valuesFor +const findDOMNode = require('react-dom').findDOMNode +const AccountListItem = require('./account-list-item') + +module.exports = connect(mapStateToProps)(AccountsScreen) + +function mapStateToProps (state) { + const pendingTxs = valuesFor(state.metamask.unapprovedTxs) + .filter(txMeta => txMeta.metamaskNetworkId === state.metamask.network) + const pendingMsgs = valuesFor(state.metamask.unapprovedMsgs) + const pending = pendingTxs.concat(pendingMsgs) + + return { + accounts: state.metamask.accounts, + identities: state.metamask.identities, + unapprovedTxs: state.metamask.unapprovedTxs, + selectedAddress: state.metamask.selectedAddress, + scrollToBottom: state.appState.scrollToBottom, + pending, + keyrings: state.metamask.keyrings, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(AccountsScreen, Component) +function AccountsScreen () { + Component.call(this) +} + +AccountsScreen.prototype.render = function () { + const props = this.props + const { keyrings, conversionRate, currentCurrency } = props + const identityList = valuesFor(props.identities) + const unapprovedTxList = valuesFor(props.unapprovedTxs) + + return ( + + h('.accounts-section.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }), + h('h2.page-subtitle', 'Select Account'), + ]), + + h('hr.horizontal-line'), + + // identity selection + h('section.identity-section', { + style: { + overflowY: 'auto', + overflowX: 'hidden', + }, + }, + [ + identityList.map((identity) => { + const pending = this.props.pending.filter((txOrMsg) => { + if ('txParams' in txOrMsg) { + return txOrMsg.txParams.from === identity.address + } else if ('msgParams' in txOrMsg) { + return txOrMsg.msgParams.from === identity.address + } else { + return false + } + }) + + const simpleAddress = identity.address.substring(2).toLowerCase() + const keyring = keyrings.find((kr) => { + return kr.accounts.includes(simpleAddress) || + kr.accounts.includes(identity.address) + }) + + return h(AccountListItem, { + key: `acct-panel-${identity.address}`, + identity, + selectedAddress: this.props.selectedAddress, + conversionRate, + currentCurrency, + accounts: this.props.accounts, + onShowDetail: this.onShowDetail.bind(this), + pending, + keyring, + }) + }), + + h('hr.horizontal-line'), + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.addNewAccount() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg', {key: ''}), + ]), + h('hr.horizontal-line'), + ]), + + unapprovedTxList.length ? ( + + h('.unconftx-link.flex-row.flex-center', { + onClick: this.navigateToConfTx.bind(this), + }, [ + h('span', 'Unconfirmed Txs'), + h('i.fa.fa-arrow-right.fa-lg'), + ]) + + ) : ( + null + ), + ]) + ) +} + +// If a new account was revealed, scroll to the bottom +AccountsScreen.prototype.componentDidUpdate = function () { + const scrollToBottom = this.props.scrollToBottom + + if (scrollToBottom) { + var container = findDOMNode(this) + var scrollable = container.querySelector('.identity-section') + scrollable.scrollTop = scrollable.scrollHeight + } +} + +AccountsScreen.prototype.navigateToConfTx = function () { + event.stopPropagation() + this.props.dispatch(actions.showConfTxPage()) +} + +AccountsScreen.prototype.onShowDetail = function (address, event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountDetail(address)) +} + +AccountsScreen.prototype.addNewAccount = function () { + this.props.dispatch(actions.addNewAccount(0)) +} + +/* An optional view proposed in this design: + * https://consensys.quip.com/zZVrAysM5znY +AccountsScreen.prototype.addNewAccount = function () { + this.props.dispatch(actions.navigateToNewAccountScreen()) +} +*/ + +AccountsScreen.prototype.goHome = function () { + this.props.dispatch(actions.goHome()) +} diff --git a/ui/responsive/app/actions.js b/ui/responsive/app/actions.js new file mode 100644 index 000000000..2c60448dd --- /dev/null +++ b/ui/responsive/app/actions.js @@ -0,0 +1,1031 @@ +const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') + +var actions = { + _setBackgroundConnection: _setBackgroundConnection, + + GO_HOME: 'GO_HOME', + goHome: goHome, + // menu state + getNetworkStatus: 'getNetworkStatus', + // transition state + TRANSITION_FORWARD: 'TRANSITION_FORWARD', + TRANSITION_BACKWARD: 'TRANSITION_BACKWARD', + transitionForward, + transitionBackward, + // remote state + UPDATE_METAMASK_STATE: 'UPDATE_METAMASK_STATE', + updateMetamaskState: updateMetamaskState, + // notices + MARK_NOTICE_READ: 'MARK_NOTICE_READ', + markNoticeRead: markNoticeRead, + SHOW_NOTICE: 'SHOW_NOTICE', + showNotice: showNotice, + CLEAR_NOTICES: 'CLEAR_NOTICES', + clearNotices: clearNotices, + markAccountsFound, + // intialize screen + CREATE_NEW_VAULT_IN_PROGRESS: 'CREATE_NEW_VAULT_IN_PROGRESS', + SHOW_CREATE_VAULT: 'SHOW_CREATE_VAULT', + SHOW_RESTORE_VAULT: 'SHOW_RESTORE_VAULT', + FORGOT_PASSWORD: 'FORGOT_PASSWORD', + forgotPassword: forgotPassword, + SHOW_INIT_MENU: 'SHOW_INIT_MENU', + SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED', + SHOW_INFO_PAGE: 'SHOW_INFO_PAGE', + SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE', + unlockMetamask: unlockMetamask, + unlockFailed: unlockFailed, + showCreateVault: showCreateVault, + showRestoreVault: showRestoreVault, + showInitializeMenu: showInitializeMenu, + showImportPage, + createNewVaultAndKeychain: createNewVaultAndKeychain, + createNewVaultAndRestore: createNewVaultAndRestore, + createNewVaultInProgress: createNewVaultInProgress, + addNewKeyring, + importNewAccount, + addNewAccount, + NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', + navigateToNewAccountScreen, + showNewVaultSeed: showNewVaultSeed, + showInfoPage: showInfoPage, + // seed recovery actions + REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', + revealSeedConfirmation: revealSeedConfirmation, + requestRevealSeed: requestRevealSeed, + // unlock screen + UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', + UNLOCK_FAILED: 'UNLOCK_FAILED', + UNLOCK_METAMASK: 'UNLOCK_METAMASK', + LOCK_METAMASK: 'LOCK_METAMASK', + tryUnlockMetamask: tryUnlockMetamask, + lockMetamask: lockMetamask, + unlockInProgress: unlockInProgress, + // error handling + displayWarning: displayWarning, + DISPLAY_WARNING: 'DISPLAY_WARNING', + HIDE_WARNING: 'HIDE_WARNING', + hideWarning: hideWarning, + // accounts screen + SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', + SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', + SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', + SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', + SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', + SET_CURRENT_FIAT: 'SET_CURRENT_FIAT', + setCurrentCurrency: setCurrentCurrency, + setCurrentAccountTab, + // account detail screen + SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', + showSendPage: showSendPage, + ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK', + addToAddressBook: addToAddressBook, + REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT', + requestExportAccount: requestExportAccount, + EXPORT_ACCOUNT: 'EXPORT_ACCOUNT', + exportAccount: exportAccount, + SHOW_PRIVATE_KEY: 'SHOW_PRIVATE_KEY', + showPrivateKey: showPrivateKey, + SAVE_ACCOUNT_LABEL: 'SAVE_ACCOUNT_LABEL', + saveAccountLabel: saveAccountLabel, + // tx conf screen + COMPLETED_TX: 'COMPLETED_TX', + TRANSACTION_ERROR: 'TRANSACTION_ERROR', + NEXT_TX: 'NEXT_TX', + PREVIOUS_TX: 'PREV_TX', + signMsg: signMsg, + cancelMsg: cancelMsg, + signPersonalMsg, + cancelPersonalMsg, + sendTx: sendTx, + signTx: signTx, + updateAndApproveTx, + cancelTx: cancelTx, + completedTx: completedTx, + txError: txError, + nextTx: nextTx, + previousTx: previousTx, + viewPendingTx: viewPendingTx, + VIEW_PENDING_TX: 'VIEW_PENDING_TX', + // app messages + confirmSeedWords: confirmSeedWords, + showAccountDetail: showAccountDetail, + BACK_TO_ACCOUNT_DETAIL: 'BACK_TO_ACCOUNT_DETAIL', + backToAccountDetail: backToAccountDetail, + showAccountsPage: showAccountsPage, + showConfTxPage: showConfTxPage, + // config screen + SHOW_CONFIG_PAGE: 'SHOW_CONFIG_PAGE', + SET_RPC_TARGET: 'SET_RPC_TARGET', + SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET', + SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', + USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', + useEtherscanProvider: useEtherscanProvider, + showConfigPage, + SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + showAddTokenPage, + addToken, + setRpcTarget: setRpcTarget, + setDefaultRpcTarget: setDefaultRpcTarget, + setProviderType: setProviderType, + // loading overlay + SHOW_LOADING: 'SHOW_LOADING_INDICATION', + HIDE_LOADING: 'HIDE_LOADING_INDICATION', + showLoadingIndication: showLoadingIndication, + hideLoadingIndication: hideLoadingIndication, + // buy Eth with coinbase + BUY_ETH: 'BUY_ETH', + buyEth: buyEth, + buyEthView: buyEthView, + BUY_ETH_VIEW: 'BUY_ETH_VIEW', + COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', + coinBaseSubview: coinBaseSubview, + SHAPESHIFT_SUBVIEW: 'SHAPESHIFT_SUBVIEW', + shapeShiftSubview: shapeShiftSubview, + PAIR_UPDATE: 'PAIR_UPDATE', + pairUpdate: pairUpdate, + coinShiftRquest: coinShiftRquest, + SHOW_SUB_LOADING_INDICATION: 'SHOW_SUB_LOADING_INDICATION', + showSubLoadingIndication: showSubLoadingIndication, + HIDE_SUB_LOADING_INDICATION: 'HIDE_SUB_LOADING_INDICATION', + hideSubLoadingIndication: hideSubLoadingIndication, +// QR STUFF: + SHOW_QR: 'SHOW_QR', + showQrView: showQrView, + reshowQrCode: reshowQrCode, + SHOW_QR_VIEW: 'SHOW_QR_VIEW', +// FORGOT PASSWORD: + BACK_TO_INIT_MENU: 'BACK_TO_INIT_MENU', + goBackToInitView: goBackToInitView, + RECOVERY_IN_PROGRESS: 'RECOVERY_IN_PROGRESS', + BACK_TO_UNLOCK_VIEW: 'BACK_TO_UNLOCK_VIEW', + backToUnlockView: backToUnlockView, + // SHOWING KEYCHAIN + SHOW_NEW_KEYCHAIN: 'SHOW_NEW_KEYCHAIN', + showNewKeychain: showNewKeychain, + + callBackgroundThenUpdate, + forceUpdateMetamaskState, +} + +module.exports = actions + +var background = null +function _setBackgroundConnection (backgroundConnection) { + background = backgroundConnection +} + +function goHome () { + return { + type: actions.GO_HOME, + } +} + +// async actions + +function tryUnlockMetamask (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + dispatch(actions.unlockInProgress()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.unlockFailed(err.message)) + } else { + dispatch(actions.transitionForward()) + forceUpdateMetamaskState(dispatch) + } + }) + } +} + +function transitionForward () { + return { + type: this.TRANSITION_FORWARD, + } +} + +function transitionBackward () { + return { + type: this.TRANSITION_BACKWARD, + } +} + +function confirmSeedWords () { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.clearSeedWordCache`) + background.clearSeedWordCache((err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + + log.info('Seed word cache cleared. ' + account) + dispatch(actions.showAccountDetail(account)) + }) + } +} + +function createNewVaultAndRestore (password, seed) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndRestore`) + background.createNewVaultAndRestore(password, seed, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function createNewVaultAndKeychain (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.createNewVaultAndKeychain`) + background.createNewVaultAndKeychain(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.hideLoadingIndication()) + forceUpdateMetamaskState(dispatch) + }) + }) + } +} + +function revealSeedConfirmation () { + return { + type: this.REVEAL_SEED_CONFIRMATION, + } +} + +function requestRevealSeed (password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.submitPassword`) + background.submitPassword(password, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + log.debug(`background.placeSeedWords`) + background.placeSeedWords((err, result) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideLoadingIndication()) + dispatch(actions.showNewVaultSeed(result)) + }) + }) + } +} + +function addNewKeyring (type, opts) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.addNewKeyring`) + background.addNewKeyring(type, opts, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showAccountsPage()) + }) + } +} + +function importNewAccount (strategy, args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication('This may take a while, be patient.')) + log.debug(`background.importAccountWithStrategy`) + background.importAccountWithStrategy(strategy, args, (err) => { + if (err) return dispatch(actions.displayWarning(err.message)) + log.debug(`background.getState`) + background.getState((err, newState) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: newState.selectedAddress, + }) + }) + }) + } +} + +function navigateToNewAccountScreen () { + return { + type: this.NEW_ACCOUNT_SCREEN, + } +} + +function addNewAccount () { + log.debug(`background.addNewAccount`) + return callBackgroundThenUpdate(background.addNewAccount) +} + +function showInfoPage () { + return { + type: actions.SHOW_INFO_PAGE, + } +} + +function setCurrentCurrency (currencyCode) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.setCurrentCurrency`) + background.setCurrentCurrency(currencyCode, (err, data) => { + dispatch(this.hideLoadingIndication()) + if (err) { + log.error(err.stack) + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: this.SET_CURRENT_FIAT, + value: { + currentCurrency: data.currentCurrency, + conversionRate: data.conversionRate, + conversionDate: data.conversionDate, + }, + }) + }) + } +} + +function signMsg (msgData) { + log.debug('action - signMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signMessage`) + background.signMessage(msgData, (err, newState) => { + log.debug('signMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signPersonalMsg (msgData) { + log.debug('action - signPersonalMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signPersonalMessage`) + background.signPersonalMessage(msgData, (err, newState) => { + log.debug('signPersonalMessage called back') + dispatch(actions.updateMetamaskState(newState)) + dispatch(actions.hideLoadingIndication()) + + if (err) log.error(err) + if (err) return dispatch(actions.displayWarning(err.message)) + + dispatch(actions.completedTx(msgData.metamaskId)) + }) + } +} + +function signTx (txData) { + return (dispatch) => { + global.ethQuery.sendTransaction(txData, (err, data) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.hideWarning()) + }) + dispatch(this.showConfTxPage()) + } +} + +function sendTx (txData) { + log.info(`actions - sendTx: ${JSON.stringify(txData.txParams)}`) + return (dispatch) => { + log.debug(`actions calling background.approveTransaction`) + background.approveTransaction(txData.id, (err) => { + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function updateAndApproveTx (txData) { + log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) + return (dispatch) => { + log.debug(`actions calling background.updateAndApproveTx`) + background.updateAndApproveTransaction(txData, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.txError(err)) + return log.error(err.message) + } + dispatch(actions.completedTx(txData.id)) + }) + } +} + +function completedTx (id) { + return { + type: actions.COMPLETED_TX, + value: id, + } +} + +function txError (err) { + return { + type: actions.TRANSACTION_ERROR, + message: err.message, + } +} + +function cancelMsg (msgData) { + log.debug(`background.cancelMessage`) + background.cancelMessage(msgData.id) + return actions.completedTx(msgData.id) +} + +function cancelPersonalMsg (msgData) { + const id = msgData.id + background.cancelPersonalMessage(id) + return actions.completedTx(id) +} + +function cancelTx (txData) { + log.debug(`background.cancelTransaction`) + background.cancelTransaction(txData.id) + return actions.completedTx(txData.id) +} + +// +// initialize screen +// + +function showCreateVault () { + return { + type: actions.SHOW_CREATE_VAULT, + } +} + +function showRestoreVault () { + return { + type: actions.SHOW_RESTORE_VAULT, + } +} + +function forgotPassword () { + return { + type: actions.FORGOT_PASSWORD, + } +} + +function showInitializeMenu () { + return { + type: actions.SHOW_INIT_MENU, + } +} + +function showImportPage () { + return { + type: actions.SHOW_IMPORT_PAGE, + } +} + +function createNewVaultInProgress () { + return { + type: actions.CREATE_NEW_VAULT_IN_PROGRESS, + } +} + +function showNewVaultSeed (seed) { + return { + type: actions.SHOW_NEW_VAULT_SEED, + value: seed, + } +} + +function backToUnlockView () { + return { + type: actions.BACK_TO_UNLOCK_VIEW, + } +} + +function showNewKeychain () { + return { + type: actions.SHOW_NEW_KEYCHAIN, + } +} + +// +// unlock screen +// + +function unlockInProgress () { + return { + type: actions.UNLOCK_IN_PROGRESS, + } +} + +function unlockFailed (message) { + return { + type: actions.UNLOCK_FAILED, + value: message, + } +} + +function unlockMetamask (account) { + return { + type: actions.UNLOCK_METAMASK, + value: account, + } +} + +function updateMetamaskState (newState) { + return { + type: actions.UPDATE_METAMASK_STATE, + value: newState, + } +} + +function lockMetamask () { + log.debug(`background.setLocked`) + return callBackgroundThenUpdate(background.setLocked) +} + +function setCurrentAccountTab (newTabName) { + log.debug(`background.setCurrentAccountTab: ${newTabName}`) + return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) +} + +function showAccountDetail (address) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setSelectedAddress`) + background.setSelectedAddress(address, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SHOW_ACCOUNT_DETAIL, + value: address, + }) + }) + } +} + +function backToAccountDetail (address) { + return { + type: actions.BACK_TO_ACCOUNT_DETAIL, + value: address, + } +} + +function showAccountsPage () { + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } +} + +function showConfTxPage (transForward = true) { + return { + type: actions.SHOW_CONF_TX_PAGE, + transForward: transForward, + } +} + +function nextTx () { + return { + type: actions.NEXT_TX, + } +} + +function viewPendingTx (txId) { + return { + type: actions.VIEW_PENDING_TX, + value: txId, + } +} + +function previousTx () { + return { + type: actions.PREVIOUS_TX, + } +} + +function showConfigPage (transitionForward = true) { + return { + type: actions.SHOW_CONFIG_PAGE, + value: transitionForward, + } +} + +function showAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + +function addToken (address, symbol, decimals) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + setTimeout(() => { + dispatch(actions.goHome()) + }, 250) + }) + } +} + +function goBackToInitView () { + return { + type: actions.BACK_TO_INIT_MENU, + } +} + +// +// notice +// + +function markNoticeRead (notice) { + return (dispatch) => { + dispatch(this.showLoadingIndication()) + log.debug(`background.markNoticeRead`) + background.markNoticeRead(notice, (err, notice) => { + dispatch(this.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err)) + } + if (notice) { + return dispatch(actions.showNotice(notice)) + } else { + dispatch(this.clearNotices()) + return { + type: actions.SHOW_ACCOUNTS_PAGE, + } + } + }) + } +} + +function showNotice (notice) { + return { + type: actions.SHOW_NOTICE, + value: notice, + } +} + +function clearNotices () { + return { + type: actions.CLEAR_NOTICES, + } +} + +function markAccountsFound () { + log.debug(`background.markAccountsFound`) + return callBackgroundThenUpdate(background.markAccountsFound) +} + +// +// config +// + +// default rpc target refers to localhost:8545 in this instance. +function setDefaultRpcTarget (rpcList) { + log.debug(`background.setDefaultRpcTarget`) + return (dispatch) => { + background.setDefaultRpc((err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks.')) + } + }) + } +} + +function setRpcTarget (newRpc) { + log.debug(`background.setRpcTarget`) + return (dispatch) => { + background.setCustomRpc(newRpc, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem changing networks!')) + } + }) + } +} + +// Calls the addressBookController to add a new address. +function addToAddressBook (recipient, nickname) { + log.debug(`background.addToAddressBook`) + return (dispatch) => { + background.setAddressBook(recipient, nickname, (err, result) => { + if (err) { + log.error(err) + return dispatch(self.displayWarning('Address book failed to update')) + } + }) + } +} + +function setProviderType (type) { + log.debug(`background.setProviderType`) + background.setProviderType(type) + return { + type: actions.SET_PROVIDER_TYPE, + value: type, + } +} + +function useEtherscanProvider () { + log.debug(`background.useEtherscanProvider`) + background.useEtherscanProvider() + return { + type: actions.USE_ETHERSCAN_PROVIDER, + } +} + +function showLoadingIndication (message) { + return { + type: actions.SHOW_LOADING, + value: message, + } +} + +function hideLoadingIndication () { + return { + type: actions.HIDE_LOADING, + } +} + +function showSubLoadingIndication () { + return { + type: actions.SHOW_SUB_LOADING_INDICATION, + } +} + +function hideSubLoadingIndication () { + return { + type: actions.HIDE_SUB_LOADING_INDICATION, + } +} + +function displayWarning (text) { + return { + type: actions.DISPLAY_WARNING, + value: text, + } +} + +function hideWarning () { + return { + type: actions.HIDE_WARNING, + } +} + +function requestExportAccount () { + return { + type: actions.REQUEST_ACCOUNT_EXPORT, + } +} + +function exportAccount (password, address) { + var self = this + + return function (dispatch) { + dispatch(self.showLoadingIndication()) + + log.debug(`background.submitPassword`) + background.submitPassword(password, function (err) { + if (err) { + log.error('Error in submiting password.') + dispatch(self.hideLoadingIndication()) + return dispatch(self.displayWarning('Incorrect Password.')) + } + log.debug(`background.exportAccount`) + background.exportAccount(address, function (err, result) { + dispatch(self.hideLoadingIndication()) + + if (err) { + log.error(err) + return dispatch(self.displayWarning('Had a problem exporting the account.')) + } + + dispatch(self.showPrivateKey(result)) + }) + }) + } +} + +function showPrivateKey (key) { + return { + type: actions.SHOW_PRIVATE_KEY, + value: key, + } +} + +function saveAccountLabel (account, label) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.saveAccountLabel`) + background.saveAccountLabel(account, label, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch({ + type: actions.SAVE_ACCOUNT_LABEL, + value: { account, label }, + }) + }) + } +} + +function showSendPage () { + return { + type: actions.SHOW_SEND_PAGE, + } +} + +function buyEth (opts) { + return (dispatch) => { + const url = getBuyEthUrl(opts) + global.platform.openWindow({ url }) + dispatch({ + type: actions.BUY_ETH, + }) + } +} + +function buyEthView (address) { + return { + type: actions.BUY_ETH_VIEW, + value: address, + } +} + +function coinBaseSubview () { + return { + type: actions.COINBASE_SUBVIEW, + } +} + +function pairUpdate (coin) { + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + dispatch(actions.hideWarning()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + dispatch(actions.hideSubLoadingIndication()) + dispatch({ + type: actions.PAIR_UPDATE, + value: { + marketinfo: mktResponse, + }, + }) + }) + } +} + +function shapeShiftSubview (network) { + var pair = 'btc_eth' + + return (dispatch) => { + dispatch(actions.showSubLoadingIndication()) + shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { + shapeShiftRequest('getcoins', {}, (response) => { + dispatch(actions.hideSubLoadingIndication()) + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + dispatch({ + type: actions.SHAPESHIFT_SUBVIEW, + value: { + marketinfo: mktResponse, + coinOptions: response, + }, + }) + }) + }) + } +} + +function coinShiftRquest (data, marketData) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('shift', { method: 'POST', data}, (response) => { + dispatch(actions.hideLoadingIndication()) + if (response.error) return dispatch(actions.displayWarning(response.error)) + var message = ` + Deposit your ${response.depositType} to the address bellow:` + log.debug(`background.createShapeShiftTx`) + background.createShapeShiftTx(response.deposit, response.depositType) + dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) + }) + } +} + +function showQrView (data, message) { + return { + type: actions.SHOW_QR_VIEW, + value: { + message: message, + data: data, + }, + } +} +function reshowQrCode (data, coin) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) + + var message = [ + `Deposit your ${coin} to the address bellow:`, + `Deposit Limit: ${mktResponse.limit}`, + `Deposit Minimum:${mktResponse.minimum}`, + ] + + dispatch(actions.hideLoadingIndication()) + return dispatch(actions.showQrView(data, message)) + }) + } +} + +function shapeShiftRequest (query, options, cb) { + var queryResponse, method + !options ? options = {} : null + options.method ? method = options.method : method = 'GET' + + var requestListner = function (request) { + queryResponse = JSON.parse(this.responseText) + cb ? cb(queryResponse) : null + return queryResponse + } + + var shapShiftReq = new XMLHttpRequest() + shapShiftReq.addEventListener('load', requestListner) + shapShiftReq.open(method, `https://shapeshift.io/${query}/${options.pair ? options.pair : ''}`, true) + + if (options.method === 'POST') { + var jsonObj = JSON.stringify(options.data) + shapShiftReq.setRequestHeader('Content-Type', 'application/json') + return shapShiftReq.send(jsonObj) + } else { + return shapShiftReq.send() + } +} + +// Call Background Then Update +// +// A function generator for a common pattern wherein: +// We show loading indication. +// We call a background method. +// We hide loading indication. +// If it errored, we show a warning. +// If it didn't, we update the state. +function callBackgroundThenUpdateNoSpinner (method, ...args) { + return (dispatch) => { + method.call(background, ...args, (err) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function callBackgroundThenUpdate (method, ...args) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + method.call(background, ...args, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + forceUpdateMetamaskState(dispatch) + }) + } +} + +function forceUpdateMetamaskState (dispatch) { + log.debug(`background.getState`) + background.getState((err, newState) => { + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + dispatch(actions.updateMetamaskState(newState)) + }) +} diff --git a/ui/responsive/app/add-token.js b/ui/responsive/app/add-token.js new file mode 100644 index 000000000..b303b5c0d --- /dev/null +++ b/ui/responsive/app/add-token.js @@ -0,0 +1,219 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +const ethUtil = require('ethereumjs-util') +const abi = require('human-standard-token-abi') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') + +const emptyAddr = '0x0000000000000000000000000000000000000000' + +module.exports = connect(mapStateToProps)(AddTokenScreen) + +function mapStateToProps (state) { + return { + } +} + +inherits(AddTokenScreen, Component) +function AddTokenScreen () { + this.state = { + warning: null, + address: null, + symbol: 'TOKEN', + decimals: 18, + } + Component.call(this) +} + +AddTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning, symbol, decimals } = state + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Add Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Address'), + ]), + + h('section.flex-row.flex-center', [ + h('input#token-address', { + name: 'address', + placeholder: 'Token Address', + onChange: this.tokenAddressDidChange.bind(this), + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Sybmol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_symbol', { + placeholder: `Like "ETH"`, + value: symbol, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var symbol = element.value + this.setState({ symbol }) + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_decimals', { + value: decimals, + type: 'number', + min: 0, + max: 36, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var decimals = element.value.trim() + this.setState({ decimals }) + }, + }), + ]), + + h('button', { + style: { + alignSelf: 'center', + }, + onClick: (event) => { + const valid = this.validateInputs() + if (!valid) return + + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (event) { + const el = event.target + const address = el.value.trim() + if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + this.setState({ address }) + this.attemptToAutoFillTokenParams(address) + } +} + +AddTokenScreen.prototype.validateInputs = function () { + let msg = '' + const state = this.state + const { address, symbol, decimals } = state + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid. ' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const isValid = validAddress && validDecimals + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) + this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + } +} + diff --git a/ui/responsive/app/app.js b/ui/responsive/app/app.js new file mode 100644 index 000000000..e7bde9605 --- /dev/null +++ b/ui/responsive/app/app.js @@ -0,0 +1,586 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('./actions') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +// init +const InitializeMenuScreen = require('./first-time/init-menu') +const NewKeyChainScreen = require('./new-keychain') +// unlock +const UnlockScreen = require('./unlock') +// accounts +const AccountsScreen = require('./accounts') +const AccountDetailScreen = require('./account-detail') +const SendTransactionScreen = require('./send') +const ConfirmTxScreen = require('./conf-tx') +// notice +const NoticeScreen = require('./components/notice') +const generateLostAccountsNotice = require('../lib/lost-accounts-notice') +// other views +const ConfigScreen = require('./config') +const AddTokenScreen = require('./add-token') +const Import = require('./accounts/import') +const InfoScreen = require('./info') +const Loading = require('./components/loading') +const SandwichExpando = require('sandwich-expando') +const MenuDroppo = require('menu-droppo') +const DropMenuItem = require('./components/drop-menu-item') +const NetworkIndicator = require('./components/network') +const Tooltip = require('./components/tooltip') +const BuyView = require('./components/buy-button-subview') +const QrView = require('./components/qr-code') +const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete') +const HDRestoreVaultScreen = require('./keychains/hd/restore-vault') +const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation') + +module.exports = connect(mapStateToProps)(App) + +inherits(App, Component) +function App () { Component.call(this) } + +function mapStateToProps (state) { + return { + // state from plugin + isLoading: state.appState.isLoading, + loadingMessage: state.appState.loadingMessage, + noActiveNotices: state.metamask.noActiveNotices, + isInitialized: state.metamask.isInitialized, + isUnlocked: state.metamask.isUnlocked, + currentView: state.appState.currentView, + activeAddress: state.appState.activeAddress, + transForward: state.appState.transForward, + seedWords: state.metamask.seedWords, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + menuOpen: state.appState.menuOpen, + network: state.metamask.network, + provider: state.metamask.provider, + forgottenPassword: state.appState.forgottenPassword, + lastUnreadNotice: state.metamask.lastUnreadNotice, + lostAccounts: state.metamask.lostAccounts, + frequentRpcList: state.metamask.frequentRpcList || [], + } +} + +App.prototype.render = function () { + var props = this.props + const { isLoading, loadingMessage, transForward, network } = props + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' + const loadMessage = loadingMessage || isLoadingNetwork ? + `Connecting to ${this.getNetworkName()}` : null + + log.debug('Main ui render function') + + return ( + + h('.flex-column.flex-grow.full-height', { + style: { + // Windows was showing a vertical scroll bar: + overflow: 'hidden', + position: 'relative', + }, + }, [ + + // app bar + this.renderAppBar(), + this.renderNetworkDropdown(), + this.renderDropdown(), + + h(Loading, { + isLoading: isLoading || isLoadingNetwork, + loadingMessage: loadMessage, + }), + + // panel content + h('.app-primary.flex-grow' + (transForward ? '.from-right' : '.from-left'), [ + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.renderPrimary(), + ]), + ]), + ]) + ) +} + +App.prototype.renderAppBar = function () { + if (window.METAMASK_UI_TYPE === 'notification') { + return null + } + + const props = this.props + const state = this.state || {} + const isNetworkMenuOpen = state.isNetworkMenuOpen || false + + return ( + + h('div', [ + + h('.app-header.flex-row.flex-space-between', { + style: { + alignItems: 'center', + visibility: props.isUnlocked ? 'visible' : 'none', + background: props.isUnlocked ? 'white' : 'none', + height: '36px', + position: 'relative', + zIndex: 12, + }, + }, [ + + h('div.left-menu-section', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // mini logo + h('img', { + height: 24, + width: 24, + src: '/images/icon-128.png', + }), + + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) + }, + }), + ]), + + // metamask name + props.isUnlocked && h('h1', { + style: { + position: 'relative', + left: '9px', + }, + }, 'MetaMask'), + + props.isUnlocked && h('div', { + style: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + }, [ + + // small accounts nav + props.isUnlocked && h(Tooltip, { title: 'Switch Accounts' }, [ + h('img.cursor-pointer.color-orange', { + src: 'images/switch_acc.svg', + style: { + width: '23.5px', + marginRight: '8px', + }, + onClick: (event) => { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) + }, + }), + ]), + + // hamburger + props.isUnlocked && h(SandwichExpando, { + width: 16, + barHeight: 2, + padding: 0, + isOpen: state.isMainMenuOpen, + color: 'rgb(247,146,30)', + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isMainMenuOpen: !state.isMainMenuOpen }) + }, + }), + ]), + ]), + ]) + ) +} + +App.prototype.renderNetworkDropdown = function () { + const props = this.props + const rpcList = props.frequentRpcList + const state = this.state || {} + const isOpen = state.isNetworkMenuOpen + + return h(MenuDroppo, { + isOpen, + onClickOutside: (event) => { + this.setState({ isNetworkMenuOpen: !isOpen }) + }, + zIndex: 11, + style: { + position: 'absolute', + left: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Main Ethereum Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('mainnet')), + icon: h('.menu-icon.diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Ropsten Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setProviderType('ropsten')), + icon: h('.menu-icon.red-dot'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Kovan Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('kovan')), + icon: h('.menu-icon.hollow-diamond'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Rinkeby Test Network', + closeMenu: () => this.setState({ isNetworkMenuOpen: false}), + action: () => props.dispatch(actions.setProviderType('rinkeby')), + icon: h('.menu-icon.golden-square'), + activeNetworkRender: props.network, + provider: props.provider, + }), + + h(DropMenuItem, { + label: 'Localhost 8545', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setDefaultRpcTarget(rpcList)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: props.provider.rpcTarget, + }), + + this.renderCustomOption(props.provider), + this.renderCommonRpc(rpcList, props.provider), + + h(DropMenuItem, { + label: 'Custom RPC', + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-question-circle.fa-lg'), + }), + + ]) +} + +App.prototype.renderDropdown = function () { + const state = this.state || {} + const isOpen = state.isMainMenuOpen + + return h(MenuDroppo, { + isOpen: isOpen, + zIndex: 11, + onClickOutside: (event) => { + this.setState({ isMainMenuOpen: !isOpen }) + }, + style: { + position: 'absolute', + right: 0, + top: '36px', + }, + innerStyle: { + background: 'white', + boxShadow: '1px 1px 2px rgba(0,0,0,0.1)', + }, + }, [ // DROP MENU ITEMS + h('style', ` + .drop-menu-item:hover { background:rgb(235, 235, 235); } + .drop-menu-item i { margin: 11px; } + `), + + h(DropMenuItem, { + label: 'Settings', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showConfigPage()), + icon: h('i.fa.fa-gear.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Import Account', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showImportPage()), + icon: h('i.fa.fa-arrow-circle-o-up.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Lock', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.lockMetamask()), + icon: h('i.fa.fa-lock.fa-lg'), + }), + + h(DropMenuItem, { + label: 'Info/Help', + closeMenu: () => this.setState({ isMainMenuOpen: !isOpen }), + action: () => this.props.dispatch(actions.showInfoPage()), + icon: h('i.fa.fa-question.fa-lg'), + }), + ]) +} + +App.prototype.renderBackButton = function (style, justArrow = false) { + var props = this.props + return ( + h('.flex-row', { + key: 'leftArrow', + style: style, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, [ + h('i.fa.fa-arrow-left.cursor-pointer'), + justArrow ? null : h('div.cursor-pointer', { + style: { + marginLeft: '3px', + }, + onClick: () => props.dispatch(actions.goBackToInitView()), + }, 'BACK'), + ]) + ) +} + +App.prototype.renderPrimary = function () { + log.debug('rendering primary') + var props = this.props + + // notices + if (!props.noActiveNotices) { + log.debug('rendering notice screen for unread notices.') + return h(NoticeScreen, { + notice: props.lastUnreadNotice, + key: 'NoticeScreen', + onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + }) + } else if (props.lostAccounts && props.lostAccounts.length > 0) { + log.debug('rendering notice screen for lost accounts view.') + return h(NoticeScreen, { + notice: generateLostAccountsNotice(props.lostAccounts), + key: 'LostAccountsNotice', + onConfirm: () => props.dispatch(actions.markAccountsFound()), + }) + } + + if (props.seedWords) { + log.debug('rendering seed words') + return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'}) + } + + // show initialize screen + if (!props.isInitialized || props.forgottenPassword) { + // show current view + log.debug('rendering an initialize screen') + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + default: + log.debug('rendering menu screen') + return h(InitializeMenuScreen, {key: 'menuScreenInit'}) + } + } + + // show unlock screen + if (!props.isUnlocked) { + switch (props.currentView.name) { + + case 'restoreVault': + log.debug('rendering restore vault screen') + return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'}) + + case 'config': + log.debug('rendering config screen from unlock screen.') + return h(ConfigScreen, {key: 'config'}) + + default: + log.debug('rendering locked screen') + return h(UnlockScreen, {key: 'locked'}) + } + } + + // show current view + switch (props.currentView.name) { + + case 'accounts': + log.debug('rendering accounts screen') + return h(AccountsScreen, {key: 'accounts'}) + + case 'accountDetail': + log.debug('rendering account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + + case 'sendTransaction': + log.debug('rendering send tx screen') + return h(SendTransactionScreen, {key: 'send-transaction'}) + + case 'newKeychain': + log.debug('rendering new keychain screen') + return h(NewKeyChainScreen, {key: 'new-keychain'}) + + case 'confTx': + log.debug('rendering confirm tx screen') + return h(ConfirmTxScreen, {key: 'confirm-tx'}) + + case 'add-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddTokenScreen, {key: 'add-token'}) + + case 'config': + log.debug('rendering config screen') + return h(ConfigScreen, {key: 'config'}) + + case 'import-menu': + log.debug('rendering import screen') + return h(Import, {key: 'import-menu'}) + + case 'reveal-seed-conf': + log.debug('rendering reveal seed confirmation screen') + return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + + case 'info': + log.debug('rendering info screen') + return h(InfoScreen, {key: 'info'}) + + case 'buyEth': + log.debug('rendering buy ether screen') + return h(BuyView, {key: 'buyEthView'}) + + case 'qr': + log.debug('rendering show qr screen') + return h('div', { + style: { + position: 'absolute', + height: '100%', + top: '0px', + left: '0px', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: () => props.dispatch(actions.backToAccountDetail(props.activeAddress)), + style: { + marginLeft: '10px', + marginTop: '50px', + }, + }), + h('div', { + style: { + position: 'absolute', + left: '44px', + width: '285px', + }, + }, [ + h(QrView, {key: 'qr'}), + ]), + ]) + + default: + log.debug('rendering default, account detail screen') + return h(AccountDetailScreen, {key: 'account-detail'}) + } +} + +App.prototype.toggleMetamaskActive = function () { + if (!this.props.isUnlocked) { + // currently inactive: redirect to password box + var passwordBox = document.querySelector('input[type=password]') + if (!passwordBox) return + passwordBox.focus() + } else { + // currently active: deactivate + this.props.dispatch(actions.lockMetamask(false)) + } +} + +App.prototype.renderCustomOption = function (provider) { + const { rpcTarget, type } = provider + if (type !== 'rpc') return null + + // Concatenate long URLs + let label = rpcTarget + if (rpcTarget.length > 31) { + label = label.substr(0, 34) + '...' + } + + switch (rpcTarget) { + + case 'http://localhost:8545': + return null + + default: + return h(DropMenuItem, { + label, + key: rpcTarget, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: 'custom', + }) + } +} + +App.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name +} + +App.prototype.renderCommonRpc = function (rpcList, provider) { + const { rpcTarget } = provider + const props = this.props + + return rpcList.map((rpc) => { + if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) { + return null + } else { + return h(DropMenuItem, { + label: rpc, + key: rpc, + closeMenu: () => this.setState({ isNetworkMenuOpen: false }), + action: () => props.dispatch(actions.setRpcTarget(rpc)), + icon: h('i.fa.fa-question-circle.fa-lg'), + activeNetworkRender: rpc, + }) + } + }) +} diff --git a/ui/responsive/app/components/account-export.js b/ui/responsive/app/components/account-export.js new file mode 100644 index 000000000..394d878f7 --- /dev/null +++ b/ui/responsive/app/components/account-export.js @@ -0,0 +1,122 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') +const actions = require('../actions') +const ethUtil = require('ethereumjs-util') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(ExportAccountView) + +inherits(ExportAccountView, Component) +function ExportAccountView () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +ExportAccountView.prototype.render = function () { + var state = this.props + var accountDetail = state.accountDetail + + if (!accountDetail) return h('div') + var accountExport = accountDetail.accountExport + + var notExporting = accountExport === 'none' + var exportRequested = accountExport === 'requested' + var accountExported = accountExport === 'completed' + + if (notExporting) return h('div') + + if (exportRequested) { + var warning = `Export private keys at your own risk.` + return ( + h('div', { + style: { + display: 'inline-block', + textAlign: 'center', + }, + }, + [ + h('div', { + key: 'exporting', + style: { + margin: '0 20px', + }, + }, [ + h('p.error', warning), + h('input#exportAccount.sizing-input', { + type: 'password', + placeholder: 'confirm password', + onKeyPress: this.onExportKeyPress.bind(this), + style: { + position: 'relative', + top: '1.5px', + marginBottom: '7px', + }, + }), + ]), + h('div', { + key: 'buttons', + style: { + margin: '0 20px', + }, + }, + [ + h('button', { + onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }), + style: { + marginRight: '10px', + }, + }, 'Submit'), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Cancel'), + ]), + (this.props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, this.props.warning.split('-')) + ), + ]) + ) + } + + if (accountExported) { + return h('div.privateKey', { + style: { + margin: '0 20px', + }, + }, [ + h('label', 'Your private key (click to copy):'), + h('p.error.cursor-pointer', { + style: { + textOverflow: 'ellipsis', + overflow: 'hidden', + webkitUserSelect: 'text', + width: '100%', + }, + onClick: function (event) { + copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey)) + }, + }, ethUtil.stripHexPrefix(accountDetail.privateKey)), + h('button', { + onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)), + }, 'Done'), + ]) + } +} + +ExportAccountView.prototype.onExportKeyPress = function (event) { + if (event.key !== 'Enter') return + event.preventDefault() + + var input = document.getElementById('exportAccount').value + this.props.dispatch(actions.exportAccount(input, this.props.address)) +} diff --git a/ui/responsive/app/components/account-info-link.js b/ui/responsive/app/components/account-info-link.js new file mode 100644 index 000000000..6526ab502 --- /dev/null +++ b/ui/responsive/app/components/account-info-link.js @@ -0,0 +1,41 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') +const genAccountLink = require('../../lib/account-link') + +module.exports = AccountInfoLink + +inherits(AccountInfoLink, Component) +function AccountInfoLink () { + Component.call(this) +} + +AccountInfoLink.prototype.render = function () { + const { selected, network } = this.props + const title = 'View account on Etherscan' + const url = genAccountLink(selected, network) + + if (!url) { + return null + } + + return h('.account-info-link', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title, + }, [ + h('i.fa.fa-info-circle.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick () { global.platform.openWindow({ url }) }, + }), + ]), + ]) +} diff --git a/ui/responsive/app/components/account-panel.js b/ui/responsive/app/components/account-panel.js new file mode 100644 index 000000000..abaaf8163 --- /dev/null +++ b/ui/responsive/app/components/account-panel.js @@ -0,0 +1,86 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') +const formatBalance = require('../util').formatBalance +const addressSummary = require('../util').addressSummary + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var state = this.props + var identity = state.identity || {} + var account = state.account || {} + var isFauceting = state.isFauceting + + var panelState = { + key: `accountPanel${identity.address}`, + identiconKey: identity.address, + identiconLabel: identity.name || '', + attributes: [ + { + key: 'ADDRESS', + value: addressSummary(identity.address), + }, + balanceOrFaucetingIndication(account, isFauceting), + ], + } + + return ( + + h('.identity-panel.flex-row.flex-space-between', { + style: { + flex: '1 0 auto', + cursor: panelState.onClick ? 'pointer' : undefined, + }, + onClick: panelState.onClick, + }, [ + + // account identicon + h('.identicon-wrapper.flex-column.select-none', [ + h(Identicon, { + address: panelState.identiconKey, + imageify: state.imageifyIdenticons, + }), + h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + panelState.attributes.map((attr) => { + return h('.flex-row.flex-space-between', { + key: '' + Math.round(Math.random() * 1000000), + }, [ + h('label.font-small.no-select', attr.key), + h('span.font-small', attr.value), + ]) + }), + ]), + + ]) + + ) +} + +function balanceOrFaucetingIndication (account, isFauceting) { + // Temporarily deactivating isFauceting indication + // because it shows fauceting for empty restored accounts. + if (/* isFauceting*/ false) { + return { + key: 'Account is auto-funding.', + value: 'Please wait.', + } + } else { + return { + key: 'BALANCE', + value: formatBalance(account.balance), + } + } +} diff --git a/ui/responsive/app/components/balance.js b/ui/responsive/app/components/balance.js new file mode 100644 index 000000000..57ca84564 --- /dev/null +++ b/ui/responsive/app/components/balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + var style = props.style + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + var width = props.width + + return ( + + h('.ether-balance.ether-balance-amount', { + style: style, + }, [ + h('div', { + style: { + display: 'inline', + width: width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, props.shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (props.shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, this.props.incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value }) : null, + ])) + ) +} diff --git a/ui/responsive/app/components/binary-renderer.js b/ui/responsive/app/components/binary-renderer.js new file mode 100644 index 000000000..0b6a1f5c2 --- /dev/null +++ b/ui/responsive/app/components/binary-renderer.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const extend = require('xtend') + +module.exports = BinaryRenderer + +inherits(BinaryRenderer, Component) +function BinaryRenderer () { + Component.call(this) +} + +BinaryRenderer.prototype.render = function () { + const props = this.props + const { value, style } = props + const text = this.hexToText(value) + + const defaultStyle = extend({ + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, style) + + return ( + h('textarea.font-small', { + readOnly: true, + style: defaultStyle, + defaultValue: text, + }) + ) +} + +BinaryRenderer.prototype.hexToText = function (hex) { + try { + const stripped = ethUtil.stripHexPrefix(hex) + const buff = Buffer.from(stripped, 'hex') + return buff.toString('utf8') + } catch (e) { + return hex + } +} + diff --git a/ui/responsive/app/components/bn-as-decimal-input.js b/ui/responsive/app/components/bn-as-decimal-input.js new file mode 100644 index 000000000..f3ace4720 --- /dev/null +++ b/ui/responsive/app/components/bn-as-decimal-input.js @@ -0,0 +1,174 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = BnAsDecimalInput + +inherits(BnAsDecimalInput, Component) +function BnAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Bn as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in bn string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated bn string. + */ + +BnAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, scale, precision, onChange, min, max } = props + + const suffix = props.suffix + const style = props.style + const valueString = value.toString(10) + const newValue = this.downsize(valueString, scale, precision) + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + step: 'any', + required: true, + min, + max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: newValue, + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const value = (event.target.value === '') ? '' : event.target.value + + + const scaledNumber = this.upsize(value, scale, precision) + const precisionBN = new BN(scaledNumber, 10) + onChange(precisionBN, event.target.checkValidity()) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +BnAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +BnAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + + if (valid) { + this.setState({ invalid: null }) + } +} + +BnAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + + +BnAsDecimalInput.prototype.downsize = function (number, scale, precision) { + // if there is no scaling, simply return the number + if (scale === 0) { + return Number(number) + } else { + // if the scale is the same as the precision, account for this edge case. + var decimals = (scale === precision) ? -1 : scale - precision + return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals)) + } +} + +BnAsDecimalInput.prototype.upsize = function (number, scale, precision) { + var stringArray = number.toString().split('.') + var decimalLength = stringArray[1] ? stringArray[1].length : 0 + var newString = stringArray[0] + + // If there is scaling and decimal parts exist, integrate them in. + if ((scale !== 0) && (decimalLength !== 0)) { + newString += stringArray[1].slice(0, precision) + } + + // Add 0s to account for the upscaling. + for (var i = decimalLength; i < scale; i++) { + newString += '0' + } + return newString +} diff --git a/ui/responsive/app/components/buy-button-subview.js b/ui/responsive/app/components/buy-button-subview.js new file mode 100644 index 000000000..87084f92d --- /dev/null +++ b/ui/responsive/app/components/buy-button-subview.js @@ -0,0 +1,197 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') +const CoinbaseForm = require('./coinbase-form') +const ShapeshiftForm = require('./shapeshift-form') +const Loading = require('./loading') +const AccountPanel = require('./account-panel') +const RadioList = require('./custom-radio-list') + +module.exports = connect(mapStateToProps)(BuyButtonSubview) + +function mapStateToProps (state) { + return { + identity: state.appState.identity, + account: state.metamask.accounts[state.appState.buyView.buyAddress], + warning: state.appState.warning, + buyView: state.appState.buyView, + network: state.metamask.network, + provider: state.metamask.provider, + context: state.appState.currentView.context, + isSubLoading: state.appState.isSubLoading, + } +} + +inherits(BuyButtonSubview, Component) +function BuyButtonSubview () { + Component.call(this) +} + +BuyButtonSubview.prototype.render = function () { + const props = this.props + const isLoading = props.isSubLoading + + return ( + h('.buy-eth-section.flex-column', { + style: { + alignItems: 'center', + }, + }, [ + // back button + h('.flex-row', { + style: { + alignItems: 'center', + justifyContent: 'center', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.backButtonContext.bind(this), + style: { + position: 'absolute', + left: '10px', + }, + }), + h('h2.text-transform-uppercase.flex-center', { + style: { + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Buy Eth'), + ]), + h('div', { + style: { + position: 'absolute', + top: '57vh', + left: '49vw', + }, + }, [ + h(Loading, {isLoading}), + ]), + h('div', { + style: { + width: '80%', + }, + }, [ + h(AccountPanel, { + showFullAddress: true, + identity: props.identity, + account: props.account, + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, 'Select Service'), + h('.flex-row.selected-exchange', { + style: { + position: 'relative', + right: '35px', + marginTop: '20px', + marginBottom: '20px', + }, + }, [ + h(RadioList, { + defaultFocus: props.buyView.subview, + labels: [ + 'Coinbase', + 'ShapeShift', + ], + subtext: { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + }, + onClick: this.radioHandler.bind(this), + }), + ]), + h('h3.text-transform-uppercase', { + style: { + paddingLeft: '15px', + fontFamily: 'Montserrat Light', + width: '100vw', + background: 'rgb(235, 235, 235)', + color: 'rgb(174, 174, 174)', + paddingTop: '4px', + paddingBottom: '4px', + }, + }, props.buyView.subview), + this.formVersionSubview(), + ]) + ) +} + +BuyButtonSubview.prototype.formVersionSubview = function () { + const network = this.props.network + if (network === '1') { + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) + } + } else { + return h('div.flex-column', { + style: { + alignItems: 'center', + margin: '50px', + }, + }, [ + h('h3.text-transform-uppercase', { + style: { + width: '225px', + marginBottom: '15px', + }, + }, 'In order to access this feature, please switch to the Main Network'), + ((network === '3') || (network === '4') || (network === '42')) ? h('h3.text-transform-uppercase', 'or go to the') : null, + (network === '3') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Ropsten Test Faucet') : null, + (network === '4') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Rinkeby Test Faucet') : null, + (network === '42') ? h('button.text-transform-uppercase', { + onClick: () => this.props.dispatch(actions.buyEth({ network })), + style: { + marginTop: '15px', + }, + }, 'Kovan Test Faucet') : null, + ]) + } +} + +BuyButtonSubview.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + +BuyButtonSubview.prototype.backButtonContext = function () { + if (this.props.context === 'confTx') { + this.props.dispatch(actions.showConfTxPage(false)) + } else { + this.props.dispatch(actions.goHome()) + } +} + +BuyButtonSubview.prototype.radioHandler = function (event) { + switch (event.target.title) { + case 'Coinbase': + return this.props.dispatch(actions.coinBaseSubview()) + case 'ShapeShift': + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + } +} diff --git a/ui/responsive/app/components/coinbase-form.js b/ui/responsive/app/components/coinbase-form.js new file mode 100644 index 000000000..f44d86045 --- /dev/null +++ b/ui/responsive/app/components/coinbase-form.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const actions = require('../actions') + +module.exports = connect(mapStateToProps)(CoinbaseForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +inherits(CoinbaseForm, Component) + +function CoinbaseForm () { + Component.call(this) +} + +CoinbaseForm.prototype.render = function () { + var props = this.props + + return h('.flex-column', { + style: { + marginTop: '35px', + padding: '25px', + width: '100%', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'space-around', + margin: '33px', + marginTop: '0px', + }, + }, [ + h('button.btn-green', { + onClick: this.toCoinbase.bind(this), + }, 'Continue to Coinbase'), + + h('button.btn-red', { + onClick: () => props.dispatch(actions.backTobuyView(props.accounts.address)), + }, 'Cancel'), + ]), + ]) +} + +CoinbaseForm.prototype.toCoinbase = function () { + const props = this.props + const address = props.buyView.buyAddress + props.dispatch(actions.buyEth({ network: '1', address, amount: 0 })) +} + +CoinbaseForm.prototype.renderLoading = function () { + return h('img', { + style: { + width: '27px', + marginRight: '-27px', + }, + src: 'images/loading.svg', + }) +} diff --git a/ui/responsive/app/components/copyButton.js b/ui/responsive/app/components/copyButton.js new file mode 100644 index 000000000..a25d0719c --- /dev/null +++ b/ui/responsive/app/components/copyButton.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const copyToClipboard = require('copy-to-clipboard') + +const Tooltip = require('./tooltip') + +module.exports = CopyButton + +inherits(CopyButton, Component) +function CopyButton () { + Component.call(this) +} + +// As parameters, accepts: +// "value", which is the value to copy (mandatory) +// "title", which is the text to show on hover (optional, defaults to 'Copy') +CopyButton.prototype.render = function () { + const props = this.props + const state = this.state || {} + + const value = props.value + const copied = state.copied + + const message = copied ? 'Copied' : props.title || ' Copy ' + + return h('.copy-button', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + + h(Tooltip, { + title: message, + }, [ + h('i.fa.fa-clipboard.cursor-pointer.color-orange', { + style: { + margin: '5px', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }), + ]), + + ]) +} + +CopyButton.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/responsive/app/components/copyable.js b/ui/responsive/app/components/copyable.js new file mode 100644 index 000000000..a4f6f4bc6 --- /dev/null +++ b/ui/responsive/app/components/copyable.js @@ -0,0 +1,46 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const Tooltip = require('./tooltip') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = Copyable + +inherits(Copyable, Component) +function Copyable () { + Component.call(this) + this.state = { + copied: false, + } +} + +Copyable.prototype.render = function () { + const props = this.props + const state = this.state + const { value, children } = props + const { copied } = state + + return h(Tooltip, { + title: copied ? 'Copied!' : 'Copy', + position: 'bottom', + }, h('span', { + style: { + cursor: 'pointer', + }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(value) + this.debounceRestore() + }, + }, children)) +} + +Copyable.prototype.debounceRestore = function () { + this.setState({ copied: true }) + clearTimeout(this.timeout) + this.timeout = setTimeout(() => { + this.setState({ copied: false }) + }, 850) +} diff --git a/ui/responsive/app/components/custom-radio-list.js b/ui/responsive/app/components/custom-radio-list.js new file mode 100644 index 000000000..a4c525396 --- /dev/null +++ b/ui/responsive/app/components/custom-radio-list.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RadioList + +inherits(RadioList, Component) +function RadioList () { + Component.call(this) +} + +RadioList.prototype.render = function () { + const props = this.props + const activeClass = '.custom-radio-selected' + const inactiveClass = '.custom-radio-inactive' + const { + labels, + defaultFocus, + } = props + + + return ( + h('.flex-row', { + style: { + fontSize: '12px', + }, + }, [ + h('.flex-column.custom-radios', { + style: { + marginRight: '5px', + }, + }, + labels.map((lable, i) => { + let isSelcted = (this.state !== null) + isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable) + return h(isSelcted ? activeClass : inactiveClass, { + title: lable, + onClick: (event) => { + this.setState({selected: event.target.title}) + props.onClick(event) + }, + }) + }) + ), + h('.text', {}, + labels.map((lable) => { + if (props.subtext) { + return h('.flex-row', {}, [ + h('.radio-titles', lable), + h('.radio-titles-subtext', `- ${props.subtext[lable]}`), + ]) + } else { + return h('.radio-titles', lable) + } + }) + ), + ]) + ) +} + diff --git a/ui/responsive/app/components/drop-menu-item.js b/ui/responsive/app/components/drop-menu-item.js new file mode 100644 index 000000000..e42948209 --- /dev/null +++ b/ui/responsive/app/components/drop-menu-item.js @@ -0,0 +1,59 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = DropMenuItem + +inherits(DropMenuItem, Component) +function DropMenuItem () { + Component.call(this) +} + +DropMenuItem.prototype.render = function () { + return h('li.drop-menu-item', { + onClick: () => { + this.props.closeMenu() + this.props.action() + }, + style: { + listStyle: 'none', + padding: '6px 16px 6px 5px', + fontFamily: 'Montserrat Regular', + color: 'rgb(125, 128, 130)', + cursor: 'pointer', + display: 'flex', + justifyContent: 'flex-start', + }, + }, [ + this.props.icon, + this.props.label, + this.activeNetworkRender(), + ]) +} + +DropMenuItem.prototype.activeNetworkRender = function () { + const activeNetwork = this.props.activeNetworkRender + const { provider } = this.props + const providerType = provider ? provider.type : null + if (activeNetwork === undefined) return + + switch (this.props.label) { + case 'Main Ethereum Network': + if (providerType === 'mainnet') return h('.check', '✓') + break + case 'Ropsten Test Network': + if (providerType === 'ropsten') return h('.check', '✓') + break + case 'Kovan Test Network': + if (providerType === 'kovan') return h('.check', '✓') + break + case 'Rinkeby Test Network': + if (providerType === 'rinkeby') return h('.check', '✓') + break + case 'Localhost 8545': + if (activeNetwork === 'http://localhost:8545') return h('.check', '✓') + break + default: + if (activeNetwork === 'custom') return h('.check', '✓') + } +} diff --git a/ui/responsive/app/components/editable-label.js b/ui/responsive/app/components/editable-label.js new file mode 100644 index 000000000..41936f5e0 --- /dev/null +++ b/ui/responsive/app/components/editable-label.js @@ -0,0 +1,51 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const findDOMNode = require('react-dom').findDOMNode + +module.exports = EditableLabel + +inherits(EditableLabel, Component) +function EditableLabel () { + Component.call(this) +} + +EditableLabel.prototype.render = function () { + const props = this.props + const state = this.state + + if (state && state.isEditingLabel) { + return h('div.editable-label', [ + h('input.sizing-input', { + defaultValue: props.textValue, + maxLength: '20', + onKeyPress: (event) => { + this.saveIfEnter(event) + }, + }), + h('button.editable-button', { + onClick: () => this.saveText(), + }, 'Save'), + ]) + } else { + return h('div.name-label', { + onClick: (event) => { + this.setState({ isEditingLabel: true }) + }, + }, this.props.children) + } +} + +EditableLabel.prototype.saveIfEnter = function (event) { + if (event.key === 'Enter') { + this.saveText() + } +} + +EditableLabel.prototype.saveText = function () { + var container = findDOMNode(this) + var text = container.querySelector('.editable-label input').value + var truncatedText = text.substring(0, 20) + this.props.saveText(truncatedText) + this.setState({ isEditingLabel: false, textLabel: truncatedText }) +} diff --git a/ui/responsive/app/components/ens-input.js b/ui/responsive/app/components/ens-input.js new file mode 100644 index 000000000..3a33ebf74 --- /dev/null +++ b/ui/responsive/app/components/ens-input.js @@ -0,0 +1,170 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const extend = require('xtend') +const debounce = require('debounce') +const copyToClipboard = require('copy-to-clipboard') +const ENS = require('ethjs-ens') +const networkMap = require('ethjs-ens/lib/network-map.json') +const ensRE = /.+\.eth$/ +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + + +module.exports = EnsInput + +inherits(EnsInput, Component) +function EnsInput () { + Component.call(this) +} + +EnsInput.prototype.render = function () { + const props = this.props + const opts = extend(props, { + list: 'addresses', + onChange: () => { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + if (!networkHasEnsSupport) return + + const recipient = document.querySelector('input[name="address"]').value + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + }) + } + + this.setState({ + loadingEns: true, + }) + this.checkName() + }, + }) + return h('div', { + style: { width: '100%' }, + }, [ + h('input.large-input', opts), + // The address book functionality. + h('datalist#addresses', + [ + // Corresponds to the addresses owned. + Object.keys(props.identities).map((key) => { + const identity = props.identities[key] + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + // Corresponds to previously sent-to addresses. + props.addressBook.map((identity) => { + return h('option', { + value: identity.address, + label: identity.name, + key: identity.address, + }) + }), + ]), + this.ensIcon(), + ]) +} + +EnsInput.prototype.componentDidMount = function () { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) + this.setState({ ensResolution: ZERO_ADDRESS }) + + if (networkHasEnsSupport) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network }) + this.checkName = debounce(this.lookupEnsName.bind(this), 200) + } +} + +EnsInput.prototype.lookupEnsName = function () { + const recipient = document.querySelector('input[name="address"]').value + const { ensResolution } = this.state + + log.info(`ENS attempting to resolve name: ${recipient}`) + this.ens.lookup(recipient.trim()) + .then((address) => { + if (address === ZERO_ADDRESS) throw new Error('No address has been set for this name.') + if (address !== ensResolution) { + this.setState({ + loadingEns: false, + ensResolution: address, + nickname: recipient.trim(), + hoverText: address + '\nClick to Copy', + ensFailure: false, + }) + } + }) + .catch((reason) => { + log.error(reason) + return this.setState({ + loadingEns: false, + ensResolution: ZERO_ADDRESS, + ensFailure: true, + hoverText: reason.message, + }) + }) +} + +EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { + const state = this.state || {} + const ensResolution = state.ensResolution + // If an address is sent without a nickname, meaning not from ENS or from + // the user's own accounts, a default of a one-space string is used. + const nickname = state.nickname || ' ' + if (prevState && ensResolution && this.props.onChange && + ensResolution !== prevState.ensResolution) { + this.props.onChange(ensResolution, nickname) + } +} + +EnsInput.prototype.ensIcon = function (recipient) { + const { hoverText } = this.state || {} + return h('span', { + title: hoverText, + style: { + position: 'absolute', + padding: '9px', + transform: 'translatex(-40px)', + }, + }, this.ensIconContents(recipient)) +} + +EnsInput.prototype.ensIconContents = function (recipient) { + const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + + if (loadingEns) { + return h('img', { + src: 'images/loading.svg', + style: { + width: '30px', + height: '30px', + transform: 'translateY(-6px)', + }, + }) + } + + if (ensFailure) { + return h('i.fa.fa-warning.fa-lg.warning') + } + + if (ensResolution && (ensResolution !== ZERO_ADDRESS)) { + return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', { + style: { color: 'green' }, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + copyToClipboard(ensResolution) + }, + }) + } +} + +function getNetworkEnsSupport (network) { + return Boolean(networkMap[network]) +} diff --git a/ui/responsive/app/components/eth-balance.js b/ui/responsive/app/components/eth-balance.js new file mode 100644 index 000000000..4f538fd31 --- /dev/null +++ b/ui/responsive/app/components/eth-balance.js @@ -0,0 +1,89 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance +const generateBalanceObject = require('../util').generateBalanceObject +const Tooltip = require('./tooltip.js') +const FiatValue = require('./fiat-value.js') + +module.exports = EthBalanceComponent + +inherits(EthBalanceComponent, Component) +function EthBalanceComponent () { + Component.call(this) +} + +EthBalanceComponent.prototype.render = function () { + var props = this.props + let { value } = props + const { style, width } = props + var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true + value = value ? formatBalance(value, 6, needsParse) : '...' + + return ( + + h('.ether-balance.ether-balance-amount', { + style, + }, [ + h('div', { + style: { + display: 'inline', + width, + }, + }, this.renderBalance(value)), + ]) + + ) +} +EthBalanceComponent.prototype.renderBalance = function (value) { + var props = this.props + const { conversionRate, shorten, incoming, currentCurrency } = props + if (value === 'None') return value + if (value === '...') return value + var balanceObj = generateBalanceObject(value, shorten ? 1 : 3) + var balance + var splitBalance = value.split(' ') + var ethNumber = splitBalance[0] + var ethSuffix = splitBalance[1] + const showFiat = 'showFiat' in props ? props.showFiat : true + + if (shorten) { + balance = balanceObj.shortBalance + } else { + balance = balanceObj.balance + } + + var label = balanceObj.label + + return ( + h(Tooltip, { + position: 'bottom', + title: `${ethNumber} ${ethSuffix}`, + }, h('div.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + }, + }, incoming ? `+${balance}` : balance), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + }, + }, label), + ]), + + showFiat ? h(FiatValue, { value: props.value, conversionRate, currentCurrency }) : null, + ])) + ) +} diff --git a/ui/responsive/app/components/fiat-value.js b/ui/responsive/app/components/fiat-value.js new file mode 100644 index 000000000..8a64a1cfc --- /dev/null +++ b/ui/responsive/app/components/fiat-value.js @@ -0,0 +1,63 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const formatBalance = require('../util').formatBalance + +module.exports = FiatValue + +inherits(FiatValue, Component) +function FiatValue () { + Component.call(this) +} + +FiatValue.prototype.render = function () { + const props = this.props + const { conversionRate, currentCurrency } = props + + const value = formatBalance(props.value, 6) + + if (value === 'None') return value + var fiatDisplayNumber, fiatTooltipNumber + var splitBalance = value.split(' ') + + if (conversionRate !== 0) { + fiatTooltipNumber = Number(splitBalance[0]) * conversionRate + fiatDisplayNumber = fiatTooltipNumber.toFixed(2) + } else { + fiatDisplayNumber = 'N/A' + fiatTooltipNumber = 'Unknown' + } + + return fiatDisplay(fiatDisplayNumber, currentCurrency) +} + +function fiatDisplay (fiatDisplayNumber, fiatSuffix) { + if (fiatDisplayNumber !== 'N/A') { + return h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('div', { + style: { + width: '100%', + textAlign: 'right', + fontSize: '12px', + color: '#333333', + }, + }, fiatDisplayNumber), + h('div', { + style: { + color: '#AEAEAE', + marginLeft: '5px', + fontSize: '12px', + }, + }, fiatSuffix), + ]) + } else { + return h('div') + } +} diff --git a/ui/responsive/app/components/hex-as-decimal-input.js b/ui/responsive/app/components/hex-as-decimal-input.js new file mode 100644 index 000000000..4a71e9585 --- /dev/null +++ b/ui/responsive/app/components/hex-as-decimal-input.js @@ -0,0 +1,154 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const extend = require('xtend') + +module.exports = HexAsDecimalInput + +inherits(HexAsDecimalInput, Component) +function HexAsDecimalInput () { + this.state = { invalid: null } + Component.call(this) +} + +/* Hex as Decimal Input + * + * A component for allowing easy, decimal editing + * of a passed in hex string value. + * + * On change, calls back its `onChange` function parameter + * and passes it an updated hex string. + */ + +HexAsDecimalInput.prototype.render = function () { + const props = this.props + const state = this.state + + const { value, onChange, min, max } = props + + const toEth = props.toEth + const suffix = props.suffix + const decimalValue = decimalize(value, toEth) + const style = props.style + + return ( + h('.flex-column', [ + h('.flex-row', { + style: { + alignItems: 'flex-end', + lineHeight: '13px', + fontFamily: 'Montserrat Light', + textRendering: 'geometricPrecision', + }, + }, [ + h('input.hex-input', { + type: 'number', + required: true, + min: min, + max: max, + style: extend({ + display: 'block', + textAlign: 'right', + backgroundColor: 'transparent', + border: '1px solid #bdbdbd', + + }, style), + value: parseInt(decimalValue), + onBlur: (event) => { + this.updateValidity(event) + }, + onChange: (event) => { + this.updateValidity(event) + const hexString = (event.target.value === '') ? '' : hexify(event.target.value) + onChange(hexString) + }, + onInvalid: (event) => { + const msg = this.constructWarning() + if (msg === state.invalid) { + return + } + this.setState({ invalid: msg }) + event.preventDefault() + return false + }, + }), + h('div', { + style: { + color: ' #AEAEAE', + fontSize: '12px', + marginLeft: '5px', + marginRight: '6px', + width: '20px', + }, + }, suffix), + ]), + + state.invalid ? h('span.error', { + style: { + position: 'absolute', + right: '0px', + textAlign: 'right', + transform: 'translateY(26px)', + padding: '3px', + background: 'rgba(255,255,255,0.85)', + zIndex: '1', + textTransform: 'capitalize', + border: '2px solid #E20202', + }, + }, state.invalid) : null, + ]) + ) +} + +HexAsDecimalInput.prototype.setValid = function (message) { + this.setState({ invalid: null }) +} + +HexAsDecimalInput.prototype.updateValidity = function (event) { + const target = event.target + const value = this.props.value + const newValue = target.value + + if (value === newValue) { + return + } + + const valid = target.checkValidity() + if (valid) { + this.setState({ invalid: null }) + } +} + +HexAsDecimalInput.prototype.constructWarning = function () { + const { name, min, max } = this.props + let message = name ? name + ' ' : '' + + if (min && max) { + message += `must be greater than or equal to ${min} and less than or equal to ${max}.` + } else if (min) { + message += `must be greater than or equal to ${min}.` + } else if (max) { + message += `must be less than or equal to ${max}.` + } else { + message += 'Invalid input.' + } + + return message +} + +function hexify (decimalString) { + const hexBN = new BN(parseInt(decimalString), 10) + return '0x' + hexBN.toString('hex') +} + +function decimalize (input, toEth) { + if (input === '') { + return '' + } else { + const strippedInput = ethUtil.stripHexPrefix(input) + const inputBN = new BN(strippedInput, 'hex') + return inputBN.toString(10) + } +} diff --git a/ui/responsive/app/components/identicon.js b/ui/responsive/app/components/identicon.js new file mode 100644 index 000000000..c754bc6ba --- /dev/null +++ b/ui/responsive/app/components/identicon.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const isNode = require('detect-node') +const findDOMNode = require('react-dom').findDOMNode +const jazzicon = require('jazzicon') +const iconFactoryGen = require('../../lib/icon-factory') +const iconFactory = iconFactoryGen(jazzicon) + +module.exports = IdenticonComponent + +inherits(IdenticonComponent, Component) +function IdenticonComponent () { + Component.call(this) + + this.defaultDiameter = 46 +} + +IdenticonComponent.prototype.render = function () { + var props = this.props + var diameter = props.diameter || this.defaultDiameter + return ( + h('div', { + key: 'identicon-' + this.props.address, + style: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: diameter, + width: diameter, + borderRadius: diameter / 2, + overflow: 'hidden', + }, + }) + ) +} + +IdenticonComponent.prototype.componentDidMount = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + +IdenticonComponent.prototype.componentDidUpdate = function () { + var props = this.props + const { address } = props + + if (!address) return + + var container = findDOMNode(this) + + var children = container.children + for (var i = 0; i < children.length; i++) { + container.removeChild(children[i]) + } + + var diameter = props.diameter || this.defaultDiameter + if (!isNode) { + var img = iconFactory.iconForAddress(address, diameter) + container.appendChild(img) + } +} + diff --git a/ui/responsive/app/components/loading.js b/ui/responsive/app/components/loading.js new file mode 100644 index 000000000..87d6f5d20 --- /dev/null +++ b/ui/responsive/app/components/loading.js @@ -0,0 +1,53 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') + + +inherits(LoadingIndicator, Component) +module.exports = LoadingIndicator + +function LoadingIndicator () { + Component.call(this) +} + +LoadingIndicator.prototype.render = function () { + const { isLoading, loadingMessage } = this.props + + return ( + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'loader', + transitionEnterTimeout: 150, + transitionLeaveTimeout: 150, + }, [ + + isLoading ? h('div', { + style: { + zIndex: 10, + position: 'absolute', + flexDirection: 'column', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.8)', + }, + }, [ + h('img', { + src: 'images/loading.svg', + }), + + h('br'), + + showMessageIfAny(loadingMessage), + ]) : null, + ]) + ) +} + +function showMessageIfAny (loadingMessage) { + if (!loadingMessage) return null + return h('span', loadingMessage) +} diff --git a/ui/responsive/app/components/mascot.js b/ui/responsive/app/components/mascot.js new file mode 100644 index 000000000..973ec2cad --- /dev/null +++ b/ui/responsive/app/components/mascot.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const metamaskLogo = require('metamask-logo') +const debounce = require('debounce') + +module.exports = Mascot + +inherits(Mascot, Component) +function Mascot () { + Component.call(this) + this.logo = metamaskLogo({ + followMouse: true, + pxNotRatio: true, + width: 200, + height: 200, + }) + + this.refollowMouse = debounce(this.logo.setFollowMouse.bind(this.logo, true), 1000) + this.unfollowMouse = this.logo.setFollowMouse.bind(this.logo, false) +} + +Mascot.prototype.render = function () { + // this is a bit hacky + // the event emitter is on `this.props` + // and we dont get that until render + this.handleAnimationEvents() + + return h('#metamask-mascot-container', { + style: { zIndex: 0 }, + }) +} + +Mascot.prototype.componentDidMount = function () { + var targetDivId = 'metamask-mascot-container' + var container = document.getElementById(targetDivId) + container.appendChild(this.logo.container) +} + +Mascot.prototype.componentWillUnmount = function () { + this.animations = this.props.animationEventEmitter + this.animations.removeAllListeners() + this.logo.container.remove() + this.logo.stopAnimation() +} + +Mascot.prototype.handleAnimationEvents = function () { + // only setup listeners once + if (this.animations) return + this.animations = this.props.animationEventEmitter + this.animations.on('point', this.lookAt.bind(this)) + this.animations.on('setFollowMouse', this.logo.setFollowMouse.bind(this.logo)) +} + +Mascot.prototype.lookAt = function (target) { + this.unfollowMouse() + this.logo.lookAt(target) + this.refollowMouse() +} diff --git a/ui/responsive/app/components/mini-account-panel.js b/ui/responsive/app/components/mini-account-panel.js new file mode 100644 index 000000000..c09cf5b7a --- /dev/null +++ b/ui/responsive/app/components/mini-account-panel.js @@ -0,0 +1,74 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const Identicon = require('./identicon') + +module.exports = AccountPanel + + +inherits(AccountPanel, Component) +function AccountPanel () { + Component.call(this) +} + +AccountPanel.prototype.render = function () { + var props = this.props + var picOrder = props.picOrder || 'left' + const { imageSeed } = props + + return ( + + h('.identity-panel.flex-row.flex-left', { + style: { + cursor: props.onClick ? 'pointer' : undefined, + }, + onClick: props.onClick, + }, [ + + this.genIcon(imageSeed, picOrder), + + h('div.flex-column.flex-justify-center', { + style: { + lineHeight: '15px', + order: 2, + display: 'flex', + alignItems: picOrder === 'left' ? 'flex-begin' : 'flex-end', + }, + }, this.props.children), + ]) + ) +} + +AccountPanel.prototype.genIcon = function (seed, picOrder) { + const props = this.props + + // When there is no seed value, this is a contract creation. + // We then show the contract icon. + if (!seed) { + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h('i.fa.fa-file-text-o.fa-lg', { + style: { + fontSize: '42px', + transform: 'translate(0px, -16px)', + }, + }), + ]) + } + + // If there was a seed, we return an identicon for that address. + return h('.identicon-wrapper.flex-column.select-none', { + style: { + order: picOrder === 'left' ? 1 : 3, + }, + }, [ + h(Identicon, { + address: seed, + imageify: props.imageifyIdenticons, + }), + ]) +} + diff --git a/ui/responsive/app/components/network.js b/ui/responsive/app/components/network.js new file mode 100644 index 000000000..d5d3e18cd --- /dev/null +++ b/ui/responsive/app/components/network.js @@ -0,0 +1,125 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = Network + +inherits(Network, Component) + +function Network () { + Component.call(this) +} + +Network.prototype.render = function () { + const props = this.props + const networkNumber = props.network + let providerName + try { + providerName = props.provider.type + } catch (e) { + providerName = null + } + let iconName, hoverText + + if (networkNumber === 'loading') { + return h('span', { + style: { + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + }, + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-sort-desc'), + ]) + + } else if (providerName === 'mainnet') { + hoverText = 'Main Ethereum Network' + iconName = 'ethereum-network' + } else if (providerName === 'ropsten') { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (parseInt(networkNumber) === 3) { + hoverText = 'Ropsten Test Network' + iconName = 'ropsten-test-network' + } else if (providerName === 'kovan') { + hoverText = 'Kovan Test Network' + iconName = 'kovan-test-network' + } else if (providerName === 'rinkeby') { + hoverText = 'Rinkeby Test Network' + iconName = 'rinkeby-test-network' + } else { + hoverText = 'Unknown Private Network' + iconName = 'unknown-private-network' + } + + return ( + h('#network_component.pointer', { + title: hoverText, + onClick: (event) => this.props.onClick(event), + }, [ + (function () { + switch (iconName) { + case 'ethereum-network': + return h('.network-indicator', [ + h('.menu-icon.diamond'), + h('.network-name', { + style: { + color: '#039396', + }}, + 'Ethereum Main Net'), + ]) + case 'ropsten-test-network': + return h('.network-indicator', [ + h('.menu-icon.red-dot'), + h('.network-name', { + style: { + color: '#ff6666', + }}, + 'Ropsten Test Net'), + ]) + case 'kovan-test-network': + return h('.network-indicator', [ + h('.menu-icon.hollow-diamond'), + h('.network-name', { + style: { + color: '#690496', + }}, + 'Kovan Test Net'), + ]) + case 'rinkeby-test-network': + return h('.network-indicator', [ + h('.menu-icon.golden-square'), + h('.network-name', { + style: { + color: '#e7a218', + }}, + 'Rinkeby Test Net'), + ]) + default: + return h('.network-indicator', [ + h('i.fa.fa-question-circle.fa-lg', { + style: { + margin: '10px', + color: 'rgb(125, 128, 130)', + }, + }), + + h('.network-name', { + style: { + color: '#AEAEAE', + }}, + 'Private Network'), + ]) + } + })(), + ]) + ) +} diff --git a/ui/responsive/app/components/notice.js b/ui/responsive/app/components/notice.js new file mode 100644 index 000000000..d9f0067cd --- /dev/null +++ b/ui/responsive/app/components/notice.js @@ -0,0 +1,126 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const ReactMarkdown = require('react-markdown') +const linker = require('extension-link-enabler') +const findDOMNode = require('react-dom').findDOMNode + +module.exports = Notice + +inherits(Notice, Component) +function Notice () { + Component.call(this) +} + +Notice.prototype.render = function () { + const { notice, onConfirm } = this.props + const { title, date, body } = notice + const state = this.state || { disclaimerDisabled: true } + const disabled = state.disclaimerDisabled + + return ( + h('.flex-column.flex-center.flex-grow', [ + h('h3.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + title, + ]), + + h('h5.flex-center.text-transform-uppercase.terms-header', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + textAlign: 'center', + padding: 6, + }, + }, [ + date, + ]), + + h('style', ` + + .markdown { + overflow-x: hidden; + } + + .markdown h1, .markdown h2, .markdown h3 { + margin: 10px 0; + font-weight: bold; + } + + .markdown strong { + font-weight: bold; + } + .markdown em { + font-style: italic; + } + + .markdown p { + margin: 10px 0; + } + + .markdown a { + color: #df6b0e; + } + + `), + + h('div.markdown', { + onScroll: (e) => { + var object = e.currentTarget + if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) { + this.setState({disclaimerDisabled: false}) + } + }, + style: { + background: 'rgb(235, 235, 235)', + height: '310px', + padding: '6px', + width: '90%', + overflowY: 'scroll', + scroll: 'auto', + }, + }, [ + h(ReactMarkdown, { + className: 'notice-box', + source: body, + skipHtml: true, + }), + ]), + + h('button', { + disabled, + onClick: () => { + this.setState({disclaimerDisabled: true}) + onConfirm() + }, + style: { + marginTop: '18px', + }, + }, 'Accept'), + ]) + ) +} + +Notice.prototype.componentDidMount = function () { + var node = findDOMNode(this) + linker.setupListener(node) + if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) { + this.setState({disclaimerDisabled: false}) + } +} + +Notice.prototype.componentWillUnmount = function () { + var node = findDOMNode(this) + linker.teardownListener(node) +} diff --git a/ui/responsive/app/components/pending-msg-details.js b/ui/responsive/app/components/pending-msg-details.js new file mode 100644 index 000000000..16308d121 --- /dev/null +++ b/ui/responsive/app/components/pending-msg-details.js @@ -0,0 +1,50 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-row.flex-space-between', [ + h('label.font-small', 'MESSAGE'), + h('span.font-small', msgParams.data), + ]), + ]), + + ]) + ) +} + diff --git a/ui/responsive/app/components/pending-msg.js b/ui/responsive/app/components/pending-msg.js new file mode 100644 index 000000000..b2cac164a --- /dev/null +++ b/ui/responsive/app/components/pending-msg.js @@ -0,0 +1,56 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + h('.error', { + style: { + margin: '10px', + }, + }, `Signing this message can have + dangerous side effects. Only sign messages from + sites you fully trust with your entire account. + This will be fixed in a future version.`), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelMessage, + }, 'Cancel'), + h('button', { + onClick: state.signMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/responsive/app/components/pending-personal-msg-details.js b/ui/responsive/app/components/pending-personal-msg-details.js new file mode 100644 index 000000000..1050513f2 --- /dev/null +++ b/ui/responsive/app/components/pending-personal-msg-details.js @@ -0,0 +1,60 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') +const BinaryRenderer = require('./binary-renderer') + +module.exports = PendingMsgDetails + +inherits(PendingMsgDetails, Component) +function PendingMsgDetails () { + Component.call(this) +} + +PendingMsgDetails.prototype.render = function () { + var state = this.props + var msgData = state.txData + + var msgParams = msgData.msgParams || {} + var address = msgParams.from || state.selectedAddress + var identity = state.identities[address] || { address: address } + var account = state.accounts[address] || { address: address } + + var { data } = msgParams + + return ( + h('div', { + key: msgData.id, + style: { + margin: '10px 20px', + }, + }, [ + + // account that will sign + h(AccountPanel, { + showFullAddress: true, + identity: identity, + account: account, + imageifyIdenticons: state.imageifyIdenticons, + }), + + // message data + h('div', { + style: { + height: '260px', + }, + }, [ + h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), + h(BinaryRenderer, { + value: data, + style: { + height: '215px', + }, + }), + ]), + + ]) + ) +} + diff --git a/ui/responsive/app/components/pending-personal-msg.js b/ui/responsive/app/components/pending-personal-msg.js new file mode 100644 index 000000000..4542adb28 --- /dev/null +++ b/ui/responsive/app/components/pending-personal-msg.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-personal-msg-details') + +module.exports = PendingMsg + +inherits(PendingMsg, Component) +function PendingMsg () { + Component.call(this) +} + +PendingMsg.prototype.render = function () { + var state = this.props + var msgData = state.txData + + return ( + + h('div', { + key: msgData.id, + }, [ + + // header + h('h3', { + style: { + fontWeight: 'bold', + textAlign: 'center', + }, + }, 'Sign Message'), + + // message details + h(PendingTxDetails, state), + + // sign + cancel + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelPersonalMessage, + }, 'Cancel'), + h('button', { + onClick: state.signPersonalMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/responsive/app/components/pending-tx.js b/ui/responsive/app/components/pending-tx.js new file mode 100644 index 000000000..962680d30 --- /dev/null +++ b/ui/responsive/app/components/pending-tx.js @@ -0,0 +1,480 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const actions = require('../actions') +const clone = require('clone') + +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +const util = require('../util') +const MiniAccountPanel = require('./mini-account-panel') +const Copyable = require('./copyable') +const EthBalance = require('./eth-balance') +const addressSummary = util.addressSummary +const nameForAddress = require('../../lib/contract-namer') +const BNInput = require('./bn-as-decimal-input') + +const MIN_GAS_PRICE_GWEI_BN = new BN(2) +const GWEI_FACTOR = new BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) +const MIN_GAS_LIMIT_BN = new BN(21000) + +module.exports = PendingTx +inherits(PendingTx, Component) +function PendingTx () { + Component.call(this) + this.state = { + valid: true, + txData: null, + submitting: false, + } +} + +PendingTx.prototype.render = function () { + const props = this.props + const { currentCurrency, blockGasLimit } = props + + const conversionRate = props.conversionRate + const txMeta = this.gatherTxMeta() + const txParams = txMeta.txParams || {} + + // Account Details + const address = txParams.from || props.selectedAddress + const identity = props.identities[address] || { address: address } + const account = props.accounts[address] + const balance = account ? account.balance : '0x0' + + // recipient check + const isValidAddress = !txParams.to || util.isValidAddress(txParams.to) + + // Gas + const gas = txParams.gas + const gasBn = hexToBn(gas) + const gasLimit = new BN(parseInt(blockGasLimit)) + const safeGasLimit = this.bnMultiplyByFraction(gasLimit, 19, 20).toString(10) + + // Gas Price + const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16) + const gasPriceBn = hexToBn(gasPrice) + + const txFeeBn = gasBn.mul(gasPriceBn) + const valueBn = hexToBn(txParams.value) + const maxCost = txFeeBn.add(valueBn) + + const dataLength = txParams.data ? (txParams.data.length - 2) / 2 : 0 + + const balanceBn = hexToBn(balance) + const insufficientBalance = balanceBn.lt(maxCost) + + this.inputs = [] + + return ( + + h('div', { + key: txMeta.id, + }, [ + + h('form#pending-tx-form', { + onSubmit: this.onSubmit.bind(this), + + }, [ + + // tx info + h('div', [ + + h('.flex-row.flex-center', { + style: { + maxWidth: '100%', + }, + }, [ + + h(MiniAccountPanel, { + imageSeed: address, + picOrder: 'right', + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, identity.name), + + h(Copyable, { + value: ethUtil.toChecksumAddress(address), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(address, 6, 4, false)), + ]), + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, [ + h(EthBalance, { + value: balance, + conversionRate, + currentCurrency, + inline: true, + labelColor: '#F7861C', + }), + ]), + ]), + + forwardCarrat(), + + this.miniAccountPanelForRecipient(), + ]), + + h('style', ` + .table-box { + margin: 7px 0px 0px 0px; + width: 100%; + } + .table-box .row { + margin: 0px; + background: rgb(236,236,236); + display: flex; + justify-content: space-between; + font-family: Montserrat Light, sans-serif; + font-size: 13px; + padding: 5px 25px; + } + .table-box .row .value { + font-family: Montserrat Regular; + } + `), + + h('.table-box', [ + + // Ether Value + // Currently not customizable, but easily modified + // in the way that gas and gasLimit currently are. + h('.row', [ + h('.cell.label', 'Amount'), + h(EthBalance, { value: txParams.value, currentCurrency, conversionRate }), + ]), + + // Gas Limit (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Limit'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Limit', + value: gasBn, + precision: 0, + scale: 0, + // The hard lower limit for gas. + min: MIN_GAS_LIMIT_BN.toString(10), + max: safeGasLimit, + suffix: 'UNITS', + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasLimitChanged.bind(this), + + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Gas Price (customizable) + h('.cell.row', [ + h('.cell.label', 'Gas Price'), + h('.cell.value', { + }, [ + h(BNInput, { + name: 'Gas Price', + value: gasPriceBn, + precision: 9, + scale: 9, + suffix: 'GWEI', + min: MIN_GAS_PRICE_GWEI_BN.toString(10), + style: { + position: 'relative', + top: '5px', + }, + onChange: this.gasPriceChanged.bind(this), + ref: (hexInput) => { this.inputs.push(hexInput) }, + }), + ]), + ]), + + // Max Transaction Fee (calculated) + h('.cell.row', [ + h('.cell.label', 'Max Transaction Fee'), + h(EthBalance, { value: txFeeBn.toString(16), currentCurrency, conversionRate }), + ]), + + h('.cell.row', { + style: { + fontFamily: 'Montserrat Regular', + background: 'white', + padding: '10px 25px', + }, + }, [ + h('.cell.label', 'Max Total'), + h('.cell.value', { + style: { + display: 'flex', + alignItems: 'center', + }, + }, [ + h(EthBalance, { + value: maxCost.toString(16), + currentCurrency, + conversionRate, + inline: true, + labelColor: 'black', + fontSize: '16px', + }), + ]), + ]), + + // Data size row: + h('.cell.row', { + style: { + background: '#f7f7f7', + paddingBottom: '0px', + }, + }, [ + h('.cell.label'), + h('.cell.value', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '11px', + }, + }, `Data included: ${dataLength} bytes`), + ]), + ]), // End of Table + + ]), + + h('style', ` + .conf-buttons button { + margin-left: 10px; + text-transform: uppercase; + } + `), + + txMeta.simulationFails ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Transaction Error. Exception thrown in contract code.') + : null, + + !isValidAddress ? + h('.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.') + : null, + + insufficientBalance ? + h('span.error', { + style: { + marginLeft: 50, + fontSize: '0.9em', + }, + }, 'Insufficient balance for transaction') + : null, + + // send + cancel + h('.flex-row.flex-space-around.conf-buttons', { + style: { + display: 'flex', + justifyContent: 'flex-end', + margin: '14px 25px', + }, + }, [ + + + insufficientBalance ? + h('button.btn-green', { + onClick: props.buyEth, + }, 'Buy Ether') + : null, + + h('button', { + onClick: (event) => { + this.resetGasFields() + event.preventDefault() + }, + }, 'Reset'), + + // Accept Button + h('input.confirm.btn-green', { + type: 'submit', + value: 'SUBMIT', + style: { marginLeft: '10px' }, + disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, + }), + + h('button.cancel.btn-red', { + onClick: props.cancelTransaction, + }, 'Reject'), + ]), + ]), + ]) + ) +} + +PendingTx.prototype.miniAccountPanelForRecipient = function () { + const props = this.props + const txData = props.txData + const txParams = txData.txParams || {} + const isContractDeploy = !('to' in txParams) + + // If it's not a contract deploy, send to the account + if (!isContractDeploy) { + return h(MiniAccountPanel, { + imageSeed: txParams.to, + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, nameForAddress(txParams.to, props.identities)), + + h(Copyable, { + value: ethUtil.toChecksumAddress(txParams.to), + }, [ + h('span.font-small', { + style: { + fontFamily: 'Montserrat Light, Montserrat, sans-serif', + }, + }, addressSummary(txParams.to, 6, 4, false)), + ]), + + ]) + } else { + return h(MiniAccountPanel, { + picOrder: 'left', + }, [ + + h('span.font-small', { + style: { + fontFamily: 'Montserrat Bold, Montserrat, sans-serif', + }, + }, 'New Contract'), + + ]) + } +} + +PendingTx.prototype.gasPriceChanged = function (newBN, valid) { + log.info(`Gas price changed to: ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gasPrice = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.gasLimitChanged = function (newBN, valid) { + log.info(`Gas limit changed to ${newBN.toString(10)}`) + const txMeta = this.gatherTxMeta() + txMeta.txParams.gas = '0x' + newBN.toString('hex') + this.setState({ + txData: clone(txMeta), + valid, + }) +} + +PendingTx.prototype.resetGasFields = function () { + log.debug(`pending-tx resetGasFields`) + + this.inputs.forEach((hexInput) => { + if (hexInput) { + hexInput.setValid() + } + }) + + this.setState({ + txData: null, + valid: true, + }) +} + +PendingTx.prototype.onSubmit = function (event) { + event.preventDefault() + const txMeta = this.gatherTxMeta() + const valid = this.checkValidity() + this.setState({ valid, submitting: true }) + if (valid && this.verifyGasParams()) { + this.props.sendTransaction(txMeta, event) + } else { + this.props.dispatch(actions.displayWarning('Invalid Gas Parameters')) + this.setState({ submitting: false }) + } +} + +PendingTx.prototype.checkValidity = function () { + const form = this.getFormEl() + const valid = form.checkValidity() + return valid +} + +PendingTx.prototype.getFormEl = function () { + const form = document.querySelector('form#pending-tx-form') + // Stub out form for unit tests: + if (!form) { + return { checkValidity () { return true } } + } + return form +} + +// After a customizable state value has been updated, +PendingTx.prototype.gatherTxMeta = function () { + log.debug(`pending-tx gatherTxMeta`) + const props = this.props + const state = this.state + const txData = clone(state.txData) || clone(props.txData) + + log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`) + return txData +} + +PendingTx.prototype.verifyGasParams = function () { + // We call this in case the gas has not been modified at all + if (!this.state) { return true } + return ( + this._notZeroOrEmptyString(this.state.gas) && + this._notZeroOrEmptyString(this.state.gasPrice) + ) +} + +PendingTx.prototype._notZeroOrEmptyString = function (obj) { + return obj !== '' && obj !== '0x0' +} + +PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function forwardCarrat () { + return ( + h('img', { + src: 'images/forward-carrat.svg', + style: { + padding: '5px 6px 0px 10px', + height: '37px', + }, + }) + ) +} diff --git a/ui/responsive/app/components/qr-code.js b/ui/responsive/app/components/qr-code.js new file mode 100644 index 000000000..06b9aed9b --- /dev/null +++ b/ui/responsive/app/components/qr-code.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const qrCode = require('qrcode-npm').qrcode +const inherits = require('util').inherits +const connect = require('react-redux').connect +const isHexPrefixed = require('ethereumjs-util').isHexPrefixed +const CopyButton = require('./copyButton') + +module.exports = connect(mapStateToProps)(QrCodeView) + +function mapStateToProps (state) { + return { + Qr: state.appState.Qr, + buyView: state.appState.buyView, + warning: state.appState.warning, + } +} + +inherits(QrCodeView, Component) + +function QrCodeView () { + Component.call(this) +} + +QrCodeView.prototype.render = function () { + const props = this.props + const Qr = props.Qr + const address = `${isHexPrefixed(Qr.data) ? 'ethereum:' : ''}${Qr.data}` + const qrImage = qrCode(4, 'M') + qrImage.addData(address) + qrImage.make() + return h('.main-container.flex-column', { + key: 'qr', + style: { + justifyContent: 'center', + paddingBottom: '45px', + paddingLeft: '45px', + paddingRight: '45px', + alignItems: 'center', + }, + }, [ + Array.isArray(Qr.message) ? h('.message-container', this.renderMultiMessage()) : h('.qr-header', Qr.message), + + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : null, + + h('#qr-container.flex-column', { + style: { + marginTop: '25px', + marginBottom: '15px', + }, + dangerouslySetInnerHTML: { + __html: qrImage.createTableTag(4), + }, + }), + h('.flex-row', [ + h('h3.ellip-address', { + style: { + width: '247px', + }, + }, Qr.data), + h(CopyButton, { + value: Qr.data, + }), + ]), + ]) +} + +QrCodeView.prototype.renderMultiMessage = function () { + var Qr = this.props.Qr + var multiMessage = Qr.message.map((message) => h('.qr-message', message)) + return multiMessage +} diff --git a/ui/responsive/app/components/range-slider.js b/ui/responsive/app/components/range-slider.js new file mode 100644 index 000000000..823f5eb01 --- /dev/null +++ b/ui/responsive/app/components/range-slider.js @@ -0,0 +1,58 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = RangeSlider + +inherits(RangeSlider, Component) +function RangeSlider () { + Component.call(this) +} + +RangeSlider.prototype.render = function () { + const state = this.state || {} + const props = this.props + const onInput = props.onInput || function () {} + const name = props.name + const { + min = 0, + max = 100, + increment = 1, + defaultValue = 50, + mirrorInput = false, + } = this.props.options + const {container, input, range} = props.style + + return ( + h('.flex-row', { + style: container, + }, [ + h('input', { + type: 'range', + name: name, + min: min, + max: max, + step: increment, + style: range, + value: state.value || defaultValue, + onChange: mirrorInput ? this.mirrorInputs.bind(this, event) : onInput, + }), + + // Mirrored input for range + mirrorInput ? h('input.large-input', { + type: 'number', + name: `${name}Mirror`, + min: min, + max: max, + value: state.value || defaultValue, + step: increment, + style: input, + onChange: this.mirrorInputs.bind(this, event), + }) : null, + ]) + ) +} + +RangeSlider.prototype.mirrorInputs = function (event) { + this.setState({value: event.target.value}) +} diff --git a/ui/responsive/app/components/shapeshift-form.js b/ui/responsive/app/components/shapeshift-form.js new file mode 100644 index 000000000..e0a720426 --- /dev/null +++ b/ui/responsive/app/components/shapeshift-form.js @@ -0,0 +1,306 @@ +const PersistentForm = require('../../lib/persistent-form') +const h = require('react-hyperscript') +const inherits = require('util').inherits +const connect = require('react-redux').connect +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const actions = require('../actions') +const Qr = require('./qr-code') +const isValidAddress = require('../util').isValidAddress +module.exports = connect(mapStateToProps)(ShapeshiftForm) + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + isSubLoading: state.appState.isSubLoading, + qrRequested: state.appState.qrRequested, + } +} + +inherits(ShapeshiftForm, PersistentForm) + +function ShapeshiftForm () { + PersistentForm.call(this) + this.persistentFormParentId = 'shapeshift-buy-form' +} + +ShapeshiftForm.prototype.render = function () { + return h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + this.props.qrRequested ? h(Qr, {key: 'qr'}) : this.renderMain(), + ]) +} + +ShapeshiftForm.prototype.renderMain = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('.flex-column', { + style: { + // marginTop: '10px', + padding: '25px', + paddingTop: '5px', + width: '100%', + minHeight: '215px', + alignItems: 'center', + overflowY: 'auto', + }, + }, [ + h('.flex-row', { + style: { + justifyContent: 'center', + alignItems: 'baseline', + height: '42px', + }, + }, [ + h('img', { + src: coinOptions[coin].image, + width: '25px', + height: '25px', + style: { + marginRight: '5px', + }, + }), + + h('.input-container', [ + h('input#fromCoin.buy-inputs.ex-coins', { + type: 'text', + list: 'coinList', + autoFocus: true, + dataset: { + persistentFormId: 'input-coin', + }, + style: { + boxSizing: 'border-box', + }, + onChange: this.handleLiveInput.bind(this), + defaultValue: 'BTC', + }), + + this.renderCoinList(), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '48px', + left: '106px', + }, + }), + ]), + + h('.icon-control', [ + h('i.fa.fa-refresh.fa-4.orange', { + style: { + bottom: '5px', + left: '5px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + h('i.fa.fa-chevron-right.fa-4.orange', { + style: { + position: 'relative', + bottom: '26px', + left: '10px', + color: '#F7861C', + }, + onClick: this.updateCoin.bind(this), + }), + ]), + + h('#toCoin.ex-coins', marketinfo.pair.split('_')[1].toUpperCase()), + + h('img', { + src: coinOptions[marketinfo.pair.split('_')[1].toUpperCase()].image, + width: '25px', + height: '25px', + style: { + marginLeft: '5px', + }, + }), + ]), + h('.flex-column', { + style: { + alignItems: 'flex-start', + }, + }, [ + this.props.warning ? this.props.warning && h('span.error.flex-center', { + style: { + textAlign: 'center', + width: '229px', + height: '82px', + }, + }, + this.props.warning) : this.renderInfo(), + ]), + + h(this.activeToggle('.input-container'), { + style: { + padding: '10px', + paddingTop: '0px', + width: '100%', + }, + }, [ + + h('div', `${coin} Address:`), + + h('input#fromCoinAddress.buy-inputs', { + type: 'text', + placeholder: `Your ${coin} Refund Address`, + dataset: { + persistentFormId: 'refund-address', + }, + style: { + boxSizing: 'border-box', + width: '227px', + height: '30px', + padding: ' 5px ', + }, + }), + + h('i.fa.fa-pencil-square-o.edit-text', { + style: { + fontSize: '12px', + color: '#F7861C', + position: 'relative', + bottom: '10px', + right: '11px', + }, + }), + h('.flex-row', { + style: { + justifyContent: 'flex-end', + }, + }, [ + h('button', { + onClick: this.shift.bind(this), + style: { + marginTop: '10px', + position: 'relative', + bottom: '40px', + }, + }, + 'Submit'), + ]), + ]), + ]) +} + +ShapeshiftForm.prototype.shift = function () { + var props = this.props + var withdrawal = this.props.buyView.buyAddress + var returnAddress = document.getElementById('fromCoinAddress').value + var pair = this.props.buyView.formView.marketinfo.pair + var data = { + 'withdrawal': withdrawal, + 'pair': pair, + 'returnAddress': returnAddress, + // Public api key + 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', + } + var message = [ + `Deposit Limit: ${props.buyView.formView.marketinfo.limit}`, + `Deposit Minimum:${props.buyView.formView.marketinfo.minimum}`, + ] + if (isValidAddress(withdrawal)) { + this.props.dispatch(actions.coinShiftRquest(data, message)) + } +} + +ShapeshiftForm.prototype.renderCoinList = function () { + var list = Object.keys(this.props.buyView.formView.coinOptions).map((item) => { + return h('option', { + value: item, + }, item) + }) + + return h('datalist#coinList', { + onClick: (event) => { + event.preventDefault() + }, + }, list) +} + +ShapeshiftForm.prototype.updateCoin = function (event) { + event.preventDefault() + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + var message = 'Not a valid coin' + return props.dispatch(actions.displayWarning(message)) + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.handleLiveInput = function () { + const props = this.props + var coinOptions = this.props.buyView.formView.coinOptions + var coin = document.getElementById('fromCoin').value + + if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { + return null + } else { + return props.dispatch(actions.pairUpdate(coin)) + } +} + +ShapeshiftForm.prototype.renderInfo = function () { + const marketinfo = this.props.buyView.formView.marketinfo + const coinOptions = this.props.buyView.formView.coinOptions + var coin = marketinfo.pair.split('_')[0].toUpperCase() + + return h('span', { + style: { + }, + }, [ + h('h3.flex-row.text-transform-uppercase', { + style: { + color: '#868686', + paddingTop: '4px', + justifyContent: 'space-around', + textAlign: 'center', + fontSize: '17px', + }, + }, `Market Info for ${marketinfo.pair.replace('_', ' to ').toUpperCase()}:`), + h('.marketinfo', ['Status : ', `${coinOptions[coin].status}`]), + h('.marketinfo', ['Exchange Rate: ', `${marketinfo.rate}`]), + h('.marketinfo', ['Limit: ', `${marketinfo.limit}`]), + h('.marketinfo', ['Minimum : ', `${marketinfo.minimum}`]), + ]) +} + +ShapeshiftForm.prototype.activeToggle = function (elementType) { + if (!this.props.buyView.formView.response || this.props.warning) return elementType + return `${elementType}.inactive` +} + +ShapeshiftForm.prototype.renderLoading = function () { + return h('span', { + style: { + position: 'absolute', + left: '70px', + bottom: '194px', + background: 'transparent', + width: '229px', + height: '82px', + display: 'flex', + justifyContent: 'center', + }, + }, [ + h('img', { + style: { + width: '60px', + }, + src: 'images/loading.svg', + }), + ]) +} diff --git a/ui/responsive/app/components/shift-list-item.js b/ui/responsive/app/components/shift-list-item.js new file mode 100644 index 000000000..32bfbeda4 --- /dev/null +++ b/ui/responsive/app/components/shift-list-item.js @@ -0,0 +1,204 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const vreme = new (require('vreme')) +const explorerLink = require('../../lib/explorer-link') +const actions = require('../actions') +const addressSummary = require('../util').addressSummary + +const CopyButton = require('./copyButton') +const EthBalance = require('./eth-balance') +const Tooltip = require('./tooltip') + + +module.exports = connect(mapStateToProps)(ShiftListItem) + +function mapStateToProps (state) { + return { + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } +} + +inherits(ShiftListItem, Component) + +function ShiftListItem () { + Component.call(this) +} + +ShiftListItem.prototype.render = function () { + return ( + h('.transaction-list-item.flex-row', { + style: { + paddingTop: '20px', + paddingBottom: '20px', + justifyContent: 'space-around', + alignItems: 'center', + }, + }, [ + h('div', { + style: { + width: '0px', + position: 'relative', + bottom: '19px', + }, + }, [ + h('img', { + src: 'https://info.shapeshift.io/sites/default/files/logo.png', + style: { + height: '35px', + width: '132px', + position: 'absolute', + clip: 'rect(0px,23px,34px,0px)', + }, + }), + ]), + + this.renderInfo(), + this.renderUtilComponents(), + ]) + ) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +ShiftListItem.prototype.renderUtilComponents = function () { + var props = this.props + const { conversionRate, currentCurrency } = props + + switch (props.response.status) { + case 'no_deposits': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.depositAddress, + }), + h(Tooltip, { + title: 'QR Code', + }, [ + h('i.fa.fa-qrcode.pointer.pop-hover', { + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + style: { + margin: '5px', + marginLeft: '23px', + marginRight: '12px', + fontSize: '20px', + color: '#F7861C', + }, + }), + ]), + ]) + case 'received': + return h('.flex-row') + + case 'complete': + return h('.flex-row', [ + h(CopyButton, { + value: this.props.response.transaction, + }), + h(EthBalance, { + value: `${props.response.outgoingCoin}`, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + needsParse: false, + incoming: true, + style: { + fontSize: '15px', + color: '#01888C', + }, + }), + ]) + + case 'failed': + return '' + default: + return '' + } +} + +ShiftListItem.prototype.renderInfo = function () { + var props = this.props + switch (props.response.status) { + case 'no_deposits': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'No deposits received'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'received': + return h('.flex-column', { + style: { + width: '200px', + overflow: 'hidden', + }, + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, `${props.depositType} to ETH via ShapeShift`), + h('div', 'Conversion in progress'), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, formatDate(props.time)), + ]) + case 'complete': + var url = explorerLink(props.response.transaction, parseInt('1')) + + return h('.flex-column.pointer', { + style: { + width: '200px', + overflow: 'hidden', + }, + onClick: () => global.platform.openWindow({ url }), + }, [ + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, 'From ShapeShift'), + h('div', formatDate(props.time)), + h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + width: '100%', + }, + }, addressSummary(props.response.transaction)), + ]) + + case 'failed': + return h('span.error', '(Failed)') + default: + return '' + } +} diff --git a/ui/responsive/app/components/tab-bar.js b/ui/responsive/app/components/tab-bar.js new file mode 100644 index 000000000..6295e7dd9 --- /dev/null +++ b/ui/responsive/app/components/tab-bar.js @@ -0,0 +1,36 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = TabBar + +inherits(TabBar, Component) +function TabBar () { + Component.call(this) +} + +TabBar.prototype.render = function () { + const props = this.props + const state = this.state || {} + const { tabs = [], defaultTab, tabSelected } = props + const { subview = defaultTab } = state + + return ( + h('.flex-row.space-around.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + paddingTop: '4px', + }, + }, tabs.map((tab) => { + const { key, content } = tab + return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', { + onClick: () => { + this.setState({ subview: key }) + tabSelected(key) + }, + }, content) + })) + ) +} + diff --git a/ui/responsive/app/components/template.js b/ui/responsive/app/components/template.js new file mode 100644 index 000000000..b6ed8eaa0 --- /dev/null +++ b/ui/responsive/app/components/template.js @@ -0,0 +1,18 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +module.exports = NewComponent + +inherits(NewComponent, Component) +function NewComponent () { + Component.call(this) +} + +NewComponent.prototype.render = function () { + const props = this.props + + return ( + h('span', props.message) + ) +} diff --git a/ui/responsive/app/components/token-cell.js b/ui/responsive/app/components/token-cell.js new file mode 100644 index 000000000..19d7139bb --- /dev/null +++ b/ui/responsive/app/components/token-cell.js @@ -0,0 +1,72 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') + +module.exports = TokenCell + +inherits(TokenCell, Component) +function TokenCell () { + Component.call(this) +} + +TokenCell.prototype.render = function () { + const props = this.props + const { address, symbol, string, network, userAddress } = props + + return ( + h('li.token-cell', { + style: { cursor: network === '1' ? 'pointer' : 'default' }, + onClick: this.view.bind(this, address, userAddress, network), + }, [ + + h(Identicon, { + diameter: 50, + address, + network, + }), + + h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + /* + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + */ + + ]) + ) +} + +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation() + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + +function navigateTo (url) { + global.platform.openWindow({ url }) +} + +function etherscanLinkFor (tokenAddress, address, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` +} + +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` +} + diff --git a/ui/responsive/app/components/token-list.js b/ui/responsive/app/components/token-list.js new file mode 100644 index 000000000..20cfa897e --- /dev/null +++ b/ui/responsive/app/components/token-list.js @@ -0,0 +1,192 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const TokenCell = require('./token-cell.js') +const normalizeAddress = require('eth-sig-util').normalize + +const defaultTokens = [] +const contracts = require('eth-contract-metadata') +for (const address in contracts) { + const contract = contracts[address] + if (contract.erc20) { + contract.address = address + defaultTokens.push(contract) + } +} + +module.exports = TokenList + +inherits(TokenList, Component) +function TokenList () { + this.state = { + tokens: [], + isLoading: true, + network: null, + } + Component.call(this) +} + +TokenList.prototype.render = function () { + const state = this.state + const { tokens, isLoading, error } = state + const { userAddress, network } = this.props + + if (isLoading) { + return this.message('Loading') + } + + if (error) { + log.error(error) + return this.message('There was a problem loading your token balances.') + } + + const tokenViews = tokens.map((tokenData) => { + tokenData.network = network + tokenData.userAddress = userAddress + return h(TokenCell, tokenData) + }) + + return h('div', [ + h('ol', { + style: { + height: '260px', + overflowY: 'auto', + display: 'flex', + flexDirection: 'column', + }, + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + tokenViews.length ? null : this.message('No Tokens Found.'), + ]), + this.addTokenButtonElement(), + ]) +} + +TokenList.prototype.addTokenButtonElement = function () { + return h('div', [ + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg'), + ]), + ]) +} + +TokenList.prototype.message = function (body) { + return h('div', { + style: { + display: 'flex', + height: '250px', + alignItems: 'center', + justifyContent: 'center', + padding: '30px', + }, + }, body) +} + +TokenList.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenList.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress } = this.props + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalances.bind(this) + this.showError = (error) => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalances(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenList.prototype.componentWillUpdate = function (nextProps) { + if (nextProps.network === 'loading') return + const oldNet = this.props.network + const newNet = nextProps.network + + if (oldNet && newNet && newNet !== oldNet) { + this.setState({ isLoading: true }) + this.createFreshTokenTracker() + } +} + +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' + }) + this.setState({ tokens: heldTokens, isLoading: false }) +} + +TokenList.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + +function uniqueMergeTokens (tokensA, tokensB) { + const uniqueAddresses = [] + const result = [] + tokensA.concat(tokensB).forEach((token) => { + const normal = normalizeAddress(token.address) + if (!uniqueAddresses.includes(normal)) { + uniqueAddresses.push(normal) + result.push(token) + } + }) + return result +} + diff --git a/ui/responsive/app/components/tooltip.js b/ui/responsive/app/components/tooltip.js new file mode 100644 index 000000000..edbc074bb --- /dev/null +++ b/ui/responsive/app/components/tooltip.js @@ -0,0 +1,22 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const ReactTooltip = require('react-tooltip-component') + +module.exports = Tooltip + +inherits(Tooltip, Component) +function Tooltip () { + Component.call(this) +} + +Tooltip.prototype.render = function () { + const props = this.props + const { position, title, children } = props + + return h(ReactTooltip, { + position: position || 'left', + title, + fixed: false, + }, children) +} diff --git a/ui/responsive/app/components/transaction-list-item-icon.js b/ui/responsive/app/components/transaction-list-item-icon.js new file mode 100644 index 000000000..431054340 --- /dev/null +++ b/ui/responsive/app/components/transaction-list-item-icon.js @@ -0,0 +1,68 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const Tooltip = require('./tooltip') + +const Identicon = require('./identicon') + +module.exports = TransactionIcon + +inherits(TransactionIcon, Component) +function TransactionIcon () { + Component.call(this) +} + +TransactionIcon.prototype.render = function () { + const { transaction, txParams, isMsg } = this.props + switch (transaction.status) { + case 'unapproved': + return h(!isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') + + case 'rejected': + return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { + style: { + width: '24px', + }, + }) + + case 'failed': + return h('i.fa.fa-exclamation-triangle.fa-lg.error', { + style: { + width: '24px', + }, + }) + + case 'submitted': + return h(Tooltip, { + title: 'Pending', + position: 'bottom', + }, [ + h('i.fa.fa-ellipsis-h', { + style: { + fontSize: '27px', + }, + }), + ]) + } + + if (isMsg) { + return h('i.fa.fa-certificate.fa-lg', { + style: { + width: '24px', + }, + }) + } + + if (txParams.to) { + return h(Identicon, { + diameter: 24, + address: txParams.to || transaction.hash, + }) + } else { + return h('i.fa.fa-file-text-o.fa-lg', { + style: { + width: '24px', + }, + }) + } +} diff --git a/ui/responsive/app/components/transaction-list-item.js b/ui/responsive/app/components/transaction-list-item.js new file mode 100644 index 000000000..dbda66a31 --- /dev/null +++ b/ui/responsive/app/components/transaction-list-item.js @@ -0,0 +1,165 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const EthBalance = require('./eth-balance') +const addressSummary = require('../util').addressSummary +const explorerLink = require('../../lib/explorer-link') +const CopyButton = require('./copyButton') +const vreme = new (require('vreme')) +const Tooltip = require('./tooltip') +const numberToBN = require('number-to-bn') + +const TransactionIcon = require('./transaction-list-item-icon') +const ShiftListItem = require('./shift-list-item') +module.exports = TransactionListItem + +inherits(TransactionListItem, Component) +function TransactionListItem () { + Component.call(this) +} + +TransactionListItem.prototype.render = function () { + const { transaction, network, conversionRate, currentCurrency } = this.props + if (transaction.key === 'shapeshift') { + if (network === '1') return h(ShiftListItem, transaction) + } + var date = formatDate(transaction.time) + + let isLinkable = false + const numericNet = parseInt(network) + isLinkable = numericNet === 1 || numericNet === 3 || numericNet === 4 || numericNet === 42 + + var isMsg = ('msgParams' in transaction) + var isTx = ('txParams' in transaction) + var isPending = transaction.status === 'unapproved' + let txParams + if (isTx) { + txParams = transaction.txParams + } else if (isMsg) { + txParams = transaction.msgParams + } + + const nonce = txParams.nonce ? numberToBN(txParams.nonce).toString(10) : '' + + const isClickable = ('hash' in transaction && isLinkable) || isPending + return ( + h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, { + onClick: (event) => { + if (isPending) { + this.props.showTx(transaction.id) + } + event.stopPropagation() + if (!transaction.hash || !isLinkable) return + var url = explorerLink(transaction.hash, parseInt(network)) + global.platform.openWindow({ url }) + }, + style: { + padding: '20px 0', + }, + }, [ + + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h('.pop-hover', { + onClick: (event) => { + event.stopPropagation() + if (!isTx || isPending) return + var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}` + global.platform.openWindow({ url }) + }, + }, [ + h(TransactionIcon, { txParams, transaction, isTx, isMsg }), + ]), + ]), + + h(Tooltip, { + title: 'Transaction Number', + position: 'bottom', + }, [ + h('span', { + style: { + display: 'flex', + cursor: 'normal', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: '10px', + }, + }, nonce), + ]), + + h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [ + domainField(txParams), + h('div', date), + recipientField(txParams, transaction, isTx, isMsg), + ]), + + // Places a copy button if tx is successful, else places a placeholder empty div. + transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}), + + isTx ? h(EthBalance, { + value: txParams.value, + conversionRate, + currentCurrency, + width: '55px', + shorten: true, + showFiat: false, + style: {fontSize: '15px'}, + }) : h('.flex-column'), + ]) + ) +} + +function domainField (txParams) { + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%', + }, + }, [ + txParams.origin, + ]) +} + +function recipientField (txParams, transaction, isTx, isMsg) { + let message + + if (isMsg) { + message = 'Signature Requested' + } else if (txParams.to) { + message = addressSummary(txParams.to) + } else { + message = 'Contract Published' + } + + return h('div', { + style: { + fontSize: 'x-small', + color: '#ABA9AA', + }, + }, [ + message, + failIfFailed(transaction), + ]) +} + +function formatDate (date) { + return vreme.format(new Date(date), 'March 16 2014 14:30') +} + +function failIfFailed (transaction) { + if (transaction.status === 'rejected') { + return h('span.error', ' (Rejected)') + } + if (transaction.err) { + return h(Tooltip, { + title: transaction.err.message, + position: 'bottom', + }, [ + h('span.error', ' (Failed)'), + ]) + } +} diff --git a/ui/responsive/app/components/transaction-list.js b/ui/responsive/app/components/transaction-list.js new file mode 100644 index 000000000..3b4ba741e --- /dev/null +++ b/ui/responsive/app/components/transaction-list.js @@ -0,0 +1,79 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const TransactionListItem = require('./transaction-list-item') + +module.exports = TransactionList + + +inherits(TransactionList, Component) +function TransactionList () { + Component.call(this) +} + +TransactionList.prototype.render = function () { + const { transactions, network, unapprovedMsgs, conversionRate } = this.props + + var shapeShiftTxList + if (network === '1') { + shapeShiftTxList = this.props.shapeShiftTxList + } + const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList) + .sort((a, b) => b.time - a.time) + + return ( + + h('section.transaction-list', [ + + h('style', ` + .transaction-list .transaction-list-item:not(:last-of-type) { + border-bottom: 1px solid #D4D4D4; + } + .transaction-list .transaction-list-item .ether-balance-label { + display: block !important; + font-size: small; + } + `), + + h('.tx-list', { + style: { + overflowY: 'auto', + height: '300px', + padding: '0 20px', + textAlign: 'center', + }, + }, [ + + txsToRender.length + ? txsToRender.map((transaction, i) => { + let key + switch (transaction.key) { + case 'shapeshift': + const { depositAddress, time } = transaction + key = `shift-tx-${depositAddress}-${time}-${i}` + break + default: + key = `tx-${transaction.id}-${i}` + } + return h(TransactionListItem, { + transaction, i, network, key, + conversionRate, + showTx: (txId) => { + this.props.viewPendingTx(txId) + }, + }) + }) + : h('.flex-center', { + style: { + flexDirection: 'column', + height: '100%', + }, + }, [ + 'No transaction history.', + ]), + ]), + ]) + ) +} + diff --git a/ui/responsive/app/conf-tx.js b/ui/responsive/app/conf-tx.js new file mode 100644 index 000000000..63b77ef7f --- /dev/null +++ b/ui/responsive/app/conf-tx.js @@ -0,0 +1,213 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const ReactCSSTransitionGroup = require('react-addons-css-transition-group') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const NetworkIndicator = require('./components/network') +const txHelper = require('../lib/tx-helper') +const isPopupOrNotification = require('../../../app/scripts/lib/is-popup-or-notification') + +const PendingTx = require('./components/pending-tx') +const PendingMsg = require('./components/pending-msg') +const PendingPersonalMsg = require('./components/pending-personal-msg') +const Loading = require('./components/loading') + +module.exports = connect(mapStateToProps)(ConfirmTxScreen) + +function mapStateToProps (state) { + return { + identities: state.metamask.identities, + accounts: state.metamask.accounts, + selectedAddress: state.metamask.selectedAddress, + unapprovedTxs: state.metamask.unapprovedTxs, + unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, + index: state.appState.currentView.context, + warning: state.appState.warning, + network: state.metamask.network, + provider: state.metamask.provider, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + blockGasLimit: state.metamask.currentBlockGasLimit, + } +} + +inherits(ConfirmTxScreen, Component) +function ConfirmTxScreen () { + Component.call(this) +} + +ConfirmTxScreen.prototype.render = function () { + const props = this.props + const { network, provider, unapprovedTxs, currentCurrency, + unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props + + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + + var txData = unconfTxList[props.index] || {} + var txParams = txData.params || {} + var isNotification = isPopupOrNotification() === 'notification' + + + log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`) + if (unconfTxList.length === 0) return h(Loading, { isLoading: true }) + + return ( + + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + !isNotification ? h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.goHome.bind(this), + }) : null, + h('h2.page-subtitle', 'Confirm Transaction'), + isNotification ? h(NetworkIndicator, { + network: network, + provider: provider, + }) : null, + ]), + + h('h3', { + style: { + alignSelf: 'center', + display: unconfTxList.length > 1 ? 'block' : 'none', + }, + }, [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + style: { + display: props.index === 0 ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.previousTx()), + }), + ` ${props.index + 1} of ${unconfTxList.length} `, + h('i.fa.fa-arrow-right.fa-lg.cursor-pointer', { + style: { + display: props.index + 1 === unconfTxList.length ? 'none' : 'inline-block', + }, + onClick: () => props.dispatch(actions.nextTx()), + }), + ]), + + warningIfExists(props.warning), + + h(ReactCSSTransitionGroup, { + className: 'css-transition-group', + transitionName: 'main', + transitionEnterTimeout: 300, + transitionLeaveTimeout: 300, + }, [ + + currentTxView({ + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress), + sendTransaction: this.sendTransaction.bind(this), + cancelTransaction: this.cancelTransaction.bind(this, txData), + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + }), + + ]), + ]) + ) +} + +function currentTxView (opts) { + log.info('rendering current tx view') + const { txData } = opts + const { txParams, msgParams, type } = txData + + if (txParams) { + log.debug('txParams detected, rendering pending tx') + return h(PendingTx, opts) + } else if (msgParams) { + log.debug('msgParams detected, rendering pending msg') + + if (type === 'eth_sign') { + log.debug('rendering eth_sign message') + return h(PendingMsg, opts) + } else if (type === 'personal_sign') { + log.debug('rendering personal_sign message') + return h(PendingPersonalMsg, opts) + } + } +} + +ConfirmTxScreen.prototype.buyEth = function (address, event) { + event.preventDefault() + this.props.dispatch(actions.buyEthView(address)) +} + +ConfirmTxScreen.prototype.sendTransaction = function (txData, event) { + this.stopPropagation(event) + this.props.dispatch(actions.updateAndApproveTx(txData)) +} + +ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { + this.stopPropagation(event) + event.preventDefault() + this.props.dispatch(actions.cancelTx(txData)) +} + +ConfirmTxScreen.prototype.signMessage = function (msgData, event) { + log.info('conf-tx.js: signing message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signMsg(params)) +} + +ConfirmTxScreen.prototype.stopPropagation = function (event) { + if (event.stopPropagation) { + event.stopPropagation() + } +} + +ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { + log.info('conf-tx.js: signing personal message') + var params = msgData.msgParams + params.metamaskId = msgData.id + this.stopPropagation(event) + this.props.dispatch(actions.signPersonalMsg(params)) +} + +ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { + log.info('canceling message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelMsg(msgData)) +} + +ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { + log.info('canceling personal message') + this.stopPropagation(event) + this.props.dispatch(actions.cancelPersonalMsg(msgData)) +} + +ConfirmTxScreen.prototype.goHome = function (event) { + this.stopPropagation(event) + this.props.dispatch(actions.goHome()) +} + +function warningIfExists (warning) { + if (warning && + // Do not display user rejections on this screen: + warning.indexOf('User denied transaction signature') === -1) { + return h('.error', { + style: { + margin: 'auto', + }, + }, warning) + } +} diff --git a/ui/responsive/app/config.js b/ui/responsive/app/config.js new file mode 100644 index 000000000..62785c49b --- /dev/null +++ b/ui/responsive/app/config.js @@ -0,0 +1,211 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const currencies = require('./conversion.json').rows +const validUrl = require('valid-url') +const copyToClipboard = require('copy-to-clipboard') + +module.exports = connect(mapStateToProps)(ConfigScreen) + +function mapStateToProps (state) { + return { + metamask: state.metamask, + warning: state.appState.warning, + } +} + +inherits(ConfigScreen, Component) +function ConfigScreen () { + Component.call(this) +} + +ConfigScreen.prototype.render = function () { + var state = this.props + var metamaskState = state.metamask + var warning = state.warning + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + currentProviderDisplay(metamaskState), + + h('div', { style: {display: 'flex'} }, [ + h('input#new_rpc', { + placeholder: 'New RPC URL', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = event.target + var newRpc = element.value + rpcValidation(newRpc, state) + } + }, + }), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + var element = document.querySelector('input#new_rpc') + var newRpc = element.value + rpcValidation(newRpc, state) + }, + }, 'Save'), + ]), + + h('hr.horizontal-line'), + + currentConversionInformation(metamaskState, state), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('p', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '13px', + }, + }, `State logs contain your public account addresses and sent transactions.`), + h('br'), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + copyToClipboard(window.logState()) + }, + }, 'Copy State Logs'), + ]), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + event.preventDefault() + state.dispatch(actions.revealSeedConfirmation()) + }, + }, 'Reveal Seed Words'), + ]), + + ]), + ]), + ]) + ) +} + +function rpcValidation (newRpc, state) { + if (validUrl.isWebUri(newRpc)) { + state.dispatch(actions.setRpcTarget(newRpc)) + } else { + var appendedRpc = `http://${newRpc}` + if (validUrl.isWebUri(appendedRpc)) { + state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')) + } else { + state.dispatch(actions.displayWarning('Invalid RPC URI')) + } + } +} + +function currentConversionInformation (metamaskState, state) { + var currentCurrency = metamaskState.currentCurrency + var conversionDate = metamaskState.conversionDate + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'), + h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`), + h('select#currentCurrency', { + onChange (event) { + event.preventDefault() + var element = document.getElementById('currentCurrency') + var newCurrency = element.value + state.dispatch(actions.setCurrentCurrency(newCurrency)) + }, + defaultValue: currentCurrency, + }, currencies.map((currency) => { + return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`) + }) + ), + ]) +} + +function currentProviderDisplay (metamaskState) { + var provider = metamaskState.provider + var title, value + + switch (provider.type) { + + case 'mainnet': + title = 'Current Network' + value = 'Main Ethereum Network' + break + + case 'ropsten': + title = 'Current Network' + value = 'Ropsten Test Network' + break + + case 'kovan': + title = 'Current Network' + value = 'Kovan Test Network' + break + + case 'rinkeby': + title = 'Current Network' + value = 'Rinkeby Test Network' + break + + default: + title = 'Current RPC' + value = metamaskState.provider.rpcTarget + } + + return h('div', [ + h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title), + h('span', value), + ]) +} diff --git a/ui/responsive/app/conversion.json b/ui/responsive/app/conversion.json new file mode 100644 index 000000000..155ffc4fc --- /dev/null +++ b/ui/responsive/app/conversion.json @@ -0,0 +1,207 @@ +{ + "rows": [ + { + "code": "REP", + "name": "Augur", + "statuses": [ + "primary" + ] + }, + { + "code": "BCN", + "name": "Bytecoin", + "statuses": [ + "primary" + ] + }, + { + "code": "BTC", + "name": "Bitcoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BTS", + "name": "BitShares", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "BLK", + "name": "Blackcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "GBP", + "name": "British Pound Sterling", + "statuses": [ + "secondary" + ] + }, + { + "code": "CAD", + "name": "Canadian Dollar", + "statuses": [ + "secondary" + ] + }, + { + "code": "CNY", + "name": "Chinese Yuan", + "statuses": [ + "secondary" + ] + }, + { + "code": "DSH", + "name": "Dashcoin", + "statuses": [ + "primary" + ] + }, + { + "code": "DOGE", + "name": "Dogecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "ETC", + "name": "Ethereum Classic", + "statuses": [ + "primary" + ] + }, + { + "code": "EUR", + "name": "Euro", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "GNO", + "name": "GNO", + "statuses": [ + "primary" + ] + }, + { + "code": "GNT", + "name": "GNT", + "statuses": [ + "primary" + ] + }, + { + "code": "JPY", + "name": "Japanese Yen", + "statuses": [ + "secondary" + ] + }, + { + "code": "LTC", + "name": "Litecoin", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "MAID", + "name": "MaidSafeCoin", + "statuses": [ + "primary" + ] + }, + { + "code": "XEM", + "name": "NEM", + "statuses": [ + "primary" + ] + }, + { + "code": "XLM", + "name": "Stellar", + "statuses": [ + "primary" + ] + }, + { + "code": "XMR", + "name": "Monero", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "XRP", + "name": "Ripple", + "statuses": [ + "primary" + ] + }, + { + "code": "RUR", + "name": "Ruble", + "statuses": [ + "secondary" + ] + }, + { + "code": "STEEM", + "name": "Steem", + "statuses": [ + "primary" + ] + }, + { + "code": "STRAT", + "name": "STRAT", + "statuses": [ + "primary" + ] + }, + { + "code": "UAH", + "name": "Ukrainian Hryvnia", + "statuses": [ + "secondary" + ] + }, + { + "code": "USD", + "name": "US Dollar", + "statuses": [ + "primary", + "secondary" + ] + }, + { + "code": "WAVES", + "name": "WAVES", + "statuses": [ + "primary" + ] + }, + { + "code": "ZEC", + "name": "Zcash", + "statuses": [ + "primary" + ] + } + ] +} diff --git a/ui/responsive/app/css/debug.css b/ui/responsive/app/css/debug.css new file mode 100644 index 000000000..3e125bcd4 --- /dev/null +++ b/ui/responsive/app/css/debug.css @@ -0,0 +1,21 @@ +/* +debug / dev +*/ + +#app-content { + border: 2px solid green; +} + +#design-container { + position: absolute; + left: 360px; + top: -42px; + width: calc(100vw - 360px); + height: 100vh; + overflow: scroll; +} + +#design-container img { + width: 2000px; + margin-right: 600px; +}
\ No newline at end of file diff --git a/ui/responsive/app/css/fonts.css b/ui/responsive/app/css/fonts.css new file mode 100644 index 000000000..3b9f581b9 --- /dev/null +++ b/ui/responsive/app/css/fonts.css @@ -0,0 +1,36 @@ +@import url(https://fonts.googleapis.com/css?family=Roboto:300,500); +@import url(https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css); + +@font-face { + font-family: 'Montserrat Regular'; + src: url('/fonts/Montserrat/Montserrat-Regular.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-size: 'small'; + +} + +@font-face { + font-family: 'Montserrat Bold'; + src: url('/fonts/Montserrat/Montserrat-Bold.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat Light'; + src: url('/fonts/Montserrat/Montserrat-Light.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-Light.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Montserrat UltraLight'; + src: url('/fonts/Montserrat/Montserrat-UltraLight.woff') format('woff'); + src: url('/fonts/Montserrat/Montserrat-UltraLight.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} diff --git a/ui/responsive/app/css/index.css b/ui/responsive/app/css/index.css new file mode 100644 index 000000000..c82c1b21b --- /dev/null +++ b/ui/responsive/app/css/index.css @@ -0,0 +1,674 @@ +/* +faint orange (textfield shades) #FAF6F0 +light orange (button shades): #F5C26D +dark orange (text): #F5A623 +borders/font/any gray: #4A4A4A +*/ + +/* +application specific styles +*/ + +* { + box-sizing: border-box; +} + +html, body { + font-family: 'Montserrat Regular', Arial; + color: #4D4D4D; + font-weight: 300; + line-height: 1.4em; + background: #F7F7F7; + width: 100%; + height: 100%; + margin: 0; + padding: 0; +} + +.css-transition-group { + flex: 1; +} + +input:focus, textarea:focus { + outline: none; +} + +#app-content { + overflow-x: hidden; + min-width: 357px; +} + +button, input[type="submit"] { + font-family: 'Montserrat Bold'; + outline: none; + cursor: pointer; + padding: 8px 12px; + border: none; + color: white; + transform-origin: center center; + transition: transform 50ms ease-in; + /* default orange */ + background: rgba(247, 134, 28, 1); + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); +} + +.btn-green, input[type="submit"].btn-green { + background: rgba(106, 195, 96, 1); + box-shadow: 0px 3px 6px rgba(106, 195, 96, 0.36); +} + +.btn-red { + background: rgba(254, 35, 17, 1); + box-shadow: 0px 3px 6px rgba(254, 35, 17, 0.36); +} + +button[disabled], input[type="submit"][disabled] { + cursor: not-allowed; + background: rgba(197, 197, 197, 1); + box-shadow: 0px 3px 6px rgba(197, 197, 197, 0.36); +} + +button.spaced { + margin: 2px; +} + +button:not([disabled]):hover, input[type="submit"]:not([disabled]):hover { + transform: scale(1.1); +} +button:not([disabled]):active, input[type="submit"]:not([disabled]):active { + transform: scale(0.95); +} + +a { + text-decoration: none; + color: inherit; +} + +a:hover{ + color: #df6b0e; +} + +/* +app +*/ + +.active { + color: #909090; +} + +button.primary { + padding: 8px 12px; + background: #F7861C; + box-shadow: 0px 3px 6px rgba(247, 134, 28, 0.36); + color: white; + font-size: 1.1em; + font-family: 'Montserrat Regular'; + text-transform: uppercase; +} + +button.btn-thin { + border: 1px solid; + border-color: #4D4D4D; + color: #4D4D4D; + background: rgb(255, 174, 41); + border-radius: 4px; + min-width: 200px; + margin: 12px 0; + padding: 6px; + font-size: 13px; +} + +.app-header { + padding: 6px 8px; +} + +.app-header h1 { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +h2.page-subtitle { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; + font-size: 1em; + margin: 12px; +} + +.app-primary { + +} + +.app-footer { + padding-bottom: 10px; + align-items: center; +} + +.identicon { + height: 46px; + width: 46px; + background-size: cover; + border-radius: 100%; + border: 3px solid gray; +} + +textarea.twelve-word-phrase { + padding: 12px; + width: 300px; + height: 140px; + font-size: 16px; + background: white; + resize: none; +} + +.network-indicator { + display: flex; + align-items: center; + font-size: 0.6em; + +} + +.network-name { + width: 5.2em; + line-height: 9px; + text-rendering: geometricPrecision; +} + +.check { + margin-left: 7px; + color: #F7861C; + flex: 1 0 auto; + display: flex; + justify-content: flex-end; +} +/* +app sections +*/ + +/* initialize */ + +.initialize-screen hr { + width: 60px; + margin: 12px; + border-color: #F7861C; + border-style: solid; +} + +.initialize-screen label { + margin-top: 20px; +} + +.initialize-screen button.create-vault { + margin-top: 40px; +} + +.initialize-screen .warning { + font-size: 14px; + margin: 0 16px; +} + +/* unlock */ +.error { + color: #E20202; +} + +.warning { + color: #FFAE00; +} + +.lock { + width: 50px; + height: 50px; +} + +.lock.locked { + transform: scale(1.5); + opacity: 0.0; + transition: opacity 400ms ease-in, transform 400ms ease-in; +} +.lock.unlocked { + transform: scale(1); + opacity: 1; + transition: opacity 500ms ease-out, transform 500ms ease-out, background 200ms ease-in; +} + +.lock.locked .lock-top { + transform: scaleX(1) translateX(0); + transition: transform 250ms ease-in; +} +.lock.unlocked .lock-top { + transform: scaleX(-1) translateX(-12px); + transition: transform 250ms ease-in; +} +.lock.unlocked:hover { + border-radius: 4px; + background: #e5e5e5; + border: 1px solid #b1b1b1; +} +.lock.unlocked:active { + background: #c3c3c3; +} + +.section-title .fa-arrow-left { + margin: -2px 8px 0px -8px; +} + +.unlock-screen #metamask-mascot-container { + margin-top: 24px; +} + +.unlock-screen h1 { + margin-top: -28px; + margin-bottom: 42px; +} + +.unlock-screen input[type=password] { + width: 260px; + /*height: 36px; + margin-bottom: 24px; + padding: 8px;*/ +} + +.sizing-input{ + font-size: 14px; + height: 30px; + padding-left: 5px; +} +.editable-label{ + display: flex; +} +/* Webkit */ +.unlock-screen input::-webkit-input-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 18- */ +.unlock-screen input:-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* Firefox 19+ */ +.unlock-screen input::-moz-placeholder { + text-align: center; + font-size: 1.2em; +} +/* IE */ +.unlock-screen input:-ms-input-placeholder { + text-align: center; + font-size: 1.2em; +} + +input.large-input, textarea.large-input { + /*margin-bottom: 24px;*/ + padding: 8px; +} + +input.large-input { + height: 36px; +} + +.letter-spacey { + letter-spacing: 0.1em; +} + + + +/* accounts */ + +.accounts-section { + margin: 0 0px; +} + +.accounts-section .horizontal-line { + margin: 0px 18px; +} + +.accounts-list-option { + height: 120px; +} + +.accounts-list-option .identicon-wrapper { + width: 100px; +} + +.unconftx-link { + margin-top: 24px; + cursor: pointer; +} + +.unconftx-link .fa-arrow-right { + margin: 0px -8px 0px 8px; +} + +/* identity panel */ + +.identity-panel { + font-weight: 500; +} + +.identity-panel .identicon-wrapper { + margin: 4px; + margin-top: 8px; + display: flex; + align-items: center; +} + +.identity-panel .identicon-wrapper span { + margin: 0 auto; +} + +.identity-panel .identity-data { + margin: 8px 8px 8px 18px; +} + +.identity-panel i { + margin-top: 32px; + margin-right: 6px; + color: #B9B9B9; +} + +.identity-panel .arrow-right { + padding-left: 18px; + width: 42px; + min-width: 18px; + height: 100%; +} + +.identity-copy.flex-column { + flex: 0.25 0 auto; + justify-content: center; +} + +/* accounts screen */ + +.identity-section { + +} + +.identity-section .identity-panel { + background: #E9E9E9; + border-bottom: 1px solid #B1B1B1; + cursor: pointer; +} + +.identity-section .identity-panel.selected { + background: white; + color: #F3C83E; +} + +.identity-section .identity-panel.selected .identicon { + border-color: orange; +} + +.identity-section .accounts-list-option:hover, +.identity-section .accounts-list-option.selected { + background:white; +} + +/* account detail screen */ + +.account-detail-section { + display: flex; + flex-wrap: wrap; +} +.name-label{ + +} + +.unapproved-tx-icon { + height: 16px; + width: 16px; + background: rgb(47, 174, 244); + border-color: #AEAEAE; + border-radius: 13px; +} + +.edit-text { + height: 100%; + visibility: hidden; +} +.editing-label { + display: flex; + justify-content: flex-start; + margin-left: 50px; + margin-bottom: 2px; + font-size: 11px; + text-rendering: geometricPrecision; + color: #F7861C; +} +.name-label:hover .edit-text { + visibility: visible; +} +/* tx confirm */ + +.unconftx-section input[type=password] { + height: 22px; + padding: 2px; + margin: 12px; + margin-bottom: 24px; + border-radius: 4px; + border: 2px solid #F3C83E; + background: #FAF6F0; +} + +/* Send Screen */ + +.send-screen { + +} + +.send-screen section { + margin: 8px 16px; +} + +.send-screen input { + width: 100%; + font-size: 12px; +} + +/* Ether Balance Widget */ + +.ether-balance-amount { + color: #F7861C; +} + +.ether-balance-label { + color: #ABA9AA; +} + +/* Info screen */ +.info-gray{ + font-family: 'Montserrat Regular'; + text-transform: uppercase; + color: #AEAEAE; +} + +.icon-size{ + width: 20px; +} + +.info{ + font-family: 'Montserrat Regular', Arial; + padding-bottom: 10px; + display: inline-block; + padding-left: 5px; +} + +/* buy eth warning screen */ +.custom-radios { + justify-content: space-around; + align-items: center; +} + + +.custom-radio-selected { + width: 17px; + height: 17px; + border: solid; + border-style: double; + border-radius: 15px; + border-width: 5px; + background: rgba(247, 134, 28, 1); + border-color: #F7F7F7; +} + +.custom-radio-inactive { + width: 14px; + height: 14px; + border: solid; + border-width: 1px; + border-radius: 24px; + border-color: #AEAEAE; +} + +.radio-titles { + color: rgba(247, 134, 28, 1); +} + +.radio-titles-subtext { + +} + +.selected-exchange { + +} + +.buy-radio { + +} + +.eth-warning{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.buy-subview{ + transition: opacity 400ms ease-in, transform 400ms ease-in; +} + +.input-container:hover .edit-text{ + visibility: visible; +} + +.buy-inputs{ + font-family: 'Montserrat Light'; + font-size: 13px; + height: 20px; + background: transparent; + box-sizing: border-box; + border: solid; + border-color: transparent; + border-width: 0.5px; + border-radius: 2px; + +} +.input-container:hover .buy-inputs{ + box-sizing: inherit; + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.buy-inputs:focus{ + border: solid; + border-color: #F7861C; + border-width: 0.5px; + border-radius: 2px; +} + +.activeForm { + background: #F7F7F7; + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; + +} + +.inactiveForm { + border: none; + border-radius: 8px 8px 0px 0px; + width: 50%; + text-align: center; + padding-bottom: 4px; +} + +.ex-coins { + font-family: 'Montserrat Regular'; + text-transform: uppercase; + text-align: center; + font-size: 33px; + width: 118px; + height: 42px; + padding: 1px; + color: #4D4D4D; +} + +.marketinfo{ + font-family: 'Montserrat light'; + color: #AEAEAE; + font-size: 15px; + line-height: 17px; +} + +#fromCoin::-webkit-calendar-picker-indicator { + display: none; +} + +#coinList { + width: 400px; + height: 500px; + overflow: scroll; +} + +.icon-control .fa-refresh{ + visibility: hidden; +} + +.icon-control:hover .fa-refresh{ + visibility: visible; +} + +.icon-control:hover .fa-chevron-right{ + visibility: hidden; +} + +.inactive { + color: #AEAEAE; +} + +.inactive button{ + background: #AEAEAE; + color: white; +} + +.ellip-address { + overflow: hidden; + text-overflow: ellipsis; + width: 5em; + font-size: 14px; + font-family: "Montserrat Light"; + margin-left: 5px; +} + +.qr-header { + font-size: 25px; + margin-top: 40px; +} + +.qr-message { + font-size: 12px; + color: #F7861C; +} + +div.message-container > div:first-child { + margin-top: 18px; + font-size: 15px; + color: #4D4D4D; +} + +.pop-hover:hover { + transform: scale(1.1); +} diff --git a/ui/responsive/app/css/lib.css b/ui/responsive/app/css/lib.css new file mode 100644 index 000000000..910a24ee2 --- /dev/null +++ b/ui/responsive/app/css/lib.css @@ -0,0 +1,268 @@ +/* color */ + +.color-orange { + color: #F7861C; +} + +.color-forest { + color: #0A5448; +} + +/* lib */ + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} + +.flex-column { + display: flex; + flex-direction: column; +} + +.space-between { + justify-content: space-between; +} + +.space-around { + justify-content: space-around; +} + +.flex-column-bottom { + display: flex; + flex-direction: column-reverse; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-space-between { + justify-content: space-between; +} + +.flex-space-around { + justify-content: space-around; +} + +.flex-right { + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.flex-left { + display: flex; + flex-direction: row; + justify-content: flex-start; +} + +.flex-fixed { + flex: none; +} + +.flex-basis-auto { + flex-basis: auto; +} + +.flex-grow { + flex: 1 1 auto; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.flex-justify-center { + justify-content: center; +} + +.flex-align-center { + align-items: center; +} + +.flex-self-end { + align-self: flex-end; +} + +.flex-self-stretch { + align-self: stretch; +} + +.flex-vertical { + flex-direction: column; +} + +.z-bump { + z-index: 1; +} + +.select-none { + cursor: inherit; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.pointer { + cursor: pointer; +} +.cursor-pointer { + cursor: pointer; + transform-origin: center center; + transition: transform 50ms ease-in-out; +} +.cursor-pointer:hover { + transform: scale(1.1); +} +.cursor-pointer:active { + transform: scale(0.95); +} + +.cursor-disabled { + cursor: not-allowed; +} + +.margin-bottom-sml { + margin-bottom: 20px; +} + +.margin-bottom-med { + margin-bottom: 40px; +} + +.margin-right-left { + margin: 0 20px; +} + +.bold { + font-weight: bold; +} + +.text-transform-uppercase { + text-transform: uppercase; +} + +.font-small { + font-size: 12px; +} + +.font-medium { + font-size: 1.2em; +} + +hr.horizontal-line { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +.hover-white:hover { + background: white; +} + +.red-dot { + background: #E91550; + color: white; + border-radius: 10px; +} + +.diamond { + transform: rotate(45deg); + background: #038789; +} + +.hollow-diamond { + transform: rotate(45deg); + border: 3px solid #690496; +} + +.golden-square { + background: #EBB33F; +} + +.pending-dot { + background: red; + left: 14px; + top: 14px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + z-index: 1; +} + +.keyring-label { + z-index: 1; + font-size: 11px; + background: rgba(255,0,0,0.8); + bottom: -47px; + color: white; + border-radius: 10px; + height: 20px; + min-width: 20px; + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; +} + +.ether-balance { + display: flex; + align-items: center; +} + +.menu-icon { + display: inline-block; + height: 9px; + min-width: 9px; + margin: 13px; +} +.ether-icon { + background: rgb(0, 163, 68); + border-radius: 20px; +} +.testnet-icon { + background: #2465E1; +} + +.drop-menu-item { + display: flex; + align-items: center; +} + +.invisible { + visibility: hidden; +} + +.one-line-concat { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.critical-error { + text-align: center; + margin-top: 20px; + color: red; +} diff --git a/ui/responsive/app/css/reset.css b/ui/responsive/app/css/reset.css new file mode 100644 index 000000000..9ce89e8bc --- /dev/null +++ b/ui/responsive/app/css/reset.css @@ -0,0 +1,48 @@ +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +}
\ No newline at end of file diff --git a/ui/responsive/app/css/transitions.css b/ui/responsive/app/css/transitions.css new file mode 100644 index 000000000..393a944f9 --- /dev/null +++ b/ui/responsive/app/css/transitions.css @@ -0,0 +1,42 @@ +/* universal */ +.app-primary .main-enter { + position: absolute; + width: 100%; +} + +/* center position */ +.app-primary.from-right .main-enter-active, +.app-primary.from-left .main-enter-active { + overflow-x: hidden; + transform: translateX(0px); + transition: transform 300ms ease-in; +} + +/* exited positions */ +.app-primary.from-left .main-leave-active { + transform: translateX(360px); + transition: transform 300ms ease-in; +} +.app-primary.from-right .main-leave-active { + transform: translateX(-360px); + transition: transform 300ms ease-in; +} + +/* loader transitions */ +.loader-enter, .loader-leave-active { + opacity: 0.0; + transition: opacity 150 ease-in; +} +.loader-enter-active, .loader-leave { + opacity: 1.0; + transition: opacity 150 ease-in; +} + +/* entering positions */ +.app-primary.from-right .main-enter:not(.main-enter-active) { + transform: translateX(360px); +} +.app-primary.from-left .main-enter:not(.main-enter-active) { + transform: translateX(-360px); +} + diff --git a/ui/responsive/app/first-time/init-menu.js b/ui/responsive/app/first-time/init-menu.js new file mode 100644 index 000000000..cc7c51bd3 --- /dev/null +++ b/ui/responsive/app/first-time/init-menu.js @@ -0,0 +1,179 @@ +const inherits = require('util').inherits +const EventEmitter = require('events').EventEmitter +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const Mascot = require('../components/mascot') +const actions = require('../actions') +const Tooltip = require('../components/tooltip') +const getCaretCoordinates = require('textarea-caret') + +module.exports = connect(mapStateToProps)(InitializeMenuScreen) + +inherits(InitializeMenuScreen, Component) +function InitializeMenuScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + // state from plugin + currentView: state.appState.currentView, + warning: state.appState.warning, + } +} + +InitializeMenuScreen.prototype.render = function () { + var state = this.props + + switch (state.currentView.name) { + + default: + return this.renderMenu(state) + + } +} + +// InitializeMenuScreen.prototype.componentDidMount = function(){ +// document.getElementById('password-box').focus() +// } + +InitializeMenuScreen.prototype.renderMenu = function (state) { + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.3em', + textTransform: 'uppercase', + color: '#7F8082', + marginBottom: 10, + }, + }, 'MetaMask'), + + + h('div', [ + h('h3', { + style: { + fontSize: '0.8em', + color: '#7F8082', + display: 'inline', + }, + }, 'Encrypt your new DEN'), + + h(Tooltip, { + title: 'Your DEN is your password-encrypted storage within MetaMask.', + }, [ + h('i.fa.fa-question-circle.pointer', { + style: { + fontSize: '18px', + position: 'relative', + color: 'rgb(247, 134, 28)', + top: '2px', + marginLeft: '4px', + }, + }), + ]), + ]), + + h('span.in-progress-notification', state.warning), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createVaultOnEnter.bind(this), + onInput: this.inputChanged.bind(this), + style: { + width: 260, + marginTop: 16, + }, + }), + + + h('button.primary', { + onClick: this.createNewVaultAndKeychain.bind(this), + style: { + margin: 12, + }, + }, 'Create'), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: this.showRestoreVault.bind(this), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'Import Existing DEN'), + ]), + + ]) + ) +} + +InitializeMenuScreen.prototype.createVaultOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewVaultAndKeychain() + } +} + +InitializeMenuScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +InitializeMenuScreen.prototype.showRestoreVault = function () { + this.props.dispatch(actions.showRestoreVault()) +} + +InitializeMenuScreen.prototype.createNewVaultAndKeychain = function () { + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + + if (password.length < 8) { + this.warning = 'password not long enough' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + + this.props.dispatch(actions.createNewVaultAndKeychain(password)) +} + +InitializeMenuScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/responsive/app/img/identicon-tardigrade.png b/ui/responsive/app/img/identicon-tardigrade.png Binary files differnew file mode 100644 index 000000000..1742a32b8 --- /dev/null +++ b/ui/responsive/app/img/identicon-tardigrade.png diff --git a/ui/responsive/app/img/identicon-walrus.png b/ui/responsive/app/img/identicon-walrus.png Binary files differnew file mode 100644 index 000000000..d58fae912 --- /dev/null +++ b/ui/responsive/app/img/identicon-walrus.png diff --git a/ui/responsive/app/info.js b/ui/responsive/app/info.js new file mode 100644 index 000000000..e8470de97 --- /dev/null +++ b/ui/responsive/app/info.js @@ -0,0 +1,154 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(InfoScreen) + +function mapStateToProps (state) { + return {} +} + +inherits(InfoScreen, Component) +function InfoScreen () { + Component.call(this) +} + +InfoScreen.prototype.render = function () { + const state = this.props + const version = global.platform.getVersion() + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + state.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Info'), + ]), + + // main view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + // current version number + + h('.info.info-gray', [ + h('div', 'Metamask'), + h('div', { + style: { + marginBottom: '10px', + }, + }, `Version: ${version}`), + ]), + + h('div', { + style: { + marginBottom: '5px', + }}, + [ + h('div', [ + h('a', { + href: 'https://metamask.io/privacy.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Privacy Policy'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/terms.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Terms of Use'), + ]), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/attributions.html', + target: '_blank', + onClick (event) { this.navigateTo(event.target.href) }, + }, [ + h('div.info', 'Attributions'), + ]), + ]), + ] + ), + + h('hr', { + style: { + margin: '10px 0 ', + width: '7em', + }, + }), + + h('div', { + style: { + paddingLeft: '30px', + }}, + [ + h('div.fa.fa-github', [ + h('a.info', { + href: 'https://github.com/MetaMask/faq', + target: '_blank', + }, 'Need Help? Read our FAQ!'), + ]), + h('div', [ + h('a', { + href: 'https://metamask.io/', + target: '_blank', + }, [ + h('img.icon-size', { + src: 'images/icon-128.png', + style: { + // IE6-9 + filter: 'grayscale(100%)', + // Microsoft Edge and Firefox 35+ + WebkitFilter: 'grayscale(100%)', + }, + }), + h('div.info', 'Visit our web site'), + ]), + ]), + h('div.fa.fa-slack', [ + h('a.info', { + href: 'http://slack.metamask.io', + target: '_blank', + }, 'Join the conversation on Slack'), + ]), + + h('div.fa.fa-twitter', [ + h('a.info', { + href: 'https://twitter.com/metamask_io', + target: '_blank', + }, 'Follow us on Twitter'), + ]), + + h('div.fa.fa-envelope', [ + h('a.info', { + target: '_blank', + style: { width: '85vw' }, + href: 'mailto:help@metamask.io?subject=Feedback', + }, 'Email us!'), + ]), + ]), + ]), + ]), + ]) + ) +} + +InfoScreen.prototype.navigateTo = function (url) { + global.platform.openWindow({ url }) +} + diff --git a/ui/responsive/app/keychains/hd/create-vault-complete.js b/ui/responsive/app/keychains/hd/create-vault-complete.js new file mode 100644 index 000000000..c32751fff --- /dev/null +++ b/ui/responsive/app/keychains/hd/create-vault-complete.js @@ -0,0 +1,76 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen) + +inherits(CreateVaultCompleteScreen, Component) +function CreateVaultCompleteScreen () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + seed: state.appState.currentView.seedWords, + cachedSeed: state.metamask.seedWords, + } +} + +CreateVaultCompleteScreen.prototype.render = function () { + var state = this.props + var seed = state.seed || state.cachedSeed || '' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + // // subtitle and nav + // h('.section-title.flex-row.flex-center', [ + // h('h2.page-subtitle', 'Vault Created'), + // ]), + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: 36, + marginBottom: 8, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Vault Created', + ]), + + h('div', { + style: { + fontSize: '1em', + marginTop: '10px', + textAlign: 'center', + }, + }, [ + h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'), + ]), + + h('textarea.twelve-word-phrase', { + readOnly: true, + value: seed, + }), + + h('button.primary', { + onClick: () => this.confirmSeedWords(), + style: { + margin: '24px', + fontSize: '0.9em', + }, + }, 'I\'ve copied it somewhere safe'), + ]) + ) +} + +CreateVaultCompleteScreen.prototype.confirmSeedWords = function () { + this.props.dispatch(actions.confirmSeedWords()) +} diff --git a/ui/responsive/app/keychains/hd/recover-seed/confirmation.js b/ui/responsive/app/keychains/hd/recover-seed/confirmation.js new file mode 100644 index 000000000..4ccbec9fc --- /dev/null +++ b/ui/responsive/app/keychains/hd/recover-seed/confirmation.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits + +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../../actions') + +module.exports = connect(mapStateToProps)(RevealSeedConfirmation) + +inherits(RevealSeedConfirmation, Component) +function RevealSeedConfirmation () { + Component.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +RevealSeedConfirmation.prototype.render = function () { + const props = this.props + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Reveal Seed Words', + ]), + + h('.div', { + style: { + display: 'flex', + flexDirection: 'column', + padding: '20px', + justifyContent: 'center', + }, + }, [ + + h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), + + // confirmation + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'Enter your password to confirm', + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: '12px', + }, + }), + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + // cancel + h('button.primary', { + onClick: this.goHome.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.revealSeedWords.bind(this), + }, 'OK'), + + ]), + + (props.warning) && ( + h('span.error', { + style: { + margin: '20px', + }, + }, props.warning.split('-')) + ), + + props.inProgress && ( + h('span.in-progress-notification', 'Generating Seed...') + ), + ]), + ]) + ) +} + +RevealSeedConfirmation.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +RevealSeedConfirmation.prototype.goHome = function () { + this.props.dispatch(actions.showConfigPage(false)) +} + +// create vault + +RevealSeedConfirmation.prototype.checkConfirmation = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.revealSeedWords() + } +} + +RevealSeedConfirmation.prototype.revealSeedWords = function () { + var password = document.getElementById('password-box').value + this.props.dispatch(actions.requestRevealSeed(password)) +} diff --git a/ui/responsive/app/keychains/hd/restore-vault.js b/ui/responsive/app/keychains/hd/restore-vault.js new file mode 100644 index 000000000..06e51d9b3 --- /dev/null +++ b/ui/responsive/app/keychains/hd/restore-vault.js @@ -0,0 +1,152 @@ +const inherits = require('util').inherits +const PersistentForm = require('../../../lib/persistent-form') +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../../actions') + +module.exports = connect(mapStateToProps)(RestoreVaultScreen) + +inherits(RestoreVaultScreen, PersistentForm) +function RestoreVaultScreen () { + PersistentForm.call(this) +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + forgottenPassword: state.appState.forgottenPassword, + } +} + +RestoreVaultScreen.prototype.render = function () { + var state = this.props + this.persistentFormParentId = 'restore-vault-form' + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Restore Vault', + ]), + + // wallet seed entry + h('h3', 'Wallet Seed'), + h('textarea.twelve-word-phrase.letter-spacey', { + dataset: { + persistentFormId: 'wallet-seed', + }, + placeholder: 'Enter your secret twelve word phrase here to restore your vault.', + }), + + // password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'New Password (min 8 chars)', + dataset: { + persistentFormId: 'password', + }, + style: { + width: 260, + marginTop: 12, + }, + }), + + // confirm password + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box-confirm', + placeholder: 'Confirm Password', + onKeyPress: this.createOnEnter.bind(this), + dataset: { + persistentFormId: 'password-confirmation', + }, + style: { + width: 260, + marginTop: 16, + }, + }), + + (state.warning) && ( + h('span.error.in-progress-notification', state.warning) + ), + + // submit + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ + + // cancel + h('button.primary', { + onClick: this.showInitializeMenu.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.createNewVaultAndRestore.bind(this), + }, 'OK'), + + ]), + ]) + + ) +} + +RestoreVaultScreen.prototype.showInitializeMenu = function () { + if (this.props.forgottenPassword) { + this.props.dispatch(actions.backToUnlockView()) + } else { + this.props.dispatch(actions.showInitializeMenu()) + } +} + +RestoreVaultScreen.prototype.createOnEnter = function (event) { + if (event.key === 'Enter') { + this.createNewVaultAndRestore() + } +} + +RestoreVaultScreen.prototype.createNewVaultAndRestore = function () { + // check password + var passwordBox = document.getElementById('password-box') + var password = passwordBox.value + var passwordConfirmBox = document.getElementById('password-box-confirm') + var passwordConfirm = passwordConfirmBox.value + if (password.length < 8) { + this.warning = 'Password not long enough' + + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + if (password !== passwordConfirm) { + this.warning = 'Passwords don\'t match' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // check seed + var seedBox = document.querySelector('textarea.twelve-word-phrase') + var seed = seedBox.value.trim() + if (seed.split(' ').length !== 12) { + this.warning = 'seed phrases are 12 words long' + this.props.dispatch(actions.displayWarning(this.warning)) + return + } + // submit + this.warning = null + this.props.dispatch(actions.displayWarning(this.warning)) + this.props.dispatch(actions.createNewVaultAndRestore(password, seed)) +} diff --git a/ui/responsive/app/new-keychain.js b/ui/responsive/app/new-keychain.js new file mode 100644 index 000000000..cc9633166 --- /dev/null +++ b/ui/responsive/app/new-keychain.js @@ -0,0 +1,29 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(NewKeychain) + +function mapStateToProps (state) { + return {} +} + +inherits(NewKeychain, Component) +function NewKeychain () { + Component.call(this) +} + +NewKeychain.prototype.render = function () { + // const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + h('h1', `Here's a list!!!!`), + ]) + ) +} diff --git a/ui/responsive/app/reducers.js b/ui/responsive/app/reducers.js new file mode 100644 index 000000000..11efca529 --- /dev/null +++ b/ui/responsive/app/reducers.js @@ -0,0 +1,52 @@ +const extend = require('xtend') + +// +// Sub-Reducers take in the complete state and return their sub-state +// +const reduceIdentities = require('./reducers/identities') +const reduceMetamask = require('./reducers/metamask') +const reduceApp = require('./reducers/app') + +window.METAMASK_CACHED_LOG_STATE = null + +module.exports = rootReducer + +function rootReducer (state, action) { + // clone + state = extend(state) + + if (action.type === 'GLOBAL_FORCE_UPDATE') { + return action.value + } + + // + // Identities + // + + state.identities = reduceIdentities(state, action) + + // + // MetaMask + // + + state.metamask = reduceMetamask(state, action) + + // + // AppState + // + + state.appState = reduceApp(state, action) + + window.METAMASK_CACHED_LOG_STATE = state + return state +} + +window.logState = function () { + var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) + console.log(stateString) + return stateString +} + +function removeSeedWords (key, value) { + return key === 'seedWords' ? undefined : value +} diff --git a/ui/responsive/app/reducers/app.js b/ui/responsive/app/reducers/app.js new file mode 100644 index 000000000..2fcc9bfe0 --- /dev/null +++ b/ui/responsive/app/reducers/app.js @@ -0,0 +1,585 @@ +const extend = require('xtend') +const actions = require('../actions') +const txHelper = require('../../lib/tx-helper') + +module.exports = reduceApp + + +function reduceApp (state, action) { + log.debug('App Reducer got ' + action.type) + // clone and defaults + const selectedAddress = state.metamask.selectedAddress + const hasUnconfActions = checkUnconfActions(state) + let name = 'accounts' + if (selectedAddress) { + name = 'accountDetail' + } + if (hasUnconfActions) { + log.debug('pending txs detected, defaulting to conf-tx view.') + name = 'confTx' + } + + var defaultView = { + name, + detailView: null, + context: selectedAddress, + } + + // confirm seed words + var seedWords = state.metamask.seedWords + var seedConfView = { + name: 'createVaultComplete', + seedWords, + } + + // default state + var appState = extend({ + shouldClose: false, + menuOpen: false, + currentView: seedWords ? seedConfView : defaultView, + accountDetail: { + subview: 'transactions', + }, + transForward: true, // Used to render transition direction + isLoading: false, // Used to display loading indicator + warning: null, // Used to display error text + }, state.appState) + + switch (action.type) { + + // transition methods + + case actions.TRANSITION_FORWARD: + return extend(appState, { + transForward: true, + }) + + case actions.TRANSITION_BACKWARD: + return extend(appState, { + transForward: false, + }) + + // intialize + + case actions.SHOW_CREATE_VAULT: + return extend(appState, { + currentView: { + name: 'createVault', + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_RESTORE_VAULT: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: true, + forgottenPassword: true, + }) + + case actions.FORGOT_PASSWORD: + return extend(appState, { + currentView: { + name: 'restoreVault', + }, + transForward: false, + forgottenPassword: true, + }) + + case actions.SHOW_INIT_MENU: + return extend(appState, { + currentView: defaultView, + transForward: false, + }) + + case actions.SHOW_CONFIG_PAGE: + return extend(appState, { + currentView: { + name: 'config', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_ADD_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + + case actions.SHOW_IMPORT_PAGE: + + return extend(appState, { + currentView: { + name: 'import-menu', + }, + transForward: true, + }) + + case actions.SHOW_INFO_PAGE: + return extend(appState, { + currentView: { + name: 'info', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.CREATE_NEW_VAULT_IN_PROGRESS: + return extend(appState, { + currentView: { + name: 'createVault', + inProgress: true, + }, + transForward: true, + isLoading: true, + }) + + case actions.SHOW_NEW_VAULT_SEED: + return extend(appState, { + currentView: { + name: 'createVaultComplete', + seedWords: action.value, + }, + transForward: true, + isLoading: false, + }) + + case actions.NEW_ACCOUNT_SCREEN: + return extend(appState, { + currentView: { + name: 'new-account', + context: appState.currentView.context, + }, + transForward: true, + }) + + case actions.SHOW_SEND_PAGE: + return extend(appState, { + currentView: { + name: 'sendTransaction', + context: appState.currentView.context, + }, + transForward: true, + warning: null, + }) + + case actions.SHOW_NEW_KEYCHAIN: + return extend(appState, { + currentView: { + name: 'newKeychain', + context: appState.currentView.context, + }, + transForward: true, + }) + + // unlock + + case actions.UNLOCK_METAMASK: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + detailView: {}, + transForward: true, + isLoading: false, + warning: null, + }) + + case actions.LOCK_METAMASK: + return extend(appState, { + currentView: defaultView, + transForward: false, + warning: null, + }) + + case actions.BACK_TO_INIT_MENU: + return extend(appState, { + warning: null, + transForward: false, + forgottenPassword: true, + currentView: { + name: 'InitMenu', + }, + }) + + case actions.BACK_TO_UNLOCK_VIEW: + return extend(appState, { + warning: null, + transForward: true, + forgottenPassword: false, + currentView: { + name: 'UnlockScreen', + }, + }) + // reveal seed words + + case actions.REVEAL_SEED_CONFIRMATION: + return extend(appState, { + currentView: { + name: 'reveal-seed-conf', + }, + transForward: true, + warning: null, + }) + + // accounts + + case actions.SET_SELECTED_ACCOUNT: + return extend(appState, { + activeAddress: action.value, + }) + + case actions.GO_HOME: + return extend(appState, { + currentView: extend(appState.currentView, { + name: 'accountDetail', + }), + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + warning: null, + }) + + case actions.SHOW_ACCOUNT_DETAIL: + return extend(appState, { + forgottenPassword: appState.forgottenPassword ? !appState.forgottenPassword : null, + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.BACK_TO_ACCOUNT_DETAIL: + return extend(appState, { + currentView: { + name: 'accountDetail', + context: action.value, + }, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + transForward: false, + }) + + case actions.SHOW_ACCOUNTS_PAGE: + return extend(appState, { + currentView: { + name: seedWords ? 'createVaultComplete' : 'accounts', + seedWords, + }, + transForward: true, + isLoading: false, + warning: null, + scrollToBottom: false, + forgottenPassword: false, + }) + + case actions.SHOW_NOTICE: + return extend(appState, { + transForward: true, + isLoading: false, + }) + + case actions.REVEAL_ACCOUNT: + return extend(appState, { + scrollToBottom: true, + }) + + case actions.SHOW_CONF_TX_PAGE: + return extend(appState, { + currentView: { + name: 'confTx', + context: 0, + }, + transForward: action.transForward, + warning: null, + isLoading: false, + }) + + case actions.SHOW_CONF_MSG_PAGE: + return extend(appState, { + currentView: { + name: hasUnconfActions ? 'confTx' : 'account-detail', + context: 0, + }, + transForward: true, + warning: null, + isLoading: false, + }) + + case actions.COMPLETED_TX: + log.debug('reducing COMPLETED_TX for tx ' + action.value) + const otherUnconfActions = getUnconfActionList(state) + .filter(tx => tx.id !== action.value) + const hasOtherUnconfActions = otherUnconfActions.length > 0 + + if (hasOtherUnconfActions) { + log.debug('reducer detected txs - rendering confTx view') + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: 0, + }, + warning: null, + }) + } else { + log.debug('attempting to close popup') + return extend(appState, { + // indicate notification should close + shouldClose: true, + transForward: false, + warning: null, + currentView: { + name: 'accountDetail', + context: state.metamask.selectedAddress, + }, + accountDetail: { + subview: 'transactions', + }, + }) + } + + case actions.NEXT_TX: + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context: ++appState.currentView.context, + warning: null, + }, + }) + + case actions.VIEW_PENDING_TX: + const context = indexForPending(state, action.value) + return extend(appState, { + transForward: true, + currentView: { + name: 'confTx', + context, + warning: null, + }, + }) + + case actions.PREVIOUS_TX: + return extend(appState, { + transForward: false, + currentView: { + name: 'confTx', + context: --appState.currentView.context, + warning: null, + }, + }) + + case actions.TRANSACTION_ERROR: + return extend(appState, { + currentView: { + name: 'confTx', + errorMessage: 'There was a problem submitting this transaction.', + }, + }) + + case actions.UNLOCK_FAILED: + return extend(appState, { + warning: action.value || 'Incorrect password. Try again.', + }) + + case actions.SHOW_LOADING: + return extend(appState, { + isLoading: true, + loadingMessage: action.value, + }) + + case actions.HIDE_LOADING: + return extend(appState, { + isLoading: false, + }) + + case actions.SHOW_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: true, + }) + + case actions.HIDE_SUB_LOADING_INDICATION: + return extend(appState, { + isSubLoading: false, + }) + case actions.CLEAR_SEED_WORD_CACHE: + return extend(appState, { + transForward: true, + currentView: {}, + isLoading: false, + accountDetail: { + subview: 'transactions', + accountExport: 'none', + privateKey: '', + }, + }) + + case actions.DISPLAY_WARNING: + return extend(appState, { + warning: action.value, + isLoading: false, + }) + + case actions.HIDE_WARNING: + return extend(appState, { + warning: undefined, + }) + + case actions.REQUEST_ACCOUNT_EXPORT: + return extend(appState, { + transForward: true, + currentView: { + name: 'accountDetail', + context: appState.currentView.context, + }, + accountDetail: { + subview: 'export', + accountExport: 'requested', + }, + }) + + case actions.EXPORT_ACCOUNT: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + }, + }) + + case actions.SHOW_PRIVATE_KEY: + return extend(appState, { + accountDetail: { + subview: 'export', + accountExport: 'completed', + privateKey: action.value, + }, + }) + + case actions.BUY_ETH_VIEW: + return extend(appState, { + transForward: true, + currentView: { + name: 'buyEth', + context: appState.currentView.name, + }, + identity: state.metamask.identities[action.value], + buyView: { + subview: 'Coinbase', + amount: '15.00', + buyAddress: action.value, + formView: { + coinbase: true, + shapeshift: false, + }, + }, + }) + + case actions.COINBASE_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'Coinbase', + formView: { + coinbase: true, + shapeshift: false, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.SHAPESHIFT_SUBVIEW: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: action.value.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + }, + }) + + case actions.PAIR_UPDATE: + return extend(appState, { + buyView: { + subview: 'ShapeShift', + formView: { + coinbase: false, + shapeshift: true, + marketinfo: action.value.marketinfo, + coinOptions: appState.buyView.formView.coinOptions, + }, + buyAddress: appState.buyView.buyAddress, + amount: appState.buyView.amount, + warning: null, + }, + }) + + case actions.SHOW_QR: + return extend(appState, { + qrRequested: true, + transForward: true, + + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + + case actions.SHOW_QR_VIEW: + return extend(appState, { + currentView: { + name: 'qr', + context: appState.currentView.context, + }, + transForward: true, + Qr: { + message: action.value.message, + data: action.value.data, + }, + }) + default: + return appState + } +} + +function checkUnconfActions (state) { + const unconfActionList = getUnconfActionList(state) + const hasUnconfActions = unconfActionList.length > 0 + return hasUnconfActions +} + +function getUnconfActionList (state) { + const { unapprovedTxs, unapprovedMsgs, + unapprovedPersonalMsgs, network } = state.metamask + + const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + return unconfActionList +} + +function indexForPending (state, txId) { + const unconfTxList = getUnconfActionList(state) + const match = unconfTxList.find((tx) => tx.id === txId) + const index = unconfTxList.indexOf(match) + return index +} diff --git a/ui/responsive/app/reducers/identities.js b/ui/responsive/app/reducers/identities.js new file mode 100644 index 000000000..341a404e7 --- /dev/null +++ b/ui/responsive/app/reducers/identities.js @@ -0,0 +1,15 @@ +const extend = require('xtend') + +module.exports = reduceIdentities + +function reduceIdentities (state, action) { + // clone + defaults + var idState = extend({ + + }, state.identities) + + switch (action.type) { + default: + return idState + } +} diff --git a/ui/responsive/app/reducers/metamask.js b/ui/responsive/app/reducers/metamask.js new file mode 100644 index 000000000..e0c416c2d --- /dev/null +++ b/ui/responsive/app/reducers/metamask.js @@ -0,0 +1,137 @@ +const extend = require('xtend') +const actions = require('../actions') + +module.exports = reduceMetamask + +function reduceMetamask (state, action) { + let newState + + // clone + defaults + var metamaskState = extend({ + isInitialized: false, + isUnlocked: false, + rpcTarget: 'https://rawtestrpc.metamask.io/', + identities: {}, + unapprovedTxs: {}, + noActiveNotices: true, + lastUnreadNotice: undefined, + frequentRpcList: [], + addressBook: [], + }, state.metamask) + + switch (action.type) { + + case actions.SHOW_ACCOUNTS_PAGE: + newState = extend(metamaskState) + delete newState.seedWords + return newState + + case actions.SHOW_NOTICE: + return extend(metamaskState, { + noActiveNotices: false, + lastUnreadNotice: action.value, + }) + + case actions.CLEAR_NOTICES: + return extend(metamaskState, { + noActiveNotices: true, + }) + + case actions.UPDATE_METAMASK_STATE: + return extend(metamaskState, action.value) + + case actions.UNLOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + + case actions.LOCK_METAMASK: + return extend(metamaskState, { + isUnlocked: false, + }) + + case actions.SET_RPC_LIST: + return extend(metamaskState, { + frequentRpcList: action.value, + }) + + case actions.SET_RPC_TARGET: + return extend(metamaskState, { + provider: { + type: 'rpc', + rpcTarget: action.value, + }, + }) + + case actions.SET_PROVIDER_TYPE: + return extend(metamaskState, { + provider: { + type: action.value, + }, + }) + + case actions.COMPLETED_TX: + var stringId = String(action.id) + newState = extend(metamaskState, { + unapprovedTxs: {}, + unapprovedMsgs: {}, + }) + for (const id in metamaskState.unapprovedTxs) { + if (id !== stringId) { + newState.unapprovedTxs[id] = metamaskState.unapprovedTxs[id] + } + } + for (const id in metamaskState.unapprovedMsgs) { + if (id !== stringId) { + newState.unapprovedMsgs[id] = metamaskState.unapprovedMsgs[id] + } + } + return newState + + case actions.SHOW_NEW_VAULT_SEED: + return extend(metamaskState, { + isUnlocked: true, + isInitialized: false, + seedWords: action.value, + }) + + case actions.CLEAR_SEED_WORD_CACHE: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SHOW_ACCOUNT_DETAIL: + newState = extend(metamaskState, { + isUnlocked: true, + isInitialized: true, + selectedAddress: action.value, + }) + delete newState.seedWords + return newState + + case actions.SAVE_ACCOUNT_LABEL: + const account = action.value.account + const name = action.value.label + var id = {} + id[account] = extend(metamaskState.identities[account], { name }) + var identities = extend(metamaskState.identities, id) + return extend(metamaskState, { identities }) + + case actions.SET_CURRENT_FIAT: + return extend(metamaskState, { + currentCurrency: action.value.currentCurrency, + conversionRate: action.value.conversionRate, + conversionDate: action.value.conversionDate, + }) + + default: + return metamaskState + + } +} diff --git a/ui/responsive/app/root.js b/ui/responsive/app/root.js new file mode 100644 index 000000000..9e7314b20 --- /dev/null +++ b/ui/responsive/app/root.js @@ -0,0 +1,22 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const Provider = require('react-redux').Provider +const h = require('react-hyperscript') +const App = require('./app') + +module.exports = Root + +inherits(Root, Component) +function Root () { Component.call(this) } + +Root.prototype.render = function () { + return ( + + h(Provider, { + store: this.props.store, + }, [ + h(App), + ]) + + ) +} diff --git a/ui/responsive/app/send.js b/ui/responsive/app/send.js new file mode 100644 index 000000000..a21a219eb --- /dev/null +++ b/ui/responsive/app/send.js @@ -0,0 +1,288 @@ +const inherits = require('util').inherits +const PersistentForm = require('../lib/persistent-form') +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const Identicon = require('./components/identicon') +const actions = require('./actions') +const util = require('./util') +const numericBalance = require('./util').numericBalance +const addressSummary = require('./util').addressSummary +const isHex = require('./util').isHex +const EthBalance = require('./components/eth-balance') +const EnsInput = require('./components/ens-input') +const ethUtil = require('ethereumjs-util') +module.exports = connect(mapStateToProps)(SendTransactionScreen) + +function mapStateToProps (state) { + var result = { + address: state.metamask.selectedAddress, + accounts: state.metamask.accounts, + identities: state.metamask.identities, + warning: state.appState.warning, + network: state.metamask.network, + addressBook: state.metamask.addressBook, + conversionRate: state.metamask.conversionRate, + currentCurrency: state.metamask.currentCurrency, + } + + result.error = result.warning && result.warning.split('.')[0] + + result.account = result.accounts[result.address] + result.identity = result.identities[result.address] + result.balance = result.account ? numericBalance(result.account.balance) : null + + return result +} + +inherits(SendTransactionScreen, PersistentForm) +function SendTransactionScreen () { + PersistentForm.call(this) +} + +SendTransactionScreen.prototype.render = function () { + this.persistentFormParentId = 'send-tx-form' + + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate, + currentCurrency, + } = props + + return ( + + h('.send-screen.flex-column.flex-grow', [ + + // + // Sender Profile + // + + h('.account-data-subsection.flex-row.flex-grow', { + style: { + margin: '0 20px', + }, + }, [ + + // header - identicon + nav + h('.flex-row.flex-space-between', { + style: { + marginTop: '15px', + }, + }, [ + // back button + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', { + onClick: this.back.bind(this), + }), + + // large identicon + h('.identicon-wrapper.flex-column.flex-center.select-none', [ + h(Identicon, { + diameter: 62, + address: address, + }), + ]), + + // invisible place holder + h('i.fa.fa-users.fa-lg.invisible', { + style: { + marginTop: '28px', + }, + }), + + ]), + + // account label + + h('.flex-column', { + style: { + marginTop: '10px', + alignItems: 'flex-start', + }, + }, [ + h('h2.font-medium.color-forest.flex-center', { + style: { + paddingTop: '8px', + marginBottom: '8px', + }, + }, identity && identity.name), + + // address and getter actions + h('.flex-row.flex-center', { + style: { + marginBottom: '8px', + }, + }, [ + + h('div', { + style: { + lineHeight: '16px', + }, + }, addressSummary(address)), + + ]), + + // balance + h('.flex-row.flex-center', [ + + h(EthBalance, { + value: account && account.balance, + conversionRate, + currentCurrency, + }), + + ]), + ]), + ]), + + // + // Required Fields + // + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '15px', + marginBottom: '16px', + }, + }, [ + 'Send Transaction', + ]), + + // error message + props.error && h('span.error.flex-center', props.error), + + // 'to' field + h('section.flex-row.flex-center', [ + h(EnsInput, { + name: 'address', + placeholder: 'Recipient Address', + onChange: this.recipientDidChange.bind(this), + network, + identities, + addressBook, + }), + ]), + + // 'amount' and send button + h('section.flex-row.flex-center', [ + + h('input.large-input', { + name: 'amount', + placeholder: 'Amount', + type: 'number', + style: { + marginRight: '6px', + }, + dataset: { + persistentFormId: 'tx-amount', + }, + }), + + h('button.primary', { + onClick: this.onSubmit.bind(this), + style: { + textTransform: 'uppercase', + }, + }, 'Next'), + + ]), + + // + // Optional Fields + // + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginTop: '16px', + marginBottom: '16px', + }, + }, [ + 'Transaction Data (optional)', + ]), + + // 'data' field + h('section.flex-column.flex-center', [ + h('input.large-input', { + name: 'txData', + placeholder: '0x01234', + style: { + width: '100%', + resize: 'none', + }, + dataset: { + persistentFormId: 'tx-data', + }, + }), + ]), + ]) + ) +} + +SendTransactionScreen.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} + +SendTransactionScreen.prototype.back = function () { + var address = this.props.address + this.props.dispatch(actions.backToAccountDetail(address)) +} + +SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) { + this.setState({ + recipient: recipient, + nickname: nickname, + }) +} + +SendTransactionScreen.prototype.onSubmit = function () { + const state = this.state || {} + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') + const nickname = state.nickname || ' ' + const input = document.querySelector('input[name="amount"]').value + const value = util.normalizeEthStringToWei(input) + const txData = document.querySelector('input[name="txData"]').value + const balance = this.props.balance + let message + + if (value.gt(balance)) { + message = 'Insufficient funds.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (input < 0) { + message = 'Can not send negative amounts of ETH.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) { + message = 'Recipient address is invalid.' + return this.props.dispatch(actions.displayWarning(message)) + } + + if (!isHex(ethUtil.stripHexPrefix(txData)) && txData) { + message = 'Transaction data must be hex string.' + return this.props.dispatch(actions.displayWarning(message)) + } + + this.props.dispatch(actions.hideWarning()) + + this.props.dispatch(actions.addToAddressBook(recipient, nickname)) + + var txParams = { + from: this.props.address, + value: '0x' + value.toString(16), + } + + if (recipient) txParams.to = ethUtil.addHexPrefix(recipient) + if (txData) txParams.data = txData + + this.props.dispatch(actions.signTx(txParams)) +} diff --git a/ui/responsive/app/settings.js b/ui/responsive/app/settings.js new file mode 100644 index 000000000..454cc95e0 --- /dev/null +++ b/ui/responsive/app/settings.js @@ -0,0 +1,59 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +module.exports = connect(mapStateToProps)(AppSettingsPage) + +function mapStateToProps (state) { + return {} +} + +inherits(AppSettingsPage, Component) +function AppSettingsPage () { + Component.call(this) +} + +AppSettingsPage.prototype.render = function () { + return ( + + h('.account-detail-section.flex-column.flex-grow', [ + + // subtitle and nav + h('.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: this.navigateToAccounts.bind(this), + }), + h('h2.page-subtitle', 'Settings'), + ]), + + h('label', { + htmlFor: 'settings-rpc-endpoint', + }, 'RPC Endpoint:'), + h('input', { + type: 'url', + id: 'settings-rpc-endpoint', + onKeyPress: this.onKeyPress.bind(this), + }), + + ]) + + ) +} + +AppSettingsPage.prototype.componentDidMount = function () { + document.querySelector('input').focus() +} + +AppSettingsPage.prototype.onKeyPress = function (event) { + // get submit event + if (event.key === 'Enter') { + // this.submitPassword(event) + } +} + +AppSettingsPage.prototype.navigateToAccounts = function (event) { + event.stopPropagation() + this.props.dispatch(actions.showAccountsPage()) +} diff --git a/ui/responsive/app/store.js b/ui/responsive/app/store.js new file mode 100644 index 000000000..ba9e58b49 --- /dev/null +++ b/ui/responsive/app/store.js @@ -0,0 +1,21 @@ +const createStore = require('redux').createStore +const applyMiddleware = require('redux').applyMiddleware +const thunkMiddleware = require('redux-thunk') +const rootReducer = require('./reducers') +const createLogger = require('redux-logger') + +global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' + +module.exports = configureStore + +const loggerMiddleware = createLogger({ + predicate: () => global.METAMASK_DEBUG, +}) + +const middlewares = [thunkMiddleware, loggerMiddleware] + +const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore) + +function configureStore (initialState) { + return createStoreWithMiddleware(rootReducer, initialState) +} diff --git a/ui/responsive/app/template.js b/ui/responsive/app/template.js new file mode 100644 index 000000000..d15b30fd2 --- /dev/null +++ b/ui/responsive/app/template.js @@ -0,0 +1,30 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect + +module.exports = connect(mapStateToProps)(COMPONENTNAME) + +function mapStateToProps (state) { + return {} +} + +inherits(COMPONENTNAME, Component) +function COMPONENTNAME () { + Component.call(this) +} + +COMPONENTNAME.prototype.render = function () { + const props = this.props + + return ( + h('div', { + style: { + background: 'blue', + }, + }, [ + `Hello, ${props.sender}`, + ]) + ) +} + diff --git a/ui/responsive/app/unlock.js b/ui/responsive/app/unlock.js new file mode 100644 index 000000000..1aee3c5d0 --- /dev/null +++ b/ui/responsive/app/unlock.js @@ -0,0 +1,118 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') +const getCaretCoordinates = require('textarea-caret') +const EventEmitter = require('events').EventEmitter + +const Mascot = require('./components/mascot') + +module.exports = connect(mapStateToProps)(UnlockScreen) + +inherits(UnlockScreen, Component) +function UnlockScreen () { + Component.call(this) + this.animationEventEmitter = new EventEmitter() +} + +function mapStateToProps (state) { + return { + warning: state.appState.warning, + } +} + +UnlockScreen.prototype.render = function () { + const state = this.props + const warning = state.warning + return ( + h('.flex-column', [ + h('.unlock-screen.flex-column.flex-center.flex-grow', [ + + h(Mascot, { + animationEventEmitter: this.animationEventEmitter, + }), + + h('h1', { + style: { + fontSize: '1.4em', + textTransform: 'uppercase', + color: '#7F8082', + }, + }, 'MetaMask'), + + h('input.large-input', { + type: 'password', + id: 'password-box', + placeholder: 'enter password', + style: { + + }, + onKeyPress: this.onKeyPress.bind(this), + onInput: this.inputChanged.bind(this), + }), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + h('button.primary.cursor-pointer', { + onClick: this.onSubmit.bind(this), + style: { + margin: 10, + }, + }, 'Unlock'), + ]), + + h('.flex-row.flex-center.flex-grow', [ + h('p.pointer', { + onClick: () => this.props.dispatch(actions.forgotPassword()), + style: { + fontSize: '0.8em', + color: 'rgb(247, 134, 28)', + textDecoration: 'underline', + }, + }, 'I forgot my password.'), + ]), + ]) + ) +} + +UnlockScreen.prototype.componentDidMount = function () { + document.getElementById('password-box').focus() +} + +UnlockScreen.prototype.onSubmit = function (event) { + const input = document.getElementById('password-box') + const password = input.value + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.onKeyPress = function (event) { + if (event.key === 'Enter') { + this.submitPassword(event) + } +} + +UnlockScreen.prototype.submitPassword = function (event) { + var element = event.target + var password = element.value + // reset input + element.value = '' + this.props.dispatch(actions.tryUnlockMetamask(password)) +} + +UnlockScreen.prototype.inputChanged = function (event) { + // tell mascot to look at page action + var element = event.target + var boundingRect = element.getBoundingClientRect() + var coordinates = getCaretCoordinates(element, element.selectionEnd) + this.animationEventEmitter.emit('point', { + x: boundingRect.left + coordinates.left - element.scrollLeft, + y: boundingRect.top + coordinates.top - element.scrollTop, + }) +} diff --git a/ui/responsive/app/util.js b/ui/responsive/app/util.js new file mode 100644 index 000000000..ac3f42c6b --- /dev/null +++ b/ui/responsive/app/util.js @@ -0,0 +1,217 @@ +const ethUtil = require('ethereumjs-util') + +var valueTable = { + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', + szabo: '1000000', + finney: '1000', + ether: '1', + kether: '0.001', + mether: '0.000001', + gether: '0.000000001', + tether: '0.000000000001', +} +var bnTable = {} +for (var currency in valueTable) { + bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) +} + +module.exports = { + valuesFor: valuesFor, + addressSummary: addressSummary, + miniAddressSummary: miniAddressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, + numericBalance: numericBalance, + parseBalance: parseBalance, + formatBalance: formatBalance, + generateBalanceObject: generateBalanceObject, + dataSize: dataSize, + readableDate: readableDate, + normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, + normalizeNumberToWei: normalizeNumberToWei, + valueTable: valueTable, + bnTable: bnTable, + isHex: isHex, +} + +function valuesFor (obj) { + if (!obj) return [] + return Object.keys(obj) + .map(function (key) { return obj[key] }) +} + +function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { + if (!address) return '' + let checked = ethUtil.toChecksumAddress(address) + if (!includeHex) { + checked = ethUtil.stripHexPrefix(checked) + } + return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' +} + +function miniAddressSummary (address) { + if (!address) return '' + var checked = ethUtil.toChecksumAddress(address) + return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' +} + +function isValidAddress (address) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isAllOneCase (address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper +} + +// Takes wei Hex, returns wei BN, even if input is null +function numericBalance (balance) { + if (!balance) return new ethUtil.BN(0, 16) + var stripped = ethUtil.stripHexPrefix(balance) + return new ethUtil.BN(stripped, 16) +} + +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance (balance) { + var beforeDecimal, afterDecimal + const wei = numericBalance(balance) + var weiString = wei.toString() + const trailingZeros = /0+$/ + + beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' + afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') + if (afterDecimal === '') { afterDecimal = '0' } + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns an object with three properties. +// Its "formatted" property is what we generally use to render values. +function formatBalance (balance, decimalsToKeep, needsParse = true) { + var parsed = needsParse ? parseBalance(balance) : balance.split('.') + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + var formatted = 'None' + if (decimalsToKeep === undefined) { + if (beforeDecimal === '0') { + if (afterDecimal !== '0') { + var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits + if (sigFigs) { afterDecimal = sigFigs[0] } + formatted = '0.' + afterDecimal + ' ETH' + } + } else { + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ' ETH' + } + } else { + afterDecimal += Array(decimalsToKeep).join('0') + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ' ETH' + } + return formatted +} + + +function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { + var balance = formattedBalance.split(' ')[0] + var label = formattedBalance.split(' ')[1] + var beforeDecimal = balance.split('.')[0] + var afterDecimal = balance.split('.')[1] + var shortBalance = shortenBalance(balance, decimalsToKeep) + + if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { + // eslint-disable-next-line eqeqeq + if (afterDecimal == 0) { + balance = '0' + } else { + balance = '<1.0e-5' + } + } else if (beforeDecimal !== '0') { + balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` + } + + return { balance, label, shortBalance } +} + +function shortenBalance (balance, decimalsToKeep = 1) { + var truncatedValue + var convertedBalance = parseFloat(balance) + if (convertedBalance > 1000000) { + truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) + return `${truncatedValue}m` + } else if (convertedBalance > 1000) { + truncatedValue = (balance / 1000).toFixed(decimalsToKeep) + return `${truncatedValue}k` + } else if (convertedBalance === 0) { + return '0' + } else if (convertedBalance < 0.001) { + return '<0.001' + } else if (convertedBalance < 1) { + var stringBalance = convertedBalance.toString() + if (stringBalance.split('.')[1].length > 3) { + return convertedBalance.toFixed(3) + } else { + return stringBalance + } + } else { + return convertedBalance.toFixed(decimalsToKeep) + } +} + +function dataSize (data) { + var size = data ? ethUtil.stripHexPrefix(data).length : 0 + return size + ' bytes' +} + +// Takes a BN and an ethereum currency name, +// returns a BN in wei +function normalizeToWei (amount, currency) { + try { + return amount.mul(bnTable.wei).div(bnTable[currency]) + } catch (e) {} + return amount +} + +function normalizeEthStringToWei (str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while (decimal.length < 18) { + decimal += '0' + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) +function normalizeNumberToWei (n, currency) { + var enlarged = n * 10000 + var amount = new ethUtil.BN(String(enlarged), 10) + return normalizeToWei(amount, currency).div(multiple) +} + +function readableDate (ms) { + var date = new Date(ms) + var month = date.getMonth() + var day = date.getDate() + var year = date.getFullYear() + var hours = date.getHours() + var minutes = '0' + date.getMinutes() + var seconds = '0' + date.getSeconds() + + var dateStr = `${month}/${day}/${year}` + var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` + return `${dateStr} ${time}` +} + +function isHex (str) { + return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) +} diff --git a/ui/responsive/css.js b/ui/responsive/css.js new file mode 100644 index 000000000..7c394a87b --- /dev/null +++ b/ui/responsive/css.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const path = require('path') + +module.exports = bundleCss + +var cssFiles = { + 'fonts.css': fs.readFileSync(path.join(__dirname, '/app/css/fonts.css'), 'utf8'), + 'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'), + 'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'), + 'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'), + 'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'), + 'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'), + 'react-css': fs.readFileSync(path.join(__dirname, '..', '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'), +} + +function bundleCss () { + var cssBundle = Object.keys(cssFiles).reduce(function (bundle, fileName) { + var fileContent = cssFiles[fileName] + var output = String() + + output += '/*========== ' + fileName + ' ==========*/\n\n' + output += fileContent + output += '\n\n' + + return bundle + output + }, String()) + + return cssBundle +} diff --git a/ui/responsive/design/00-metamask-SignIn.jpg b/ui/responsive/design/00-metamask-SignIn.jpg Binary files differnew file mode 100644 index 000000000..2becdb032 --- /dev/null +++ b/ui/responsive/design/00-metamask-SignIn.jpg diff --git a/ui/responsive/design/01-metamask-SelectAcc.jpg b/ui/responsive/design/01-metamask-SelectAcc.jpg Binary files differnew file mode 100644 index 000000000..239091a98 --- /dev/null +++ b/ui/responsive/design/01-metamask-SelectAcc.jpg diff --git a/ui/responsive/design/02-metamask-AccDetails.jpg b/ui/responsive/design/02-metamask-AccDetails.jpg Binary files differnew file mode 100644 index 000000000..d7d408ffc --- /dev/null +++ b/ui/responsive/design/02-metamask-AccDetails.jpg diff --git a/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg b/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg Binary files differnew file mode 100644 index 000000000..f26ff31e8 --- /dev/null +++ b/ui/responsive/design/02a-metamask-AccDetails-OverToken.jpg diff --git a/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg b/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg Binary files differnew file mode 100644 index 000000000..8a06be6b9 --- /dev/null +++ b/ui/responsive/design/02a-metamask-AccDetails-OverTransaction.jpg diff --git a/ui/responsive/design/02a-metamask-AccDetails.jpg b/ui/responsive/design/02a-metamask-AccDetails.jpg Binary files differnew file mode 100644 index 000000000..c37e0f539 --- /dev/null +++ b/ui/responsive/design/02a-metamask-AccDetails.jpg diff --git a/ui/responsive/design/02b-metamask-AccDetails-Send.jpg b/ui/responsive/design/02b-metamask-AccDetails-Send.jpg Binary files differnew file mode 100644 index 000000000..10f2d27fd --- /dev/null +++ b/ui/responsive/design/02b-metamask-AccDetails-Send.jpg diff --git a/ui/responsive/design/03-metamask-Qr.jpg b/ui/responsive/design/03-metamask-Qr.jpg Binary files differnew file mode 100644 index 000000000..9c09de42f --- /dev/null +++ b/ui/responsive/design/03-metamask-Qr.jpg diff --git a/ui/responsive/design/05-metamask-Menu.jpg b/ui/responsive/design/05-metamask-Menu.jpg Binary files differnew file mode 100644 index 000000000..0a43d7b2a --- /dev/null +++ b/ui/responsive/design/05-metamask-Menu.jpg diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png b/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png Binary files differnew file mode 100644 index 000000000..805cc96b6 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/final_screen_dao_accounts.png diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png b/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png Binary files differnew file mode 100644 index 000000000..9d9e33930 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/final_screen_dao_locked.png diff --git a/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png b/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png Binary files differnew file mode 100644 index 000000000..d56a5ce62 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/final_screen_dao_notification.png diff --git a/ui/responsive/design/chromeStorePics/final_screen_wei_account.png b/ui/responsive/design/chromeStorePics/final_screen_wei_account.png Binary files differnew file mode 100644 index 000000000..d503ff301 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/final_screen_wei_account.png diff --git a/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png b/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png Binary files differnew file mode 100644 index 000000000..3560c51ff --- /dev/null +++ b/ui/responsive/design/chromeStorePics/final_screen_wei_notification.png diff --git a/ui/responsive/design/chromeStorePics/icon-128.png b/ui/responsive/design/chromeStorePics/icon-128.png Binary files differnew file mode 100644 index 000000000..ae687147d --- /dev/null +++ b/ui/responsive/design/chromeStorePics/icon-128.png diff --git a/ui/responsive/design/chromeStorePics/icon-64.png b/ui/responsive/design/chromeStorePics/icon-64.png Binary files differnew file mode 100644 index 000000000..7062cf4f1 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/icon-64.png diff --git a/ui/responsive/design/chromeStorePics/metamask_icon.ai b/ui/responsive/design/chromeStorePics/metamask_icon.ai new file mode 100644 index 000000000..27400c5a4 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/metamask_icon.ai @@ -0,0 +1,2383 @@ +%PDF-1.5
%
+1 0 obj
<</Metadata 2 0 R/OCProperties<</D<</ON[5 0 R]/Order 6 0 R/RBGroups[]>>/OCGs[5 0 R]>>/Pages 3 0 R/Type/Catalog>>
endobj
2 0 obj
<</Length 47428/Subtype/XML/Type/Metadata>>stream
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> +<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c111 79.158366, 2015/09/25-01:12:00 "> + <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <rdf:Description rdf:about="" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:xmp="http://ns.adobe.com/xap/1.0/" + xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/" + xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" + xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#" + xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#" + xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/" + xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/" + xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#" + xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/" + xmlns:pdf="http://ns.adobe.com/pdf/1.3/"> + <dc:format>application/pdf</dc:format> + <dc:title> + <rdf:Alt> + <rdf:li xml:lang="x-default">metamask_icon</rdf:li> + </rdf:Alt> + </dc:title> + <xmp:CreatorTool>Adobe Illustrator CC 2015 (Macintosh)</xmp:CreatorTool> + <xmp:CreateDate>2016-06-15T14:23:12-04:00</xmp:CreateDate> + <xmp:ModifyDate>2016-06-15T14:23:12-04:00</xmp:ModifyDate> + <xmp:MetadataDate>2016-06-15T14:23:12-04:00</xmp:MetadataDate> + <xmp:Thumbnails> + <rdf:Alt> + <rdf:li rdf:parseType="Resource"> + <xmpGImg:width>240</xmpGImg:width> + <xmpGImg:height>256</xmpGImg:height> + <xmpGImg:format>JPEG</xmpGImg:format> + <xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA
AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK
DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f
Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAADwAwER
AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA
AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB
UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE
1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ
qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy
obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp
0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo
+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7
FXnP5r/mvB5Tg/RmnAT6/cJyUMKx28bVAkf+ZjT4U+k7UDYuo1HBsObl6bTce5+l5X+Wf5t6jonm
KZtfu5rzTNVcG+lkZpHilACLOAamgUBWC/sgUrxAzEwakxl6uRczUaYSj6eYfS9vcQXEEdxA6ywT
KskUimqsjCqsCOoIObUG3UkUvxQ7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY
q7FXYq7FXYq7FXln5rfnHb+X1l0bQnWfXCCs0+zR2tfEGoaTwXoO/hmJqNTw7Dm5mm0plvL6fvfO
U889xPJcXEjTTzM0ksshLO7saszMdySTUk5qybdsBSzAl7R+Rv5ni0dPKutTqto5ppNw/KqyOwH1
c0BHFuVVJpTpvUUz9Jnr0n4Ov1mnv1D4ve82LrHYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq
7FUn1Pzl5W0yF5r3VLeNI9no4kYfNU5N+GY89XijsZC/mfkHIhpMstxE18h8ynCmqg7iorQ7HMhx
3Yq7FXYq7FXYq7FXjf5v/nG2nPL5e8tXAN9Ro9Rv039A7D04WBp6nUM1Ph7fFXjg6nU16Y83P0ul
v1S5PAWZmYsxJYmpJ3JJzWu0axV2KuxV9Ifkr+Zq67py6FrF1y121+G3eUjlcwKtQeRPxyIAeXci
jbnkc2mlz8Qo83U6vT8J4h9L1PMxwnYq7FXYq7FXYq7FXYq7FXYq7FXYqlN95s8t2I/0jUYQQaFE
b1HHzWPk34Zi5Nbhh9Uh9/3OVi0Waf0xP3fex7UfzX0WEOtjBNdSCoR2AjjPgak8/wDhcwcvbWIf
SDL7B+v7HOxdi5T9REftP6vtYre/mf5ouD+5eK0UdoowxPzMnqfhmsydsZpcqj7h+u3aY+x8Eedy
95/VTFdc803n1dptVv5powSVjeRmqx7IhNMxPEy5jRJPx2cwY8WEWAB8N0l8gRXnm/z/AKXazpXT
7WX65NCo5II4PjHqVrXk3FDX+btXNtodLETDqddqiYH7H1LnQPOuxV2KuxV2KuZlVSzEBQKknYAD
FXhX5t/nOsyzaB5XuCEDBbzVoXZSSrA8Ld0I2qKM/foNt81+o1X8Mfm7LTaT+KXyeI5r3YuxV2Ku
xV2KqtpdXNpdQ3VrI0NzA6yQyoaMroaqwPiCMINboIsUX1j+XH5g6f5w0WOcNHDq0I439irbqwoD
IiklvTaux7dK1GbnBmEx5ukz4DjPky3Lmh2KuxV2KuxV2KuxVKb/AM2eW7CoudQhDA0KI3qMD7rH
yYZjZdbhh9Uh9/3OVi0Waf0xP3fex6//ADY0OHktnbzXTKaKxpFGw8QTyb/hcwMnbWIfSDL7B+Pg
5+PsXKa4iI/afx8WO3/5ra9OJEtYIbVG2RqNJIo/1iQv/C5gZe2sp+kCP2n8fBz8XYuIVxEy+wfj
4sVvdY1a+FLy8muFBLBZJGZQT4KTQZrMmfJP6pE/F2ePBjh9MQPghMqbXYqler+YLPTgUJ9W57Qq
en+se2X4dPKfuaMueMPewW+vrm9uGnuH5Ox2G/FR/KoPQZtYQERQdZOZkbL3L/nG7y8Y7PVPMEqj
lOy2VqxBDBEpJKQSPsszINu6nNpoYbGTqdfPcRe1ZnuvdirsVdirsVeF/n1+Y96l1N5O04+lCERt
Vn35uXAkWBfBOJVmI+1XjsAeWv1ec3wD4uy0eAVxn4PEM17sXYq7FXYq7FXYq7FU18seZNU8uaxD
qumymOeKqsBQh0YUZCGDDceI2O+ESlH6TRYmEZbSFh7bbfmL5mubeO4iv6xSqHQ+lD0YV/kzVy7U
1MTRl9kf1Owj2XpiLEftP61T/Hvmv/lt/wCSUP8AzRkf5W1P877I/qZfyTp/5v2n9bv8e+a/+W3/
AJJQ/wDNGP8AK2p/nfZH9S/yTp/5v2n9bv8AHvmv/lt/5JQ/80Y/ytqf532R/Uv8k6f+b9p/W7/H
vmv/AJbf+SUP/NGP8ran+d9kf1L/ACTp/wCb9p/W7/Hvmv8A5bf+SUP/ADRj/K2p/nfZH9S/yTp/
5v2n9bHtW1/WtUeuoXTz8eiGioCNtkUKv4ZDNqsmX6zbdh0uPF9ApL8ob3Yq7FXYq7FWO695pW0d
rWyo9wtRJKd1Q+A8WH3ZmYNLxby5OJn1PDtHmw6SR5JGkclnclmY9STuTmzArZ1xN7uhhlmmSGJS
8sjBI0HUsxoAPpwhiS+zvLGhxaF5e0/SIiGFlAkTOoIDuB8b0JNOb1bN5jhwxAdBknxSJ70zybB2
KuxV2KpX5o12HQfL2oaxNxK2UDyKjHiHkpSOOu9ObkL9OQyT4Yks8cOKQHe+Nr6+u7+8mvLyVp7q
4cyTTOaszNuSc0ZJJsu/AAFBRwJdirsVdirsVdirsVdirKvI2tmC6OmzN+5uDWEmlFkp03/mp9/z
zX67BY4hzDm6PNR4T1Z5mpdo7FXYq7FXYqg7laSn33y2PJgVLJIdirsVWu6IjO7BUUEsxNAAOpJO
IFqTTD9c81yT1gsGaKIH4px8LtT+Xuo/HNlg0gG8ubrs2qJ2ixzM1w3Yqzz8k/L66z5/s2kAMGmK
2oSAkgkwkCKlO4ldDTwBzJ0sOKfucbVz4YHz2fU+bd0rsVdirsVdirxT/nIzzWi2ll5ZtZ1Mkj/W
dRjQnkqoB6KPTajli9D/ACqcwNbk2EQ7DQ49zIvB81zs3Yq7FXYq7FXYq7FXYq7FW0d0dXRirqQV
YGhBG4IIxItQXp3ljWjqunc5Cv1qI8J1Xb/Van+UPxrmh1WDw5bcnc6fNxx35pvmO5DsVdirsVQ1
2v2W+gnJwYlD5YxdiqheXttZwGe4cRxjap6k+AHc5KEDI0GM5iIssE1fzBeakeB/dWw6Qqevux7n
Nth08Ye91eXPKfuSzL2h2KuxV9If84/eVk03ys+tyEm51lqhSKcIYHdEArv8Zq3uKZtdHjqN97qN
bkuVdz1PMtw3Yq7FXYqp3V1b2ltNdXMgit4EaWaVjRVRByZifAAYCaSBez418169Nr/mPUdYl5Vv
Z2kjVyCyRVpEhIp9iMKv0Zo8k+KRLv8AHDhiB3JVkGbsVdirsVdirsVdirsVdirsVTPy7q50vU45
2J9B/guFHdD36H7J3/DKNTh8SFdejdgy8Er6PUYpY5okljblHIoZGHQqwqDmhIINF3QNiwuwJdir
sVUrlaxH23yUeaCg8tYJfq2t2Wmx/vW5TleUcC/abtv/ACj3OXYsEp8uTVlzRhz5sD1DULm+uGnu
GLE/ZX9lR/KozbY8YgKDqsmQyNlDZNg7FXYqiNN0+51HUbXT7UBrm8mjggUmgLysEWp7bnDEWaRK
VCy+0tM0+307TbXT7YEW9nDHbwgmp4RKEWp+QzfRFCnnpSs2UThQ7FXYq7FXmX59ebIdL8ovpEMw
Goauyx+mrFXW2U8pH2/Zbj6dD15HwOYmryVGupczR4uKd9A+ac1Tt3Yq7FXYq7FXYq7FXYq7FXYq
7FXYqzXyJrSem2lztRgS9sSQAQT8SD3ruPpzV6/Bvxj4ux0Wb+EsxzWuwdirsVadeSlfEEYhDE9b
8zwWLNb24E10pKuK/AhHjTqa9s2ODSme52Dh5tSI7DcsJmmlmkaWVy8jmrOxqTm0AAFB1hJJsrMK
HYq7FXYq9S/5x+8qvqXmp9bkIFtoq1CEV5zTo6IBXb4RVq9jTMzR47lfc4WtyVHh730jm0dS7FXY
q7FXYq+X/wA+dQmuvzGu4JPsWMFvBF/qtGJ/+JTHNTq5Xk9zudHGsY83nmYrlOxV2KuxV2KuxV2K
uxV2KuxV2KtqrMwVQSxNABuSTirN/Lfl9LBVurhQ1626g7iMeA/yvE/R89VqdRx7D6XZ6fT8O55s
tUggEdDuM1zmt4pdirsVYZ5s8s+pLJfWS0lNXmhH7fcsv+V4jv8APrs9JqaHDJ1+p01+qLDM2brn
Yq7FXYq7FX0n/wA48to48kyR2cwfUPrLyanEdmjZvhiA2rwMaAj35ZtdHXBtzdRrr49+XR6hmW4b
sVdirsVdir5I/Ne/+vfmJrs1QeFx6G3/AC7osP8AzLzTag3Mu800axhieUN7sVdirsVdirsVdirs
VdirsVdirMPLHl8wAXt4hE5/uYmFCg/mI8f1ZrdVqL9MeTsdNgr1HmyXMJzEXatWOndf1ZVMbswr
ZFLsVdiqGu13VvoOTgWJYV5o8vFS9/aL8O7XEQ7eLj28c2ml1H8MnXanT/xBi+Z7guxV2KuxVP8A
yLf+abLzPZP5ZDyarI4SO3XdZVO7JKKgenQVYkjiPiqKVFuKUhIcPNqzRiYni5PsKIymJDKFWXiP
UCElQ1N6EgEivtm7dCuxV2KuxV4P+Zn5i/m7o189tNbRaNYszi2urVBOsqEkD/SJAw5bV2VG8QNs
12fNlie4Oy0+DFId5eL3FxPczyXFxI01xMzSTSuSzu7GrMzHckk1JzBJt2AFLMCXYq7FXYq7FXYq
7FXYq7FXYqyvyv5fZWF9ex0IobaNvv5kfq/2s1+q1H8Mfi5+mwfxS+DKswHOdiqrbuVkA7Nsf4ZG
Q2SEZlTN2KuxVTnTlEfbcfRhid0FBZcwYd5k8uNAz3tkg+rdZYl6oe7KKfZ/V8umy02pv0y5uu1G
nr1R5MbzNcN2KuxVnH5Z/mdP5MuXjayhudPu5Fa9cJS6CAEUjkqoIFa8W2/1ak5kYM/B02cbUafx
Ou76X8s+ZdL8x6RDqumGU2s32TLG8R5DZl+IUbi1VJQlajrm1hMSFh1GTGYGimmTYOxV2KqN9HZS
Wk0d8sT2boVuEnCmIodiHDfDT54DVbpF3s+b/wAyfI/kCy9fU/LvmWzJZix0f1BOQSWJWF4OZXsq
q6/N81efFAbxkPc7bBmmdpRPveZZiOY7FXYq7FXYq7FXYq7FXYqybyz5cMhjv7xaRD4oYSPteDN/
k+Hj8uuDqdTXpi5um09+osvzXOwdirsVdiqYIwZA3iMoIZt4pdirsVS9l4sV8DTLg1tEAih6YVYT
5k8vGzY3dsC1qxJdf99knpt+z4ZtNNqOLY83W6jT8O45JBmW4jsVZP8Alp5c07zH5z0/SNQd1tZz
I7rH1f0o2l4V/ZDcNzl2CAlMAtOomYQJD65gggt4I7e3jWGCFRHFFGAqIiiiqqjYADYAZugKdGTa
/FDsVdirHfNvkDyv5rjUava87iNGSC7iYxzRhvBhs1DuA4I9sqyYYz5tuLNKHJ4v5q/5x58xWBlu
NAuE1S1G6270iuQCTtv+7fitN+QJ7LmDk0Uh9O7sMeuifq2eUzQzQTPDMjRTRMUkjcFWVlNCrA7g
g5hkU5oNrMCXYq7FXYq7FXYq7FWR+XfLJuAl5eDjBUGKEjdx4nwX9fy64Wo1NemPNzNPpr9UuTMs
1rsXYq7FXYq7FUVavVSh6jcfLK5hkFfIMnYq7FUHdLSWviK/wyyHJgVLJoWuiOjI4DIwKsp3BB2I
OINKRbB/MPl19Pb6xb1ezY713MZPY+3gfo+e10+o49j9Tq9Rp+DcckkzKcZmP5P3EsH5k6G8cTTM
ZZIyqgkhZIXRm27IrFj7DL9Mf3gcfVC8ZfWWbl0jsVdirsVdirwr87POX5jaTqj6dFIdP0O4VTaX
dorK8o6lXuDusgZTVUI+HrUHfX6rLOJrkHZaTFjkL5l4jmvdi7FXYq7FXYq7FXYqn/lzy6t8purr
kLZTREG3Mg77/wAvbbMTU6jg2HNy9Pp+Lc8maqqooVQFVRRVGwAHYZqyXZAN4q7FXYq7FXYqvhfj
Ip7dD9ORkNkhHZUzdirsVULpKoG/l/jkoFiULlrF2KrJYYpozHKiyRt9pGAIP0HCCQbCCAdiwTzD
obabcB4qm0lJ9M7nif5Sf1ZttPn4xvzdXqMPAduT6E/Jz8tofLejx6pqMStrt+iyNzSj20bLtCOY
DK9G/edN/h7VO+02DhFnmXn9Vn4zQ+kPSMynEdirsVdirsVS7zBoGl6/pM+l6nCJrScUI6MrD7Lo
ezKehyM4CQos4TMTYfKfn3yJq3lDWHs7pWkspCTYX1KJMgp4Vo61oy9vlQ5p82EwNF3WHMMgsMYq
MpbnVGKuxVvFXYqnnlvQDfSfWbhSLRDsP9+MOw9h3zF1Oo4BQ5uVp8HEbPJm6IiIqIoVFACqBQAD
YADNUTbswKXYq7FXYq7FXYq7FXYqjoX5xg9+h+eUyFFmF+BLsVWyLyRl8RtiChAZewdirsVTjyfZ
JeeZ9NidOarOkpUio/dH1Afo45mdnx4s8R5/du4faE+HBI+X37Pds7N4t2KuxV2KuxV2KuxV5Z+Y
esC91gWcZBhsKpUb1kahf7qcfozke2dTx5eEcoff1/U9b2PpuDFxHnP7un62K5p3buxV2KuxV1Bi
qFukowcdDsfnlkCxKhk2LsVdirsVdirsVdirsVRFo/xFfHcZCYZBE5WydirsVQEq8ZGHv+vLgdmB
W4UOxVmH5WQCTzOzn/dFvI4+kqn/ABvm27GiDm90T+h1PbUiMI85D9L17OpeVdirsVdirsVdiqG1
K/i0+wuL2X7ECFyK0qR0Ue7HbKs+UY4GZ6Btw4jkmIjqXh000k0zzSsWkkYu7HqWY1Jzz+UjIknm
XvYxEQAOQWYGTsVdirsVdiq2ROaFfHp88INIKAIIJB6jrlrB2FXYq7FXYq7FXYq7FV0bcXDeBwEJ
CPylm7FXYqhbtTyDdiKfTlkCxKhk2LsVZ7+UduW1S+uO0cCx/TI4P/MvN32HC5yl3Cvn/Y6PtydQ
jHvN/L+16jnSPNuxV2KuxV2KuxVhP5l60IrOPSY6+rccZZj29NSeI+l1r9GaHtzUgQGMc5bn3f2/
c73sTTEzOQ8o7D3/ANn3vOM5d6d2KuxV2KuxV2KuxVCXiFT6iqWB+1Sn8csgejEhBfW4/Bvw/rlv
Chr64n8px4Va+uD+T8ceBWvrv+R+P9mHgVv67/kfj/ZjwK19cP8AL+OPArX1yT+UY8KtfXJfBfx/
rjwhU2s5Ge3Ut9rv/DMeYosgr5FLsVUL3l9WZlFWX4hX26/hkoc0FKDdSnwHyGZPCGKhZ6oLy3We
CTlGxIBoBupKnt4jLMuA45cMhu1Yc0ckeKPJ6F+UmpSxapdQMapPGrGvjG1BT/kYc2vYs6nKPeL+
X9rqO3IeiMu418/7HsA3GdE807FXYq7FXYq7FXi/mfVP0nrl1dA1i5cIaGo9NPhUj/Wpy+nOF7Qz
+LmlLpyHuH4t7jQYPCwxj15n3n8UlWYbmOxV2KuxV2KuxV2KuIB2PTFUDdWaV5caqe/cZbCbAhAy
WjCpQ8h4d8tElUCCDQ9ckrWKpZrHmHTtKUeuxeY9II6F6eJBIoMztJoMmf6dh3nk4Os7Rxaceo2e
4c2Far5x1a+5JE31W3bb04z8RHu/X7qZ0ul7IxYtz6pef6v7XltX2zmy7A8EfL9f9id+R9d9WP8A
Rc5q8YLW7kjdR1Tfeo6j2+WaztrRcJ8WPI8/1/jr73adh6/iHgy5jl7u78dPcy5RyYL4mmc+9GnN
o1HK9iP1ZjzCQisrZOxVp1DoynowIPyOIKscl/dc+ewSvL2p1zNiL5NZNCy888na79QvDa3DgWlw
d2Y0CPTZvDfofo8M67tfQ+LDiiPXH7Q8b2Nr/CnwSPol9h7/ANb2PytcvZ6rYSqwX96odu3FzRq/
Qc5fRZTDPEjvr57PT6/GJ4ZA91/J79A3KJT7Z2bxS/FXYq7FXYqk/m7VRpug3UyvwnkX0oN6Hm+1
V91FWzC7Rz+FhkevIe8/i3N7PweLmiOnM/D8U8azhnt3Yq7FXYq7FXYq7FXYq7FXEAih6Yqg54Ch
5L9j9WWRlbAhQeNHFGFcmChJ9b0DXL6ALpN8lt2kVwysfcSLyI+hfpzP0WqwY5XliZfju/b8HB12
DPkjWKQj+O/9nxYTe/l55tilc+gt11Zpo5VPInc7OUcn6M6XD23pSBvw+RH6rDzGXsXUgnbi8wf1
0Uiu9K1SzAa7s5rdTsGljZAfkWAzZYtTjyfTKMvcQ67Jp8kPqiY+8KNrczWtxHcQNwliYMje4yeX
HGcTGXIscWWWOQlHmHrei39tqUMVzAQyMKuvdGAqVb3GcDqsEsMjGX9vm+g6bUxzQE4/2eSco3Fw
3gcwyHITAEEAjodxlLJ2KXYqx/X7J5UubeJgj3MThHaoVWcFakgHau+Z2kyiMoyPKJH2OPqMZnjl
EcyCEB5f/LjSLBEm1AC+ux1Dbwqd+iftf7KvyGZ2t7dy5DUPRH7fn+p1ej7DxYxc/XL7Pl+tkDoI
3KqAoX7IGwA7UzUg3u7iq2fQWlzGfTbadl4mWJHK+HJQaZ30JcUQe8PAzjwyI7iiskxdirsVdirz
L8y9S9fV4rFSeFmnxj/iySjH/heOcp25n4sogP4R9p/ZT1PYmDhxmZ/iP2D9tsPzSO7dirsVdirs
VdirsVdirsVdiriARQ9MVQc8BT4hun6ssjK2BDdq1JCP5h+IxmNkhF5WydiqAu9A0O8Ltc2FvLJJ
9uQxrzP+zpy/HMnHrc0KEZyAHnt8nGyaPDO+KEST5b/NRsPLOlaYH/R0RgEn205u6kjvRy1D8snn
12TNXiG68h+hjp9Hjw2MYoHzP6VVlZTRhQ5SC5CJt5l4hCaEdK98hKKQVfIMnYqo3dstxEV2DjdG
8DkoSooKhp8jrW2mqJE3UH+XJZB1ChddLSWviMYcmJfQsESwwRxL9mNQo+QFM9BAfPyV+FDsVdiq
jeXdvZ2st1cOEhhUs7HwGQyZIwiZS2AZ48cpyEY7kvD7+7kvL2e7kFHuJGkYDoORrQfLOAzZDOZk
ept73FjEICI6ClDK2x2KuxV2KuxV2KuxV2KuxV2KuxVxAIoeh64qhZIjE4dd0B+7LAbY1SKBBAI6
HplbJ2KuxV2KrJI1kWh69jhBpBCElhaM77jscsErYkK8NwGor/a7HxyEopBV8iydiqhcW5kKyRkL
Mn2GPT3ByUZVz5IbVDcyQLTizuI2U9mYgZZijcuEdWGSXDEnufQeegPn7sVdirsVYb+ZmqGDS4dP
Q/Fdvyk6f3cRBp47tT7s0fbmfhxiA/iP2D9tO67EwcWQzP8ACPtP7LeaZyr1TsVdirsVdirsVdir
sVdirsVdirsVWvJGgq7BR2qaYgEoQc2qW4BVVMn4A/x/DLRiKLVIbscAGQjbpWtPbtgMFtEJIj/Z
NfbvlZFJtdil2KuxVogEUIqMUIWa3K1ZN18O4yyMkEKkFxyoj9ex8cEoqCr5Bk7FUTpEdv8Apmxe
YqkQuYWmZjReIcVJJ2+z3zI0kgMsCeXEPvcfVRJxSA58J+57pnfPBuxV2KuxV5B531M3/mK4INYr
b/R46eEZPL/hy2cV2rn8TOe6O3y/bb2fZeHw8A75b/P9lJDmudi7FXYq7FXYq7FXYq7FXYqoSXtq
nWQE+A3/AFZIQJRaEk1c7iOP5Fj/AAH9csGHvRaFkv7t+shA8F2/VvlgxgLagSSanqckhVtY+UnI
9F3+nBIqjcrQ7FVRZ5V/aqPA75ExCbVUuwdnFPcZEwTauro32SDkCEt4pdiqhNbhqsmzeHY5KMmJ
DoJzXhJs3YnDKPUKCr5Bk7FWd+RvOPp+npOoyfu9ltJ2/Z7CNj4fy+HTpnQ9k9pVWKZ/qn9H6vk8
92r2dd5YD3j9P6/m9CzpXnHYq7FXhnnLS30vzHeW4BWF3M1vQED05PiAX2U1X6M4vX4PDzSHTmPj
+Ke00GfxMMT15H4fi0l5N4nMOnMdybxONK7kfHFXVPjirVT44q6p8cVdU+OKuqcKrZEEiFT9B8Di
DSoB0ZGKt1GWgpW4q7FWwCTQdT0xVHxR+mgXv1Jysm0L8CuxV2KuxV2KqiXEq96jwORMQm1dbpD9
oFfxGQMCm1VWVhVSD8sjSVssSyDfY9jhBpSFkbsh4S9f2W7HCRfJVbIpdir0fyR5zW4WPStRci5A
421wxr6ngjH+bw8fn16jsvtTjrHk+roe/wAvf9/v58x2n2Zw3kx/T1Hd5+77vdym2b50TsVef/m1
pJktbTVY1FYCYJyAa8X3Qk+CtUf7LNH21guImOmx/H45u97Ez1IwPXcfj8cnmOc49G7FXYq7FXYq
7FXYq7FXYqpTw+otR9odMINKgiCDQ9csS1iqItI6vzPRenzyMiqLyCHYq7FXYq7FXYq7FXYq4Eg1
HXFVVLmRdj8Q9+uRMQm1UXETijinz3GR4SE2vRgopXkg6N1p88iUoTWNf0bRoBNqd3Hao1eAc1Zq
UrxQVZqV3oMtw6fJlNQFtWbUQxC5mmMXn5w+TbYqYJbi8J7wRFeP/I4xfhmwx9i6g86j7z+q3Ayd
sYByuXuH66ehfl//AM5Q+UtVuk0rzAH0mT4Y7XUp6GGToo9dgW9JjWpY/B1qV79Tp4zEAJkGQeX1
BgZkwFRe3wzRTRJNC6yRSANHIhDKyncEEbEHL2hC6xpcGq6ZcafOSIp1oWHUEEMp+hgDlWfCMkDA
8i24MxxTExzDwG5t5ra5ltpl4zQO0ci9aMh4kfeM4ecDEkHmHuYTEoiQ5FTyLJ2KuxV2KuxV2Kux
V2KuxVDXMNayL/shkolKGAJIA6nYZNUwRAihR2yolC7FXYq7FXYq7FW1VmPFQWJ6AbnEC+Skgc0V
DpGpzbpbPTxYcR/w1MyIaTLLlEuPPV4o85BEDy3rJNDBT3Lp/A5aOzs3837Q1HtHD/O+wq48pamR
UvCPYs38Fy4dk5e+P4+DSe1sXdL7P1q8Xk+cj97cqp/yVLfrK5ZHsiXWX4+xql2vHpH8farR+Tog
f3l0zDwVAv6y2Wx7IHWX2Ncu1z0j9quvlLTlIPqzVH+Uv/NOWfyTi75fZ+pq/lbL3R+39b5x/OyS
VfzBvrLmWt7JII7ZDT4VeFJW6UrV5Cc2uk00MUKiHV6rUTyyuRYJmS4zsVfU/wDziJp/mFdA1bUJ
72QaC8/oWOnHiUNwqq00+68h8JRBxeh+LkKgHCgvoLFDx/8AM3SBZeYfrMakQ36erWlF9RfhkA8e
zH55yna+Dgy8Q5S+/r+v4vV9kZ+PFwnnHb4dP1fBiOat2rsVdirsVdirsVdirsVdiqvaWF9euUtL
eW4cdViRnI+fEHJ48Up/SCfcwyZYw+oge9PY/wArfMqQrdskahhX0ORaVK+KqD+GbQdkZjGzQdbL
tnCDQstDybIrFJboJKv2k4EkfOrKckOxz1l9jWe2B0j9v7FaLyfbj++uHf8A1AF/XyyyPZEesj+P
m1y7Xl0iPv8A1Ky+U9MBqXlYeBZf4KMsHZWLvl+Pg1HtXKekfx8VceW9G/5Z6+/N/wDmrLv5Ow/z
ftP62r+Uc3877B+pXTSNLQUFrER/lKGP3tXLY6TEP4R8mqWryn+I/NWis7SI1igjjPiqgfqGWRww
jyAHwapZpy5kn4q2WNbsVdirsVdirsVdir5U/O7/AMmdrP8A0bf9QsWZEOTRPmwbJMU28p+XL3zL
5l03QbIH6xqNwkAcKX9NWPxysq78Y0q7ewOKv0B8teXNJ8t6FZ6HpEPoafYpwgjJLHclmZierMzF
ifE4WKZYqxn8wtFj1Hy7PMIw11YqZoHrSiggyj6UB28QM13amAZMJPWO/wCv7HY9l6g48wH8Mtv1
fa8XzkXr3Yq7FXYq7FXYqmOm+Xdc1Ir9SspZkbYS8eMe3/FjUT8cvxaXLk+mJP3fNx82qxY/qkB9
/wAubK9I/KjUpZEfVJ0t4OrRRHnL8q04D51ObTB2LMm8hoeXP9X3usz9tQArGLPny/X9zL9N/L3y
tYgH6r9akH+7Lk+pX5rtH/wubTF2Zgh0s+e/7PsdVm7Tzz60PLb9v2sghhhhiWKFFjiQUSNAFUD2
A2zPjEAUOTgSkSbPNfhQo3NnaXScLmFJV3oHANK+HhjSQUovPKNhIpNo720gHwgMXSvuG5fgcrOM
MxkKQ3fl7zBa1IjW6Qb8o9zT/V+E/cMgcZbBkCXNdCNzHNG8Ug2ZWFCCPHvkKZ2vW4gbo4+nb9eB
VTFLsUOxV2KuxV2KuxVpnVBViAPE4q8U89flBq3mfzvf6yt/b2un3Xo8Kh3mAjhSNqpRF6pt8eWx
nQa5Qsr7b/nH3yssai51C+llH2mjaKNT/sTHIR/wWPiFfDD178qfyf8AKnli6/Ttrp3p35jMVrcT
SPI4jcDm4ViVUsNgwANKjocnC+rXOuj1DJsHYq0yqylWAZWFGU7gg9sVeBa/pbaXrN3YN0gkIQ+K
H4kP0qQc4fVYfDySj3H+x7nS5vExxl3j+1L8ob21VmYKoJYmgA3JOICkp5p3kjzRfjlFYPHHt8c9
IhuKggPQn6Bmbi7OzT5Rr37OFl7RwQ5yv3bsp0z8o3qj6nfLQN8cNupNV9pG40/4DNli7E6zl8B+
v9jrM3bnSEfif1ftZlp3lLy5p9Da2EQdSGWRx6jgjuGfkR9GbbFosOP6Yj7/AL3U5dbmyfVI/d9y
bZlOK7FXYq7FXYq7FXYq7FVKe1tbheM8KSgdA6huvz+WNKCkWoeSdNnFbVjav4CrqfoJr+OQOMNg
yFILvylrlpVolE6AVLQtv16cTRvuyswLYMgSx5b23k9OZWRx1SRSp/GhyBDMFUXUF/aQj5Gv9MaV
VW7ganxUJ7EYFVVZWFVII8RviriQBUmgHUnFUNNfINo/iPiemGlQTu7mrmpxVrFWReVfLr3cyXt0
g+poaorb+ow26fyg9a/LxyyEba5zrZneXNDsVdirsVYV538iXeuajBe2Uscb8PSnEpIFFJKsOIap
3pmo7Q7OlmmJRIG1G3cdndpRwwMZAnexShp35S6ZEQ1/eS3J2PCICJfcGvMn6KZDF2JAfVIy+xnl
7bmfpiI/b+plmk+X9H0lWXT7VYOf22BLMfYsxZqfTmzwabHi+gU6vPqcmX6zaYZe0OxV2KuxV2Ku
xV2KuxV2KuxV2KuxV2KuxVRu7K1vITDcxiSM9j/AjcYCLSDSQ3vkbTpSWtZHtif2f7xfuJDf8NkD
jDMZCkV55O1m3HJEW4UVJMR3FP8AJbifurkDAsxkCXjRtX/5Yrjb/ip/6YOEsuIK40PX5lANpKQO
nIcev+tTHgK8YVB5S8wEf7y/8lI/+asPAUcYVU8ma4w3RE9mcfwrj4ZR4gRlr5EvfXT61NGIK/vP
TLF6eAqoGSGNByBmccaRRrHGOKIAqqOwAoBlrSuxV2Kv/9k=</xmpGImg:image> + </rdf:li> + </rdf:Alt> + </xmp:Thumbnails> + <xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass> + <xmpMM:OriginalDocumentID>uuid:65E6390686CF11DBA6E2D887CEACB407</xmpMM:OriginalDocumentID> + <xmpMM:DocumentID>xmp.did:d4d07395-aa96-47c2-a9e5-d0351947bb0c</xmpMM:DocumentID> + <xmpMM:InstanceID>uuid:c63c1031-e157-9748-9c58-86481308e954</xmpMM:InstanceID> + <xmpMM:DerivedFrom rdf:parseType="Resource"> + <stRef:instanceID>uuid:1abccb90-0c26-4942-b156-fd2eb962e3e1</stRef:instanceID> + <stRef:documentID>xmp.did:58fdc1b8-1448-3a44-9e20-282d8ec1cf95</stRef:documentID> + <stRef:originalDocumentID>uuid:65E6390686CF11DBA6E2D887CEACB407</stRef:originalDocumentID> + <stRef:renditionClass>proof:pdf</stRef:renditionClass> + </xmpMM:DerivedFrom> + <xmpMM:History> + <rdf:Seq> + <rdf:li rdf:parseType="Resource"> + <stEvt:action>saved</stEvt:action> + <stEvt:instanceID>xmp.iid:d4d07395-aa96-47c2-a9e5-d0351947bb0c</stEvt:instanceID> + <stEvt:when>2016-06-15T14:23:10-04:00</stEvt:when> + <stEvt:softwareAgent>Adobe Illustrator CC 2015 (Macintosh)</stEvt:softwareAgent> + <stEvt:changed>/</stEvt:changed> + </rdf:li> + </rdf:Seq> + </xmpMM:History> + <illustrator:StartupProfile>Web</illustrator:StartupProfile> + <illustrator:Type>Document</illustrator:Type> + <xmpTPg:NPages>1</xmpTPg:NPages> + <xmpTPg:HasVisibleTransparency>True</xmpTPg:HasVisibleTransparency> + <xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint> + <xmpTPg:MaxPageSize rdf:parseType="Resource"> + <stDim:w>128.000000</stDim:w> + <stDim:h>128.000000</stDim:h> + <stDim:unit>Pixels</stDim:unit> + </xmpTPg:MaxPageSize> + <xmpTPg:PlateNames> + <rdf:Seq> + <rdf:li>Cyan</rdf:li> + <rdf:li>Magenta</rdf:li> + <rdf:li>Yellow</rdf:li> + <rdf:li>Black</rdf:li> + </rdf:Seq> + </xmpTPg:PlateNames> + <xmpTPg:SwatchGroups> + <rdf:Seq> + <rdf:li rdf:parseType="Resource"> + <xmpG:groupName>Default Swatch Group</xmpG:groupName> + <xmpG:groupType>0</xmpG:groupType> + <xmpG:Colorants> + <rdf:Seq> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>White</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>255</xmpG:red> + <xmpG:green>255</xmpG:green> + <xmpG:blue>255</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>Black</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>0</xmpG:green> + <xmpG:blue>0</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>RGB Red</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>255</xmpG:red> + <xmpG:green>0</xmpG:green> + <xmpG:blue>0</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>RGB Yellow</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>255</xmpG:red> + <xmpG:green>255</xmpG:green> + <xmpG:blue>0</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>RGB Green</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>255</xmpG:green> + <xmpG:blue>0</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>RGB Cyan</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>255</xmpG:green> + <xmpG:blue>255</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>RGB Blue</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>0</xmpG:green> + <xmpG:blue>255</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>RGB Magenta</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>255</xmpG:red> + <xmpG:green>0</xmpG:green> + <xmpG:blue>255</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=193 G=39 B=45</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>193</xmpG:red> + <xmpG:green>39</xmpG:green> + <xmpG:blue>45</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=237 G=28 B=36</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>237</xmpG:red> + <xmpG:green>28</xmpG:green> + <xmpG:blue>36</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=241 G=90 B=36</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>241</xmpG:red> + <xmpG:green>90</xmpG:green> + <xmpG:blue>36</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=247 G=147 B=30</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>247</xmpG:red> + <xmpG:green>147</xmpG:green> + <xmpG:blue>30</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=251 G=176 B=59</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>251</xmpG:red> + <xmpG:green>176</xmpG:green> + <xmpG:blue>59</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=252 G=238 B=33</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>252</xmpG:red> + <xmpG:green>238</xmpG:green> + <xmpG:blue>33</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=217 G=224 B=33</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>217</xmpG:red> + <xmpG:green>224</xmpG:green> + <xmpG:blue>33</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=140 G=198 B=63</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>140</xmpG:red> + <xmpG:green>198</xmpG:green> + <xmpG:blue>63</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=57 G=181 B=74</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>57</xmpG:red> + <xmpG:green>181</xmpG:green> + <xmpG:blue>74</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=0 G=146 B=69</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>146</xmpG:green> + <xmpG:blue>69</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=0 G=104 B=55</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>104</xmpG:green> + <xmpG:blue>55</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=34 G=181 B=115</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>34</xmpG:red> + <xmpG:green>181</xmpG:green> + <xmpG:blue>115</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=0 G=169 B=157</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>169</xmpG:green> + <xmpG:blue>157</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=41 G=171 B=226</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>41</xmpG:red> + <xmpG:green>171</xmpG:green> + <xmpG:blue>226</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=0 G=113 B=188</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>113</xmpG:green> + <xmpG:blue>188</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=46 G=49 B=146</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>46</xmpG:red> + <xmpG:green>49</xmpG:green> + <xmpG:blue>146</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=27 G=20 B=100</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>27</xmpG:red> + <xmpG:green>20</xmpG:green> + <xmpG:blue>100</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=102 G=45 B=145</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>102</xmpG:red> + <xmpG:green>45</xmpG:green> + <xmpG:blue>145</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=147 G=39 B=143</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>147</xmpG:red> + <xmpG:green>39</xmpG:green> + <xmpG:blue>143</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=158 G=0 B=93</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>158</xmpG:red> + <xmpG:green>0</xmpG:green> + <xmpG:blue>93</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=212 G=20 B=90</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>212</xmpG:red> + <xmpG:green>20</xmpG:green> + <xmpG:blue>90</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=237 G=30 B=121</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>237</xmpG:red> + <xmpG:green>30</xmpG:green> + <xmpG:blue>121</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=199 G=178 B=153</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>199</xmpG:red> + <xmpG:green>178</xmpG:green> + <xmpG:blue>153</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=153 G=134 B=117</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>153</xmpG:red> + <xmpG:green>134</xmpG:green> + <xmpG:blue>117</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=115 G=99 B=87</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>115</xmpG:red> + <xmpG:green>99</xmpG:green> + <xmpG:blue>87</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=83 G=71 B=65</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>83</xmpG:red> + <xmpG:green>71</xmpG:green> + <xmpG:blue>65</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=198 G=156 B=109</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>198</xmpG:red> + <xmpG:green>156</xmpG:green> + <xmpG:blue>109</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=166 G=124 B=82</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>166</xmpG:red> + <xmpG:green>124</xmpG:green> + <xmpG:blue>82</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=140 G=98 B=57</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>140</xmpG:red> + <xmpG:green>98</xmpG:green> + <xmpG:blue>57</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=117 G=76 B=36</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>117</xmpG:red> + <xmpG:green>76</xmpG:green> + <xmpG:blue>36</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=96 G=56 B=19</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>96</xmpG:red> + <xmpG:green>56</xmpG:green> + <xmpG:blue>19</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=66 G=33 B=11</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>66</xmpG:red> + <xmpG:green>33</xmpG:green> + <xmpG:blue>11</xmpG:blue> + </rdf:li> + </rdf:Seq> + </xmpG:Colorants> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:groupName>Grays</xmpG:groupName> + <xmpG:groupType>1</xmpG:groupType> + <xmpG:Colorants> + <rdf:Seq> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=0 G=0 B=0</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>0</xmpG:red> + <xmpG:green>0</xmpG:green> + <xmpG:blue>0</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=26 G=26 B=26</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>26</xmpG:red> + <xmpG:green>26</xmpG:green> + <xmpG:blue>26</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=51 G=51 B=51</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>51</xmpG:red> + <xmpG:green>51</xmpG:green> + <xmpG:blue>51</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=77 G=77 B=77</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>77</xmpG:red> + <xmpG:green>77</xmpG:green> + <xmpG:blue>77</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=102 G=102 B=102</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>102</xmpG:red> + <xmpG:green>102</xmpG:green> + <xmpG:blue>102</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=128 G=128 B=128</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>128</xmpG:red> + <xmpG:green>128</xmpG:green> + <xmpG:blue>128</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=153 G=153 B=153</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>153</xmpG:red> + <xmpG:green>153</xmpG:green> + <xmpG:blue>153</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=179 G=179 B=179</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>179</xmpG:red> + <xmpG:green>179</xmpG:green> + <xmpG:blue>179</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=204 G=204 B=204</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>204</xmpG:red> + <xmpG:green>204</xmpG:green> + <xmpG:blue>204</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=230 G=230 B=230</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>230</xmpG:red> + <xmpG:green>230</xmpG:green> + <xmpG:blue>230</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=242 G=242 B=242</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>242</xmpG:red> + <xmpG:green>242</xmpG:green> + <xmpG:blue>242</xmpG:blue> + </rdf:li> + </rdf:Seq> + </xmpG:Colorants> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:groupName>Web Color Group</xmpG:groupName> + <xmpG:groupType>1</xmpG:groupType> + <xmpG:Colorants> + <rdf:Seq> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=63 G=169 B=245</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>63</xmpG:red> + <xmpG:green>169</xmpG:green> + <xmpG:blue>245</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=122 G=201 B=67</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>122</xmpG:red> + <xmpG:green>201</xmpG:green> + <xmpG:blue>67</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=255 G=147 B=30</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>255</xmpG:red> + <xmpG:green>147</xmpG:green> + <xmpG:blue>30</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=255 G=29 B=37</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>255</xmpG:red> + <xmpG:green>29</xmpG:green> + <xmpG:blue>37</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=255 G=123 B=172</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>255</xmpG:red> + <xmpG:green>123</xmpG:green> + <xmpG:blue>172</xmpG:blue> + </rdf:li> + <rdf:li rdf:parseType="Resource"> + <xmpG:swatchName>R=189 G=204 B=212</xmpG:swatchName> + <xmpG:mode>RGB</xmpG:mode> + <xmpG:type>PROCESS</xmpG:type> + <xmpG:red>189</xmpG:red> + <xmpG:green>204</xmpG:green> + <xmpG:blue>212</xmpG:blue> + </rdf:li> + </rdf:Seq> + </xmpG:Colorants> + </rdf:li> + </rdf:Seq> + </xmpTPg:SwatchGroups> + <pdf:Producer>Adobe PDF library 15.00</pdf:Producer> + </rdf:Description> + </rdf:RDF> +</x:xmpmeta> + + + + + + + + + + + + + + + + + + + + + +<?xpacket end="w"?>
endstream
endobj
3 0 obj
<</Count 1/Kids[7 0 R]/Type/Pages>>
endobj
7 0 obj
<</ArtBox[19.792 16.0 109.0 112.0]/BleedBox[0.0 0.0 128.0 128.0]/Contents 8 0 R/Group 9 0 R/LastModified(D:20160615142312-04'00')/MediaBox[0.0 0.0 128.0 128.0]/Parent 3 0 R/PieceInfo<</Illustrator 10 0 R>>/Resources<</ColorSpace<</CS0 11 0 R>>/ExtGState<</GS0 12 0 R>>/ProcSet[/PDF/ImageC]/Properties<</MC0 5 0 R>>/XObject<</Im0 13 0 R>>>>/Thumb 14 0 R/TrimBox[0.0 0.0 128.0 128.0]/Type/Page>>
endobj
8 0 obj
<</Filter/FlateDecode/Length 106>>stream
+HwVu6PprqV*234R04S32P4ճT(J +W*w6PH/H+ +8;W:dYmnJk$j=`^PKX*GV"-/6MPPhMW4o*<SJ[.r.2B:%l2U+:>jFegTA5n:ROqi. +8M?-(/t#IN>re.=TbIMqYWQK1D%b&pOLGa]H?hKs'8Gqa4A/k;[i&\e-=4:h!/H6BW;~>
endstream
endobj
16 0 obj
[/Indexed/DeviceRGB 255 17 0 R]
endobj
17 0 obj
<</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream
+8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1 +VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH< +PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~>
endstream
endobj
13 0 obj
<</BitsPerComponent 8/ColorSpace 11 0 R/DecodeParms<</BitsPerComponent 4/Colors 3/Columns 880>>/Filter/FlateDecode/Height 947/Intent/RelativeColorimetric/Length 90241/Name/X/SMask 18 0 R/Subtype/Image/Type/XObject/Width 880>>stream
+Hoi@Hy&8_nyA'? +I + +W<*
'%Y%Vmao!ǩkv>wu{=Q<\ȃ*fƸmqY%ŏRp + +iHF'>hd,I#_ыTj~Q5cR`n:s
e8P +WWzٶ:lgl7ɃHiJ&/Ӻg.}C'dD|V֪'9TL*4I]6 +>#gV-?}=TjOK<>Nh auOBnY#qkB)fiQ@
+ +ztut&ksg1twuO/J>_9Ezb(Jr2h+ɓ)⇻A!_:۪.%ٱ4E3w~ sOq9F$~E<GJ? +7"P 16?w'PƔPm?U
AJOC/%-n :d}_HR0Iyc5SjV}nǡ!1I,C8LaP)GcA1(E4F|Q#0#0PW{ V<^FLz@%k0u'I1q<$p5SyV(%AǓ8MYĶQEY'd72k/Q;L&^1*
ev<7V2Sy+G F"wA +P&rA\9gJC +c1BuUU!hB +m?IXqBf=O-uS]*pbLp ++Qf1XGbu.AL}{;j:1XM;`m)ݒr2??b<g2|NDq)}s*[SwLGER>ӥ^"T.4{7V:7cO
n]&IIʴ]׳L&ټ~e?618qW6$уS-J+j &HR#Y(u3C"vaVO qˤg/{Nd* w4~8`ਡODT +( ƃ(rVu6z0F|iU6_Up_ |7//y
e26kaE9JTh<'| e\xy(7QQ1Z)7#5eoӈ0g+ۨwCxcXSg")-nkEJN[* FAKLLR7R.LBME#@* +~U݊tEEVOt7U콊ؾxԜ'isjf=O[ZO ((A>&]r"т|f0`A|0/2}+58{:!ELTǝuB1HzGQ0g[|Q_[VSor^Gy?lD$g=?ȩՕLN9 +K*RYģpu0%K'*-lpD <P#YG}Ũ4Iϸ.V;IQ&+=u PaqUS_q(R o*B&Bc@-٫m大+R/rlpך`m{ͯ-VrteO&^LOF"e59h9Ą˵Jhoq{50ށ:58Ƀ*ôx'/>ID2MnݾbL)OYׯR3OF"R j"iO8%{Pqs?Hee}-XJNm\-H/}G<b"µŭ|xy}}YM>ϩZ&L/u>7&I
wQ) /+P.bP"$N55B]={2`[Hnk?-s\粓y*dqP9I,1k[`^A/n3ՕcVna-_s%YSM{b FǠoZnkE%yt$mAs%Ev]JW^xA
Xk0v_KӉ i$Fߪ/u( jLIO%k/S<zYWnZ <Qb誒&[qDz1'MwcU0s)/PzRmu/X?OydR<ɇ0m'hˤS!m(/6oGF"HCs cf䐧YBhߔsyڡy߿n,@21crXS>r7B>Y,770ݙs)]ubQ9OΈL12$jn*2*쾊O&iV"sr+2LjȞCxҜJ9:Jʌ H(hxA&xk
i&Iu$6ԕj:.E Q\-^*%V@V;RM]dLW<}*w&Kߊ{-7@-&;. +C66 @9TBUfI[#v1r`,f/5nTLֹޤosIwT&\ߍ#UBXJ&uo6TE-EHLu[FUf +x謖Xz{FEr6qiVd>սl +\Uv^dKCR&p6kڄo@)ɛzxZZfBv5nFC
`r{Lŷy7g2H&;x@kASYQC,29Wp +c!{)r*Rj!ˁScM}Zi*H&Mf$\P +Ŵ\Id eDҐIЍF1|CeH ldԬ6i.2K8t&t(Q+ZfB*R&~?g4|W~!$1[NIkqS(T['iͧ4*m~@?>KT)ΕJ3t +dEÀ."!|g_F>";,o)%OQ~Z2FB +3%f_,%.u;}oZ_`>19)ۂ֙Ĥ
b-
2z[&;BEz1i6Cӯ!G9htj9'I#8ˁB!=*t-T:zTG\2z;F3}ZgLhHӍָ!fiVL:,N0EwHVsR I !-UL1#4zvB`V|u4i$\"&^Xjbޟ mR q6H JER/-N&w',͌ƹӿ8!fI|1TBS?D%}35fW̊CȊ\!/5n:VC1@#&R&2rÚ!"H <O"$;,ƔG)! Piq6k^6垾nmGţt,Q\W +Ho[]H&lB<iΌ')Lt`=CB>OS&c1[pUyuN'v9 +L凍\L2铥Akqq$CkDyQcOzq*8 +U_D"QAPA@Iw涱DZIPPiG~;9]6ovp3U$lJ'o0bRvL~cIFeA[QIPY1o7xGvΛji$-Ig2lc.Ť=I !;e0'ZJZwzT6b3; )gSVG1, xt$(!b1Ѯ-~$"3V[fK@MR+3ϟePIa~Fp2
ÞqZJZ +0 '0$:HJ\ ;``pPL=NM.H1Eb~ԓvsK`TO=hwѓ +mcrE?m}F!e_JRPF +7b1T}<x4zV,&읲yeTJ=q#cz>)Rv:5[QГO"5o) c^mXyTعT=%o-oK2U~c͠B>(h1*h|: +ؐ<pDŤG|i-jzREQˠ' +c'z>wTҺ辣QEx=D19-d!}?d!}3Z#)nmDzly_|1^Nd`Q0l9'0Nn<X +(iC4P+$ +cT6^b-4je˷O|zS~?_qCjRr̖E˓>jEk.C-n#E<IO{vE5ӄ&EN& ݆ +w&Bgw)#S]\QVQ>$I_jJX,\^YSd|'zD%{o1!䅇qx';qڈ><aX//
:z556hz^`tNʦ]arDhKUvXegf¤~ubU4u+&gb85InݣTc%NN~(dX'Bh%=fj-NK2z]W"Ly't l^fZ&a,- EKv|deKhߙDs8^{2yc"~tto`Vxhv۠dN`}ʼnChVrfg֭iљO т|,I@3&7r~x]5K~ <'ɸNjG==`jߖ}ƋWǭnZ\Lҳo`M2iCɍ|a'BN$ɸN~(<'x[)!tDJ`ɨuO&7R,R6=<{.q;!t&O6vse8-@ʨmk$bfa%܌* +sMzC*d\'\z1zADd&<u;e] x^,Yo`ΓZA> +9$Y"?LtzK_14*Y|!ԯ)7$U +L&\Pby?ޓur!m FZKGbrΓͲc)eE+WqytT?w ]]}["֫<O?ZI(BڸR9S!&[J9,^ٟ,9&GU += + +7]=!I +AQש'=FE4b2&al6> +hB");Is*QY9c"1鲒z2klvy07> +d֕MW.oBgDtʿv(uX4Kp}Gߓ-8,]o^ѓ[J^c(NY]$eo
h9[ƣ:1vt᥌e| q'Kp4 b +44@ߩ߿NN d ;tSl}OJ*IprG
l? +ע('㵢đgi +&!&;=@OK1Ŏ=5:2J.778&$k4RJGL*sͽ֨+IN,Iij&}`K闍sEvDRϿd}OIb_93Da`(r\|@O@Moߎ$gi)R~cb]_+$H!n~F]1GL*vZFi + &NA@Ot\ᏻNoV0 +'?Ztw +٫ + +w~Lwɰd_X2oĂϼ0aL&5QPRK00*?% +GC;0SO!䒢(m @bc_v(JkE@mK8h/M Hhb IYeIQ}vr7byH~Լ0qi_VS!=#_mLĤgiҰ=
X.o7=<ZdR'4!sdqak5kMMJ(PtKx#I +d!թXKtkcZ*yͅ(
w¯<\*Db,$=OVp'1K.:|/lӥ+<LZdg4b5H<R+188{ x#2sI +|?$d}Q3]Aʰ''[0'&lR3 =brNG]>i&o练Ɏ[UlL=t_wHY{D'C%A)Cyt)8tDzPˠ|!B!DDEg̓~WF/Vs'A\U/! +.a{0Ç +.ĕ#_uMLzb)ZOVfc+UA)" +4D')58=26L">^&Ư~nc#{Uҭ'TZ;
$U:ri +_͒K쳷x#LJ4K\4^mΔX][XVBf@)5:'7}OV2L=Piϲgc0Yh-8iҧVk6\o'Nq|$T($)y6AߓgO"Hb1flsarEtku*F?L$|)> R
ѸoB(L7H>IwUhc}[3;/)go2qCJ= RHOY$BkѧJ6)b
Fl{h-8թrbVyZgcF;HҢt@ʰߓŤA#v齌3BILODxRzI;e5Rkw'w9OD +yš|%0KeX\vIِ)H{fZ;qRC{ /Dny]&OkꉥUS=l'凯Gw%)H3ct:BxO3$0.e#PɶO +|XZE_\(ZODh<R^ihIy)҈; +rk'eG!%
:W!G{DNhJ\9\wACl +wϱR>"j'3J)_PKwG&)
wZtݠVwgc)ßHaO&nr#<r:%Bh;]d"+VKicZ_
o=)C6Ki&RLYe` +UB.t/MO0tx!Dn}~yLҿV]=2f^CQ_uyp%(I䈬!I-yBs95kIAQnԩ{Sѯp篧Gm2=d7R&Ӻ4M54<P'|->;=NGc2ncRt`AJxw&ӷ4p#BΣ)Óe6y0)%L^_rhe{-G^O9&%4j2Q/ +LAHiL"CɇvMå)30PfۿՂXa0)QqVgsIV^UB~l Gސp3 L4RI9&M?V!$)n+Ye6\V0YO'j
Sm,u^)dw>R CҠ/eO 5`2 j'#=%JXHPe9zTw?pf}iTnQntwR)OX"pO%tT&Ϗ]3)$7ePf[pr||l5pndғ+ĽH9zK6]mŤzͿ +'}nMty!/0y([v7t%OZ`bsI=P'L'ol3{%RJ}&|DQ5M,$)KBcuq\B銞SV'Ofw57'H#RΘs9'R/)Ȗ1BrTk,pY +}+;B(Ř
ɯGE'ts+mF\wO;KlTȒ_SOcf@=-M//:GnW?qPgE9o +W!DdUb;<s[#Vۦ-Q|_緍Thwr+PKR*B@E` kR.Itiai^ArȥkP_ڃbDJl2ˣfgIrlX?gw>X<}"W +*e Oh)5ЋPŅ]lxh7&\B{ԭxhvRzE,Y0C>yF UJA)_~D<KTn33[jǻ_\'?^BkB<~Uc>7DAA$;)Q&%AGt)K^y4ν*
o{MO8pr\r@x"B<Xt)Pǎ)Q'eR>qبrۑ]JƾИk7lJ){'@o<qN4M[-Fl"NA2y:q!߸fl
W2Z;fj#SWfd2SׄiՓΜ)~.yV hG<y3ߎ|RCϣw÷Ҭ.}'!3ՑLLri66v6y +0A%Aj-\v('9{-ҥ6x{"Zw(%̝ٟ(j915Ʈ#M&*=6ʹX% +&(%}-( $7&#Md1:Uˑ 4_}Sd2Y+8 k7.٩OKSn˧zZ.A<dPH {P&Wra3/B] +E<X[ .[(g5\dK<lfxR3JybebUhbP͍F'*%2):ȗ.yjH)sFȤ3AQ<21iQR0"|*$lf_JP&QZKav-/~^3P) $HPHL&R~hb*$<=g}s|R8$Pʪ.7TJٗFa#}diعgCr6C2yRhBnҁctБ/]2\ۑRH)+6rB&&|IJVQj9C 29ereR-Gw)̣Ҟ&MszbjR@6<I0)a8rn1kt?P&c +d4<@{db}rJ4E7l;|@*w&$!ϧUke2t-:] +,!l]} +Og6*beC +$_X;T1HI>~@C"a(R +tRuf:NReؚ3CQJXkl
cfS,hICc=u0_Wfk>knL1ז^O> ~Q'tz`'#W
xV +t`O=?7F{Nvfowvv*QJ*0 +D?ޙa
B J_$<z;i{wF#e={\&C[r!7&'kn¼~Ѻ{]2
@ *n{Q^Qw+eǔwT>~',U)+DBGbe!z/E"-|tʌWXbvF<6NHP&?pdrA[_Wm_ +5?&PF1J'3p|R]]9M]9LL2Q +LrHP<ɤv4ΒV^ZYv?`vFRB(M(
+H4JoէX)Ϣ G)<Ʈ@C*p&̟\q7H&5UQ^Z^u-R)E7?A|^u60H%LϐORКr{$$A@$n|^v$zn₰WSo_Z[sSrdRޛ>||R +% +X3J*%0|,ϙ"g,39!+\JdR"NtgQ^ҊRlr?R)i'a,P*Jycq?DVI1?
IM<(.-i[-gb\~{ ֟!ɥOZ:,Ø9{ٵJ:36pVݕII-oѲcݷ85kk,K(;9g'
8r[a/#<4+, +:VInI(od^r@ԛ/{w?p_&4(eDRcёD>]+Xkdqj22y{6pdRw +VNͪ^32X)mP ?sbֺVf{D0/#o`7ΒVm_Z_~BpSETTzaLtZv2Z)8Jq%S&I?IHd:A7.ɲG=Pkɳ)?(_;YDlgO_!;FJ**e' )2H& zӏz,T=Y]=+eQ/0g"cb|蛲IkF$!YrڪKK4.»5Jj;>i:':nA){;,nXepx}P<4MXyd@ +ƏA)(#nao$<2cIGG&/Zw$Q ھފ}!iD_~ϾzdÈ40 +$lWS/`_wt7U6?J)p0g%z*u#"#eDy ڔן8ȈggnW4c[4R~zŰ:,,G L}ʳAHLBoHxVۗ~,9ʛ;`:A)10C|E"<g !$,)_E,%L>Edxvۭ +tbX:OZ`
RFyxQh$TIcz78'a0y&'2Yd̟L/b]U/`_w[Q|$w +H\A=xraOjVDEI`NI&Ό8隧ϗ7E69t}y^&+tz/%vp\ԧ">AnB.WC(yS&rt+YHJ
W"U|C~KJǬwRŔ!3c[1
FdI2qǐxCo)Bi)LN0&%6$5KL#xG;n䟴T%m8<9%S4o6 I@Kq=ow;Gnۊhх|!`Jd}<v,Yl6I<i]`4M(,^H:;`%5bdɑ| 4L^r1鞽^U6\g*
rCa%BB]`IroI~V!I\>&ozIki[іp뺤u13O*QL#dI'LH;,o+R1;ϝ DDX|
R&K!R]?y;i'|WKCr0X,>{;P7`Hdt~ìsVBF*`V7'98QN; +\CD}#IgJE>Bm'Rӆ"ixؐcχn'
=B3]LItf-"`*E'GI)\R&'vؒNK)¤V3,L*>E}o|| +Dң[+lvu,ޒfZ˭+G_2T$fvS3DJUzDJF+7$;
S2[+K}t]aBz1e mл~>R"eΙFc!έ|F
W"%FI O)KHrڰ8:3Rƿ$ %R$Z㼩{W"=q t)pmyۊd@}zY:3JJ[F,qB&$Haos\7h=$%L&8ͤj߹4n1ȡ3)틤 % +n^_G,hZm|R0e"<9XiI$O.<z>>j<3!R i^LLrД/15D6ĖmEl6uA]W6<=H"Hk!<?zo+I4mչt)vu/ת@1v,u13C43շ^!<XAR % +H0i9003Lz6<9 2orPLhĆ"098Hh;IL6VtД05.3תR#RFp6/ +Q$EmHfDSɼ߽4Z +&H㑒#RʆBl, m+ +L`ڪlѠ6~TK''W"y0i%# +D'a?t1<|)zXZgz9x;$"%v)i)юec^$RӔ/7hJd]kUҳg}qOb&3IYJD~Fە30k1.zmE[_=.FZA +V$*#%RJsci$0R.dL@&@@R+Q,8+δqh_=DXq(%8wr⧟Lڼj,zIQ6ǃj\kNA[fk$^rθ%rď?;s +2 h"V <44^WGúZU6v=JIF. +ẅ́c=M~_ghf]Sɷϩ`6SVOVd-6秋1}ᓈ/U#
??I}G>;9G'#~,CڹI9=3z+{ak?qz8gd%$ +g8& Dỷ^/Z,iUJ +$ +< +J)H
®A]T*Cp&NlLnfUYT*V%6a50C00D?Փ+os9ؿAtjJ|LGq'l/-~zG7 0OhAW\m5-2V*Z<!Q=dF-V"`R!ZZͿ |;?E*80K2HGܲ,K +w0Wքa+[<.劣SUZyId h"fm!aSs,Z:ŏ>ĵ.d쑭J (h5iI5˾:b-#0o#%E?+IFxl'Qw`4smH*3Ͽ9b#|9.Ī:Id .2}'lY; {o +{0y=k=+78\ɲE*'k_k>1+m;QO +=Sb#VS2H'?]/},6P. +w0iO6si"=[Դ-=ұ7'#_Gp[rHsē%^ lJR +$EFj-3>YM#D?Vx<P|9w( +C#-%4l0VE>љxH$D#=>D +=$AYH\4:襑SO|#܊⟞={G.\?}|ٿS+Kڸ'Kz%QOC)JJ6sµ,LT&)Ъ?n8dU%璤䓉D[EJb_y +s}tHO +8TSsm֕$+F".P(. +Źڬ6:TQߵO"4TJ͕Wr'x(9$ IO= XN=? ++38B0gS[=%;ˋ/qUb'D}$C*, +@S8e1[ gY4UrgI(9+ʝ'%ItuKkK8Ӥ<h'ÎHJL%'3]j QcD$~H؝ ;A㩕bA U"})i`.|'V(*>DtT0|vƐ8{bRI6eGX(Z9-A:E1/'|Ŵɲnw4e驒/tDyC=nzu ^<CO / QL#8K&<'A˰ßɋ$;)rOt:D姻e3}lߛDObh{@Rbxi"/I)(*~]xAp=qSCfT>|{1~05$Ia6e?*/W5;glkJ<zs{]3ankGO[addx|*JSHSo֭ +wjKЮM|< + JZ$O|v؟_ +P 3>otC,
U͂d7;V %gI${r5Tpi`ԓNߛDObZjwW,[\{SD|~H(/)K$0"' +sS0Hb<)V:o(Ic\&zb2|1$m$;āko`\}|0O%_߁RȧEr|'Puqn9dԜ;x@߇uZH?JmK]T{I.;aCk(9 +ji4.;Nꌒi2:dm\xLd>v`n+̿>.ҟTQy$K!>^U+qGp)gQ9ݔw6' +-vY`+Iѩ"[pi4agi.uR1N<N#ԧ:Aa
0<IbA#ԝ=O&F&u˝m.;?z= p0ӗƚ|KK0HUf'xOuJ슝 31h;c"\#<^Aٺ;`` +ƾ`bh[4AixCx!)ICK7ۋTeRֽJUM{fmM'DiEecs飫+>gHvPJQImHoT'iWB?ZF|2.u/S(rc*'}J +^s=hg6t7;=X~|r>vfT"xL_?;Hɂ6eEknU[_NdQaUJZkшvw1qR +5mYlQlDne6$@ڡO?wd[:( +VkkWX
2.$<<HJf'oD\mX"Vs@62CR?L<M'd^s0VVt+z&9&C+%ԤWdo*x'y'nI\\{-*%q'i*C5r$EfR,L[SHiXqEmGb"푯# W0hm߳Y
B~RڦdR8K,uN{_dHANmr(ѩhP5J7dޞIEVn>y
=VCyY_)*=)$OwJRozj?D?@h|8և77_!xK}rBv6!f'up-0mA J~̀|%G||RWqTmήtkC%n'OJɕXB"ÉMRd|Or
i/@#5<֊@/wy |rayxU6E)|/Od^msN̸RvIٙ^pN}I-נ
nvTSST>rOZq,|2J}WB)mVJ`ٞ)ia
Kc=>r
6qvHBgzf;& +dKz9A|X|/㬶<X"M:ze&~:o| +.KwfZ +B/Gne^;͓SufuG%A}C<*xKVߜ('5froo?b,*^uZ
vš\>k'_27ɼ<ņ$xt{]Y)V>ʜ D 8Ҏ<'gy'G&zʃp}0c7ӳDo]BGr "$\x7533> +olMze[nwhyɞI>j[IJ)J"`>enX +EZU%RܨCRe]`&Q0,Oo2L~r ?L8v<zҍwCr 9{7'<$H}ҭG]NÌ}m~r2K +WW%60q9~Fp9Y~Ci@2:UւO*+'^aqpEz;)*l}rI6vOƇepD}2Jޭn(6l*w64wrRYZa:?+Vt&uu3=/;KKĠ.`"ҁhڋтLlHh;8l#~+D{I?8Q+Mէ|RcZ +>VS>'"+=r!cTVPv D)/n_) +Yʙ +VrB+><} +,gGCO֗$Ħ223؍{UQ0!"z^"eT*'TмM9%Tkժ$e:;__r'8j)Iԫ.]k#8O +ϓpC`:Tjϓu4-ZCIOKÅV7~fyuJoyR]eJsDx2O-vGض^DDY?pUΞ*b6IYqoe Ӳ|x9 7}tp΅ƻDJҴ%-4BDe<7JZsOټጭҵ8q
$DAiB<8DϜ9lJ.fO'A<Onum'hDwr]zy_DD).gFJ*xN2ABO9%^Ϸd'VVbg.v6qҁDJxt'jn~|YB9d/]D +foq] <)[]iyHncd~vQWi˓(g~C<w&TB6B3N|F[K^ԉ"%&k.@O̜2S+X.hŇ.iDd{T!鶈wZpIn?Y=r< OjjmCFDz=n(1E9H)jMD)CN3S´lv<^ +@;eNkMQ\,Bn/nѵr$yKԊ_{Xzdރ><TT^ӜJGi!KSNΘ'U5-1ĥF@x2^vgx:Fx(ye) +UR$A!7sT6lws,7fnzR˾cgǥ3z:j) T<HϓPKaS> A1d]-k|p"hQ6ɣTaQYVeUjNo2&I<]HIx2dZ$"]E9H fN-OişbJ|NW~1ӷbS.J_rBP.D +cA + 4"E(@cC㝣2H!:ovj'+j'*'nb`rZb$"24RrqpUwL%@`_FJYAZG̺>-Iy*Waq;zGh9@^ ;[qPAC`5OjZU6EU3]i&</i$Psra5Play2wrB/~z8 +Jm.Zs/Sg~}gwC]piaeZK|uvpj'љضsOSEZk'UV'
xr= +qLʖOz$"Jv(CH<x~<Ճs*!I'4BN|MEEaLE4TMCߘVtJ20Ij w?y<ȴ=1I Hmc
v JKrWI`\BV[h?L4EکI6m[\F\H=<R{lrH<cX=iA>IW +PJPpL>L:_HIWi͊ +5U +{2-nt
IHR2{r,҉B܀1`us% L^IJwM./?O¦x6yp8"SgQw%aTB6P!J.ԘsFb<w9٦ddJ-7e7I݃$$9 I*1& +I;џOЧVkT̬+rwKX4OSnNFRg?{7GE"{j]x"?sQ3n:JԐ
ܥ+5Vyē)Bg4mHBTiU+p*'5y{*QV-=AJG5gCkqKyZ>J'\:b҅E&VR]z94x
I(3<)1 <j]V^{-MB$$q\j PC07-em ɟ./- O=zݦ6
ZP<G`u{LoIhp,5hM7!6fD@P`sNJas(3Fʔdˈ'7+}mHB !%$BΫ;E4CVt*6<`؉;@-,?ͫڿüEe6f^"NR=^;89iy\;wbWjO`3sۙ+䑇;+oQ'U:0R2[[M<mB;%Hݳ lߑ* 5m܉FFm[5KmIǺ&w҅HtOIAlf3/wS<؋g;P z?^Rrߣzu]c6jMO3?Oj˵HZcO2͂'sOY_UF$!TI RR &aJ
ЯU75y'L}Y}zHTRN{~%oӋBQ(7BŕJ̭)IQxXgWK4R|)Ժ-<Od'Sv1C-IS#!لKRF`+=ތ2U q +4OҋcHJ{2cr2l'5)Ry<8ϡ"dAqx-,On!LPP` ɦTO\RFr!~ +YO!EJh_?X|}$I^u RaQ3nUܶ,xRӁ'^O<jb
u +$ IhRkFHڰ
$eA7j*ؕyǪ& @5:&Rci<fW]wMgOA?Etly]?&#%i0t#ix{ךsE
73ΑvSFVqyzCf~5d.nC=+FBB"$I)T9qS*XB`#_ɪ-/ +9"ɵHɿ*T#6mvv'
xypJ5 sXhyҊ&]kK2e&cu]$_\aB`ull%lh(6a3B3 <ɽy'Y1.~[yO8!BJN-Ԩ--R>kڸ9G(j2fV-9iL7j.ޓو[[E '-D
ePv|&oo8>3}=y/<2!/#ړgme_ +./gMA~5xR/Ynne@=c$5!?iHfUφ8Ucl3]\<TfVj%K*oـ['G^ +B~ri~?5c2 $HCDaAř$""WW$uwjfvvڭڣfv-P rprk췻!NBw߫OP <}-O[|'O#e#g`RJ13C_^SlFI?}I8nf`N$|@ e,ydMUn$9K1|N=@~{ 太7$ŻI_2FB"l3~n@♨z2|Rا:Dx|r])O03[<+$xa0.uN3zގZk`QS}m>@,5:,kQ0P~"@#!^)\3g%N0O|?Z}1I +DPÁ2$BGFťs>7gO`伍r_`bc RJX䨙m^CWۘZ=u[͂\ mRJݦֶ̗́{CG>0P KqQ>UD.ҐstӢSV6 &a!0ZtЄ~wQ>$bUI`5`SJ`qmN(`فX{VF)y}U|RWT"<b?<ɾRꑙ-O,_2"ƼZYk>sa8 o +r+9g[9mj6FO&@FZ{->9_buR +'TYXSpmx5t1۪Od%N?`jb9nyƎDwOe$o>9lBރT1SG%įNL&6'$;ۘXMYL+`"|2;[2}r3ye -/11JY+v"X꫟vC0d-1K$0(\.UiiڗtĒeBߎ(i&©Y)UjL6E+Ep%L
\!@}co1q쳢VLѥϸy-e>La9;\ :fYJYC=i[IqK=&\CkZn%a|Ju>~W-m(SJTӼغdÄDl.탄<' ί".2rA Quj2&jBWb̌}2d.p!vGZb0#~6z`^[<3-;iP0Gne䝒_D*(ֻ)2Rh-܆Of۳ådWዄ7<r7x9KrPTY'~a\ުPY\zllfLt'v"<<L얚v@"Wʑ +7uU٨9V/}yr
5HՄ.;>:Rn|BrKbƊ%3˔[_Dr*#B}ĩR_!/ +]bfi"p~}SL<'(%Dp)"<aec2S`0[FJ9c%wB-^r˥VY;nMZL\:\{ZwLII=-3-D7kr!nBKC@^8}I +|;d!I쓻b0[K家т>Uʑjۀ͚Khw+VN-=ƨ_SgM
23Le0/!ѫx$#}k.b+9@BRJ.O-cv{eߛI3㓐->UJ`J +Z\z服`/~κMTQ)=p HoJ b!Orw?tTŇC"b3L-E!r[FCWI_g4d`}]yyjII +tC9O皇WI=f~"Xu>:63;;n3>Њ<"*%,Z-.;гv`Vrʈ__:\?HO9S[sKf^p)UDxɡ +ꑙ&5Ԩ]U.D*K&NWl3|M7呜OTTO-Fx?֢hGbmf;M)a%5̮$y-5{.@&A)W8üܝj=P+?vu܉pHʷ)Sdt ԿE{RV \ܧw(xXEX5 ~OBHO鑚/دۧ;Й1sZiW*AY+,i)jʃ" +< +veGT +^txZ`vIr@1P^A]t3snZ9zO*O>q*eɍOB,0LfZmq'SVuǖ֡&Rj= =aٳәqG}'O>w`&mI6d`nāRid!`KaS^x{x/c瀵_]=ٲŨpqÙ_pN:XG`z·uIwEr?0OadmjV2D +s_Iǘ'JO⾑[q#%O#V\/HRDa)dfhjoeN[CBy9zRM)`w>/%I LwzÖ⪟<d|EvPBKrY}SزHyLQy$N'UR. qQ3'!a)#(KmXX?%ӡ#xZ66¦̓5#:+|I }r9
^B^*Ua{.:ȿ{WW\__SEǫ6aׅ'3v}` t4'%\<cē*<:Rf_\.DO)n)i`t\t$KfK +Gse_5N[l*Qdw홸H'5`3R}*Ӷ'l/{<Y]]eS0̓xB`0<I 7Ӵov'+gɋ.+$ye| QGm.1"5"%KJrY#MmR'e*&xv,f#@ Y>9p_x;ieeHuJni]tEJЯxő&4'E {BvhZWyڌWM.ozil2rw#>;YpQuϪ+>&ܿۗM[1gt,1湐Tjג"ela`F-xJI$-d2'1OuHd(0LNeEnrow"jS<$e:K ? (1hNpxI2i)̥]oU+Mdoa16>)a?R%]E8)TI=XVd)%EJVXp +)AdH LXZwyøEKЛL'jjE/&)6*sę|,$CJ`v1Rk݄'%$zRK='1L2)+wO"JSVs$'IO҆I65Gd 2cnx'udV/8<4_&5RZCDOrJ8kcY)tFlEA hT9mr^9MO6MM{O +'?K6H2$li0gmN:Bk"%& +X8rKfãÒ2-wsh9ȒU<YS(}`0KvH YP|GqLM&IX7
o$eaYH]Zk'diRwUO'uZx}c0=fVĂ7̃IrPZ)#EQI& x2!;we6O&Y}[A1vk|Ayd[ ᕘ"C%HLTR]dHi~)gܖF8WHj/r1ϰ3w9gC>6!KR<<^B>aBIk >Ʀ%W*aKkջ)h7'k'G
x>2Â{f*v +oH\6_? +AEdR
+-QO^g*J= \gyhT +ոͩ.;'sRrO^)s2t"CwUuŲ^cN譛g^ +%RqY(%m~ apink_)%II_)9N2?'Kl[*|2%i/ytTw) +Ly +K@)IcUR#O|\);i(d=pm\?5Sy[ +۩D֢vV)rmjhg%dKJ*ھ{ +%
opV RnǙ3T6(ja?!%QO\m +X\H)iN/wp'*>'0+O6d݂5sP + +(RIQ!y|r3II~3Odo +Z֛ +lÆHFO=Ac7RNk% +g/o:l!
Wv)R&/<d ux}UE9DaJgnh?t,h2$?* +hT`HN90/,9ckaَqvЅEqH#uۀ,X;: R>R嬢p?|,cד02T
?/:
IF{rgkazX,E^-)Ja"ŜHF}):^W{)zZ\i|wmL +ifɍIpq!,iT*X6},I[G"c59G6@BOvV E#)qeȿb;$[<?L*,]8s;}PYL--RV_QTw@JVλbYL,y_/u{ +U-!*a^xIcne&7'y~
XrDXF%ME}FZ2}Z)9!Rei4QADʇ8k飕P +vːdl{g`ed{{/pK;W+X$@4*.?_tjD)Lx9!olS5(FZB.bΰqʍ
#!)'% {QYW|#?YO}EtSg'MқeSRȦH +.mkiw|XZL*{(`|R-]}bJFVHy1e;RR4S hy,q`;8CF"#{ :xsr%(,#"
Ci4-kuVf}9,F%uEĠD +p~gK仳ls3cGׂ+ݞ כ)X,`YZtd<!PRD>/p'g"]8WvJ:HJ;5)z֧b3C"Ͽb[S +ӭ?2g+XiT\$$lR_0(LʠRu_Nt)Hȓw8Qw0uL ӾEu=x43ȋ8Dq # +JEFYxGðZڼp6//g}C(Y0vf8'ILp<B*ZQYK×{A"HILeOw8?^:'Ӿ|EuPG;'Iq;6'QdvTɝ9~dmֿ,,&.I#:YvRRiJQ@1)1ɹYIJGQ}SC6`ȋ8// + X>egQQLeNVH Po$dzHa#f<IQ}22wFE)$3:ʍ!1ccKI&ʖAy'q^}
|Nj+뾜^Y3gO4NuJDQTOz;Gz_*^X`{sdאTYvXj5%@`9,;[2Ce|;o]L)o709_VM 3{Vjѐrrc%FvL7vh
UvX.ʒ%Z]m~wYg2IK5HDJo<\gQ(J{>#YdEB- ߛCC*ƈi/ ,S;3KuiQFqo(u/ '%hۛ()F+eȿb;39tl`ܚo*KERXv~KR +F|Oo6riwȐhsv{ɓN-߳e9,1
Kp5$Lh@֤l
?ehZ)_$Z'ъ3T`0醴*,<q
a`e?R[桵)߮뱿g_vv;W_ׯtG˿ÀGB߃5S,n,1rY=7)RXT㲑қݞun %=S2&|
%RݚO.?]HZ^(Wt8c_\u]|ď|t0lhXna23D'u٪a,#U*irmR4ݒ-xOR#}g<?|t>6툢,1rj]dQnpW?R g=r`0E$+HĞ0og
NߵACFؙc}4sȏ;{l[D] +7DH;~аLf +Sf 6D~^#eA +}!ORԤ{6XrKH~P.A^ +㨨%Dx`U@4nrEʙrh +sr
KU+m)7{biƬw"X,wrI 3 ak')jB=D;`)T(?et@T+Hhe/
斗5~,(YΡoOrkW8:<}g7GQ_ކuC4,AtI0RH)úlgŅ\DWXAIIQL%A8>2IXbe +<&)j#H9`za>(jrٰ,*QjL6t4.~XDʷYѓf(*RJ6 +xoMɤT!կ ? K3(/1t9=2arU6,a8kX6:;8bH)o#e7Ii +W#R\_>_? "t94_s]0R4R"BnqD%H=NJ%;dʩ|@yUМGr~#R23D_G\*ᠨD9"j 3i>ib8
qRͲ&kaҡ8m3ug=r`vu&r_}X<o8
pΠHY[-SE][5 +Rv! 6&uW,3t9Fw*ʃ{ٰu +s.}93e(;=aÇ.4s@_5
``V +Y\e0I:T'%ybH͌HٽTۄ<BHD{(jJTPR2ϓeF7TKp8I9?ɌO3LL9Г9zi#8οvwIxΜːV6j+5UWizjWEj6UwەY !!`\CUO"c}Z. !m`n1$ߙ, ig`~W3g,j.,Xh#&HE^,eD8٤>fugy5sR宺_y(iMF2lnL^UKa+;*[.2cڙ%j>TyYI SKcJg)exJ_7HJ+sZG~58cL2a~ɁeRZXa PҖՄ_"!1aRDgT.c 5!`f#Mt'$>0r`9-E* 9 M;6НH')ZL +MȺ6v1zDR>Փ.1|(aKvX.(Xfj"C>1L@d"'Fփ(W+ +J|=jR ?~cU>H[09Dڄ°fX·82ӫ蒋1vJw[I{-NKnp6D`6KNsKv9g{,Ťu +N8W5@/32WP-;E/jRF-,RFŃ/Zmҙ_lox`cGvȹ!#M4.cg)p31R'c&SA_ Z>&)Oü<<^HJǓ/3+a^< +Mn-MHx .b[k`&]Oz5^B뿓1̳<H!{;ҾRTP +^jRV͉ao6pj#MNb(ޟ
,sT;T\K=('HJ!!8JngDoi rǘ_u* >r;U:;Pg^\Kô'V>ܨ~{{-Lu0lm JDr!pOw +{DJУj1 + 娟C_+gO'Z%;0$l +΅s#%a-ƲJdeJY>UJYIɁIiVaӽK0;oU0m)Uc6 +D|SPS"2]%OKz۔&%\wG&a3-e+ɠοI!{B_}V'$4TKdzaHงI9^[bD5SðXyT&e6r`b63` +qX +*F0`9ÁJrqI +`鰼!o@U-@ʁLFV{[De%:BgO<><9P>=a ++ԗ֒oxػ*b R*ŰJ +bFoUpvhRJ_ß<ǕmNyjȸ+M5*+rgC< ++e]iEOyXfA0Ko0,}jݳnҐ29ُ ace(Սbt#h)EZ|tQ-_zh|w۰zsrIjZ|]GcW(;Gq(>f13ƄdΓpa5IJ$*/&ia'mI aXGK1h^b,]~ּYx8(6T.f\m:X-=FPbZ4>ѓI({ndaبIb0$-Űza 4ԍNc̈ۦS˗Kõ? }ЮsQ14z4]rO%pZĸlPbИ)7'$Vkf=94tb=x-/#nȟ#YaNMJ4n#݊q\Kj 7C{9'n6\:Kwe'cυu1&)#z=i/ZN)t?|0Linw )Z^u 8`ؔ*FG8
(]ֈqu%ڌgiZx1\jxC{|7IDb^ϹC#JIJ|蘄FA\/%L`cRIEZ8P>;Ma.;nI g"&1,mb:4:.11hLF(9juW.Zط*pd};]9_t|\<\:`9]&a46q=ԞV{ɡ":HJIb0\&p.e}ısH4R74mk_|_|9w~\Y.Уբlp$iFaS\%2mg;wתk8wkQ +> +8)W`㥨b@#QC&ƴi1b7:l\T|W)y#|峿^k.Vu8(4Gnã\D4hh{U4ъ#Q4'ILF= H+ם@QRqlgV!?^.DGyBfy"U4Jq&rpwCk{K^㟦>!q8ч-nwkQ?ߑFFLKI<$,/rV|Ơ(j86gA'!ٮ0W%ܞhKŕxr|f\AO*s]o +#*T4{U1^\F@Ejݨ?xU<6|Ѩ'gЏ8+=JulY+IP]*
Y4Ba]l FbُC
U/}G9RK}Yې1btrЈ9]MPDT1Ӫ>rziE-@Rt:%ȐS}l2GňhdЍU=ǔe[B,t5{uo}w.J=WŁ&aDbD+@c܍oIb'YI +Orx_GȓR, %.4>"Jc,mZ +Ew~=|ts||f|~7>ګˢ[lFF0Уq0w87OjEL<^*)a2FuⰊ_ua(Ƹ3-*r1?%b7Lb2&;ʑ \Prտ~Xk)/z0$Fc!DD^6w9t$(RyRK`I6Or}"OR+T`E`zЪ@X\7XG##uEqf(%o O$|Q1^KsFi[끐Wցح~xA4)"O03Ƀ<X<^8,>bU=R86gy>Lj暧_dRܘnۀhteOn}sOY[_{uI) +^iFrLj.ub0 +2FC1=tGHȓ'SSg
33GL0>v9RZ)9H̳̚zhPhԔXS[`/gSk4N:S1T,uTKݗEOD̍{~0!vƚgE'!33|3
}R|?]S
f@>)2HBn.Y%"V"yЍX.}3\%\5T#x+{
B:p0%n8=XO@ąHh^ȓXbR}qxVaS-9VDZgä:.UtAt1b7#aܤݬ+tP{r.5{u-eRG'ychcg\R8E%C'-+&Lr{1Sb$E8oS\>N)BBU~PJ4h[WuhҤp~iRPB;qqbqǹlv9>vHy$& <G_t|=ڭfOֆ-TK2$qIr`tKṇYڜ8tu/N={G.VG{>y?z~>me)+qe~j$RZFJѤSkL=x@)v*bhbbLCI@W+6"~رN6;\ +\8ꁋ3!OSk?'険Q +D!H _E*߮h$l4.4/Y*ɥpEKXQX%QLȎ'/~7"m(. +V +^~q|zh=ԊtT|bzR'7RJk}>z&[`߾]ѽZ9aExS*)&|/?SQi]=µI45O$E@cfg`{G=7fJf.VFai%@B͔O(1f]7NŮňnİ5=0)}OJl3 MIaf+1){rf{ynVB1/<Xġ>ױ㿛=ٿP1X"G#]B}
1<VLdP\YMVM̓k)hߪ6ʓa#pn2L +oiГL&chbP\bf»ӈ?b|eoϴTDZFaP0h'ꑹ$Iؓ)bİM_s۶mRxR"0Up0PYՉ&C©R~wh\JJfa&YV,ͣ14%'ErX9qx[英{glHu:{64|mN>*F>hhYa*JIكIvGRiaC(7oieR=~̭F32icInTIFYӉڭTGvĎvkцweÑΏC&aN; +=j&Zy 7*ѓ.Vޣf3.W$dɏR~,"*&=?BD?ogދtԅ]gB?σYE[ҟdJKhD7bJjF2L.)~bL6e=pl Ё'C{T&Yn(Z65?Mz.~wΞ=1:=BsK
Ђ!(.\Tbf(F,zR$ I)ѓh'1\$yzTݎ'j_"j"pt! ~;U(qb2@/Qh%Q=TIYI=3F"a>`$Rf*ɛU[F;sf]z봽n*x[c=d8}0'=]Qa:S' +!%Ub#$FOIP0E)yٚ0O +wՀǕ/}e_t8C7#FVj휜@O$`$ݪ6ГEC^ݩ5-Src<yUx0iw]b!PpF]O
ðǓzF[P oYI8^f4I$R%C^h2Lh\i#sr2kaG"dFoy5e_v|59[aē{KَIIjI_$fz%zÔΔnbYe!JH(I6e&3~j]`.w4썃ơΦ.뤻,i7l~.\{0OZmAL-Rf2),L"tD(EҒL5R$-w-[`$~ڜ<dolrQmvkkj2lJo4g
a;5xT~>RZ7dSI4`x]}Hi#oV)#1II1^mXX[vRF>C8vXp3AmeO;jvx%0l}Oj +uI >hߪ6'1%rbQuwkV("͑Lpɦ
09nt:),G:ݶqɾ+^;RCðuٺQ~~<p$[z)=v1lbgXe⾮7^4qr.Uai5c0cq+` +uڂ=-nqpeOl{Laغ$+(&<)cNa'p5Hfp)A"[2bdcTj+ K̋VX/ߦ3oʼnؾβP4 +]`/ݺnnSѴvlTUhNN]hR-ߝWB MjqL~{ιc&&yս'`=1JL +Jr0~~*r:!8MݒBIT*|RIT},oa +ec1a{m"nA#YSݿPM#M|,\x@^[9@'V)%@9&cݒBP>IZ|_
1j[H!F^P6.u֬lRKֵ>12m{Ntuϱ%qr#ݝ=T'%X-Ap|{>"nyIꪢP(IQ&u:K`5ׄy)goX#!,n4O3FJaF1*|L`lq7v
!GolN/[Xƈ[v#c}-) +eYJ'P2)v~MMsc5s +%쓂Ї5c*%R FF3I"LjC悈@2%%zZS-Y$+㕜az[Eǔ6v'"ީ-CBY{J'5*P2IYϚ[jRw7. +_33̭f"&l_H&MwBhJo{^+HOPؕ>N\Q薶cu}?PP6'EutQ*UBYP("lă|R1n41')wobC* +)8[\ب+&|l
M!hO'qK]jH*P(I:g:FFTj~mpHR%z掯}h/̚쓒;du|R;kgz+eikwL,p/sd2pcn6klx=J-Lcc3MP@t2$yR({d=!5*B^UUQYRR1mٽ~G^<{̟|mgY,^AffQ5*yS(_FEtn%(& +r
ADPoK8I(φRedЭ̤yR +(:F4BU]
ƀF*ޯ?xgק;p} +8ǃ@B=d9a®36&w֬>Iٸd5Ks̭fuMm!X4>!pprl"&C6l|!:>?[tKNuOT6:랈x +R-iKlaX"A]+Mx5"!Hwxg"❌/U)QԪspż=:nIPo<~٧~C?=qD[>Im( b!S[ω +$$.VH+jsݴ$!^ ,4cO$>Ӻ)Hۍ$mzb+ϙuv$c;Gq{k_w9{*ֹsm6.e۲F6{1N&CxLjƬ1R+Dp GD _fS0%A,OᓐɎ';N8ucǎ=zȑc>j~~zgu/5HHbih +,d"00&y9`-Ǽ!&Rp{GYNԯ3|䓟z)|{?zo]u:Oކ.!샓q1-Fuo|:q>ol3t 7\Ds$%+]UX%*̾zs'8NFWR=np\ZJ +PsmA?!3Ҙ~3C!۵$$ŸG'vr{_k (%&{v9 H0r?aL1ƬQ"65OmE8!En?0H&wK赑Fw?{FP?G"'3thq4RH@%< %mZp<aUPQVַ}1'fsLq\Qq0sܻ
44R\ر4Yu8d쳩Р\aFwSl*]#$ggq4<?7 +uwcݽ\wқxZ|J^:/A0I8 +#a`)9b& ޡ˩6\E^.9"CI, b}1Nˑxpͱ<Z7xdabp[fJx+U6?*'ۑk$#H0XO01|&<3un97<0.gu- Ruܒ85 +[n4&it,Ay>TI[iwQ^?6@W[ɤ"U8_jhCoE$.ؓ=ǾʇAͦBŨWBaRPՉzxp>Z<Ơ"ki2YKQ"|Jٶ|9\2|UτWᒱ<2i߹`oh>FR=Qīy9
YW +pp*`z<&9Lj-}d +P-1/r!Zo|t1!Ēhr,gut2Z:ޭNד?ՎNAfV +ф1Ecx+pkwtݥi$Uo>Xe[h<B +M
Zx2%d?3<]vK r>ak3Xrf%êG?$%r!!/JVEN\6c+PQHչ?H^@-yL%`2/i "ZDWJfS\r 96ܑ3Í`!0(qت2nRCRc%?ﻥnʹfضT^g3i~#l"RJnS[z'Tpa$,cۈb.WuxRi5!~5/M- +(,⠨8
dk{Wp^Uk}eiH6H,G:g"Ckg[9n9<\Ӆ^B,M$$Y}Э&l'DmP-XgR6ǰǪ0IC#_k?rvw؋;ZGmFRs1USG]XΦP1.gu%JCsh! +s' wa/f8 +?|"tABeQF#m<hDٴMm,ʆ<0KkKxKJͲ)\cYK& +kmW_waIBacU#lAh(*Z!ۇ>4^D8oMӋ8A #1ab/3.,rj}>Q6Y6qx
fgan`˽2%w`лc}q<#I[[HښTqhN%y-'(vs0)EXVP<o9H"3`.'yoS)-lkT5+QL;(OڲFX6j&Xk,k`K IX3Q'd,_HM:ܽ=y|M÷'˪hH +"Щ$KyǒP=ԖeSլ{j{R2L|}qRO~V +XF6UMNJ$Iӭ-Yθo=^?=$^zOnHAO',yr{Ԑw#2lE3R2dw'YC6JcLVNLD[ޠbQI.kLc$IqSn0B_ѓKK(Ey@vsLxf$uH/k)zw| +/`n3vYZTu+jgwya6WSbFfHJ +S%̻Յd͊3*eϖeSclj'-U]VcEscqpZx<<%]9y1CM'$ez0OOmmà?L#cwπ*!Z2.*aUұFVf,yW\nVbcin{Eظ+[PW;%{ʥ*2tM491bB$vᎮ+5d!"0EXh(kfCrZD8#K`F&t%6E}6d:R2;UƴR}[U닳em'/{)+SI6aWgIhR
fqy7'-]_$;aݴ ; +H1WI+=7+"dGoO8($y4`0c?Y^&jdђFgeT6jV܅e2l Nfbkɭ`P%kg]>/qWIضh<e/=^o8j:Onۇ>6)t MRR^'#$u``>lGpf1)h2|r3*2#C([N^-`˽-p@9b$L)KZ^BsWz着ՔؕS
EXM<{𭳷xc0 +0"ꌭSIںsgtЊ Ll,5liA$oZ&<ZIYxIV@tI=A||"8T !#1D +&\eb׀&`ͿX_Ƙo#b\CގJN'T
8Rҹjq0RnE\.$۳ZV.x%גߠ+ڶ젫To.[~TgI+|kKpBL^(wѮ:A%TϏ{zM.L´Ur4df`B)1pH
0e[0͵B*^DWfkIzt&]od^ʬQoL0YAh +X%_z7HJC }H{_|raf8!f0#~-5ogj˰2X9˹R6*%%d+z>ԼTtR
Q53 9ҜAL[J_wݧr UvUUv鯺g$O;`0gv.,=ZZv`Y2t$"p&[DWfϟR +.-mSc9sNRD
nx[<@Fx^@=֒.k0RN'7?%nҋhzG0fC)mpH6euA'-LZ>R0[H9ğ-$7z*vlsW1[(ZwZPa/y@OGoH8#OʷF# +n<b l;茷Yϧ(ɫ0yrELGMkoh{Rmy:`dCH%' #1;ageU2QeBf:&fsS09/ +&]A;v7\y+3DL\0DLW=80R<ާ`$ር©?+s5)2`7g+Æn~XQo<1Vo[*̎-;o&FnO~M¤Z`NW+6uN(lǴ +L꼷HXR|2L|m> \r^¾SxdVZh^ _}H<)*$89C?##
'gwϛx.d$\/⇔۲k$$ +#mq< p`-$[7vL0`07XgAga U'HQA*JGG$RǼo@tǁ>y%F?$ +rޝ;He{쳷쫑;2pb$EQu)mX %YKEC4?|\9bx.& j-`pnC&ӗS~>K%U|n +!Ü.Vimt«~P"Sg]C(]*yg? +EQEQuFL +H
pT/!&_aBA+ʄbJZDhjhb"SlSF[qvBP +P)-7Bd I&;=&_{{|!;Lsw9W=!ግPڇ#=^v^fV$Ax 6G<=\
k˰n +]J{X/-0GOKK;ֺW̷Baפ
=R2X + 0M5bcдp;NY#pMg|ӌ4W4,v_}&:2Z=lX͚j0T<6g2n}Md~=}.5+۳)H8tYyhJ-k~'3~,Ϩ&XƂd
Xnp.ùFіP>7`&&X护M5*~Z${S:ƎVBLJ`Y+TH$\gR|%1닶43hXz4Jw@(_R
`_g,]ƜrlY.8.lg;jȾX8w`oO:ݔpZDE-#*5D^A;LI56(VX
2"sAVãTl1qVx`L2%
4MTદpuwVUn2"`Cpٯ+sAGETdTQXRP#m5)hՖg}Ԉ $!)f,Q*>ZEBַG0ujEq@{usIg0ufs_7ZY<kK1{&F[gVG:e7
˷T[531ɊDÖDy3w+q!JA(ov +si-q8Z,lQ7Oi FYk}8b'<n8YR88k}oiL#ky3=L,݆4.(tn67Ux^_U5w-62}[7ܵl"-]óo6Ufs煝0/>_z_W9.Or?KSy/?X?'[Ư +q'+l魌~_$ۘ\zFܠ=F_.LkQG+YI1ӠMå;o]syk.Z5FĖ .?Ǐ}Q$^k0p2E~L4Z^b7k'7-Xx]#]Z\)߽+k"IJj~ނU7:>"p3{+P֬&Y}4cI,ҝ~eݣV_y{&!DqK;]F>pqO׳UQ+fRn r[mEeZv7ap^k͖wpR{YP^:y-[W!=AvZk^w7nquEٍ+y*+E/lͼn=XN|qڏۏ}kO*lXZu^{Nj7hk
k!9n>JTO2A: 8A߆xm8Wgqys;[Y5kmx]~G7K~ t"\ZjU\^<K;;!niVuC0sTYPW*n;2ovޯBlB:?LגkUTY(|W~j__w+_ ]FRZkAɈ>y[sΜ~lm}r~̜~=oT*ZKc2J +에T{aku)\`f+Qg>q,^yײjv}5}_CC9?5py¸<{{n9ZFp699%D@7˚qDuX7MA +0PGDrz-oD]][,Sghۃ,o)ZOXSK[j_Md,o)
=doٽ]w:I߭V +0D3ܩIF)
rv7u2Vysr5NZ_kWG,7.W1n*I'cT/#^ "DA7Yy#H^GN(JPysRn@ͽZWm$Bd8bnPy!X,o*%`b
͵vaDm9*k`U74\U䷌*G`R 7{~oỲIFAJYAj'c*ay#Pvs$^e#'$;#%#7Btu /]PY(n6
}j2%5JoN3o*(SQZ@{`%(n~̼*8_$75vQ89m}3 +Nyʼ0 Nz$7k'{7uv!D P´vYsv.Jy3wh9#</X卐%:鍐m%>5P%(-+y#d^oɡ7B"Cd\¼2k'qIYiT!J}q#d!7'a/:$nVަ4A7'FٯX2>c:$Ok +a"TZKX3@-DL~sCBؚJRa#jdSaSB=Pj~>?7}uݼH:ӕ8Ps%!kQ}լ'0@5s7y1zޚ: ym184hR3̷݃]0@5Ou
|d
TsZB
tS%dN=ycՌyh
:z˨oy:I9jJ +
<Sx!q +ष=lC*'-g^IP`4ŽRߊ M; O8C<o D~3=zd==uRup6p9DgV_@g*epZ$FQAj1ѥ$DZa4}_@gKRt#7Hlm%% +pNUo1cpMCp?!;24 J9o[o 8)ur> 8ЩʏG7$8\)An^F<jO|2}"+tqlgG[&.ߺ-c2Ӹ8hcOP"ËA' rg!-uFzԨ/NR1x=,xs7PC~E[I7Pq~Oɇ-ej8>=3u|lRbz=,p%s[p}h{3t~7P$翛h{Ki1rM,oMbqEkoM/ +#S-3cyw;߁\FoP/][o-?$ +5|k҆@1JNPlgƶfҟMTo8NV>P'z
y(ɘos|l]k^.dRgs +L"57B,泪q9tիK +0N-8$_])yts\YOlӉX4bBoޅ
tPp
˛
\sZ#N˚5[6iHom^=vh\gSr#7Г\`|tPߗQ߄VMc=-4$aykh؟uPڅu4%ݔ|`Yͷě=J=>KzwR1j5L҄<Yno8Ol7B_:ono'Ǜ墚o kޛKN`fz|%3zh;hK.4$cb.m@o.Do+)NTo\:ޛGwrR\-
WSj!?ӛz78{qlpw0ǯkTL*1ӛΥK[x~Ω Z{#h|wU+gR uoEb}7YLm
Vog +>~[V\rg;pmU +tR͡Ƴjlor7|HR̦z7#ۍ@1J"9!wc,w{{j6PjޛGkNcQ[SԪloOu'70jb.7}{s~L4 +2/jyi$!C|.M{{
, }{si 7-"zt+XC|>mo.5:h.E<ձ7օj!'3x9܃
Grxu8{r}Ft#7G닩t<u!%ߑi,eAT.¥=_ +X2q
etӴ"ݓ +H9{I5+H7*Po|s -=z +N욾!yۙ7G~2o(1Ņ-ۅU]Ϸ_t +qOƲJ~/(ȾUzC~R_9<Pvۧ[,Dy}vjz,]z_cNM*@o*sԄ%3v%WU-V>PvNɿXXμBkGr +My9 +䝛W +꿖9-gPAwo7Tg["W*k3r- +ˇ6ͩߟB=$1(#ѴnҜtO*i=.71o'upL{y4]'|βhzĞG
.Y=:$&KaLn٭0YpL9 pB,+kUpW](y"D?-qet@x<({aހt^erӳn2zށ{+[3А +(x@-Sz506{xgF?PP9"Q].Lpe۵g +ƣ.3ug[,<ӧ -V08]55刭4O镅Hj+
h x],ݥg0OXl\6˔AzK&Ɉ8(lf\"s8(Ƭs3 .3p@c^w?
Pp|.<M8|DE88+'\"'=>}`E24tۧn{M9}dB}|? +PxqK~
h.]['_Z`8n$2yzZ`\u\$О +#Q˙AC?3 +"{QiL-s@7V&7;WǞq̓;Np@wgܚkM'.1孢k\\$=KlT\"6qPuFWc}KnL{ +$AQ#+X +>x4 "2h;NA* +%
a)Pa HA) ;gY{w?;ݻ;{o4Hz%"ADo)E$8P"HH $9Lo8T98I"/!S9R{K[# #xVVX)Aȏ| NUl@pPo&vPfKo|& ]`M~.48q +8'%& {PnE$"4)m778&Lwnoepxo%?L,pn\㨷1Ln+5qtA7X]6%xPrɋ
`(9gwre';ZvQ.Yt^T$m7 +O!_k#5ݯ4cH$JIz j{.i +LoW_0H>4`z8YtЪ2:ʑn/qHw7&yU]T]5)ly=5HN\o'`9rsGn&=/l+洄nYMjJc!YC{쒀,J(`b__4߀vMdd`U,{7aZ~ҏ1r~En$7Ħ7ʯ<൦!x\oou~lQTlaboқ|La(L(pݳ2EFӑ-z]\ш`b;$B^4yV}["l{e+Oã1*-{#$@@Szj.l 6a䬮TΔa}ܫ)rQ i"eͿ?Ckĭ4z\*ʡۻgo`pSEo>gz>iJbӄzJ| +sN/ߗs)K.|"T=Kz3H=7t}wjjC,"[_)ZzD%E_/bL!8Ӂ)R&x-wޏ/0rރn
ʜT$2fRUWY.{j +GmdaҲrJVʂ>l̘
n/Jj{Sư5B҇d[$g$;|gp\-AW-<>nT="8
ydt|C}aLÀkA܄3Q1.r?)xұ[V +h3 dt"=T͖'[wFeK!)R6V +49{Yx}-m?zΛPQ;Fvw(97uUK6S7M^uJ.*cGׄ
.$SF\ˌ_k<l.=uu^Qp`N-R-°SY9r -&aJ`U5{mxZgF:
|qk +=gG(%z+ +%QSE@EXݒ?lVC]AEإ +*hE`/ymiiݖ7ܙs?sWO.X}[2t6BmE`6NBnn l@)'?X>Ye
^#S?YB\ܑnskl_m^FPB-/+?faP!E2
.dup}R-Ԩg +
Y1T.k'Qlo܅φoKll}aW%rɳ&$Rxؾb11ԁ'lO\)d9ҭ)>Mp]7ZG':u(q) +u$dlM +'wkS-|
O;y] +1ԍ]7T5ھGOݸ$kTF`OV|PčM]tMcY
}v,n \fcj{.)ٚUŠrLV$B@u*B7F6y2|rr;+\[ιrptrCx!r](P +g=c(1
fB8P +G]di9++m'A<jn79TM0LٙfS;qP2 j@2OيP'8}cPQ猪sQborI` +2bd3o$yUݸ뜺UMAW3]sWL#:$`d3`RJ>UdQn3%ilrO8Ӫo+9ETOiSWhrFdw3:{ϣlW
.6lY{ex!8t_xW>mΈȀ5{J=WFsLhPv$}_RMפ} +˴{Bzr5x.UI&${C[#Eqx(կa9EP\%wEOwS8q'^ӘD٢#AJSTY+v0ZGM0٪]lQm>Pvy$+^D& H}Bp8o/wo$_u.7Zհ 7D-z VwԼuYa0N@Ye囍'cmt-ƗYkC#01*SuS,٩p܅[C_qbhjF Y.&L0W7O.(Uf?UʍlE0%ݹ@"qy*`@vyIy%Yqp +~&enc*wnjIcw5kΗ@,'k;}Y.Z.\\)+;=V~M+\`I)^*-O0 !
AB\u߃7%˵
.}w[<Ċsdr#Go?"Z-6K1 +$d/:0\}]7> +vTUC:ˉAe>Ś<Ovx_'M8jdc3tS˷}Å17{ĨAL--3"\&AڒC(D9=ڭz&b] 0 +HyTSwoɞc
[5laQIBHADED2mtFOE.c}088GNg9w߽ + +V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'K +x- +ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r9\A&GrQhE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mDeԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel
}}Cq9 +N')].uJr +wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó tizf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 +n3ܣkGݯz=[==<=G</z^^j^ ޡZQB0FX'+t<u-{__ߘ-G,}/Hh8mW2p[AiAN#8$X?AKHI{!7<qWy(!46-aaaW @@`lYĎH,$((Yh7ъb<b*b<~L&Y&9%uMssNpJP%MIJlN<DHJIڐtCj'KwKgC%Nd|ꙪO=%mLuvx:HoL!ȨC&13#s$/Y=OsbsrnsO1v=ˏϟ\h٢#¼oZ<]TUt}`IÒsKV-Y,+>TB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY.=b?SƕƩȺy
چk5%4m7lqlioZlG+Zzmzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś
nLl<9O +zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs
2F[p(@Xr4Pm8Ww)Km +%!PS-Adobe-3.0
%%Creator: Adobe Illustrator(R) 17.0
%%AI8_CreatorVersion: 19.2.1
%%For: (Zachary Mitton) ()
%%Title: (metamask_icon)
%%CreationDate: 6/15/16 2:23 PM
%%Canvassize: 16383
%%BoundingBox: 98 -140 188 -44
%%HiResBoundingBox: 98.7919746568114 -140 188 -44
%%DocumentProcessColors: Cyan Magenta Yellow Black
%AI5_FileFormat 13.0
%AI12_BuildNumber: 147
%AI3_ColorUsage: Color
%AI7_ImageSettings: 0
%%RGBProcessColor: 0 0 0 ([Registration])
%AI3_Cropmarks: 79 -156 207 -28
%AI3_TemplateBox: 180.5 -120.5 180.5 -120.5
%AI3_TileBox: -163 -488 449 304
%AI3_DocumentPreview: None
%AI5_ArtSize: 14400 14400
%AI5_RulerUnits: 6
%AI9_ColorModel: 1
%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0
%AI5_TargetResolution: 800
%AI5_NumLayers: 1
%AI17_Begin_Content_if_version_gt:17 1
%AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1
%AI17_Alternate_Content
%AI9_OpenToView: -39.6666666666679 23.666666666667 3 1419 866 18 0 0 -5 38 0 0 0 1 1 0 1 1 0 1
%AI17_End_Versioned_Content
%AI5_OpenViewLayers: 7
%%PageOrigin:-220 -420
%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142
%AI9_Flatten: 1
%AI12_CMSettings: 00.MS
%%EndComments
endstream
endobj
24 0 obj
<</Length 22700>>stream
+%%BoundingBox: 98 -140 188 -44
%%HiResBoundingBox: 98.7919746568114 -140 188 -44
%AI7_Thumbnail: 120 128 8
%%BeginData: 22554 Hex Bytes
%0000330000660000990000CC0033000033330033660033990033CC0033FF
%0066000066330066660066990066CC0066FF009900009933009966009999
%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66
%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333
%3333663333993333CC3333FF3366003366333366663366993366CC3366FF
%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99
%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033
%6600666600996600CC6600FF6633006633336633666633996633CC6633FF
%6666006666336666666666996666CC6666FF669900669933669966669999
%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33
%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF
%9933009933339933669933999933CC9933FF996600996633996666996699
%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33
%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF
%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399
%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933
%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF
%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC
%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699
%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33
%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100
%000011111111220000002200000022222222440000004400000044444444
%550000005500000055555555770000007700000077777777880000008800
%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB
%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF
%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF
%524C45FD24FFA776FD75FFA04A4AA1FD73FFA04A754A75A8FD71FF7C4475
%4A6F4A6FA8FD6FFFA04A754B754B754A75FD6EFF764A6F4A754A6F4A754A
%76FD6CFF764A754B754A754B754A754AA1FD69FFA8754A6F4A754A6F4A75
%4A6F4A6F4AA1FD44FFA7C9A075A8FD1EFFA8754A754B754B754B754B754B
%754B754ACAFD3FFFCFC9C299C1997476FD1EFFA76F4A754A6F4A754A6F4A
%754A6F4A754A4B4AFD3BFFA7C99FC198BB98C198754AA8FD1DFFA8754A75
%4A754B754A754B754A754B754A754B6F76FD37FFC9C99FC198C199C199C1
%99754A75FD1DFFA74B4A754A6F4A754A6F4A754A6F4A754A6F4A754A4A76
%FD31FFA8C9A0C1999998C1999F99C199C1746F4A4A76FD1CFFA1754A754B
%754B754B754B754B754B754B754B754B754B6F7CFD2CFFCAC9C89FC198C1
%99C199C199C199C1C1C175754B754ACAFD1BFF7D4A4A6F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A6F4A6FA8FD27FFCAC9A1C2989998C199C199
%C199C199C199C199C16E4B4A754A75A8FD1AFF7C6F4A754B754A754B754A
%754B754A754B754A754B754A754B754A75FD24FFC9C99FC199C199C199C1
%99C199C199C199C199C1C1994A754B754A6F76FD1AFF764A4A6F4A754A6F
%4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A76A8FD1DFFA7C9A0C1
%99BB989998C1999F99C1999F99C1999F99C199C199994A4B4A754A6F4AA8
%FD19FF756F4B754B754B754B754B754B754B754B754B754B754B754B754B
%754B754A76FD1AFFCAC299C198C199C199C199C199C199C199C199C199C1
%99C199C199994B754B754B754A7CFD19FF764A4A754A6F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A99FD04C199C1C1C199C1
%C1C199C1C1C199C1C1C199C1C1C199C198C199C199C199C199C199C199C1
%99C199C199C199C199C199754A754A6F4A754A4A7DFD18FF7C6E4B754A75
%4B754A754B754A754B754A754B754A754B754A754B754A754B754A754BFD
%05C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C1BBC1C1C199C199C199C199C1
%99C199C199C199C199C199C199C199C199754B754A754B754A754BFD19FF
%754A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A
%754A6F4A4B74C199C199C199C199C199C199C199C199C199C199C199C199
%9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1994B4A754A6F4A
%754A6F4A76FD18FFA14A754B754B754B754B754B754B754B754B754B754B
%754B754B754B754B754B754B754B7599C2FD16C199C199C199C199C199C1
%99C199C199C199C199C199C175754B754B754B754B754B75A1FD18FF4A4B
%4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75
%4A6F4A754A6F99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1
%99C199C199C199C199C199C199C199C199C199C16E4B4A6F4A754A6F4A75
%4A6F4AFD18FFA16F4B754A754B754A754B754A754B754A754B754A754B75
%4A754B754A754B754A754B754A754B75FD16C199C199C199C199C199C199
%C199C199C199C1999F6F754A754B754A754B754A754A7CFD18FF764A754A
%6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A
%754A6F4A754A9999C199C199C199C199C199C199C199C199C199C199C199
%9F99C1999F99C1999F99C1999F99C199994A6F4A6F4A754A6F4A754A6F4A
%4A7DFD17FFCA4A754B754B754B754B754B754B754B754B754B754B754B75
%4B754B754B754B754B754B754B754B7575FD17C199C199C199C199C199C1
%99C199C1C1994B754B754B754B754B754B754B754BFD18FF764A4A754A6F
%4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75
%4A6F4A754A4B74C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1
%99C199C199C199C199C199C199754A6F4A754A6F4A754A6F4A754A6F4A7C
%FD18FF754B754A754B754A754B754A754B754A754B754A754B754A754B75
%4A754B754A754B754A754B754A754B7599FD13C1BBC199C199C199C199C1
%99C199C199754A754B754A754B754A754B754A754B75A8FD17FFA04A754A
%6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A
%754A6F4A754A6F4A754A7599C199C199C199C199C199C199C199C199C199
%C199C1999F99C1999F99C199C174754A6F4A754A6F4A754A6F4A754A6F4A
%6F75FD18FF4A754B754B754B754B754B754B754B754B754B754B754B754B
%754B754B754B754B754B754B754B754B754B754A9FFD14C199C199C199C1
%99C199C175754B754B754B754B754B754B754B754B754AA7FD17FF7D4A4A
%754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A4B4AC1C1C199C1BBC199C1BBC199C1BBC199
%C1BBC199C199C199C199C199C16F4B4A754A6F4A754A6F4A754A6F4A754A
%6F4A75A8FD17FF764A754A754B754A754B754A754B754A754B754A754B75
%4A754B754A754B754A754B754A754B754A754B754A754B7575FD13C199C1
%99C199C1BBC16F754B754A754B754A754B754A754B754A754B6F75FD17FF
%A84A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A
%754A6F4A754A6F4A754A6F4A754A6F4A754A4B74C199C199C199C199C199
%C199C199C199C199C199C199C199994A4B4A754A6F4A754A6F4A754A6F4A
%754A6F4A754AA1FD17FF76754B754B754B754B754B754B754B754B754B75
%4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75
%9FFD11C199C199C199994B754B754B754B754B754B754B754B754B754B75
%4B75A8FD16FFA86F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75
%4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A99C1C1
%99C1BBC199C1BBC199C1BBC199C1BBC199C199994A754A6F4A754A6F4A75
%4A6F4A754A6F4A754A6F4A6F75FD17FFA74A754A754B754A754B754A754B
%754A754B754A754B754A754B754A754B754A754B754A754B754A754B754A
%754B754A754B754AFD13C199754B754A754B754A754B754A754B754A754B
%754A754B754ACAFD16FFCA4A6F4A6F4A754A6F4A754A6F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A
%754A4B4AC1BBC199C199C199C199C199C199C199C1996F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A6F4A754A75FD17FF7D6F4B754B754B754B75
%4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75
%4B754B754B754B754B754B754B7599FD10C19F4A754B754B754B754B754B
%754B754B754B754B754B754B6F7CFD17FF764A754A6F4A754A6F4A754A6F
%4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75
%4A6F4A754A6F4A754A7599C1BBC199C1BBC199C1BBC199C1BBC199C1C199
%4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754AA8FD17FF75754A
%754B754A754B754A754B754A754B754A754B754A754B754A754B754A754B
%754A754B754A754B754A754B754A754B754A7599C199FD12C1994A754B75
%4A754B754A754B754A754B754A754B754A75FD17FFA8754A6F4A754A6F4A
%754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A7599C1999999C199C199C199C199C199C199
%C199C199C199994A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A7CFD
%17FF75754B754B754B754B754B754B754B754B754B754B754B754B754B75
%4B754B754B754B754B754B754B754B754B754B754B7599C199C199FD13C1
%99994B754B754B754B754B754B754B754B754B754B754A7CFD15FFA8754A
%6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A
%754A6F4A754A6F4A754A6F4A754A6F4A7599C199C199C199C1BBC199C1BB
%C199C1BBC199C1BBC199C199C199994A6F4A754A6F4A754A6F4A754A6F4A
%754A6F4A754A76A8FD13FFA84A754B754A754B754A754B754A754B754A75
%4B754A754B754A754B754A754B754A754B754A754B754A754B754A754B75
%99C199C199C199C199FD0FC1BBC199C199994B754A754B754A754B754A75
%4B754A754B754A754AFD14FFA14A4A754A6F4A754A6F4A754A6F4A754A6F
%4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A75
%75C199C1999F99C199C199C199C199C199C199C199C199C199C199C199C1
%99754A6F4A754A6F4A754A6F4A754A6F4A754A4B4AA8FD14FF7C4A754B75
%4B754B754B754B754B754B754B754B754B754B754B754B754B754B754B75
%4B754B754B754B754B754B7599C199C199C199C199C199FD11C199C199C1
%C1754A754B754B754B754B754B754B754B7576FD12FFA8A151754A6F4A75
%4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F
%4A754A6F4A754A6F4A754A4B74C199C199C199C199C199C199C1BBC199C1
%BBC199C1BBC199C1BBC199C199C199C199754A754A6F4A754A6F4A754A6F
%4A754A757DFD11FFA14A4A4A754B754A754B754A754B754A754B754A754B
%754A754B754A754B754A754B754A754B754A754B754A754B754A7575C199
%C199C199C199C199C199C1BBFD0FC199C199C199C199754A754B754A754B
%754A754B754A754A4A75CFFD10FFA8754A4A754A6F4A754A6F4A754A6F4A
%754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A
%4B6EC1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199
%C199C199C1999F99C199754A754A6F4A754A6F4A754A6F4A754A4A4ACAFD
%11FFA8754A754B754B754B754B754B754B754B754B754B754B754B754B75
%4B754B754B754B754B754B754B754B7575C199C199C199C199C199C199C1
%99C199FD11C199C199C199C199754B754B754B754B754B754B754B754ACA
%FD13FFA87C4A4B4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A6F4A756EC199C199C199C199C199C199C199
%C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199754A
%6F4A754A6F4A754A6F4A754AA7FD17FF75754A754B754A754B754A754B75
%4A754B754A754B754A754B754A754B754A754B754A754B756FC199C199C1
%99C199C199C199C199C199C199FD11C199C199C199C199C199754B754A75
%4B754A754B754A76FD13FFA8CA7DA176754A6F4A754A6F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A4B6EC199C1999F99
%C1999F99C1999F99C1999F99C199C199C199C199C199C199C199C199C199
%9F99C1999F99C199C198754A6F4A754A6F4A754A4B4AFD12FFA87C4A4A4A
%754B754B754B754B754B754B754B754B754B754B754B754B754B754B754B
%754B754B754B7575C199C199C199C199C199C199C199C199C199C199FD11
%C199C199C199C199C199C199754B754B754B754B754A75FD14FFA8754A4A
%754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A
%6F4A754A4B4A9F99C199C199C199C199C199C199C199C199C199C199C199
%C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C199C1994B4A754A
%6F4A754A6F4AFD16FFA8A14B754B754A754B754A754B754A754B754A754B
%754A754B754A754B754A754B754A7575C199C199C199C199C199C199C199
%C199C199C199C199C199FD11C199C199C199C199C199C199754A754B754A
%754B6FA7FD18FF516F4A6F4A754A6F4A754A6F4A754A6F4A754A6F4A754A
%6F4A754A6F4A754A4B4AC1999F99C1999F99C1999F99C1999F99C1999F99
%C1999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C199
%9F99C1754B4A754A6F4A6F4AA8FD17FFA1754B754B754B754B754B754B75
%4B754B754B754B754B754B754B754B754B754BC1C1C199C199C199C199C1
%99C199C199C199C199C199C199C199FD11C199C199C199C199C199C199C1
%75754B754B754AA7FD15FFA8A14B4A4A754A6F4A754A6F4A754A6F4A754A
%6F4A754A6F4A754A6F4A754A6F4A756E9999C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C1BBC199C1BBC199C1BBC199C199
%C199C199C199C199C199C199C174754A6F4AA1FD17FF4B6F4B754A754B75
%4A754B754A754B754A754B754A754B754A754B754A754B754AC1C1C199C1
%99C199C199C199C199C199C199C199C199C199C199C199FD11C199C199C1
%99C199C199C199C199C175754A76FD18FFCA4B4B4A6F4A754A6F4A754A6F
%4A754A6F4A754A6F4A754A6F4A754A6F4A6F4A9999C1999F99C1999F99C1
%999F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C1
%99C199C199C1999F99C1999F99C1999F99C199C16E4BA8FD1AFF756F4B75
%4B754B754B754B754B754B754B754B754B754B754B754B756F9F99C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199FD0FC1
%99C199C199C199C199C199C199C199C1A1FD1CFF4B4B4A754A6F4A754A6F
%4A754A6F4A754A6F4A754A6F4A754A4B4A9F99C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C1BBC199C1BBC1
%99C1BBC199C199C199C199C199C199C199C199C198CAFD1CFFCF4A754A75
%4B754A754B754A754B754A754B754A754B754A754B9999C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199FD0FC199C1
%99C199C199C199C199C199C199C1CAFD1DFFA84A6F4A754A6F4A754A6F4A
%754A754A754A754A754A6F4A99999F99C1999F99C1999F99C1999F99C199
%9F99C1999F99C1999F99C1999F99C1999F99C199C199C199C199C199C199
%C199C199C1999F99C1999F99C1999F99C199FD1FFFC299C1C1C19FFD0FC1
%99C1C1C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199FD11C199C199C199C199C199C199C199C2FD1EFFCABBC1
%99C1C1C199C1C1C199C1C1C199C1C1C199C1C1C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C1BBC199C1BBC199C1BBC199C199C199C199C199C199C199C1A0FD1EFF
%FD18C199C199C199C199C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C199FD0FC199C199C199C199C199C199C198C9FD1DFF
%C9C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F
%99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1
%999F99C199C199C199C199C199C199C199C199C1999F99C1999F99C19999
%A1FD1DFFC9BBFD19C199C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199FD0FC199C199C199C199C199C199C198
%C9FD1DFFC1C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C199C1
%99C199BBA7FD1CFFC9FD1BC199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199C1
%99C199CFFD1CFFC298C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C1999F99C1999F99C1999F99C1999F99C1999F99C1999F
%99C1999F99C1999F99C1999F99C199C199C199C199C199C199C1999F99C1
%999F99C1999F98C1A8FD1CFFFD1EC199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199FD0FC199C199C199C199
%C199C199FD1CFFC9C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199
%C1BBC199C1BBC199C199C199C199C199C199C199C199C199C199C199C199
%C199C1999999C1999999C1999999C1BBC199C1BBC199C1BBC199C1999999
%C19999989999999899A8FD1BFFC2FD1EC1BBC199C199C199C199C199C199
%C1999999C1999999C1999999C199BB99C1999999C1999999FD0DC1999999
%C1999999C1999998C9FD1AFFC998C199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C199999899999998999999989999
%99989999999899999998BB999998999999989999C199C199C199C199C199
%C199C199C199999899279998999999A0FD1AFFC2FD23C199C199C199C199
%C199C199C199C199C199C199C1999F515299C199C199C199C199FD0DC199
%9999C1992E4BC199C199C2A8FD18FFCAC199C1BBC199C1BBC199C1BBC199
%C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C19999989999
%99989999999899999998C175510528057598999999989999C199C1BBC199
%C1BBC199C1BBC199C19999989927286FBB99C198C9FD18FFC9BBFD25C199
%C199C199C1999999C1999999C1BB994B2E0628272E279999C1999999C199
%FD0DC1BBC199BB992E282E99C199C1A0FD18FF9FC199C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199C199C199C199C1999F99
%C1999998999999989999BB98752706052827280528279998FD0499C199C1
%99C199C199C199C199C199C1989998990528054B98C198A0A9FD16FFCAFD
%29C199C199C199C199C1999F7552282E272E272E272E272875C199C199C1
%99FD0FC199C1752E062E51C1BBC199FD17FFC998C1BBC199C1BBC199C1BB
%C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199
%C199C199C199C199992706052805280528272805280528989999C199C199
%C199C1BBC199C1BBC199C1BBC199999951057699C199C1999FA8FD16FFFD
%2AC199C199C199C199C199A07576272E2728052E2728272E277599C199C1
%99C199FD0DC199C1759FFD04C199C199CAFD15FFA7C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C1999F99C199C1BBC1C1C1999F7575272827280528057598C1
%999F99C199C199C199C199C199C199C199C198BBC1C199C1999F98BBA7FD
%15FFC2FD2EC199C199C199FD0BC17576512E27C19FC199C199FD0FC199FD
%05C199C199CFFD14FFCA98C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1
%99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1999F99C1
%999F99C1BBC199C1BBC199FD05C1999F99C199C199C199C199C1BBC199C1
%BBC199C1BBC1999999C199C1BBC198C1CAFD14FFC2FD31C199C199FD11C1
%99C199C199C199FD0DC199C199FD05C199FD14FFA8C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C1999F99C199C199C199C199C199C199C199C199C1
%99C199C1999F99C199C199C199C199C199C199C199C1999999C199C199C1
%CAFD13FFCFFD32C199C199FD15C199C199C199FD0FC1BBC1C1C199FD14FF
%A0C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1
%BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC1
%99C1BBC199C1BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C1
%BBC199C1999999C199C1CAFD13FFC1BBFD33C199C199FD15C199C199C199
%FD0DC199C1C1C1C2FD13FFC998C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C1999F99C199C199C199C199C199C199C199C198C2FD13FFFD52C199C1
%99FD0DC199C1A1FD12FFA7C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1
%99C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1
%BBC199C1BBC199C199C199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC1
%99C199C199C199C199C1BBC199C1BBC199C1BBC198C9FD12FFC2BBFD35C1
%99C199C199C199FD17C199C199FD0DC1C9FD12FF99C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199999899999998C1999999C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%98C2FD11FFC9FD31C199C199BB99C199BB99C199C199C199C199FD15C199
%C199FD0BC1BAC9FD10FFC2BBC199C1BBC199C1BBC199C1BBC199C1BBC199
%C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C1BBC199C199C1FD0499
%98999999989999C199C199C199C199C199C199C199C1BBC199C1BBC199C1
%BBC199C1BBC199C1999F99C1BBC199C1BBC199C1BBC198C9FD0EFFCFBBFD
%2BC199C1999999C1999999C1999999C199C199C199C199C199C199C199C1
%99FD11C199C199FD0BC1BAC9FD0DFFA0C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199C199C199C19999989999
%99989999999899999998FD0499C1999F99C1999F99C1999F99C1999F99C1
%99C199C199C199C199C199C199C199C1999F99C199C199C199C199C199C1
%98C9FD0CFFC299C19FC199C19FC199C19FC199C199C199C199C199C199C1
%99C199C199C199C199C199C199C1999999C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C19FFD0FC199FD
%0CC1CFFD0BFFA79999C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C19999989999999899999998999999
%989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%BBC199C1BBC199C1BBC199C199C199C1BBC199C1BBC199C199CFFD0BFF99
%C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C1999999C1999999C1999999C1999999C199C199C199
%C199C199C199C199C199C199C199C199C199C199C199C19FC1BBFD09C199
%FD0CC1CFFD0AFFC2989F99C1999F99C1999F99C1999F99C1999F99C1999F
%99C1999F99C1999F99C1999F99C199C1FD0499989999999899999998FD04
%99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1
%999F99C199C199C199C199C199C199C199C199C199C199C199CFFD09FFCA
%C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199
%FD07C199FD0CC1FD0AFF99C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199999899999998999999
%989999C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C1C1C199C1BBC199C1BBC199FD04C1
%FD09FFC999C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C1999999C1999999C1999999C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C1C1C199FD07C19975754B27A8FD07FFA8C1999F99
%C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C199
%9F99C1999F99C199999899999998FD0499C1999F99C1999F99C1999F99C1
%999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F
%99C199C199C199C14A27F827F805F8F8F852A8FD06FF9FC199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C1BBC175270027F82727272027F82752FD05FFCA98C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199
%C1999998999999989999C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199C198BB98C198C199C199C199C2A0A0A0
%C9A127F827F827F827F827F827F8F87DFD05FFC299C199C199C199C199C1
%99C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C1999999C199C199C199C199C199C199C199C199C199C199C199C1
%99C199C199C199C199C299C199C2A0C3A0C9A1CAA7CAA7CAA8CAA8CAA8A8
%2727F8272727F8272727F8274BFD06FFA0BB999F99C1999F99C1999F99C1
%999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C19999
%98FD0499C1999F99C1999F99C1999F98C1999998C198BB98C1999F99C199
%A09FA1A1A7A1A8A1A8A1A8A8A8A7A8A7A8A1A8A7A8A1A8A7A8A127F827F8
%27F827F827F827F8A8FD06FFCF99C199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C29FC199C2A0C9A0C9A0C9A7CAA7CAA7CAA8
%CAA8CAA8CAA8CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA8CA27272027272720
%272727F852FD08FFC299C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199C1989998BB99C199C199
%C2A0A0A0C3A0A7A1A8A7A8A1FD07A8A7A8A7A8A1A8A7A8A1A8A7A8A1A8A7
%A8A1A8A7A8A1A8A7A8A1A8A7A8A127F827F827F827F827F827A8FD08FFA1
%C199C199C199C199C199C199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C199C198C2A1C9A0C9A1CAA7CAA7CAA8CAA8CAA8CAA7
%CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8
%CAA7CAA8CAA7CAA8A8FD0427F8272727F82752FD09FFCF98C1999F99C199
%9F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999F99C1999998
%BB98C1A0C9CAFFAFFFA8A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7
%A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1
%CAA127F827F827F827F827F8A8FD0AFFC299C199C199C199C199C199C199
%C199C199C199C199C199C199C199C199C199C199C199C2C9CFFD0AFFA8A8
%A7A8A7CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CA
%A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8A8FD0427F827F827F87DFD0BFF
%A8C199C199C199C199C199C199C199C199C199C199C199C199C199C199C1
%989999C9A7CFFD10FFA8A87DA7A1A8A7A8A7CAA7A8A1A8A7A8A1A8A7A8A1
%A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1CAA127F82727
%5252767CA1A8FD0CFF9FC199C199C199C199C199C199C199C199C199C199
%C199C199C199C199C2C9CFFD16FFA8A8A1A8A7A8A7CAA8CAA7CAA8CAA7CA
%A8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7A8527D
%7DA8A7CAA7A8A8FD0DFFC998C1999F99C1999F99C1999F99C1999F99C199
%9F98BB989999C9A7FD1CFFCFA7A87DA17DA8A1A8A1A8A7A8A1A8A7A8A1A8
%A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A7A8A1A8A1A77DA8A1A77DA7A1A1
%A7FD0EFFCAC199C199C199C199C199C199C199C199C199C199C2A0C9CAFD
%23FFA8CAA1A8A1A8A7A8A7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CAA7CAA8CA
%A7CAA8CAA7CAA7A8A1A8A1A8A1A8A1A8A8FD10FFA0C199C199C199C199C1
%99C199C199BB98C199C9CAFD29FFA8A77DA7A1A77DA8A1A8A1A8A7A8A7CA
%A7A8A1A8A7A8A1A8A7A8A1CAA7A87DA8A1A77DA8A1A77DA7A1FD11FFCA98
%C199C199C199C199C199C198C2A0C9CAFD2FFFA8A8A1A7A1A8A1A8A1A8A7
%A8A7CAA8CAA7CAA8CAA7CAA8CAA7A8A1A8A1A8A1A8A1A8A1A8A1FD12FFCA
%C198C1999F99C1999998C2A0CAA8FD33FFA8FFA8A87DA77DA77DA77DA77D
%A8A1A8A1A8A7A8A1A8A1A77DA7A1A77DA7A1A77DA7A1FD14FFA0C199C199
%C199C1A0FD3DFFA8A8A1A8A1A8A1A8A1A8A1A8A7A8A7A8A1A8A1A8A1A8A1
%A8A1A8A1A8A1FD15FFCA98BB99C2A0CFFD41FFCFA7A8A1A17DA8A1A77DA8
%A1A77DA8A1A77DA8A1A77DA77DA17DCAFD16FFC9A7FD49FFA8A8A1A8A1A7
%A1A8A1A8A1A8A7A8A1FD04A8FFA8FD66FFA8CAA8A8A8FFA8FFA8FFFFFFA8
%FD12FFFF
%%EndData
endstream
endobj
25 0 obj
<</Length 65536>>stream
+%AI12_CompressedDatax$&?d&
C;\;fJ-n[[YUZ(xd&,f #+3q98OxOq<?wo ~7_{ˏ~/&G4M~VۯLG{4?oqwo^_^ޡݻ篞g/PWnC} 4`e߱=&7Ô?L/ޣvu&3%Co^|O߾yq7/W?>y~^|0;qv~c7/o^a|t/_/tqzWw0R<3ٯOaC)?l/Jo|ۿi[Lݘج{K̬̂1??J[x?~W~NguG|˻FѤS7_ܽD/H1oɦ >Kz3 3y}vgӭw2IM~?WyOtҺ2!Lj}.6(^{_G<Ѽb |aoSl߿DŽkѽ_bٚO1';=I~R4!o;H]cձW?y=5w7_j|ӷR}To?~_^ +!}Ho_wg߾Ͽzz_~˧ߩOn_|=w.̙/|ܿ8~_xNuTOX[˷Zߥfi>$_>ksP3|q-y=?F?!]϶j~xx7/~tS/>\?߿cwwI|+r;鳶|0e>loq\3L/pba,H=;0?)vU\) ߀%%Ӧ\ \܌x[f`*nU-LDIo^iSi.繜5Jz7ѵ][*FAiUU*'-VmӯVuY[a^^Zd]fU͛V+ebe7g]k;la]/WkdYԬU)۵Z)*և:YeXtMe@CY#չk)7ܲԓŗYUeLIɭ̍zʵؔF2 ssλɝP-Vx>'O[L
.C +S +p7vv)esU<sSH14S:t}b>sSʧ|o-&7 LNyni̕W*^rdNONtemwp +D5CpqnX. Kucq7g\DW);2UnC|Ey
x;Rٙ-خv8Pz? gx'H˗v0ܮH *`Cld!2r.y9&*w"6`ټ.b/+>P<sr
4` +fj=iv??yZqE^\ZJYYa (rrgaoе)LN,btU;ZNd6˻3C^/.҆YNavIBԩgMsН5ڣSC1ZSߜ|O3["䍈أ%l$"-
FF/ӗr6Y8*ߩ))šs]q];IxLnfI\JgeXr%>,"eXalX~PdS}bٛ2<duXE3Ϩr;.E9J\d('}(bs=U-l4W=9"}ZӬYAL6%Ts끼VJ{OX 2=ye[3UCwD翇d?Sԇos<;XԵ?ʏOBF7mLE߰s8҆+ H$" ("tAy\( !DʢJBFǭH|! ,DiȪ4$uN"e(rE"R,RQшQ ,e$JI OeSB%'ЈjFĥkK(2Qhؔ|J5t[듖|97nI?FյX4̲P,~)uuxIx" ?4
Qs
E6< KCvD1<L\$9.ҢjU˭Tk]/R)[[hJܔ4lTyQ +p*2:Za}S7zϢ-nM^_
/Y;lIr54 +rzb>l4맕aýE|r.VOoGEq3--U +ͪLTTJэEUZ*mXM]JY\9UDi%%2r5=Ҵъ
%s(It8i4fCT +a);12<L7@20{yDH?\~st-*X*;K>y?Ӌ+[ @c&*PV5m\86
V+-7bU[#w)Zf{%0<@jXp1frb=]9It=G9 [>EXy+3KoJO+ï~QQ0:1L<98Ilj>-Ѿ8Jl+u!3)]Hh%\hB#ڸLo{[Z&qk"fÊmIWCv8x}i؎u'^{߇qmCIJϞ=c@09Bk۱iccwt6sM&;XݸCI1T ++tǵl⟿Z+R>qV#6Cxji7~zQfd@,zv4./_bS;;c4`&#
46%0ySIRм
m{=jP]||s|<:@>lm/zr._S +_rUXBa3|!<4S<0< Sy+De&W@AAr-^uUjKTˈXōuXᒗe?#uFnfVŜzQ27sPf'z3mdq<9+r>3_<Uk]' +@rO"i
嚀y0sfE :gx,>;ug{YʪHNEa십]ԕd1U
w]<[
)8c(bb<;]=),IȥId,v+POD QuVoʬ*rk$V_ba_U[X.}7IʲC#rOL,lEkY,
En%ʦ"ƞD1V] ֯$XJ:vl`{6VY=+2ҡY<Wbc~Z-CqŊ}سogu8rsm;nXu}!Ց?j6U615r#In +wz·2_}q|t0>\v,нe| +(Cq7o]ͷ`['sثj[d˧1rQNت8
ETԑ<}>S|efk Hʾ_ +M^A*V +oDT7
F\'uY6@,[eh>O*r.V+÷h#exZ:i0$3QN +ߑ6V<ϒ^XEH92kV'jX\+KdP8SGYр3ɡeNqV1e'%:U9J1џP:vlu{L[5*F6Ԯ/+Bƪ-;eфaǓJږ߶<'Oo_KM ]"\EkF0EDϤE Ȕθϔog|VUu^v)H!
{'xT:UjCITҒآ(ZJ(Z4KY薮lZJa\"6Bi(D9P{i{::6KOyíO0i5*alX!X%RK努Q>E(c4,Zۙ;^o;r%( />1]3HJW.=Cq Q;
JliJB%۵C^ײҫjL[ExDZ䤵 nI˴~ԻzMj,v8'2<9>Oo_êD Oس&F + +0jhMMop-~v*fPj0<м4A͗ƪ]\ /0)zdp[_bݫjZL]kmrkX6ָ՚.XƬuɨ1i=d.LYO^IS)exZ2<NO' +O' +xm7?a>Ykpi>d8$\09fB'zgFxqO/e⎭cT1;ɸC0tmojj{(qp͂C@z
Nim)x?NvU!qnܺ//rvEi]ŧMi|T3ٷO'ؤ\L+u6u&٦|t,JtѲK]j=?Q`%֜k,pHyTak5u1'@IQAlP8MJ&7nJiQ<YOdF0DG3yvhCx/ew9ˣ/h-UPhnu&rSwOfc[x^$؝lxqq}csn<c&z}QPM5V'5K9fKrC$ۊ-h45^73 +^.E/yjRKߗi{v?|'ȍʾGK6
ʅF|,x5奔v1ab,TrThv/̞ʽhj2~s&pr|OsO!y"pQ-*X\ZY[z4]%zC@醒(MDisg'-tuh"Bm@L^u}DmAn$$y`0&gnճC7x'᥎(6Kft'8=nWlASuOx8SV*;Td>ٖIİX#! +Ma\Qݙ>Y+|Z[1z[gz[1Ԙ۴mJUr5Y|Vnodw`xNRKisl\7P||TGYugnKE$%V`t +6>+j::T\Phel銻PnC%oS5 +YSh +fWX1V *:I2SOBI44!eVk?xܓ'cOLj4-oKYuUh%
JLfd-ytu<+qeYmˇ_CUVN`7 +6<žr[D\,-9n^$D8aw2\0[ԡz*--ZLTFh7@K`nQ@밆#^MnRD_yrwQvpѹჲpb?McCa7Mt;HsM8)4y%qi$!wb- +KgѢ_ \߹D!˔]
bjQ"#ΐSm2a+|;rudȼ]A>Q?ulR0Ԝn`uEss9
+Q37fQ;NLthp)
n)n6WZnE*:]yu}; ӕdqYJZ,=*SZ +`E;p8O +n2
;aN(FXg`ᡭXbmZbwk7ax-(ÅS[/|um*][Wm!n;2._=11i8cT]#w-ՇI~ݔ÷Q~^/CQJRz$-Hi[Ȥ"k?hR{W5qn UVi+~ + +whpC=C~ӷݿOVrbXݽ}
?9DaSt~;X43a{GOl +DžI3^+0pl)=4/$M'0Y:DHkuˉf$H˟^f<$j'ɲ'pe8l,XZd.T>S +}[Ϲ0qufۿdf7_/ʃs7_ٛ?ojõL{آ!TfEbѝ(xL(2f㴸˸$n +,L"C"zJЄ \7T[ʤd+
6vBoeW]!DW1oHwm%H2kdRq'CՎXZ[i;Fx#B]yoeNl3_cD9D/*m% +dބSy]-
uHJbcx[ w?*rYrBʟ:3̑nsS.eUdSik3G>fT9i\h?qsiC;+x#@E:PlXJKun|ƛy$8C(A]Z0Oׄmx{\F0TOEȓHfwaaJ@Cr`%@|R%RU#>Lo;yp"MfP"0=Xpn |9APr- +23A(LOř\'"Dӂ3 +|=g +gZh+6,QYޚYռ9>sǹ
%ґ?l mm]㽃)Lp轑ChM + SI8\Յ<%44.i+഻2>=ρo?Rݵ41?U58?!Yި&2<OYuf\1?d1JY^UwR14Q@R'qA S"ݏV(d@DUwe%EA[ԕ +Y
BƸ4*hUNvv4N [i^:N"hw@SQAK'!S;- '?dۅld +eLQ-2ux5n wS(1&y Kٵ'؎Ԁ$FaqR'O>qňLeLf9m#p\Kdve~e K¶#}0UOe$|xA* +LTU$</k> ֎&&4yr($bx(5M0Ɉprwݗ#-F6<_ +4֣VԻs9)߲zNWQFm;-:ϱ?3RONYϹ4wJ?Y1F7lֵ, F8J\oY}68j @)7TەTEਟ +ic c&N-pd<ttdpW# +*NX
F^ӎ^prfWY˕< + +`^71q9M(IDRΖ72 z\-o79 F4B!X9f6:3m,o(a)HS
oS-pC}78r!f.. k"'W
og.mm B#Y%OňXHpd1
L +/
-"UMgh=l1{u`3Mdh<1i*"h_wmw`{{*}'㜥4+q-POUMcdm)Gt841Mb /71ru/TTØ\Bb +c}[2>d?ѭLc̍w0SkY~x;(&68`d]@i U}q@3ŭOʨ4|nނ,xʀ1MO#iڸ&H-nqdq]$v0qTɣR;{#OR?WjJ'$q +4"Fj0Z̝eHӐKaDcJj{
eY[El]lB +yi/3Do:MpqyNjz7*<7%'DfWҹr""u:N#=չ`o6H{yvw#l}}29t48½eb)tsg(]vFy9Q +wx7c""Ş/N7vCyl.D=y#ȼoJW+3@xsvޝbt։tymf{<<AtK>nХb%uV7K$3)ގI^c_SroT#xCAa#4[So+8ո{|*&D +l +1 fEh5X1FHXPIX
X|5^ɓIr=t]F3}7Q0Yuoe%D>9tɆ/Ge#*BBu Vѫ!-W'0rǺ~,!^iTvtItu:+!H{D>GYƕn\/ iǩ'+z&;vʈ!5pO| ߩw{$${b[BPO;y,ϾY[FKfa5;-gLP>]yl?w"CuQ*v>ͫБGqE{'{: +豛ɗWi+vwf7V'˒Qv]j.gT쒑hL~^eY݄6:oCت(^@Úd7^_y +DLsL^:~"r|ws5mn%n!#\ +얍y09ġxJxI&0!Sl
<ཽAz#(k$2JS` L%NO;/< &*0yf0 +XOV:GKoe'o/^wDFFWfn +8ݱ ɉ)Z\ɹxf#~2`gWuSq!nH"74w`>`ő<`z*Po1գguΐ!?穋H#o1)g5k78'*%PbNHj2pWFpJO^6GA0SXb;VF +/ƺ7EN)(ꦑ6~,VE VSӢRfn~:}t0%GQDj[1"FQQb3Q]?h+&@zLYB +,%Mw'Ia$Q=uYsTB -ݓUg&TQLQLgyiP6Ǝ}]7iR^!FKBᦢ5Jd2Ɲ:qu#U + +Gbgy@h
<):o^i&망n( +"deA53cK^3C"xfB+DuŅ*MfHV@cX=dԥ
bkug(]ꓚVI`4f !m :Ez8ӿR@LM7P[y=A + D
TCVc}jxɠj)BН+e <*%2.Aܖnb;_G韬蓺VɕVv,]ߠwjڵm?cDtp4Gb@ +(<j"y/R;θ:Q4rS%f6tw"zSv[NC*FJ""9XvZ4OZYե洣ԏ8^/\NMe4cݲ +X
dp> .hۈ~$t$kUeGʀ<K^ԷxQ$d B(^먭ŞBע}*M694O +XΛ +u,_w<Yl +r顰^SÂSi1hȤ3[IE8}R3f& VNHeAa*iq98Nq68.p̺h3hIi0TfġE"Zgy/롵 !
:x>-/ߢZ {[;=qUZ*Qu/[etlmѾQZ7nv`܆EUv +d7GWc4zvM$A=Ne6o|5%8@%&737`pT=ҙO6+6pү7hh
6M\ɳl'k_o1S]Lt&:=tJ$+[TN5Gt9CPM-d<.3%ԃqփqL;-"mնoXlo#z9xKcZ$i@5ύخ촨fL*J9JHm.g/SCނ1Z;ʌEUs.GT`rt*e&9ic]c蘄]:7JY-'ZhjC=*[6$f|D(sWB.ͪܕaZӣ̑6ţUn&2 jȢ;zr;;/\CBN(PtBSn`ߖ/5q;Z4tD=e7" 2
AtS
履Ւ-<`+p6H4]a^]țbӊ. +8< +y-30m]}RO3;uN#4fS>X)pÉꮅm38mP
d\K6oѼЋaNt43ҌGS!xzFж^pݴt3zT9BET"C ":]È@:~$@":$:Jz^.8DׁCt +6~)/v:R)hM'o +|\oa\=y)t3!-m߈7p@y7E:BҔ={R?G|?Va$T1Oب*Ed\5!soD 4@.3b$nJ{Us8^}]U~9)-ר>M!:4iR#e6ݏiXeiig#[%.hb7Ϡ=[/abrQkG0^3Y'{: Ė:
virwb+VUcն$4M?ʼӽP짠<-1$[&mI1_VbFVcF2i@TkʸLŰA[#4S~^Ʀ5u<4O>NJE(پv +s.MCoELIənܶx`;(VLG+qv^BdIkQdX佗ak4Y|X;;iӍ4h|^3\ĺ&qeߩiQfn~:\l(neGQj~ZߊU!<ji _0Ϥ.آ_+TBsG߭5rMqymvx-&o7_k^mOj߿率n,-GB"KV#9+(k-6s +"'st'2r16T2!"}ؽHؿ7EUk醺c&C]JSfz)bM76P
;TĠزMVrr"d +LL$~IXb4Ow:մ zA0ödkE3ڭVZ0>n<VQ
ܾ{M7P[Q `5uf4ф֘+3ъ6F_BDbK-ɢUdV]|gRB9AmAk8Δʁh\Kעyjݶ<PmsZNmeҶsS
Ũ[!?7?Ad${r$ж,41?P܃'D$-zT$x׃%)A*&h*c@4Fhh_2ja41zY b^Q\jo1cq + +6@aae4έ,MGq=,(zCݺ&u3FBw$l +E{,6A2 xA @ɹkAv*«;-
dCBDn}'dхt_"1chFZ@*̓dZި\yřLb +---8
"d/]T̴t>PI(vu4O8oub){4W`Chr)8E h`hK}LZ*hE4i[4gcj#;DKeuk#i[Ni9sR ,i@`-~k9N.mm] +,0+\10WsZpyt{˵ jD$}4E}
o~}wwᗿynovw_^߿~ֿgx۷o^_:7_m]iFϻ/ٛw
n}qR0Y߾y|YڛgWqn^QпOw_./GEQ ɺQ<f +ɵ,;}WCg_;1>1FF>gg|p4smYn^۬Ù߉|@c5eD!'1ծ뀑V+Fky>٩* ~y#ogclJ)+J&H4 +f`E +ZT:УRS9@3%O,#/hF1CvU@uyPk%d +y
}@Z7p1W$DI!GaTI@SVPKLXsIшҏgPl>UBn>GRW9E&?Y#Њ +lJFIVE [V +81d:(
iDd;YA'1-\ÐP 2mEy"V$QғLdQWn9"(s800Y<')HQ5YۑLK +Bn +TىVlK+<bT㤙SQ-B3Ŕ5Ip^lF*b^^+]n%B2o-k5|nhQ!s陣KP2>nKvb@LjHE# +&4240=#%sM9I
$Q,*WNXYI*J
GFO%]POH(ߢTseѲWT<0ƬF&v +FB&?iRa}4J[1Ez. +W
aҏe + +/AxC!A]U8VwC!oOsᓗCj%\B[.|&HpxQ3t+d`X)dVǩ +4+GNIeΥ3I bDI `xV4HE=sJeg
%h>g8H\;nZne3ٙYL4tR9%\UصءIT|>T~>T*YIxYq;gn0+)["|=8:W4nDL_BQI>lC$;w$tд;#/%~ԥboG_0;hAr^9\o' +QWT &?H8 5`RoF9HlIev#YB\j'ADUÒCTG|z!VXV."=X>w
8Fi$RJ[JW|_poe-v8GO| ! +IBHd(zS0w'<ziGa\Ɗ,Ag&B31eE)7\@50jVfX\5^)t=nKOBՖ@ +IEmla˴Rvg*t-#dD|n1w81ge/$R`iqE8+e^2e[5ע>)~-~(i"^Yш*W#CJ'~L#?ϸKOՀhJu38IL/EQYɭ@E'R]3D" +!#.ZUzZi)Vy~TD܄tYxw &:GX~i+;$2d9&Ee'i! VAY)VQAEṑHTjŭQaW]2Ĭ
H +j/P݉2L8zt?PsiFOIqK&W'5!4ĥj(YQ( +XվUPdlV@Dw<%ߜbF=EQ30YUJY9A}7 +jO*ijI[9p>;2U%Dc&ƴ0'NˎcyB*¥$ @k[ _K@(w˚fIP},PǼ(gn$:PIc㘊O0gT@t;)-",$U 1=ȗN/m3x6i^dMf.,&bHdIЊQH<hDe(h;i!Xz<T$J%nR֒|% 4J _$'1';q +$uRƙv+q&EnP>ğ5\j"Gs`c~\|P$=Dz8N4jsM$P<spE +28zi5Q3CL*-B1e[c_aZFq"&+_w<H22U +z$9FF +ר^`q$sA"'P'd#z3&iprēwnThH0I$jNy[8º8^+Qޓm@R}(`|ֈOE
|id]RBR+:Q)2PL@(o$Tp0 +} +%EEWg8չƩA]xnY!gŐIYW)¨{D*$q F'wc2xL=h+)WXQ/Gf[4%XCJ損Pa^04yB +3ٙĊL$-8hRdwrzjsKl +`N_7y:x5u +YOlOEa)J2jB +PHEw21hH#u;(DMv,ٓ- ;IdgzW{8C@_al=0` eC<+ܟkR<0pg3"L2bgCh49r0GGu'x, +Oͪq.H(Erm +%NX=f_'s36O4h<n4 +umɞK㾾C+RقiEE KY)D_XB@j!O7j2Z?x}pqזCZxA\M)4T/\ kWڂucϹ+M2NQ.tWVrcbuJz4=եH2S62Fti>
0ng=ܞq) l{Fn^ +4 +2Ar+Pj#\͍i&YS~IY6e mCۨjk +'-L#!<؍IMMΪn0ǟ` cu +n-K#!!?FT
4ISiAKXZ^!TztAwJ6:7~:*GCb!1;8[]>mS*|)겍@
.(W <:; :զ3 +h8qML(=\2)@xYȫ3{!n ؿ? +mD=ߞ+#QZ)N,czA-\7t6lNB{7qesP)|ïMCcK-8>4
34Lh=^QHW,7)ӣ3?P8 +!FRd%
Hi&v
* r*Y6zELS "XG}v `ͿMA7R-̫{f4-?BEf/3~bpGhvcPS|]rߘd
2J9|J6 +m!Dr<
K9 +U!|jl_p$7G:j'Rdq!U~~־
Em;np A*&<8'cQiTr[LmG@^J).bf_yXz|+ǞaV'%Sf'\<]1)fv=%LVe%wb$T*돐Cʉ +5\+T-=V
&{6ϲs?X(露>4}<ZM7Jh]5y曲71RddYl=yEpw_L!;!N?P Gk6H~)H$(Ңa~=ЋorUO_7t~o}\vS)uIM +1"z{`3:i+|RSC$2Z֨yFZy
RuNYU@\da{@b4a$4}2*/I^2iN{8W
SOeWi2SWPL&%fZp-*ޝYNg]JQ4@D+^]'@*r2N7jtHyqSQmV˨ͥ{*ݨIιEVRJ'+֎
ahRqC +#Mr/5.iw~htaCƌKSASϔ~DKV{%@H"`75KK-Ǎa3] +NoU3髜 (<s5${ν}I+Ը_\k@CG߸ +nSVfNMԴG<qҘ<35\Ҏ]5ۖt0ɰB;/1'YV4%AN$ErrR:u`+R_.5luG&Bv.̯iUsh_jә!f?PzE2<vjɪ,uj-HRS +`t\_(vkj3Wf̍~1ŝ8~qm@?]'8910xiJX^Z8%q:J9;Źk)wBop{iR$.$DElcydЍF/@A)Wg:"y7ztK(*uoef6bV +Kucbb]Qh04B-z +TKh<,ŕ0ݶd1uz$K1;:MQWw7w4s_??/?C4~QCi^rA忬^2[5A+cK|f58}9j%B0^UYFHGQ`BYQpbfӹ"vc/,!tɰnE\L"I1=͊RߋGфP"f:ϵmCCV ]
1yleEY9\f+*n/fr,i^/@%V*|:4f~ǵF*>U|\WFU:wX#= +ψ5vW=JEאd;3]助Q2ztN+_`}{"}4*T*QГB~_d)봳4Fړ{f)
$yضi9ؿ`W@hMZ,[P B0@Z,a$|3<CqrT
J;vKև`NZuJ8Sl)~6? +8O^AV +
M{ +1A-w݄)IXeYe`(aE[ +^Ŵr!ly@j94pɠ>Y4st(?~N!$s +ku{aR9'tv5e +K'S>!1=@tPWfl;Mu8Z]oTXó.?@i d30P^6@(AG`Se +?:2˴elX,&6IGt&s۠qJr6:Z$Q6D0N{+XPOM#; + +@0*'7v
?bᒏZ +^qJ P{lnDX'a-
oz. +y䲕[$,@ 豨5d* A9.i!3~:Eguק+TB,UA2;èq߽(gjE>RZLk&}9EoB1ws44&䩡4"t}nU(as!Sf6CeU3< 3ޖ,8aEv~@3ڑ*BDzL%gƟS mA92@Eh1tzE-h 6= |^qD*bT`[BDˋM9 +]l뫜%[U>C8L8d +G8x^+g +
X{3Y=aYLRIN+v\)3 +MH^d_w +8bʋң9rK֚FHU[O]Adxހ?7L|' +JytwFWr-ԼgT9ǁ77MVDFO%a=WV8s{9DUpfB)R|N9E.6݊'5&%5*G*عJpm}jH +::S.1Y6C-.ncE0ڌ{`}6\hpYe +o}un`\YZHiQסostqRj{6aΟȡ1K
mFvʫRBP%D-Ά9 xg + &\ +OX@(X8bZgw@C!'AQ{`w+9qVr6%}L +u I)#Eq&GUӒJCxzC>s4fYHx{DdǒԱp\lwMgn0.PQV<S72=rj륯pTխQd:35k3;wPgRlx_lBGR:T<sO=4 +\i7RN)m
=.6ڡX#rՄ2hj*z.WƷbαsE#.NHVe_2E:몲D)RʦdUC-Lҋ!Zkq爵S/Bg/nYv#A*BIgZV + WP<Cpsj +.Z8zbgmNn1-)8Hwh4=]
S@ 6( +f8Gq*ܵ1q,*l|XV +7\%`7Ľ?#O MMV>żyH|+D3Sry,$GyЀ@@ceJ%
=5tn+C'P|oUa$4{,g0t _}8PHG
PbΛS$@Ǣ*_A%$I)rmD1ʶFe^;^F +AQe|(UXjno*I!CuԐEVAd\d[V%$M-<wiy/uO,(9 +ŗw*8e⡩5D:O1+z+U4x.GOmXHa#gs/m1@ei)QZǠbQ4:>V;C<36لdi$s̳8ȅ(ߧ5y- dnѽW"^m1$d,_zDD9Ƕ>4#XhD;uܦ$Ks9MGjToRn_Ds??O?ӿOO?OOOͭ0'Ϸ`<១6`V1g ܪn17Zr7ŭa~|f{1S=V!osT^!<!#
+I#*6(g}|щ|BȺb+[)>$;fqZvBIUsB0W0HQLslT Rg/?/zKԾ{#'A =fy_ݧo(?[.&ʧ}!
틐09 +a__$Z_
)Y=LٞG}okzJWGz)o#z>HBd|垦2oC?W-O˷<#Iʀ~6Ĵxm]<4O.)s6Wl[KG'xoξH}'Wݻp+Avos.~tqs6=|
s~wB~Xryw6gj˓rmŎ"0ۂ}
xf;'BOu>=hKPE:t{o/u1#D.;q2]N jendYG!,aq[m
L1}QRP.C̲|>-tyahJG8402~~'woy| DQ ++t/ksG55x/FQ6J7lq((Π4~8/moct: .?
/d`!_>+/Bnz1uNi[s!˰!Afy[
_~V۶ܱ[Y9nQ2L^ї!#W~G/ݮ5(}O%0"(߲oyX3Đ|C7!Dn4\m&ᔿ NJa
!X?YEŒf]"j7Fp;,=/u[{7.OG<[|0.Sy۶jpaEnZOnS +mҝZ<'Y yF~FIDpFżvp<})?m-)e 4fZjOmIm]SlScⴎne$FV厍+m
4uΧ6~g +(=NZK*#69ON/scu/scdiS^V遼>H,aQ{HlpEJ|0#Ƕ1R.^8|)YnIak4OgA*&-yY v'v>T7{Aέ.,̂r=*f
*Qxdw'`V@ Gƿ棞Km{V.d9+`b|:g2=%Ǜ/BmVC.L{w>zYQ0wŲlg^\ko>"ۇVx:?Qo +c؋1.qZ{Ɓ6C&im\#f>o2/r/ +1mu0~[2Xtч-TbW~ǖ;meq3d-yɟ?R.TIb%ݦgY+
Du}ڻ=5S$Vk&zʴ=]=htoGX,2_Oωg*QY<yoY{K@g Ԝ"=d_l +:QF-,=-Ǜ=0SY_Foo6 +g7lQڷ]\]bi_4QMai ,}z&GU?{zǓ3ȧcsR3|֯x-|:^jimJBwҞAbr#rzjxo֭9s<^QsdĴ=\UV.M}SpuL!\}NV=eE=H]o/W
uI\mׁO3רn;ik*6sr n3רlhכ+w"x:rU ++8FN*]+
J>JT=oWWGjCUw.iWb Neۻf4>we8&]8MH,W?e9?e5iQ쀄˛+ TtU~1{`_y1h>q žËm^="㵔7Wb/UCl4,U39\UiT9\}w%Ai:yȵtܭy{k4Frկ*`r0nW, +v| p5!)ެ_(cթm8iƑOW!)Zcێ"Ηe+ۈʲInåi6V$JyWQ\aTBv|WL2'(k#_{ۆӒNEMaU!<`zZÈtz}:ӻ{2N?RC&u^;:#Ӎz ~{֙s>3fX@t:TIo۾B:)"M<Luг y^[y,ug)V=\AX KJ=ض#>ڛ
i3=oR^ádlz2}4=m:m)sQt;.(^^x~ۈ9ú9)ܛkԏnP0IZGn'6Ed6NқRnflV|->Ufa$.kOe\@ -G/϶-۾/qa։/`<:NBd,#'xrv+R$SwMf[l{lX ;ŶyMeU32l->L2 @1I)ļTn'L#-@n'LP`敍y8acNB&pGSj+Lo|`N8 O4 %ˉd:e9r%I W٥W{s1.
wTbP4^xذe9 +G78oKBt^ +=U YD[".s^Z)& 4rS0(50x&6T0)o6JlL('F0% ﺝ0mcu\
}ZRUn(Pb! ezү
oڑN+Ey(4+2yxb~E2I3.ٺfSs.3SuVggOOLɟ!cqeC\R)paĔVMo +$ϮgH( ?ζD:oLF@ΗñIb+d64M
/ME+P2
RAV49_\2P6ΧexT7CgDɥsiѦ +z>&jkҷϥY}^A +lWKl3K)᩼yXAA
a[Wvݎ]fTɱS +6S/R¿cT ?0hpj4 <LǴrL&>M͂//>3ı, +9< +v3}hw!\y;y:RpiwG-FQ$>AiO;f|8@iy +6QdDZ$]w']Z + ݟ&xv~ q(/jESSyQrlx}7.?."R7Z}EsD}iI҆@`?w7ට93t{GMmO?qJ=<lJ<ۘ*9$:f0uYN2O+Hu\^~Ʀ +=$ m'< +} כ=0}}?)fҫzsbSWOOǿ/?/l(oc˿9$B^}Y+^_:k[Vԕ#xȶG?{9~Z_I0udKr\
QM%vљ[2S1 +1xeV?à4E6S9
pMgi·VPzո}*Q~{5~]>nj㯚E7Zy_(gYj۪f+}T$J,cۏoޛ
<fan[jH#[מ(m=~绷8JF2P\7(~!stЃioQAja./cMcxuMzxAJ=qօ(-+<(jKrȐn;HZ.ERБzہL2i +TR͎%^=M]oӷjo U.7e樟ooTŀYl_pܺ6o&n@nﶞBr[O6аsCZicWm6~UDF6[=BhZ=՚^vTXH-g٦ZG.-u3
,Y=TDht7|V%73sdKY[|7+Wnqq +-j~0>f{ːYe=O2U5[ɲu͒l˹n'XdU1a2C/Tebʝ0@N|l+14(=djzui%,`}v{߬EEeU']:#ܼBb4WE( k#"#Miu)vBC/.F/OeP()#pcqAWX.@)%C!&.,4@X@֗-BZs`xi/-c%gemLlY2-i\Klr/*y^f(zx]lvbwU,Р8C+xcj∐J۪ +uh&5ET!KT,~_O\.m51ށAhcmtiH/z;;QKcۆN^}@Q/x7
vX˵)̄GT&Md/6-O'}l0n+jmT5(l>=mԗrA z8 Qu58IuP]hI5_&Hk28tFw[ roH*1;Lo~Gcin#SFdRK{352)_2Ůuys.b۱h@2*%џhE R yTߨ>i:@n!͆d7!Tr+ng! /C!1~&okj>]Igz8
Yѐ,H0bϗO?Yv?Âm|4Rz=}ˋGOKZ%6={d,_~(K Ӟ/ӕCǻʷ$H/?x4r4inx켎N[r_%,O|,Lto
wNQN 4,s/xѯq((S6))#oޗ
Zownb@6WgDyXp=/7e[S`ӭ8c!LjJ +7MA'K( ۉ3_1
h_2) :Y%,iC:Oq7D/G]_Nb=@
$k5sכ"D"ߌ\EK /&46v'[xfc䑊chsi2JLI:5e99K)`9ˁSvOj3yӰ1Cx\DB( <ޗ`zb^<ʄ +LH +V4C}H~s +TbOd[3FՊ+=DRי+u3Ϝ[)w@3x8JhL-hZG(uA) x%f }ZQKѹa7
Cjb.uv4v[XVb:R
X| 8Eu˅WS͜7Mm:B(:7RfYYWG:gxsc¢Y@vM}*0^Fo +#܄zMъYV!:{ +y\Nz{qQ7*=j7*]p?}Ă>K\j*h@>7,-k +.E[Afx^nFVQ<C"ּuH3뮟/V/'Aպk oP+E(xx@YjcIqEj +sٛۗ^$clvN#\̍xee^KGfsVP1ĞNvj"&X:8=&4T!대WBD_V + N(3\C@Sy.J6\Z\{
I=D+Vc#7`
6g\Ո*+~#}E&tDIKRE(kM帿ܔqɊ9c<ZCE4{\K=qC_2'=wwf撾y)^9\*zh5{v:U'`c{EHGӇmlN\b۩@k8=t석SԨnBomB%euZS
e-1<@aK`H%dD$՝ev`]|3,``;w+(*s{- epV
*{D`KL634`έ}@0,eicYmmY.":3w97<ˆL8r}22x9jN5-HNgvoV{# `%@Ǽ^V{ 9I+A|u˚BGNX/QiM}Lz
sЀ"9{bM` +B&{ዠo"9zɒ}!H1oq1OcF]
D_K}6\$RgCC-a*>Ih86|ƟcyϲR6icTPqtaijI#zkVRsa=?Cʤ%8L\{47T6^qj}h +BZB636tB(qj_JϑG/B'e~5~+եL%NglfHtng[̙6h>8ה)3a'ё0#z!'Luѷ)';f[4 uTʪL +&E"1@Ys'8˹B䮴jk_]%W_6J
IRZ,uB&9V5Wp9K[[[ZFV +3
zOPB<k2]݁Ɍ\ 3f"y>pc'Oj%`E6!h2&Q&ufv"b;ח]*\HhEi6 +%MGZ`v +bFy~{p:И7tC#+,<h2W!
5*,0K=1qQF$ K>o^LjQl"
Z+PG;6{ +^C19+lIoy +4FDbe =;~[pp5WaBqrd
(0]e/~1vm^젅@'U G8f +bP~lhґ Z#;[͊pm/,:%f'4h5H͋G,NC:l؇(5ۙFh +Bj hP3N +dM#/P\p7DHqF+4|
gJyk52=c +{n=qXF$_q[V(Ʀ@e}CUz =PitSAfɴ]MzT4=ۻvMM|)i`XXIc9!r;Iٱ\RfW +"+MM +Hm')5mZ͇9LېMǖ"8t8Ϥ ]V+NW F.-es" +=RV%
5#uAj|@%Rk.#:ǣG5r1RR6T>t6qL5?P`[SwҔ~PͰ-]%xLQ.-]UBҘq*SEI& +q +"p Sh[9@l45/d1i7Bjx(Xi^Q7J@=PGw+ԃS?{E'B㡕͉sc{G-hE2;9ʹL@įF;U;[ !'F.@Ls%Rζm&3yox&T5-V/p#ly +m +Bn/\qgXSpҁ܄[4{|?M0"q^F 抌8{^ +ld#p'>ܶC-S "=ܨ5yhaJ/ҕEbd>A5{tC.rudp뛨:Q. +3@ pa3a.@${.0N
{W6Q:4{B8Gyd&. +-g{BHkKi&-Ez\%@O9GӤ}5> +{IPoXC_`Ά\k+5-PZgK + \PV9(rt +%X*Gp(,%Ue?H[z㛓٫R߇f4zQBK['b9E``HT1oAĵU+^#Ek]1+V;+ͤr @4m;a")+2MK0p
% +#D5%) QdTdgD֔AW(-fFS~U/AB!P+!b{=t2IC"3MyocC!)_HMÔY:vbQeኆ5e`'z*XR]ѫz9rsF٣%FB,5ri
-|0J֑}hjRA+Q +fsii1v)\
'ÿ^KSD'g,pD%CP A:"DRo`g{QuJzj*9]05ԃ
i.2"|\ +x=1F_Cv<OL@Eo(|0P_Նu T/D#/""jz]%cZ $eI/}h`+ohēS +1m +2Oz@b}K G>cRhd-gT'jm$+9/SH_W9T +2 /`0@xTJxgTH/ϠLC4}V0 $$kX0\,ZXWk%I!@LwEHP\η>LK٫uv5x|B05Nޏ[4`BDIL9^0t +?dd4d|n:DtN߱q-GZN)HTk) W04JU*fZMֱ8sVu5O5-Dzᣌ(vP9)T6Tx.'/-/ +zWi+&## mQ2 S8=b8m3@r^ŝ]=䒣$:Q@^014~]ԃDil]%'U`0I,#;uG<Yb1rGVЦK`gðmI|8@yi^釸F+[YjI4uCM
V9%SVIF{5da U@=W=4Rr]jgX`xQ)8
oy[=$Ezy#|XGO*b:y&.X,V:ځ&54H~T2<b)wП0v4.ΜCJբ%hjGk<8,K+TFP۽&'YfL\|PmӠJRsUd5CЋ D=}WnS+m +sG2F/N}{R[k=0rƕ,'䏽fʆr+5ʬyZV=dP5MV+ tȧ:hY&}WO`jh3ﭜϘfDAQ/5%Gu +4`ӝq^r"pO)w ع&3`fuCZt7k>=r%w| +>
ԡ3˭l7I|m +JT ) )F^dEIRU=b9_EJ#C1#EzөqB(Hs9ԢMΔ(%7nzF+qbr6bMλ0 !܊@ɍT:/M +ra5qQ9!J)7ԫHwSe{\g*gIᠺK`f(MH~KT*
e~ ıFӔ@ !Bg^& +ux5x EZ xY#4!A,#
|"u5n#7JAU}r)^[xbU=13e +OJCDA
BG8Z;0J(N5䝖9b-'n;Jj#
^Fw"ʾ^%SsX)HW=lTS[D9Pt2 D2Ca"јiK&Fg`])qLKVa!%u l){$9.`1b sߥPx!(=ٗ#*`זej1&0Շ"PA:ɘOjgTϱ%BSEےװc#PΌSdlb|?eV&NƣC̽S#(F7YyQ3be={(0JmQ9+_\,UkZptw2l#NNXPcՄ72.N +>@1UXfa9a(@>p w^oD&bp2ɋ.qc&ˢkZ)INV#MuU7(XÅ#NтH~D{Rr2֠ghjկd(q<!P] +8!7{
4|>P1@N1:m6U[2=닿c)s!ș(rw +4yeMrj5@qhcP/Pj(!C.SI*\d 4oe.PسW,݆ɘ%V=epnWg"2+p9j\eD!i5ӞVP#z JT-5vE4Lft{V,cnwE*6l9#SSh9$VXC\b) ۪`Y[/Yu 9YA\nb'jI h/i/6D@iJC@%Z=VĉæK?{CWC`
+xطh^wCe
[=
+ɏW)YMDRyD$Ujsn$y.f߱Y/f&`xq-dӫ|sM`\ u
+P{'<T])&mg?ҭ8r +L"ќ
mاEm=NFI +w(!Mj챙}ǜc-~SX@ܯrN9f;ЃPF/[vHlzmj܉`v<´B'Tdz s{b/K~'qwsT!]OL~2#eȓ v"Kb^LOF(wFH-Tԋ<$FĽĴ\Pn)e}SjX2|()F'`2B{ /&O*k\6cͻ-Rpa+6K*lIQ\"2BSV^bi(aϳ% +M\V)!d!B'h|ԍ(B +,MQִ*R4!M,K x
!rH<$@ H Li"S4zc9);{ړ]\0?])%{}ZIa9w@j잤"v*
Xl[B}!֦^uGT77hޙ%ǜ2n QW7)7Ȏ,5mpa\~EOxzOM1gfFL]b$d`ثN[|[3LA5Pkcdh SDʂ6L]PGprZu"9@^M
A!Wuu1EPI7" H<IL4/kU!Dyк +e_iZ0{ +;kAje_) 'E\3P3q7BzJo4K[k)hk3$':oIQ(!r\!:!L[1^x\1@ +M!"(cT|˱H{#K=?Cz~I%Yyf+K<lyC< Z쁎M+oLjFSWU~!+].TrZaՇJ79+A-Rryf4?!R)S:c'"fllxmBb!بÉe4("rRt.D1rG}$?_uYgKH:sSIpfBV҄5[e!d^L1`6^J̛,n->.=X=e10&Rg>b--`|;`>40VIA(KHݣ+1iڥWW<!ٚgNcȋh\ţG~z¢ +;lbZ[x^<r&;%?QiQ+
&4cCdQsDe%FxB"zg,2.<<,͌*l%"xJM@0AxUmU-F_u'zh=ٜV+_IʵKyUu;Y*K +ȂᓜХ]6GHA'.3"Z~7S\fKABY\DZ.Wpcuxv/?J&(~BdxBaBv,QoKДSyrj aޫR߱pY)kI̤Y6}TFxb!*2ة-%/#%0"\ +NolIS^ǙSbU)K6ms$Lz3u4-;e`Y% ڴA5MbYkࢇJ
Q$ܝgHC - +Fcs)eU*UoUF+z9I?tz0dl&ti;H#ּ9J X#xRQsLA/XݭX]WFm2rEGp?^f,4sK@O'u2\дpT/][}7,'??_|n~Wwo~O~ywG%_5chH1v +υ2)?((7!
<'rɴr+,Pa.e.;.>y ++Ib˩;B}Jh;O/i~eRBdrR8cHA=X$4Y\L>("D7y+.mT>,Hނ<$#j<ItVDRB2!Hs}Za)0 +i5KWfFq>u{"eɷp`a%ƜAOD7=X&n$O.qb{D
iRoRl6Cկ@YU.`Qd6`{e""R>AiWsXAm2^D!9jrZ@&QHx|`7%_Jx<X4GIq"=[tID$phÌ-xC7eI +hFux(cŻ,ыqyh +.GQSC +ѫ!dPPfD2ӍWUЈw[H8K{hzH#0'V%s_DnQ|?$D *#U..yr(mȺּqmvU~WMfd)~u[%ggK +Tct9\\"$H"=lRW|9iŃS]
lB*.=R|>U=h\<pᤗLxt!\>GF$ H/$b51h3Ov~?`Co9K7-,K"j5w])r# +d] n*T_:Je)CI[uŅeZ5KZ"@R]ZVw{N +v9ٛ+?M)dD_uO瑒90{q8v_m#DdӤf"qdTPv\ud4|*b/K=O769X7+!m)M0%)$Ԭ~xuٮPOz ~c"8h; Y%l&f;G6lI{~hb
iyCuKۖ"`{u
c:6|*PK!tʧ
!2O&tUwa7Cߣn
&QUa*3w$W߯XlmA +i;=&GaށRµ%܊%ރfGjǯ5} Xa@vuS +ᐰ1PWy-_C}7t`TdҘn6#-#*2^Z=qlȿi3砺L!OfA +͍M-8ŐKƍ)I;JΑ}\.=]տE~+4PKUWmpqKAJD'Cu86
^J'Vq}bH0RVXGJy +{L-,;B/;@=W3^RMKy1Mצ*
ۏgGoT/9lFNRE]RWW +yu^覙 .p0ir0dUzQ`(lY; 1x#p63Y4]0ʤnId̃+23ҟ)*e~j6꯳k)?s3Ik2ih{5~'Z;F#Ě:/ɭ2ЇkA +~)>Ct<8
jΰ(H*ֶv͎vP$G.d~UWKc +|? +oHDiQ`)Y_awxupKTff>">B58Ԝ.ݗFcQ>`EjR; B +)ͬ`MA5}8=
[/4`C*)_K)ч +E1_
:Pv:k;W/$M;~ZF"E
K嶄5;vaCTn5WXA]
m$hv 95B@\"pm{ldEA{0-%C6GwĞՕ؏:\-Q!P~`x6<QRaD6AηgZ$x0Ym]>p :X2X+y5*Uռb9IcR
B&'QtbTj{PQi0 CH]I+Ps= ˫!VYby˓{\7Ce )!9P?ڟ&ܾv4&i.&'aY5*,o2cBC,ޛ_ғ<Òk\хԀّV +5W⒢}E;Sr,1efY^P-cO +=S9ST="~K=ĢH稙OH0וnվSww*)Aݡ7߉T Aۉj@)x\mY7zPm#G8W!֓Qb^]>L]r~ʕP M܄eSU=CkC{oT\J%U(d!aIn;W/9(W{K<CnIծl1"Sڥ1ΰ/+g銒 AvڼdyUVR"ewݓy5ґh@HAtH*a
rՓ74CױR :
#^AaFQ RyzXûb +<R8$z ELBFRqԞrB|B牔XlW,f1A+VӦu?[<>h;x!# #\W(C0Kþ#sYSp@hc"/P۪8.;)&W\CE(l%9 +*sEKV3Q).I/i + +̻t}7 +8A`b0G`K/R1)w\Sy>K +bwd~k[ PqVyAt1$DHR}P +XͣJFe<wY< kgT_Gb{|L_"ֺTFJykLU̔痱# +ʉ.1&t 'Lɴ-Ety/$Y@`k?R=T~( ,TmѪʯF{Ԭk&5T~+*$upqX+|G-CY"G3w/_d!/PɄr9m'2G +B)1aj^($ !@*%(OGWFL[RM-;=JTD2zh,|y +/P+Hx!v`^cAЀ%P[#Ģg
36:S9Fpa7Xo' +lO͌(e +Ri/}R fA噀.sDX0(&uґwMhb,F̻yFs4A:ɥ1GJC6{2ʜ!Ez +^t|v%ugK*k8#s
tt] +Rl䰊
_CV8Cᤴ:h%qu?__0CQZ$MwV?љւE{їn@ޥyb݈.rDPc3mwܢfT>A)dk0/G{29Wܷ&n= +ZhT"ہLOszqe_Y/ *ɰ-GPQwݾ;9HĪ7X9DjՔ훹2̓¨I\sl<.Y,w"{ΗS +ؿ\/r]XROjd:4*pxu}TQϢ@א\-G^bCXBô8L) ;(,\5вw!`o^i,u(eB+ZԂ'"Ԙd n g$̂ȄDP;17-RJ +xmsG"IC%v7-Edဧd$ʟO|eH%(g9";A}$f;dogDo"{ߐ$ +T
Lq?DG६(#%}î_UF=jo?K#kELlL@>T@# +1{Ű{I|ҫ1͆UңJ0jΣ +)A&Uv4;+I2\a9N|q
I@de]y]VnWI
' ^쾉gtfDZ)Vċ!Lwzln +[gѐT QAf@I(E_+,9=q3K87zU_C1א%9iʁX+/2뼔'cO]xŘM,*)tqV +ȝYRl4w4%ySG%uz+߁҆W'1q>n\Qaر'E8◙,G2=x5C"`_qeD~IK"fm)'5}n'5kl#e^tˏJ0ebŧG<.F8(]!3u<yF
Ug.FN +@V빃.<yGgD@N-h;pv"GAucf6/IQVqC$7ZcYEw$,c4]>+H({T;f$[Pi/]&$UeU-#cJ'v@T2XPviQ4z&=B} ʋQM-Il?$A.C{T>85^ +!]Ԁ!Kn1%>?^.O4ChcƙtOaYÁɦ6et,S.%Bd剅ap~$,ݻdf"aGIP"w-< +Vk/MKBN@"s:T}ܕLS-faG~IGl8д$yi~~r +ݓVN6c{om~اzXf?dT&/R.b_%~5EZ`FSUbp;'q(uɲ,~TTo2MA-$DrX{,\Ҝ8-9}%lCfڙ%}z8:OboG$2 +ET^W?юs]g'P~0qoaGꦀlRA3`IV|Fp,N<J!HshNDjͰRWj:cJEyHt$Ԋ"?ïMe~3LbPM{ +E]<5R幈ZSQՖwJXsИe|[cI(XtJ5'CAfPP[l,3a,C +]+nah2A8t$[en2BK%.kg(PIӁn*_xEұW )?uɶCZdJu|^(8fQwGO%bR 2Qe'4)Chv,ě/C( C
mbp @"~J%EMHctKTxhxN`dC +Hpٽ QW`Kع~¥ƾj!=fZDKOLY88ntqZ9m-knj^%L_vH)Rv$~_ʗ ׀ޖT@KL_ho/<%XE>f埭[M)b]LnC@7"AdžbqXjv|['6i1"qvQK+2˦ +BV +4 ++f]uײc ,"tPYM]Yʬn@y_:ph{]1J`cC!ֽ+J5ʒ{62@+`F;x@ۙ+6"!U@eԲdw +.;m^K:VJev|8{iA=_̣jxD,j)[KeIG67L-L3nޑBr`&SRW>`bnb'ThZq:cJ])(-PtIqn_Ot:DX)pMLF83iVj(oj}"qBL"6(+)V.aDW(h^TIBJډO~bcng#ܔrhT~]j|A({ڭ
#Nb +=G}:bEWa"n#2+5x!Kxfs?r9#:ň*Bʴ~0Ywlazx d<f[k9YV!F 8nq:@BogwT/ovsAKʣ*mH& T>/FR|0bH{:Wߵry9S"s%/:()uK*dtg-W&=W`}̭r ܦҥ|
8J( ̏$R +$u,u^]yB}jB(b!Tn[}
xBz5zX.ϸ* +CNőudY~"A[]iGدs:Dwأ&[eͶ@|=>h<;8^W0] +wGJ?uBqʛ~pR +2ʖ?O+h\IiΩ|D4q
ĝz En. Fˢq8X7y_Q<s<rK +9xn]snO~lܘ+]d:ţ 77O4tg[Sb:IB
*h쯓|er>x^xb+U9+=#-uij^ +NPO5Ϗw'xyibYqiTUNSW
[6^> k|ncw^A=;~f<8Wk'1{`jqĸrN %Kgh1C +ୌ6W˵ +HٯUO*P\A1&12\3H=X8i+x)ΉFE[#*M&'8sŁ3h( _7`+3L76s>s\?)zXqa~W]^}bm6_m{%q8CWϛw(;_w!T4kFعz7tsyi U||^_1c8|e79yL%zs#'qS4"2v(_ +ԝ<'ڮ◰;F8"xHD#hl2cѲohDcobfSkzij'#P_~- j)Y{Uc_?.95]9pz/aM4fј>l'YZNWl n9TpǷ
&kjmB9mnbcl7;UF9|PhuA#)ϙJ?gs=ƹvD}'%WnV$[h9.7K73iO+4W+rNGNHŧ%z\ֽ\g >}DJ!oaBܾeEh^ y)϶ʟKcȠdg}|%ϔZ(4휠?Dd<Y=3/me51Op]P};oq 0@[|nأ}]<kp8G39s𠰗eVB-'0X9fhJ'J[+Wp~qLjB#:o d9m[Gs'H:殤#ļ6!w'xbCqEn + XWK(m4%'x6tQ>PN^s;>2N$˒["ª +p=k}+ɸ|\@zyظgA)9a;)ۻ:2=zyDL]2,vO(R;A:Cɷ6X4^H_2|</.<`dxpe%,acwXi<cEU柟/=L*3֜=Sz<ˆ372 +"w*iiVk.0x^iyȭ<J!=)|J6uySWɇ"c^7>-?+<IW ֿ udew]q[/йfc;1o}v6%>69dN(<V{'n7{?gsԅGE˷<$fGg3}Ŵg@9끷wqs20%{w ެZb0 +E!jYES̩2}|F<kԩ>vL4=b
:Hrn<]½+u/ZS0YFZDQ$i8MMjq_Q}QO3SO ߍQB30\R n(23W9\!ٕ-͑''Fz]}9^տSA6'"#|2EĉM Oj'gE*S/AK$ s-u]gA3O\IchHzWc@X,,u?_1\jL +I뙪{}q%ڕtu2 d}ؕ29ۙE=\{բmmfX=AC_jG^W~r'/F=MOLyb1Ȭ+skM*CaO姜p9}(g5WR͚+Y65nq"QQ x61Ո|JN=* +}y·8A
+
P܋EΠo=_ש-@ +ٶg˙ IS,9U;(OkYN<kNLa>)2w,lA9ܘ;1NPj\s'fAcs̼HuhM:9oL)A=._g}sIm3K$lLSBi<2=|&~>|6TI_˾9U3D3xwBA8T!c8HrYgx'ȒCwhl9fkJn+?8T!dtG'xmLJ߄un5+$I.! JN|sإ;'י<",d*<Vc~PsS5m,o_[y,0knN_g>?nwוdW䥮@IY=SW*~V3U*V ^~2E.'*]#V+N0F6gV&g Ȱd]\tN%#2ΚJ~I +/bH렽ƻhHo
'ȓH}}MۜąF`rRdJ4bCWT8n|YpvE/p, GCu͚[Yя9Uצy]OQPߣ,_29I9=iNkNpber"^'Z/vئ Q, +\g'H(t'yo +/z_ +A{LXQ'xr^ +~Wf*Oz@fߧ +O IH_7pZpZh)G_JW7NЫfd~:8;X;L4d2+ +LK^_VPL;XU$Fը)bս4M=8D|XK*vfP<x6 +?`2N2a',\+PNK#2Վ>(
J&0cUQdkZDb*fPPGG |jQTi5UL#`HhBBx*2x_}ʄ#U23o /w"/f&%sՍYCxS58F(Xg*KU6(lõTTW. n6|@M2vBTLQ4zv +TW9a&bh( +3&S/㭎Û75$DfVi FJeB蘚è5jd-JnQFFRME]Z +ex U9
J +h'PUɍLj,rtu5Hkh F
5h5D苾T%-S3S5k`5ve*mIlR9Iwv3hоfq/]T`WyMՁpLP_k*bGa]8AZ6iz&Qo&̔ %UFPGIQo ʽp/!/S5LT(' e*2T2S~Ѓ$Tc\Bi +EhJ! +,[+z.*k[Ruؾ-̭>T:a+YpHd +F}K-? +jjbҗo~0R\H +)@ß;Дǀfqqje :T[̠Q8}@]P[MkMRoi nXP*ƏNDk
+܁N-y{+5ybۯn4Eɮ +<";U.'UE6Uvڤ9鏵܄,Z5xZLфXPXRGj*U,Uo&$jND|CaCDn\PPal"f5RO}Q2SUVH':kՇbu }\cm]5 +%!j%mi ͌LjP<PL,Kg uPMjT-ƍ`˄z
&}c4z9꼘܉!VB0RF&Ss7Ƶ?74IxXJ<l/j26XR"¥S"eBm('.eKn-*pjŋG'䙊S],~`M(@?Y'6T +ňL-U-r#%4@1RM +:e_+/~T*g,%U'Z4+ +M'4X ӼX![*-TD1l N\UR88LMpp.So[v%/uT& u R)A?BdXpZ^F͚o0!3Ds)N!@+͛Gm
kWZ:{W:yyQp{"@on?/)D@_o9-613u^TD^TΨ +Cc±K@+[GJGg+&q_0aYcؐQxLb +'2q_PY#)Ǡ_7n(aMvZ%eA9!Y$V,m$c]A{jY*nb%9r%ezihLѿ7YҒ57I,mS
$R+˷Z3Zr~]ZeFbùkQy73,$8ڇgsGЁ(rGˊԲRjo@mXe!T!9ϡ`ܴlluHWtϹ
bA#mc +A榳irЂ\td>Q)̇E<̇e=Cn!/Nf="1Ssfl #>&w\C#١Cў9ĺ|JpoyIJa>4k,=$W8 +G_]Uu&u/$)$3SIL`p9\\d>~;L#2#A oOIΆ"NŇN3,ds.C3^1C( \B4.n2KmÅgRG2ICA6lJtS7TtLD6(g48ԆGsrFπ9a<'S^a||t:(m$2
"y#<uXgO]:a28ĒqZȵk7>XǚՖ3h.uckmHl$Vr~r61p +AkޡB)fnZ:c +|p.0m7s;dIow6c65.=kR%|\l
ZS _{yGfb2qS$ҫljLs<)D}S~IC)Ϙت)l|T6h]8
JF +E<_lw̤Ӑ}Gp
pMN]N9ӆ>éȢqZ\ropЩ; Lm& +,۾Y#[nV5_r}lꯋ)؏+2WLyt@2yX:c{&ml*B:}]9.@ER_Dd;ZRzjɃ1_0kXk`R!'S"1 +X_dc0yc{V`>D4{_)j{& +N,b맂|bܨ:p5!'٭ Gxv`~
|LȾ0ȾƘ2<]48,pCCg<oa{]DdCXSl]7l-35&rM9ÄeϠa;gC::q{)hgH!{G9"WIe~ڂ dž}J#K!lցSXc=$t@H'g6(kq® +k#ԟDؔʇ l3E1g#^<Zd}=Ekn]pt)5A=裢sFT'Vǫ)7!Q!y#51鉮] ~!k +qK[O#NAQHv"#yn#c,Z atC:;a\`h +ߨǃ[=)jMb>Cx yH@-:Xe,멣illyLk!7G .p_%!Y ̧V`3[璥eĶ'+_ Μx[KEPE
`' +4F]*=)Ej>K_0I0>F6B:슱| 7U|)wO`s"<O}Ӈ3`UlE!?p:&atP1oHˀw',2I'frGmC9hn1c@MKp(}u|TΨ\lƞy@/႓Fcc'#8Lr4EOľ[Q@,9-Α +THH
KttRt̟wcK6=&0_`'Ɇ˩Tb4Ld$举2yc:u*8o4l^9)z;+ooҋ&>sK£Kii$Z-/a$ox53|v&wuTX8 +|z`l|X/$H߂L8<%bĄmj55Wt%ws d81[2(=O~=HIYGN"w@;$qNG!?9Ħ8$\ph>+|by> {4x%N~p +JAl)?O~9'=Jyk@)|o9>p!UBe~s > +fy
\|蚩\%L\`(٠D@ |tdr +wDE}*2"ͧ +PY +]yr<dDx
ރ`yC21e:SO7{z2B2vT8Z=b6:V]?+$A(z zA|Q/0'$p|%ʔ1 d-uV18bvH +^&f2̏*ހK;>@Α2r<9r:Ta*(8}G|02G5=xG|+|uڷ*Ύww_n
>{/j*=| +:j^G^1fZ3}U: 0q<)T!.Dpn#B +y㍯ĶD@H2t,yz`:|^\RtS1U~l Oe +醻+54v UxZJ?(>42O,cVT\1_-֤l*Ϡn:y
p#Kت٦udErGbq$vo'ax7+`-gPaa5/8p@H*jϛMW#<hzBKYdߑ缣( Ny
+.;2Av9:o1!iQB|%䊁|E/ibJ5-8!Ey +J ל#]Yد~e&v2pEAmh;\dx6ukanmݫn8]N|,8fslkIn= +`v3p.2YL웏 +\R+G<fq>%+p +|=[fL9FkocgĊ
w&jn +u~4*G|K6>3VxVW@.OP:q*UxސekAax
ұ!7pE叧c&A^ +]p@5egK&Dn<Qۤ/1/W}lхSTb3w㲎.K.c +8,x<10X/OyJɪKf<6"L+_%'q;glg8'dψ[ߑgd8n:+q~6RQEgDUDEc UrD2Q4uf!{>ƴfC'>/[Qi3Z{Yp +oWLZF}I:= +N.,Xm@Oײ' +3Wq<}/}E +y[c_b>nP=VpW^n{J뗒߈tzY*߮F";Go{aBc.jTyuk:o>_>e>?Ϣ|~E..p3I{OOsy[i/Ͻwy~R\Yw6yEO_ǏNV*?,v];G:w=b~b=
otu\Hcrs/_n>DxNjpÏx<[W_dm]ihX"sa>LUU|Q`dpr'#mn͍SY~g7(LsGc<sk%>YR1FhcO?r=w;Xy'#[}RqM{^xÝ}͝x~Aq͊c)dsO;ww3_])q~ɮ:חޝd?Ieov??{۽\lӽ=A몃Ww5Ty<i/p&YmP8kN4>_F`Noo
l<tOŏv.8<=_jbӋS/~`:#W~rwM֎7JHY3;^OumXk`}=ˊ=vg)@=fAϺ=W"Ru#d2ۏϗ ++zPeF|qeۮ'4VHn^)s{7{ߓ\9CMDouO!dg ⅛V+/.X"?+|ґt5o;Ǐ3Zƿȡ_lk,^'ԝ'A_]WaN_(g:u-m̶-{7ZwqkU;I9Iu=uίW?)~z|~$';}_ls߮w 1W]=gv#¶oxjNjL;DX{y?VK-U_Zl]z}GfS.N(_ew~YUWr?>~n]CGBU&t@WK6?̨(~XWSNl z߲Y9׳rd>X M2yu='/^w}wXl>ӿLY߳gGWŽ,O7wMq-.Zn>ۻ֢rثOVl9Mu7un\K%&pzяYnOW;>;Qp]PwȮҪ{eKގ/?]!W:".މ,Vp^XMwËn܈*>z3fBY*G͕(`?tf;:Qx_~6'zkCՆ:edI5J27_oSϓϷoe)({{z2?m+K;rf4mw|y{%JW]^\RYp/`GtEVx < SQ%GкkPVByԻ9-n2E7]rGor.ݸdJk%.?yUwn9w+l)9w6?RʜOs&?NeĠF'{^A|ݻk%/_)._Uy?q[zAۋ5IݹǗ.ρ%ߣy:w+쵸Sb܈-\lgq;Uy#ԋq?(}/ +F4$Y*nVwV_K̹]G|ocuW+Oy2kVl\W}}ᝌzwlćq/nw7?۫kSAG{y;}mq~y~t;}+0~a+|ݍ,s/dQ%W5=py}u^7we[oqvOtTͨTz%,nYÆ2=7^*tϻr'Y=9C2&Z*=STY?©_;\^+wxwYUGH}p>ލ[~m>1}Kxh[
n%x+^bV
vӛ^n^~^}/c/V
HHQsǽܦ{]뉰ב`]̽Ʋu"ws]g샻 GZ:J"VT~r\IjOW).vx}ՑYMuO*J#_a^w!yJ=UURd2{W?E/~J>m*1
zWX͛n^kXz3īdNȎ]/bݞIWwR|%G.%ᅵPzr\鶫N/}wk֝쪓WcK/\.jG:!y +u+qoA./o[y;ģ#"Sn岿{hRo<ݻ|Ktuګ$+%F_}'16[)14^!7VxDBLoMԉQk{k,*v.|Ϗeʫg7_L.-SOW[M;ŮOT!\TSx3rjն;nR8Ϧ<l?/v'o? +,;?,JGJNѓ/./"#}~!YlʕdN7dΨ)ɒIIa%#LѓL8G7fd#$2K_ʌ;֟zsG/#_A^GOv/_Nx97#N^)XQq3*nN]݂˩&V_M,^RŋCwo*;C>GkFwz-}ݣ[Fn=8~cU=?o8?2c_W>M2Kod/%zgw'&"*$>#п&HJIfѓ,5|gW<5]gso_e3]שg9nVuV\N-k\RR)ҋeSʿ[ː>.=})Njq7J,/Zʷ|X埽/V%8[.W$sI-'Yc淋!exֱk"gOדJt轴$Z렖i㶡iK
YDwWJV.+YZq!jZYmSʶ_H.;x>̅ңK! ++*ֿdޤyHGgJ''ZmJaF%LHD(Y1{crx<U~>ⅸIeWː)>{%(N^)A6_-9p5ZJӶ29?b;ĕXkuSn
a ~ a"~m_?v}n0oZ,z2n͞[>/ޤ*sx|Ϊ{EmޓD%"(Q9CUb2DI3JT$&0ۆVYQn[y}κsLsyyS#59b1l ⦳ȸ/=q1W]įJX/B
1 }9~E߯وh +4wCb^uG79!o>ZP_s)F4A^u%ν
_4n9[sڎa6BV;bď?߃gM.B(2{=\u3 +jq|ohSAw=kiL~.g6ѰB8`dcDd5Ҝ5ll4\Wr(^yKo6+ +p&cOa_ٍw`zzvóip͙!£i|hel+?SGNEc
qd 2̌!Syt\d7>=3g4eA8rڭ6 ؊^ ёOTQ%p{qŒ_]+y{xǷ +~n;s;r-,? O2ײFFQx!n}D4FLt쐩4vds?<R[/zcƿ9z/ڮ](hu)mo^zr~k?={- t<s\#tg#x͵&qx,Mx3{d1~%ꎬlF,;3";Z,繁aӹukIrySK
87kj^܀f=^ni|p9F|
~?Kc6,tıu|^'GϠI>sPd#BYEcWvJ4}I<l1rYNi;QƘqJ_'oTomm~p4|}3Pvdwܿ
rR_͢јErQg1Oԛ,g"s97@fFvx-Eˑrdjx`4F&ύC3|ʑ}a_9l]C{Zq7r0Wrz_~(`.%ʷ>VL?sEd [ c
\!ye眩D(0a +zr88wλEĈ=
ާ<}GU|]1>cDwdn+F]֢9T)MZ0fC}ժ3x|\K-zh8z\y%W5-ۣ"pes疟ƻ/B)W|WM+Ƨ-Wmÿo_>[ʏ!?V:q/Zqyw +*:)4L5!0ӌGN¹4Z& +F6hG^ќh=vGhb-Ԗ$tU]qWpy(BɍjVʤ?.L믳}^+ +bM !'9&w_U1>KnJٝ_p(.chpÖ⡚fΏ&[/R6{yDo +\.D<UhYlҺkF.3=~$b:eOd!iz߅5"/'!uk!ثx߸zmK0qޞB }~˽mQ&?`G1Zh[E;XF06M<w#r;+CnB߯B/|KXFp:A}֡{5ga
۞Y`uK=B$e)~WFS@P%A%cz81u3Wo50D)?I7zbdi0XyαF٢)xdԩ ?| "ĉ"$|/D>ƾ@(& d~#Ċn \iKN\|J^:ݷtK!o֒m?x1-7SgSg3ʏ]S(o] +yt?Mэr`/e=eCRU-t_1i
':z4@56_&$:+*9mף>vNjOdn$D梄՚uEy%~Ml +>'%ܓC%r)Ԟ_e5L-7rŹqܙag?ٝʕ;P'u&M_I?8f̞+CK +53N $B +1??,þ{C'Ox|x䭗ɵw?m +{ChH}v~7Ƿ炓j4z_|R7b"
J
!JAt@?ۊisTեd ی'I$FktRqmB4AԜތs>-v}$u"o*B>{̃w#sSW?+<)R>}PP:|}RJY%)n6WVa}в=爐{dmc i\]WKTGR/$Ԯoܨ֛&&76SMvl4a+훈'0[jX p~^|_?G-o`*E`Q1a+v~0irbc8
s0^qQd;?':&G K-Y;ڃzfU)}^~ionsv?З>F0ަ=Un/XzջcFx^pkOv'm۟-~\":"20hr'4zZ`id^(4~n䆜1eO,̕ъlHG#dٕTi'"GasVFo+іhɢHע]xJJn̓T*8<cf.<yN*4
qV!>t;Jqoy7Ln
(aR`UQ{QҚ33̞tJ 9cdWxz#++)!2U:Sf)m4_zqtE{mQ;ߺG^r_d_IlASyޚ竂n^S^^;UPYz>Exl:c2Tc2 +1F&#q=Py"wiS[~<y
+8sLWkRb zj&]m&_Nc +Xx6 +6ucyybZQ5lCٍc?}´_Nמ#r+F*[YVm)m\H=^J +BB +gޯ;m`(y*V?>y*93'Kn6r[ĒDz4ʿ*OHhYh̅(+4Sr#sMBVe;//}GԱ$ݯWzf+쫕Cr;zI:ͩ,&~2f?jshd7a-`ץ~{m^(< g':#m>eퟐ퉖Μ\( +1˨+L^d!þPyvlӥo[#S'+.}k2gKҽuƲ/mŗ ȋO =O'L=:#ya;!
Ia̼|?>'wtrY3u=/b禎lmXBГ.yJ6^4vC}40d۟9GUJ'o22635GgBq +PiHRG +WDNi=\6̢=<r"Ua!I=SBg@opxq_Z~kWYN]c;'YTj4J_7'g졷uٰM{'SelŇ~u
Ӫy4(e +(;e&Ӂ/ZLd5KT[bb;z@KOZnZTkog<w"ev1\F.ےj݂7d{W` $=?Jw}Js[b-Et/. +Cf}!}s[ݲooyX >#=f|i +Sk%aZ4M1FϿhG蔊Ѳ]8ü @3tDMUJ担|3$뽻4 T*o\LnXo"zA<i
]>}퓝/w`l:=9X.'u?]}h:]kJ嵎dw(zm(}*M݃WUdju?q,XyU +۪PšJzps^+:c q` +hR=OqqdI>+iVYB/rcӨ`TW*]Aa>"%S5t#Gf:}]L2jgj3WOa;dzi[L#zCy$;^L{b|Ͼ
.|E{/Ճ>^.W*DϟQՀ-zSݾR1ÜywBOim>2?G~,DoЖ٠VHӖk:\HxlTr!' TX'*Rʌ 3*:CKRu@+,V|`}Hӫ}=AאI23}]'E]=U}Ӄ̎Ѓ4w``zg15g<aG'IƒUglɭF^_/vEΎ^_}RivFM10AGG+pUP~$s'OzgEP>8AS"$ZH.-//du-ù펽oi+$UjeTe^de6nD5.bd>{!eν +a/OfpLtv[IctDt<DH:#-qEG1wīꌰ-L`[a?処S:TR>߃A61
>,2/k;>:4dOOI;AԲJ +I;IO%d :L]]@7:8*l<IG_VIS[=Bd0e.Z6orssC~6r'A^"#F
Yz1hmZ?S$P|~1ԍ}TbS{ +irl-e嵎M,~C%;GO -kZЕ;lۮ3^KSR}VV=8[!督tDqx_#WiR[%kw@s'LC U~oJ0"A^qJJd*(baY@?ۧA)sJt/5]͜ЋgSIO^N8_Ot+ΠOϣNaJ&HwZI69FLmѕx2$$
Z]IŔ9H4ZꂑlN%[~Ԗj~4q^1IE-7tجm{ ~n'뺹z+qR}TJ?\M6Prc!cl37ט_I~rtKF +Kw$M]@Lk"E:ii\(~@NtY^ܖya +6̭lf9I6łhl`ŋvLds[,dF:!SN(#z;ے)[5ρ>|ШԆ>i*૾C'Rj Lq0_߹Jvqv}pu?[m<0
A{ +k=DkXS<]/CeJt{gf@w↬B;jXW7Ug<[9~o)?hGJxiPڀ +B*h' #.b4o2hSrE}4MKcGJkJ5A94U3/}މ=Jq]Z(}GG|cpl>M +t4h{Hwum3/hu2C&˒)q,LVٔj4q}وIb̬5%4n*Jw^:W5jpzb./@cM).̾OÏܾR@3NP^[s@{ 4GOie0g@ÌLR>Wny[@l`AHA]&U[;. +<ܑңD˛C^^^9*QB_>d
_̑D톃/K֔tu峖 o\kGLP&k°ClX6hO jFb@+ +%ϦךkRF@V~v[zF<ХM%B sGxǏL!аgpNY<6$P[,xMm4 +tXRCi5LAnH?M'{^'^ѧ<G|ֱd-?w' S +9{<;]<؆S.y{ϙKخPwzu3Rj9!|?m=jMN<^LצbZjy8\"iRt9C!tVcfӾ)LJă*+kg<sQ᳁>bhqw]Al}Y-Gsms{9cLb!꾸 UVvIOO!]1H18YzǁIPu& Y\b^hcȵ_Z8o|.5`lեlUԘ\r0(Z*&@Yyh6hm*vߓлd֕Bc5 tdYdwaXԙh1F1<+U/:Vp``-
؝_{)7lv86ے5_=w_z*)1]WQ^mL-CуoYLbk7(D/'<G^6AW;N1mӁd8aJwL±nӋ/7&&x/-`Ҟ$'a?qTXar&7)uTb:C[Ć9pC˗BpO(R;h1]O@Ǘ\#\%,#ⴀQzƁ0W6-p-}C~lӕ%|^8Б璋
zg6w2t^w"[ZW&s)h2kpl2FN:t
N]z.2^+(q*Zd5 +~<Hɸ`5c z[
ztZ-?2rಂ(a+nҔv[|B)vŭ L<.7YyWnbvYtJhE:lBmg'3Lb7EnA֙mې5~|f%Ѯ{*5/0`]Õ6xZol3J`FX3LU+@'D\džxRț
?tTuZ0aI|gY8h=Fyǝnvu!ܫf3ۿwz^JN%e6DnHuv'As}TF}uq
uZ
t
&<~}# lw6JKJʪT%;q+YlU^+-D+]+0;;jyفbDSvQbq\"h*s:'(Ӷ* +`!OXxF|ŶAw}䭃N彶P8NYqt0C8})Ӈ-3sCѪ3@Zܽʝ;S=W-n)5zD.ߐ +Ź {47M,MqҰT-{_"/$30.L\4HL`[R.^|`mUOfC+"50Հ2({Ơq6p
zm{(?9t1VgCj)F
ΦS@stwDaxݞp;-A;xze>SzdSsTlي3'#NNQ\-ѕ==|i.X`}: + +H-
n̨cK
h甠1To|4ĵr#/714.?l<x$zvgq&, g(udpR%CG\iUBXmk +'}@lv̍16N]mX6Ԍ|Z^=Ed +E3I.0C4YcEJ`gEdTewMמWLSxFtnWf8P̬02 +YqG=?? +4lieFM}ӂ:}B
uSy*?ux!ME4mX0[_c1YTVGh
sqGoЖ2ja;$uţ=HbLtx&
D(yes.!?w/Βr +5Ov$X#( +P +Z\J1_f\]gIa
PՃ}#U; [7YiIXN8f;{R`gAbgVYK?Y,c5!Rj)0wA_r gB;z~>ɠM}k4_<Tg<tLXLcvw}+Y`<NX`Zo1m Dz x$#r$"ՠv +GŦG +0Fk>kz3U^50*[}ȩ@ 7uLf!FB;96η]T9
1yzAYeu[s_.\Lvr"=ӨŶt$쬜V`gi\T$sy;@7&,%𓁝U8R;+eATr^`m}oo@N,0ejV uFՀK9"`jdXx<='^?Bv൯2RQH~$-G#wB
զ7x]X +Arkȝ鸶n=d-ZbhþGE%U7|((Ą
Txr8\%Yag_o+{> ,|}:2g$*M.;֗9@J*) +X^fm;K^l`WsaqX7QwZ9'ܘvk]rqUtp"wb9<?4Z[ZO9`}s`~-
cɵ[@Xk_0PxD5eK{٘L). +WI_Ϩ6`Q$T
۠+3Ve,0O'sR>':pbuH\R!BJ!a=KAStL 15م5O\4X/a +Ø0 ٺp_r>r}1Nb7=\\nxвfȚXϪY /Y|K FVV$~-3ͧZ|eݓI5_k"µ{.ɥF\\ +,2C[s|6XiYx";s8S- c]Qfu{&=Z8UsֳƖrgHB֬gk6\){
SxոﹻJ.Mڗt=vp]Qy|77Orc'\T +dnz3"ENK|o +{ݸܑHzQQg&UQgpNM}bhu,R$1c8/"X)T#,O{mo"` +&5{9\RÆ&iS}=ޗDl25>:kRqt4-HZu`/ oxj!+ze߅ͤ1XĤ}3C.%O/6"yJv8eQ5';zvxb.\C=/ǩ0Up\!>L' 8o?=[ϖ9[B>!y(57=cw>
n/g- +ɝ]St-rplgyߪ|TEA%Wpl>y}%p3a"nDzTx}r&C^%1ϚϖH^ZL5_^T66Kl\|{H +vOWHo2VX8媶c$M3ue*dLσk|iJ)y4&Pu< rfbL^9Y +'A<-aϖnifP;<pW=GOl){>˨73 ~Ŝ6V@[XPnBXM>p^a0ǹ-LJO#+`:;cT| +"A\-d꿜9!b>O~(aʌȽRM?1i8߆m,:+tP'66EW]B/ڂRuL qas6uM&vZӆ|r]u|Vo +97~alE%j'\Jg_p_s53Qq`L(3$csOǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>Oǧt|:>SBCX/ދגuIS:'%E%G'ć$[;·X?/$=")h^}eֶ'ΰuޜs%!ֶ/jl-JߔF20-,~jqH2~J7]"sӷmpk]kmE3Y; +D[#td
Vyr<p}ݒy[/0:aü9~k×#:6qSr6_gs"R"\9_k6@nmGyɟâ%sspVZ_/^ +Zj}4A)2QLVSחʩ(Ϭ52|=Ӷ*Y*AAZe +zD+쇋MաSk)K'9;.>MzIa8h6B6@L0
4(uZg2tPEt&cru٤}Te7a>PC4-KVmf2iOGkk@CBm#zfɫQ6q9zlB.9z37[Ht/.[C_$3=-ԗ*X`{ ؞w2h3>\|.](6VwL֤֗$)GǍEjqY]\\Pp>I<ߔ)>QCL֤C¯QQ/;Qk
T<Ĕ +~+?esF@?W~:b*\-R#K3 +t$
s=|=Й z{AIlLHb="QC~4(AS@Oe2XIׁVUF7em&gx|t kI4U$C/#KC)a,_}b.NBo7uM{DYV=3Db4ep0gB
scd>vej~fU)>zHR#bo/+S*MeF<sZDg<M;*\SlYlHYqDEnxQ!tymwONC [j=a%]no|v|}(ߠ~*<`}']QqZ;:`W>"'A %c-zOd<~>ItXLD +{%SL@tz@CC\m:nRĪˡ'*_ +^ zDW^+}!EL.h.ȓFoRvM8*ʺl'm2G=-gPUsl:'*J +4**ߤظ|2u1hkZ-u#Ub|]^;tEh#@<j>i,Ķ}Чђ#m=dԙF%sWMzx..G)gN6+xm#Vv-sAS@YY{2 +2=S|b!ѓz]WbHbzz9&_
G|*A$[Mtב
た}p=@O]ʪԀF㸰&GjdM.4Ct@c~RDCNaz + +ob>n˦A/5s*"M/-+nӡöDG.,Yþ9yh:o6~x 誱D +bآHw#9g;p{{~~b˜s}c>umh- -]'80k0k&Ff(.<} ;>4(hs)S^*]=Kc)Et[@n
+`N{)/Oy^Yc3zN(Nz?0$?||G`5lg`QCpE4Ȁ< +*;2F`\bǵW +h3g.O +WLA/ p!m=xq9 ' +s-Yy=C~ِuB1 k
@> +{<fQWZ(֊Ҟs +Iī1FBaX9bHТ<h& en3p<P%N> +_%> +Z1Tоחc?O0p,ŶA +!FY/{-`/~S$b|.#n[ը_dlˍ(<sc*nZ >f7t阒TJ^ +]Is!p%#+&ԩjԄ 媠2K\X40.{beje@\:+S~Ih"?({9^ ʟf#ӁFhV[-&C^=aЁ]xgccg}&9HsWNbC O"w}2`=rmH/FM\SCg@-/߀^aMx(O`|S.sqOzH>W~DQˀvz}VKAxu5 +TCE<97Z=fND~e;GAA
Z#rg +WAW[Ș?~uõ'M
X럻NDUng2~(D<ZO+땾4:j9iJ0DqtBn*fY
뷜 r +)3@뱁|@VYҴkY8|u&u< +Q>@&EF|!Zq<
Q +̛
iZM=|koN/Y
9-ƄZ +_>G/71W"DcކhX-I/:OΆ_Is0_4Gcb蹘">hlq+s1yBk˱^DaJ(Pxdvj =?99"KyJ|V~HKq`n9g +A<(wQnPaE~)m[1ᝤO ?.?`ln/G
➠ +ixq[<p9v.uc1`PB +zX"(@'kC.匥XRȌ!1@s<):ťD?t{١,)GAGRTp=(ιDJ(ZQcB =/cv3 +=8_<+:*{^]۰"F;e(j_|݀ +c 4OXKɸ|1Aǖ?~}cB_tZ|wPg3~x` ! zLX1a569&Oak¨ӱ,gwc99mŞ1)+T*En Й߆9/hi_aޛg=&}6a3C:_=4ђޓ>Axu2<5t!!>ycr&iDz(N:ZkAzX| +Ό|"F1J".
.N)dQmýwxr8l\Mo!9!fzLX8?Z=&n~y)[D5%1s$ +pK#%spŐX\xd,\Fo(xB( F zFJ/xU\W,* +:mt%~+ +56TN)S3^nDyk)P ++\\YJ=[sa]_ +csX +>Xa)J
TQg+UuORTa|' +?|'/S!-r q5rQj&Z^XK4#nPO^<:Q@eAK&%|.p2Ή@NmQxFxP{.+P}Gu^|axZX.9b}eX;eE!h3宒d,5!
(a}% +|yf.o=:/x]#q>ĕl:Xq(F2w4wO@\%_P'`?^5t-P%YK +@\@> +~X}9Gdg{@?bjhh5Ox +Ç| +7YZĚS(cx$ar}b1zEh,@a)l:FM{m[c{Q?2@M`||hO[1yq,yf$ 2 HGm + zK//lh&K.Q,#lk(pҗ #=ScRy[i/ +iLX&h[`DzTӸ֡蓉jӂoYkՎFe?$55):p2jj>} +R.`VX*l +4v͞OFn":@_.OfL<HO:duv_UṢ1ѭƼe|9i8Kཫ螁$˄Aֳ)jkuiV`Z9iZ)ccR|WRջ?# +~ ePIջKsNLV֧$u8繴fe<FS1/?^ڮIjVH݁8Z{x+uc t-{JkG_m: j2^mH2~#oɄʝ'Tj +h3y=@z +[wY5gƢ7'Ϛ0Z-tߕܭ1f4 DwJrMZ +}iLvg5j32ڣ%lUJ׀'T>hwLl!̥Lr>A feV3j!oփZP״|zmFdÊB
8DV]d
Һ\K<|3wa\|)]\o^iHWaN+j/z'"YXiI{fҌ$J]TwIiڏr.W*j&UE|J%Jj?]\-a!x9Wa8?/(A}dn076i浙STXKѩh!yoUш;`m{]|QHtY,ɭ9dW|N҄Io&ӻTѡ1DŽ^q 1F_fjz0bS_zެ25Tj +>6
忤&eX >(W{]z]G=Gq}@7K(a_vFxK[N rO>l#77!_| +K[ĥ&]0yn֬dhຂQ"閰,Imj'̬%>/6JŷEVJr5k2٫ڬU[FGož[ +ߴwgJG}y[vt~)>üd~>FqzX*dʔuYJJ}$}Ee5E&!3>هbqn){eZuȫjV飊#ҧe;-}ˈޡ'F4 +R7jw?걷H*O>ꢨ>)0ے-8Ǽ:T|8Tv^.jWD tA!Je>\<$h,y59Sa˼=LeҢP
4q.Gxakx&Hޔ_4wQ_?ZͿ]MG3C>Y +bz%jI3[H{UǥolK\n_b'~QsBrI$j7XJ&496_ab+q5OŃ%O_|د}@x4xkV4Wy`\x嘣d.⾦#ĚtJڛڎ~Z2ٗi*FK2'W=GK,~hmR$.mn>)*n=')T9T\o7e_vӯ;Q<VVA9(k'^lFޝrFTQQ[,DW"j5vK4!K~5T|9Fh/l| +5z#DW|8K27z
ȰW먨ڭ:yҼ2KiNac*3Ụ61䃔|Ų/[E
yuOZwxVsqEETcUqēv>;2F4P+nXӯLgjz-n%X6Y']mLh͒0=_Oi"mJ66]
S>ne +
1kˏ4-?,PǫhaU軩N|!H=mcDN7s:^NNjLO?0Ѹ{]k4|?髊s&CD[V2h$ބ<1Uu9*̲9%ZpWNԱ[1`qsΗ2aRY5]'K+_<9/yq~2Z^DZoϼy'ƽU7qgwQΪ +Ĺ%oϚ4G6d
}j&QaɛBT_~1zT:WGE +[-k }`MnV)'Zo%\mW!Zr;Gf$KLuuOlLa;\u܍wo +LuYJh^G;>+v-q-:ޞ% loVW]Z7X˫anD_Fq/+v/u +M* (O;{\^s|^#w(OAeE^E2(_II>IѹQzo9=לO6I
+ʶlaޙ6 +λ׆^qe]jLjr{5E
a
GPvpnjqH~l$ر*"6'&=jG}eT䨳)Ǜ"=oͺEJzC$%ͭ~Gn]GfMQg2SO7g%p7`C=Ϣ}Ȟln7ѷ#ef}"9#j#c]#_6xg~ۢdMTǤMQbCcd]"~urƺ=XqB~̌rg~pV*O{'勓^'֍jZfӤ8$.n5=r/ѱ:*%;j[mcl2@Xֵۉ*]%
g2"m+cnuC9y<ܬ0ڼ+'BsR_KMx≶tGpܘmYqcccbT7;^-g_ܡ;S'8{V1~JkewLZ[${7"J"N6eDw܈`<[էèa;ߍJ%{7;o]sd;ÝC}})!,嬒JAQrGZ-;guJSLQӥ'
v9
vQM^ɢePMDSwyR~y=|1}6y^b}ƭ{5[U[
eq)[QiP_ڝJ"_zn +/ +="C/#p13VkU~n,E obɲn.o +Kߞ5Zinh:rX5WX8\O(silt(WMطODh4n;w)s9,[ +dJK iks7+V([-}>3vUqBAV[gKwYo=b +:-v/({1"-:+ֻ2 ѭ2$!Wm<nq.DŽ>4jSt)b0C>7@k8ޘm1Y鞂N;%/rE!S;n +Xip"BIa1b1CE$"1PA,DfD5 W|j<&Nw+gc"D/lt5u{}s*0.WTZG7(/F]-=.v|!J[Ǒ8G.t/|Z&knuY~iI\p9o-j۰SXTXf9nzFNlW"&(b1@%>r?^z,'yx˄"b/JW?*727*BoYzgԕ{nQ/ +\"s\"rbEQoK}*ٯ䯟\/DǼjF5bhts.Nqb(sJ_8t#rj7L F
N$FMD? fNYMrU@ucsB9갸9zlVGTasշQYܢP +r5yYSwΑŞQmQ_[>X7;GΝ/{=w]Bit
DgO@4ET|?\_{h.{Pk~[?e;zAf-#d.Zb^BX$aoc+Bg<nKD5;Ew9D?\c
{Nc91Fw;=SXPXqZ5I;@}**I +Nx'#iI[̓" +\t~7ҵ"4&7:[vrțI2WJUtUs[uk3&99|q|
C8? +Ͻy8FPV@$XLQO,eAl5MӨ4ϝݏ\#d'ZFlH.^W/j[}-;^~Q0{}uֵMu5ˉDBz)tԼY3pt\{$3c>';QrkGh9-CVGGqx!5&-}Kt"7nMj_uF){R*K(r
bƄixnwyuwpMc~H9_$jĂebEb8q؞<Ϝ[I/|e<^ž-pM(+pK)-vNz^[RX[EPѕnhr:m__P6,/c:{cѻa3I8MC9ii#/&&/"YMT8@Tv!v}=B[8!Ԅ(h +S''ZGL +ߏ@(GJ,xf%?[n3oxJ(`{/<Pk}\5Ǖ2JJRD]ʹǿ \/JĔIh)7ČˉiVƬ!H̜Xa?
gb#<l0UnW@/SK.ǽw-+t-P^9{\ +P>}I{Fq+ש导 9b,~~?1?QqBb696s6%,Q'-2$f-sԉbN(˱e[nAQz\,#hw +~AX{Ǩ7Qo5F~[i'<9g84b42!75v91k:b̽Ē5bS sR̥$1s1CQ= +m_b{;ps[?rG]<#5SuCT[S<̹ZǴ*z䷥NKr{_еxMELG ͫihM^OLDc8 F-"f_m#0k+OU&ļĂ}}bO
fOv6C@_X&<M`{е=|${j\i11!6sܮYf||j"8r('=q=($ht5XÕXBW!yzTp+®7N犓#=Iz\3=?E!7֪1>2{܇}|yYd֬uv-`DTZ6N E(G949]N]bEĴqPnD䕄tiJkL Ă
'{ +G%Ejp[&/q(LDׂ/%-t*Ĭj(W( +3Q
L4\;k71g^b +1w1oM,xXJmt+!y/~Wmzy,L/*t~M/~9m +VsP=^XaM,fF,Y#CCr5耚;{;Ă4`XzXHcGo7[SV
z9Huч:Dl̊M}'TӵӞ(ڈԲ2ې +b}Axm#Ly28ᯀA _N1ah>*SGDL@Xs[xR1lwf읶=k;.=ǯ|j.b-j;k;-Re(lc|$95w-t=QZb!j9SA4C|2V5;e-pF!r};r1<ގ^wuy#x<~o-Foӣb¾ܚ}n"flnf-S t8n'r.VD +HK7hxm|m>Pi4q.`O{ o턶:Q>F2QAd1W|*l9w0wIZT1z!1gؔG8aIˉ[f$Am AI_9KOaj9R#usz`3NqadCR7+8L?rּ6B7ϭ9J-ԵG,?db5Wc$}'ٛtImLuLnc2@]hP1vQuEyq\byԉB,XCӳ!5|wݷZ%'/=<_(8:V偢+Ni*p#^:X4zm9(EGi9QWHv5z-^^-eDALxvNUI6͌-j>!~_z(k`7u2CQ]A*AsY,ۋy~LE5c:7i%}s(_
Qݥ|s
ܠe{mvMb
Fz9iLnM>ԛ+>TpBS1xvl4яԳvg$y1f 'Nﻤ̣v1K_7^\gd7Ulzg6-~b<k37˶ Ҁa*٪/j^8{6wz?ӁXc[<ھoWSFwIi3; &mtTu;_?4
߭]Ab7Y> z)n&/m)[8)1)-=%i[ydf~2dBQam w0 +WT#VX-o[ш_ +( uai
RgRb6I3ihsSi<mTxXb*'nqs1 G W{m,]U/`\Q4%OV0.)vW[s}t3ghk%zI̘DL +$*jwphns뚹PV6 4
%<Bdugs@Bhh*7{yOihZ߀"DR3yӳvMRz\[I36]ӻGE>> (+?"7O\SE|Kg]lyfB-a62W{waJfû?ԌU6+]o1U9qF}.+e3r)M1 3C +Sٺضd9gVB;hx
$};bݥ%_#>wiF>էE?\Camg)shxoga&75ɇylQa~ &VIm· lgagSחsUܧЦ_Jli.ٍ\nHSݩ:BdAf*}xhfn1srBtqгM´Tj j/r +1[\9&cf
W_:q/<7./5ܜc\zᕁn\GeB^T>fXrr6kz{^>s3g[p_R4g>/:/wsEgbTw(ڝan^Irz-_o[_nn6HO^=Zlp/wb^g>E"&ՌᓪFN,]x_/Žg
ݟ}
Ӟ=_(; yX`~P?)u٣{<<+?WY<~ftyf~Q
IօZ{XrI:c5[ܺv#ظr!:NdX!G%ڪ ŎČ +I,Dh%'y矩
)|rF{k($U3jOwn|ܐG}oߋ4)L:ۙ\: ']{eiG*57i6ݩqٸOs@`9KmxK4l4hW>930g8rmpOoV/kDc%iW8Jɫ,Jx&K/<\wWOpز.u7rmO7qO7}?^u~P7;~oxpVx{?ׁ?,&nm<0{͞O܇ckFe
B4f0MgU.܊p7w<.<ˆ-!tx~ )&Yue&<w?{~aNCb_c,|je%ڞnG2lpL\}˵=?n:t͎{=4-,qY7{f]vi\{hvcnޒC';B{3Z֟sZ{@sb\tP>4[<pKgI͓%J\}jR?yr
c/fp
ډ|y,]ۧs?{7 Н{w+-yw/uE{dXy1n +afOĬ?t_䲾#eq`8^ox?w_-x1f2'Q~+X~ewrRl][?y~+8zZcp\bɅy 3oM=ot.fvNK_WLb+F1|D->ڑbYgh%/6`?ՇgQO~7}Jd__&b~Mx[2;Gzmį;0ܡhcTI*mWIɖ;6k6/ޠٵiKCkt>Ֆѹ#4[mp%h'78I7CqjppE +07's{f-6A |fxth&ld8
sRũ{(/]g|<RxϞgŒYbvBٿUOVGs
\[l~Z_vq>QW|y>if՚=[kKcYIp4:aL%d~SDv/soX0JN
757nۤBjTxn6X|Zvl<⤘44uRY=rbVDҒLiGQvS|P3Ny<sШJƜ4[K\G{"줬X/ݩ;^no]/H%B:B7;!ɤa͝~4x^ J
"Ts?r;7WVrW'W0rȑCڑ:\j=l?Yw+:hfFI%3M)NgrkGZ>V4[+OҸQfB*]l=n]_!6oI{f8Nrp0>ҍo}Gӌ>+7~q>,]} +&Z8~g]ybfm}Dr'K'wD.g>ېnd\1fc*F!q4Ux`taEΟM7pEfʘb!s,/D+qRJ~t꣄BB_;Z,nq@Hs1 \.\7ۡ!uN#VQs?]x!1`$4\3=WՋ_3_P#@3*?=_,tJhπ
³:?+w/{uݯ6a@L&6{:y.,pwY- t`&l\d +Feu0ӟMl
|Z>EiavЊbr0qpäˡKM%U
ߴQ(0B)o-jxfĢ3gQWFZB۟;\b@.j.2<;12Gz)V4I&'p{%␉q2)1oZ:x_B[.==zu,4gB"ҌJt!>=_o_X6]K;BbXfR`xS0oĠa!bfD>tO1ut<
5'Q<d
0 +"V&x+n>fr3}@_9ZNHHV~fx6۷A7QjuTmiU_Zf'}'xVϙF҆lxo֨Ո/ʟjx +~̗Z,>vNb+GWŧ5C_o}~͆y5hthWVriC*!A/?g*'<@6G?[c8l8R+/-&}!y=|9w'KkՀ;z0i{]E┱<@Le1=lN>0īdM(OYʱ +Fl-eL +)NBD> +)1K/lM,.gp֗'AW`1zWN"L~phեc1-BRbtLU'=)9.{Qx)7W.e=ssb.\mRFNN`۟m3xm$jB;.4[4炒p\D+WO<;5lXVͼ-:GșBBhCK8jF-}3P4}">y<hI]vB|Sl}ǗI~)\70wgo^{_7] +?w^o+UNN Lf99UC˛4GHI6_i3(~ek!( +`O6mY5r]oS7Y-bh-mPk +-s^6S>Aqg7{_SѩbHe˱eR%|rݽuЊG
4h%)7bF$1k2U.^mkzty+XJr=Zy9S)(P<Z]r7nǞJ#w'TkfȮ]Y)v}Cn:Xn#`nE:w<". + +:bW8s\~c#/8P3]9JT-4;pg^{;'h2z*,8B|(褋%7]z)('cv1T`Aq34zzۂ7+ҟ,91G9&Bs~eT5ZHs/Z0bڰ{Q}F.W^bE|B=4Ÿ|YʲCnރg 'g=A`챘1 +||O.'
9:&v]ӝ·Q +g
_֛rz+5#t~M脢'B,VCBwwBZ9^?luZseE|3XP:[> +G.#FP_䝜䜚AS+@q
a[:)\>ؿxZzu1_h `^Pw9z[k]-:CBs/JIn1t<qz]<w2vE`BB]VS]/]\?nq/{ThN>9ӉSw`w=w{Mw7Z塾foxs<!( +qY1JA!6Xwj\HAhBYu?ګx81`gʷ'X--b9
Q;0NHS`D9#HSl`@$cWX!qtqf$Tr +:IK.;1+u'rde ,g,CLZjytf|5^w%Ьy4Fژ)0e +jطl5xH\{n~m<ny<Fw;k1
M5m;]d>4u_nw̓5Ӄj\zrZ$?Tkk*f'a=\0X쉅즩Z
kŅB]cy2{5w#vNR!&zz5,#>Sl!6J`09(e8|4Xħj4MRlwt:0~H>yr}7)(gehޠW +-n)Q`|3rR +'/L,#}-_ Uck]h+,q1f";dabICo_:^- mb!h|>ŧ*3`K# Ycƣ؉|krk V"b +y<pVR1;K/H,fOre}n/& O
ɷQH?b>Y.
"j;V-*I
`T]_F}T&>Bb$#m#=Yuo1q!,7aO:덧(+/-;r-;Klb#2βpZT̥&6!bgO;+2+;+
;+abTYގ충9 X`b>&@gu̞jfR!g _qe}l]K%"ێd!Vқ&G?\%&9`/j5"~]g5y`0˩N#"~Ei.?'JW6UA-$2\7Q-/,n'%ZFN%[q}/\d쬤Y6FbgY\^!~ZujOAl<[S=vt鱷|x,KCZaZrŖO7˝vn[<p>jNg/JhܣZzy1j114͖kS6#X-kKYlwWNde'Yq!y߰_sJ%]sʳKܚs[ӋwWJ/,RbXr:ţV&nLQ3~CrÑլ +0-pĎF{` 28Au"sx.ri,_]}ᚊEbXN4#vRMJ'=[{LO6D;5L +8?bݵed`ˤS,+~)yԞ'~eOp<dN=2l/%Կol-Y}`gg`C*)euO@=VY>{{b|;K>oe1ߐ<rMlA>,Zeﻸk\=XsԆk'-\FX} +]쎩`nȅ31cB}D+QrLaĀaT[bGڋI#S.=g&߈"q`Ả%w| +4RTՐ\> Xf5} /*rٵ<>t/.FOCj(v?!DZ굲WĨɮD/pސE䏠~¿Ć;nm"^[|\hw}PbR8ҔYcxU1l# k61Y=
@ 8CڋK똍mPB=;
W=bkTd|mG<;wFÞZ~m)L)}Ih>2<z`^3|>Kص/陃nRpp\nTR錡˩a6Cz4:Q#C~%%X=9L̈R3|+_"&Qō׀99Z(18 +-\OebE#F!6!e+Ezjzۘ%kƞ1Ä]_[!oƻܒ\C%{7Ǖ2
kK{9$F<k$֍Ź7s_+JמbMAWoZ?ߪ#9,磜/PrOK<zsb'bf(&Y?\JI.MyJIvgbo;z}8uJ9Taq|^Q;ΰ#n7)Fu/V/-Oh%X*3q>0 +XQv_A`m
v|lGd2j/A>Bj0f;/'Og.sC9|Nj|ۣMyJ9%UB~=ҁzb}%:q|]~EzJIb֘o2яM9r4#?I.'[
-PK[L81ř2?e*6Nƫ+Ď϶J>a)qX,+sZ>^O5'cI|͙\ǧr?XYT1<|L1"F)C|8!)k&V6G>C.Mk_RSJb9)c5yFK{k5:N< ]\wIoJ)6d`LkuLY92VE<o +xZqfK-읅XWx/VC<et,j'ǜ(|0Lg bf1U>ܢGZ:XMYAWLX5.\3#O#X賳5$k'ȋ`Z%49Z>'~(OT<JYN> +n"8/E9Wi*.,!&8{ґΙdkĤ'spXO +'\{ۭKyH';
r1ݻ98#Qi]_]_oooooooooooooooooooooooooooooooooooooooo??/^g?uX{9>\bvDžDE&;oė9]KYbM]\ٿ3ط8/f?|˴~Mՙ}36$8$}QhM~ɺ
V,]bWZzuKW-__/%//9d/G{opY;ǘŞd~p~ٮ$wg^q^Lo?^:W-_vyڕoO`?IZ:̡W[ykً\4]lAsOg>wYgVhCf,k92Aڀ+/5|f}=ƃ30Ƙ{].ޚ}{4@22"rm)VZσ>,ôI>ݼ
w4ZlQ߿ߗ~[ +1ヒ!ebH"cv4cԕdzs0L-Gc8zqLC~t`?c`<'i˪129"Ö$(g%4FDIxI4 ~#HAX!I"Dgؑ4B`#żьS!Gi! +#c!$
n!V8
oǒ$tKBBU5DㇱbWWKO.0dwO(b&[AZ{l߃P\#I(}70Cl$9qUcZ'!8{!Rt8>dMh&>> L8>O!fKG2CF2̞Af^Z?3w/U%Fps<_ +ݞ/荡ti0>-cv@9Vdk9rW=Z(́h6Ys i^_3Hu@H`)-$k545B/YՎqs={5+I5NjFdCN3Ng$9R'؏hÇ$Z-@"jCfDlGLHIӘIe:l'n1&dgH*sdI2udw +}B,H+˲c3G`Ҙql +|<%(Æ$NȕT$g +[CFqb`$$f E12o(=HԐxti'Fe19"И^r8sT6cQ4IE1y#tv/=qḡ"mi$ѐ>]/%&QFxAVRzd)"K4߳ݍ=֛!R+G[0v/,9Cxwф1FZdL`"[f^QU>ȵ0~#D~`-2{YH6:Cd)0AO(FLՐHKhOk&Ռ5Ė;vjX
FQ)$:XcUJQ1Hf`T[Bc@<INj\#FIJ/a,ݧ reu(6FQdv]!A|/EE49@%)tg ild31*Tvİ;GctƏ1Z7=4_4eCB5xm?EXi4:JcG +NZSfC478 +YS cE1Y#)5q碞 +F00QNKϴ%lI
NŨQ;^
YWUHFv/aa> y +18 +n]Ըj -55z=$?Jo2!فaf_agwO[0S,oC^6&Yak#R4^sH/`P1rCՁWY?]{5n`l&8ǖ<ɠK2Ga)HPbG7BjTMHZ9c-9'44<ӆ|Xb
Ҩ4ojڱcZ;S!)Q/,oRKNχ/2$8B +K-[!a4UM-.-'ɐA +9N$ +˅l;|Wl(\K>Ƌc,~f %foZht_JQgA#//,62UkWGbV>HQHy@SH#_LH'fB^ +AՂYRt|*d7h<~j +Mzf\P@֜<7dX;X;(Ll⭰HJMXnQdVC[&|b֣t>qG+$!dkc``Q@G:ctKj$xmpsPsC}gg!&\F)RlH'efE[b,).Ʌ$[C2ZVd-ћ"xxK/J`z'$t)$}8$ V{< gi}eVO:mqӟun&Sc-%7R+JG. +=R`
1#7άCǧ@FИrjT-O5d S o5EYd
9NYn )q*ǒe1
`ggK5W$\0~'|"YPv.dd@מy|'5-pŔ8RQ x2ڧ4q B>?j
H5pc$ւzY\p}&[~Vִ~^ +2²m%SȐqDql-
4p(c8joVx|4JYC::9%|R-i1%|',)Q/2} +{rzJe'cvtߐ +f05[AiɇBT9BPS:Uk<=C5j!$Q;咞Y|->J,~&?5PJ {_AxOK5wWBP+X
>X"OIқ& ?B҆Cv\hb|+I1Yh7G- =.w<)w\Iŕ)KIK@:S~>+Kţukˀ]pכufWXS`u?Iȇ&Gdhm2G)AfudѷCd?5$ A$Sn-GW_F0daqc*G}ͥL9(% s8lC
Wfvߙ^7A-=g@rQH$.o$OZ?Yǝ/_ca^j9FkEXOP~)GȩOIE_R2z}襰|!Bob*cLrC,e~j}Ce@1ɓ$QL5B>bj#_GA6e +{.Bۓ|Ǔ-RGHn%QhdLk/B$6裆[HR,,V8ez]D8&S.\77ȵC#cF*ͦz=tE\|~Psk\zexl4'{mf.6jq&@2Y
`OP{n~
+/gQ[p0Hq6e~GB7ݯ\~f7H)(krÖՎ[㐥{S*C2g^H'z^m$:ߒ^GF9-S咡9v^@us&& 1c@_S~f!jzbDub{ +Q<C)Hg)8aO^L+#^'DW: wD;934Ւ,C3Օ,fC:MX +߉Fې4?syKl +T2GDd!'BL(c8["n#$DTq?0hT
s f<ߑ{3AB7ZPpzr)=RV?8p,'E&}I,䏥kG뀍2]wLEw|{fQBDoAK=7 @g|w."mI>COg%'B/Fa}`.m9|H0`!p@֮<Ѝ'( '@M5}Q,[lVo,% +9p3|I#HPMw5ҡF7䡤ߴiEppJQnQ}f9Jݵ3!{ + $7Z=Q[JWWH_A~{(dKf+=o=p\BB_l $qGDL P.l +!!rMH_oւoS=a{lXI=GL=:^-<7OvJީYrFda=QkK8>[0EԍE]juԿ@x"CM
Jn* d>!/+4|FEAƝԪ!{w{j\vոxJS|
r/GK?cMDյ8[K{W?\FJ'Ylȶ8sHߺb"w$Dp!!.W25[O<Ξ\Ӑ4ـPצ`X3q梠o6|O Θ;)jQLHn,O_-E6}`tZq$ѻ 3-; +K]'>z=`{ݔ?v=?ƹ`sk߇4zJBG+p`;'RQxj${QwhO䂵 TY*&w=jMK|rR_턽|m~jYW#3쌈#@7Vйb%~ +!cgpEReWrf`O/aذGv/qV>yjLHʴQX}jRsc$ +}|jTǑG[n./}?SxT&±B`iov)t>&5|V9raR88OU`O{YJBӁ/G=obr|D-^lldXx~pЄoci$՜["1P84ߛ*մsŶϷ(w _`N߱vO]9LZc1[$yS?ADӳpF +/D3Mg>P gguYƊ+KAo>D+Sa{CQଂLʁoPa,J#D
32,!rԌhzƩj٩8+ሜ
S}߷Bd1;>؈ $ɷ&*>=84*WuSnRVu3scq&#bO; Q;A؝7B*Ȅ,8pCtqvk]/|
YJ氚a0.$ + }TԔӎc
r] p&c{~bp*}c!<i8m윂=Z>| 1@wNbetΠ\jrFQyo<,2NeD>{nE/<7*,"tI.gXE} +[YccbBE?`&g|/壇<CJ*M~$5Fk-'\ +Kz)ϭ83{ńgot|l8/~byyG]O6qy:+"ɄfIm^[sh(Fh?ΉP̿'Pu +}?,N +-:c(}BMg,[DYgEfr)S"tMk?X)rc71gs Aq@;\TsgJg*n.:y[a8'9R~X9ruM@beWH
yXJxsrQ0r:^;%{v] +$@
7V +wE bOvM3˭B5oU +ŎAQK/--(0ӧym7 +GfH+w@qX\GGa>^%$u'HEf P^ +y'sWgz<]uvooOO߂u_xtJW#}K8<LA֯zpͯ"|.>7=Z6t<'v|&WbvoS^fR~s)jEꉾ};C&`<O(S<tIJn}{P4XMr[m&}ۥj߈"<՛t[Ag+ER5\}{ʵb{/koT}O
(P&iz@XKK݁-PAR+u{of<wzk4iʚ{n3c}PG6GFٌqWismry
f
M^i3ԑKkSCY`YkCDސ>q3bh%KgSу^mƂ.~,?lv|,XX=LPE{+;ω+k]^:=-=!)yd>:7*h;l\.P:̨.-9E$A5}I^tߔ.ou$$
D9Zx.]Ev2E;P"ydQv(>ٯ%!LvCm1ÍpeI9ї--';u3A<&,i7뭒z"i(&|˚/H_tHwTY*r2TvJ|^|v +P1<~ZCktN!jvz)7nm +]N_\f&zxTNX苐x:,uģ!~mI:J}&
&+O϶xoW_h9Yg/骼,.V?B"wTK:]t^A=SZ(2֣Etrj#>:F }&cN!jN?2o~5 +>S0[=CR5UoOU!N5Β=#bAbbP
{4͊l8GGG[=[>;=7x'}" +P٨.Ci(>*~Tkх=ɻjG/%ɍ:d[u>ؓB0ap ^Q43[wTٴۢYQEq6(ۄU:qև9N¤]ogOPhE(CK_ %:tT̕lϊMpV>x-f2@G-hqiO^ZskbY#꒗fUG8O,.ou2(WoDeg$%Β:wE$EEW=6ě^fh5vŵ35ҷW|?a+?Od谨9@W!^/[C.*0*~|%>|tG[%"m&?5NA.~3z۸&zbcA^ZXc-)h1ޯ4-3'u'س>za#zrRIq%V{8'a^uڈkj\E^Ҷ}%1G?u^O={?zUh>PQ lp"^Q坧z/1V/uJoJ:לИw5=XgnHQ9:'l8hG|&REymA1 ߆b"^ǝd]E2`gV=6aws}FC|)e) y%&K˞qv]εfgv<L5m.DqݎZMDޔ;5I_ל$;DPݭ>ǻr%U#k:6J]Ck +ػqh}]$õWqdˈ3c~#$~3{pV!,|#?n7RYd<HdhG|٢4V[B ? +>dM9a÷#d_'qJW_uWrFpP1ߏu\K̬IHM +iz=#զ~KMsg+D3Īa䣝qC?Ev8?K>\GElC+o{w`ך.* _+~igZS#s~Ɣmm7aOG1IcR=OT>4V875y'4fJ:.M]g͇Ke-/G_6G߬퍽TRRs9|sBj`>I>1<k9.yb/yW(~bKm'&AQƙ{ɮ=?tt4[钞 1~$Xߏr3<㗌7oǽN8qdǀ+Y{>g[6HtAJV6kydu'ɪIѮڲ]Zen1a1תdwb"+[K.DDU48E;ETk**\T\
LoJ0~!i{"Ú8%xw ȇ"ѣ:#-%{A2FvP$ÍOՑȀ;H:\e6X+#;z`>`
ǥm$ufe>
!͗3϶\}', p p߭x玘zunFNcQsdQs:טB82k㲫ҫ}jCކs+UFhFD{}w*|ZJBͻbD[O sG+CZ6Q]W_,%
!5L]y y0dPDkM8y+l¢x¸mq)^ 9۶ ٷ-4ů981Kv-n18r,L|VWP!K=ڝ+BwH.oN'qu+Юオ}#"BS/7=SXU`rbI@|v_[S,ȚiG#cXvw2#BC;o]-Z/Ug}#qBǫZ1^1w\cJZ2[${[3OfObCj/'$W^+u?՜{9#h{L:.ܲL2!*ib!k˶m9'sS(H|k5,}ՙ};s;r̤9f>zPr^wsLiSEkƐm'"s2=m'mI"|Xo^bdobROM-ɯY^h3!6fHQ+ssVO}Ǿ|0`Fmx]+qYwSc3b/^IК)
[nQSd%t˕4Ϻ)7}qjN>yt:ڲ8ަ0=-3)(ӫ!:xWN[Eǝo:ڦA]WnC{bbBOb@Sp +=|s72cxotQdle /aj|d0n*Cbڮ܈!GO0Z{<lR*w)|w7{OYv_u/C%('>--sʅ>9b1#<Jk +NS|gw#|YaKlx[@{U{md_3G? ݚ`ރ`Muؼ +a^Y> 1'.
(ǢWп=kuy)r8l,UY`>Ps>_;n.4w}Ga~>sofisl``Ftvfkf`1s{Ɗ~ެT/{2跕.QOKcҪ}+/ՆFH͎,bBtx2РkkTBCp8C>-W/z7
L3;0tAakдRo/^Z0L$0LT*+UMLA('i$.icty__g`uW p+b]hRج~|e/^+{\!{S}'cw7ŞљU>q5IL}._΄Ɉ;١FYn^-jb~jm~J^?oڶV,Z)`"{WRw9 ϊIo?)M׀[E`,*%>
ſ
/X|).?[ػE>/zy; +/['E> OJ<zƿ|O"؈@wMdBxe`|Xk5vݬIefvjܞasA~Ɲ`Tdzgڜ< +چ8?{?6'B:ӕy}N,yfFn(4;&,(xǵXT۷dơՕ0
bmerj778Eu"S- +e0k`߾W?P{|gǰ=3xޟR7kVv1m%a?dq/tmveOd6{71l.Ji{VPW<ϸ'q)%qd̜έ/?P9c}cvdQwĩr1?36-.?oSdTy=/N|PQ/sEOMX,Z_UJpݪv;~zCiL|o|w"O
f*, +0Uq9L̞֪yG(ie6Qo=`" +}e#נ*en17agrV|%xdPTǭ +}|_.,:P}e+{#-#]Ω +o_~)hT銪pQS~%L@ez2~1i+=X~:`+.Δ?cIH|{Ť"Ībw)Şy%-}R8z\LXIn_~ gqipUplgmsVhx`2TiKU$` wLVlx",[]}\%Y\B/0+/wm|#Qfs,\~ٶ0Nۅ83(spL3g9V97y$``vx֊8qLUm0w&~<8Yb-[^My[/oJ[ߔ>Tx$Ը%7$46Tx#Y3tSmϘ&wYcyW9U7oo3Tm_&K7ف5`]=}՚eNGᦼ@>X;S}[lWG2sݍY]u]ͮ+=`-XN߶͟mx,?(+ + +8f(,-SNe`m[9;}j@䀹[`;X kdO[,uZ%,;X.::]hL9̽`{=1===-g̾9S|CE +@uئg'È={± `ެ`R,v<xZ^`3X},v1`=S՟0kǙPߢg/=Ooz]TjsɉŰNhre!E_]Tì3gӿ\0z)X6q1ˀRAڦ +ϐ۷̞L\}#<f@ +DN1x8Z\p{PXTnbJuAC0p3 } +[n +6`g +[#{&~fԴ>0QƆ;ߊkʉcךRbڛˉ'Rbc2.D8G&
1Skưi@ٜ"r/g(-9|8U:S69K25zx rh"gjO=fVOۜA}xUMsA50k52&ӼOIKIУӭ2Tϡ嚏QZ=5-v3# Y|m3#Dy``:CAX;U6Q`
l
*M(nr#jI;'p/өh^ɚ
~2YZ*FFװu1"62bPڕsgڨ.ƚ +lz"$ێ]W0e@%Zff-}!Mw(lX=vוxUN9ءuV:P_J|
Z/bQlj^Dj<#?8iP0=nN=lNJmo;N8%8Dazi\+X3v+u?Xwl<`m`j~'
vb!r>Bͬ/թcAæfsEoN?\;J/}
o0ۍ+!5e$F}|o*N;hq3a폣Too]DjMx%KRUm0Ser0oNb تkva~`OU]:]V1gDzaָ?ڜ83Gu3\;#1X3з
bڄ1*fU'<gX=w϶f뾓b8'yYvK|>NnS-TVn<eZiwP*zWcmk1{5mY~gLb̫>,F4@y}lbtm99%U>CFczL^cn`
>Jt#ժND"r2_|"j:`!`KZ"pQ5 ~+̛m*<XSW;h4M~X +>łs>1ֆ-42c^3c)a\Lz*`x
<킚߭/GƉ3c'/p1?qYx4J *`/c/wwLDJŒW!^s*m]}|F +Wr+r>l*- +0V9/ZA_W._ 1?jÏhr^&&KFO[}%ԃ1v!m!ɕyOK.?4&mruCaBGl?}8(k%X4p6,h<AvN>{K!l$}?XΚ,\ڃǬ`X_Akt#eмY+=-0 +382;c%_q +yc{ѱf.ĢMG= lT!#c K1%Kh}:d˭DffEƮvzAp^<jM4:p{+FCr +*?q,S~@kq<R5^Bf^Iޏ&IW[B=]`q3.EL~&wيkj~b 0J[2ym%~^bڂW+ٳcJBJ쾗>ٓSEM؋Wx7kt''uh׃YbORR
s"JN#AU٠9[g{6v,%&!x1ss +^\2g *sl|Dd=-} x\^(5'| nݎ'<&Ly6E4{]%w<oDWieeӊ2OIQ9Qmsu47gsZ(߇qu\Y<M9d0>7iry$090Z/c I)h
tv5m@ؽB,;'wSl+ +V6g=T,NyNCҏͰ|,.?b,8qO&Lw#~|B*vu<3Sٲ1bjvެf=`txwC{n}uf[f012͌Q)cy1%chtI1K{ +`E@s.` +]d.7[I|=T^yAbAg-{,BaGul"82IxuIQY``\HQE7b>)sqeZ&$Un^SskR7IH_S]0$vn&/:FNr;]+Vw#8p[qh/bIU~ kio +!},+y=rϘG&C<Ca-m]Ig>7-AG5AM +]Aӕ)y"?*WοM@
#C4aN7T13W:=Fgᙕ¬=Āޮ ٤_"폧._Y<^hK>z-͢GtmC<_O\~:A<7?bxh*-r86xg7t隊vBPxVrB8e)jt + M־C@k~`le9\&9✺4Md}\ mE/SwKr{ +}m3LIXtH7.i0-auRǠtY:$OڨMO9B&h|6IK~7\g +OLDbx"C[>X+0a\ xR^2]qψ :ho\ڪk8)W|| +~>l:~A6C| ~auaOJ<ͨa鰈WM1e.3;oṟ%OVW/%1n庂`rn"8o{a1&1cs weذS{ω|sy+蓎Y([IEq<b%y-<1Qk$ǝBM{}kHNEMZ'6%OmE<K\E{(K Y."bb<f"'}QǕm2qU{_yLp6q6wjzo%Y߾rX6aX>Y6ʯLҗ"nB{$nk䯡N*4 Y!8?M=}xPQj +>SwpՎHG84.QO7b)M}A=vYM\A4!u +{ɷ>Ľoq\tԹ8^p칈xwDOGۍh +7bH{NM"2a<Y짏+\U#25=\<_mh0m0~:jYt7|X*2z~?>tTIDU
&fb!wVbVeY{y7 u9`۪`Ì`M`СC@߈c +pz<< tVFA_mZD?`~\Tf!<]11{DprzGrFlYFy&EL,FPIBv[\,6E Ó>#W RJM]V[C_Ve2+gģOx1)cl6-\ jFo + +b_lƣn$ +8DA?Etwey"v,p mz3g%CG8=}Xo܉1a^82?8wؑ߭e=G{JL%jeIM`DP_h
ڛo-_5iӄ>9yÚ(߁)X`çКWl%rZ+3wYsM̜5C]GR^h
kK{IӊHeX9*N13'q]ѳćƙv/%KI 1pU<0w)rhP's;>W*jijZ5D7ĝ6 wP&xh;B +r[K9HϘXݵ[bah*p9(cCjxlvaGKT448`@@:HK 0,$;ET.$69ݺXVoe2KX<DF<وQ[pWJG\A^6pK*gsփa.{u0eI:OONdy;AVӱîck}G*1h4QA!6L0+9 {NL<"%5]rFKW";]ZCxO5ףKtBqxA
k!w۰;CحOز{&tSCl?r,~?x)ùfa]PAk¬dvw.
`dHl4RRĦ|EXpm"k"䭥"mEld^Y{eiX!H*&j^"2n<r}@(t+D~C=6]e~hyK'A\=n6ۉFh/.~=o.RBIDF&ӫbW\a`rC2Eu4f)@|cvK +j=\BLyP>(=Lj .aq0
1Ckm%4BXtH8S^ga}AB_AfQ3Y§Ĺ I)8H醰:SǃI_j:'tCxHi3!71\FVn(@5bF!|c5y1}1b=[{iNGQ]Af>4gÌB3='Q>Pނ]?DZtwM`-j2D^[l149| .<8FxcmuU%>LhY:(Ѿ)Gq^'lt
)`LXC銛VD,g_[ƲT㥼t` +::tgr)rGW|ዳw˭}GǚF%]̻"^9*߮'Ѿ| =(50| k0*QqZ<nktb)IFHXrRgqVѐk % - UӤC,;DĊB%O:E:Eq[rZjqx3!zQ0'~~K,C{"6*&bk978j E\عigL$sRo9q𰣒1B + N2 XG`q4P>S*ˈڅtP +`Ⱥnˌr8!j>X-Xjʻ8৽' l0?ucJaJn1~Wd'oBBHXˑ6cQ!SyvʎyfbvTld.@1(
pSJH)hϨ9HsÊ۹Gd<<V.csӉ.c1SD!V{*xu97ҨGdTS̻{f'_oZ<s3'ӛ5R
z7Q,[%'>=T#+af}Q1Щh +wIj#~#gnfV{}Xj`.
sH-!&7O#~)bgay6 +@mcvHn6Ғzo=.K^_1wOL0:+ظ\gcG* +)!!D6%Î:a2g`~bccc +b}z3JGZ]#qT| +a@|j'9cljtxzRt+K;sωdzE9,UrK=iS[`usB־HMO$ OĎz7l`Q_8*s)Xh]+w=vm
崐W7'TYa@e[$̹@'V
EףBTFb1$aZ^bМxI%wEc}87
aeldxv0F'uu@O;a34wlG,o}ue~<f~NeCH%֤#mBvMUB|02dV_{,6J(ލgG:yb%H@\=z>ӫK8Ehq\9HY* +[tɜA"oАm,)r6`ycJSO6-]tИ4&"}e5O!R|=F*CҕdI2HQ:\Xg@|c+{s=XKƖ>$Y\ZJ!'¹Q䓴P9WU(ӤS+mbs2WX41dΰ!7h-:)AظKW#ĥC2f2;<Ki8S;!vXi#ު8l'nm4*Q/whBYmZ8Q_kȸiϸ#O®wk|!3%럵!q.d⤓ +V!߀9d;_ꙶX + +Ywע>D%N#]>dzmij8z +)k9\@qi^!Bφ+#%1HP ++Pd#⨣Ph[yTt$A4lb˶ni;O<GB_I5Uj2jIG7FMT"9=靶 +Vf/Dgd{igX;sOm!y1j6"E蜨)kױH_^pAaJoAၹ+a9먘EiuڈKeu"#ڪc˝ǢgH +--_y5q[kuCwm̮+'^@k|suLüuIV9 +圬^1Eby؊X6Sc.WΎA96EڍY,طig#,{M{GX{jg'al|HpJBSBeR +m(eV1vMlT"gBLo{rF:[0NiH5rrj:h7XAyZ=,L'&Ҷ?YܬPR^34w؝
YGc{.j|HyNBQ⓮jgE/ s 刴Ez&cN
酡8$;?OH;e:NꖾPb'3{4Pt t,%^/qw"kߑ/wx8~~-sbV#&OB[=qrJӴJ:[7ew;߹͚;/}44n)E^ًϏAk£RQGYB etTn.sQC~Յ1Lh'tgcO64/]KdBDh}Q +8|_lQ|X;v/u/>7q|4f=b抽 It=wi|sA(o\bٓ]DֲmIu:VXܔC2&7R4PN +P͊Ч_mCzH;Lv +T0h"[Zj$DR8"]:jרN3X>jN:$ +p'7,YTy-=\Ne(jƃjSVS,%֣g,?va--%ہy\8 +@FϾ +k-E\Arrۀ>xPm|F
t ' +hsn1e6簇1R|4hR\I +-&aErY{EqN~,8[M6MN4pǘol|{+]!U |2rlTĈ cb{@_|#zb\ejU{~HBS}Kc$kTg5Oa=%U_<?:JT,(<eT>R⩹"UfpҰ*CS/,p +$n
x +XMy߲oa4ubT>l.Rbc̫КUx
E| +&Q99M9@rYԢ8{gmhm~rI'g9+{AGkbѩTB`tPF/vu֬!j3СVa
փ-<L89vO&qڵPd@#DGpmľscfQl@5{PCӧqE~$TOм%bģӱ>@
,.Bžf1Ƥ3灥Dd}/ңkBĿ+-gu&+ ++EaT&2﹤8Ix'P{ϸM +$GAP6|bGf&I5ko/Л|p;{aGu>3|M39.;p[yb~Զ1MV-;K@O lI{'aŢW.rgoH?9WF8WSxL]h:S=aW̧tnQN>7꼢fBp(8zA8sX{F'EA-d3/8=uR=1BuɽҵR&V<5BE6F{` +^G Ov6)f&c+tA_#{.ƥ㳩&8f>d"ӸI3gs^#aKOTcM:;|aaWu*OeF;}80GhFM_z|=_z|=_z|=_z|=_z|=?wl[gZۤzbK]::sW{Z`ur<mo^\oko[y,7m,NכE^:oEzsmmK K-~yK}?o9,Z^pK9xz1}{7qYgCLgz#cֻyr-4.zi%u&n$_ͣ8oƑ<Go<=#=Q9Oo^o3yD\?9%^Hn;ql[z5_\|=nullzHt6ΞO~.X`a(O^ +̆u持Ș˶kXF.οڦ&xXar49in0u͘ML*k6vӒ9F764dL2 Kfʫ%l3%yf9ckl14vAҢI 2&Up @/% vfnڴ3a(gMh4UM@
=[{h_p8%?% _EG~WG(hW[#b҃|g0n(郭Oo4K+TrFDK M`kY
ѼGoyFn{Eރ\wFPZȬ4,GIq+&Lr`m<AA=R_};3*a=qiy}p_3-nA)H@DM9ߠIt#>H((,&M ĀLM3%4"2Fj$ck!h(l=%?s-gAX:D;Dm) YMQùClܵfFI])C92Uf]Z6鰕Hp [UbzK.AEϨ!i _.6sRGHc!z iPIIJ7;p[߱"7sIkĴIcë%(JX% +.@ n}/ծ@&C
P @w{FcqdDDPiO0eH3z︧/mAC/7hh] +V1g|Se.i'2z\p42cCGGC!sFTb@$--X ʷi \y~J-|7\-: +AfZM bwhă-TL2Z__<DFa.yKpb|RᝑhLV\aw{Mԧ_B +7ƃ
q?@n4 9Ĺ^V ٟE L~` +d vb_ @=7Mr^ cONLv6yKi+u z1o&%M ~rV(m{ɵ .A8{Q(
3t)"<`qԞAzC~ݣY +Q>9chs#vCJ@pw.%"NmGAl
CYchs9)JИL4|ż&1o@,#Roq +7]4I%zM(ß
1gft6A&=Sчs3A䎆`v8\P7x(@ M y@FϹG@\ LJ + +0;$։[ +!w&(3Me$WQXi\sc-;k$qqh +:SFKWpUdޢsH>l's֣/i@UXħ\{&
oF
{@MBrq HT!S|b0 nǜɵ F
j+l #T]dApqze,Pl + 3 %sH}$6#9%c&hPL<|خ_ob"_\ v>1eV;{9F|,$E:D +ڱ)fq#)p+1TZ=& +' +NQk!#w%U6R>"?w4!le֟(Y,rE=!.v_tPܴV([
"2FWB!/D D\K^=C?=O8˺XƠ02Į0)9㞾"rN?ԍMbHu{[퍜dU W=3(/&Bz$!ul4y~Gq[`_&+C;#i$Ԩzh)hQ @v֣,S∢o}2Ph,|O 34Q8p%<'~s`њ>jHzd}Se@"-Z +$_jJOS}G)J$͡b@hxd*)yuנ"+Mkll"(Arf7uM=CQ_D9?1B4v9|Kx!N gf%/VqY
sa". +~cs+!LG<LHb'9zb~jiG^}{)%RGi@It@ti,"Yi,)?U:|q&$X;}$dlC9o^O;n1xl!T6!aO.gTy=HstA+ +Z >ELFrxք1{PBIFHax_.C[S-*E՛ S삷Zbr:ag0-wF!?ڔD쐒s03;RQ +pB)qRA]=r,%/Ty:͛k
5|5 g OςOT[ʥ^%æÇ(HmC )#ʿ>w+ȹ9\܉[ 4Ԯ$=|@q$
Bnl ++I)Lrׂ5bӵAƀH&q +.&%17o'ٔƼsş:0ST4Éy [!<1XO +-@I <@p)B.,>Bm$W #Dآ?|}12E|9oB
8s'_| +vbw3$_[ZwLe<+`%nB]T'69Wr2*v@6ݴV+9@ +ȁfX;h¿JzG$5XG ˕[hcmG$Mw'O
]%HIQ1Gԍ@Gl +e ٥b1zER +>Wu"k?{lM,4ֳyjB[`A.K<D)QO(SBx9j -FD8P$˯_!&\C=c h_-/x̢G4.M 6]Hn|֮M'>V.ifvۤy5>Q@<ĺ k.=.etJ:@yw_+_bJרA~ؚ|кG +~ +B-$֣Lݽɷ*3?.J7~viQA/i$=.0R?]]G*ݨIdۗM!%;RA2P7djwqG +/}-Zͦ_OQ^<=Ct+Q?1 +9=<ȩ!2/+ kE.خibjo1eHޯ<ȇ)/BuFYܐY&ⰨZ8@,WĨa
LIsz!j!H + 37Vty̩f[ja%\]Co輸״>ݫ/ƛkЦ_ܴqDg ?@a!-2/~Ne\\e_[ H)|}~Ajh^uTJ<qf>_l +~G|*v#v!۟bY+-I.քIgjs- 5CGh5J<:+\{m(ui1$!p,[յBr47%#*1I#!N +dW3b\Xq.`1o%j +\;5GO♝C_bѣBͅt? eso-2χCk`ϑ*";et2\4%iu1Ci= 㴆]w0Q[Q7sk~Ic3Q{Qt:aj +/@."z.sK hA +ˑ<%%
4?)m[5AOCz16g{H> "5AWJ,kȦݞ +},RGYt)qJmC[[w|2S&qʤ3 ˖?_Qe%yk{|Y5c7cjtk]'_PD#?\ +<Kxs +h3}OWC0C"%O:8Z}WP&a +HQ +B}&tEiXPG fA\whӇ}ػsT_:yj*_a0ಘj}3̇@
&y_:*rJ~9{ օOPX{b͏qc:{{[KabDuYh#Oxܑ`*=!fpP>f0t֢8l +O
+QWL
)~2,ɧf#g0UkQ"Maj=G˄ki]
CMqniX+7"=f 1~Y/`5
an/b_o5-v%
U8н)b]96M/KEg|3GVD>] +H}#t+}&M?~w +;Fݣ{QPGY:쩷qڒj>!>t<vsh)H^iA|,p1Rq=B +Rԫ{ATzxq[ +Q\ +>QI4/ ɉ33S7b_R|%dOLP&oa/|ՅKbN) 46W&uGԌ#K'PD♢Olt5jDܰ/z~u!byPaOXGb`lهSL
!Nce&YEJ.;CYq +I#4.*;כE-+ݧEQ$f-:*ɏ-F#ŝɧKk>b([VN"U9Ц4/(^.}V.B+kW4:8-{Fh3;79<Q/ۧu@n{xګrhwQQ#`O2|pi#.ͣ#~x"xz<|.0O3}"x4z`\]P~&;cogφG_YHןK7Zwp$F>*KнA̒өBy|iZ +:qkyܺ\̻ +/{P{RQx3bʕؗ(~F4$؞z~cȕ[ϕ<_u< D\yJbem?ski]O9?SQٹ+n_K +WұF;O-i_iyaǶlů; +_C +kZg!zOo|%d?^0_TOMכQON!h6j<<DMOv]Lq7_[Vn/1\@;36Lo +c/|K%JmTMʂ⧛ҶuqU^?[%k7T!? &\\*p2 eΨaB֎R!*b;i~oƟ~;RVS{s;`BQaYUNss#c(?D~Wc'3fN=ʂ?!'%噦mVWkvgdOVZM`s\yKkNئjBu^*6(VĚJy5>c~e$?a'#űO&*[^+73/G54[{9j~5X~g^v߬W~gRm.ߡ<ޠqUo6h\!HqR8Zɗ_`_|V-3^Z;ުunז*H~Pofv1_ڀ??b]|쉩f HE!<f@,-{֢ղyW閛u;IeWE/ܻOBkR9v|~8W4Y|"A՜bzʧ
>J_ +7_;J5;k-XG.[>W.|W^VlpLv["2P3:2U{,>`ky +&[-TtmduK۬īOW[\dE~{J]ʑVŞ{;m[m;ݹoݸ?;(nVq7Kٵ_ܣWn統qb#k[q0],SδY'[xkPDuI8G_cYTp鹵㟌#ӝsT_mG];\.ŕwؤVT|!{'N쭶m\s-cKͼNn˗Vqf6P˰I}9W?m~ +;u{_ݣ,xK97G3]_ZTe,ߖ%7:'UwXN͇~ҕ&g6Q4XeۍX7LD7^ߴTvSyo6۟/?^ [_pEHjSU]˳3En:][SyV7IζIr?ݸkmeUXfuQ|p={ꣂJz[u$KvN@[vO'n'nVZ{Vj3z孇w,*^-H8o״ؿ7m^|z(U1U|+k^&^Y<Wkb%'cKscݫd{;D~wz}"OxNbgyn#7z[KթFܹJ~_oJW.<swȏn$?#N<Q5Zx˳E_ʷ}w-W8Nꨏk!Vfn{{5B|O=ZKUg[~}|!kߌPv
+'(J_d07oC쒅\`Zi wvλ6oNz''omd`ɰt;S.Ϗ{qȲ&q?ڮ}`ӛ;mʣ":\n 9A',?JW~jdÐ>vk/Q}u:\}{ײ>g((+zqa +MVv)/t9+e<9˿qgv]w^(|ۘ!tI/c|٘tn87S٥ +3gܫ!m//dkNeIㅦUJ݆mst+-Ol-oJ)^ZdTtgo>فlw3,}鱿b"~FBٙ|꽅\kCsOv(C[V>)K?)r/QQŲ6LY[$\xc-Yf\yʝs?ʕOj÷Thr6I|e6swkQQ䓤'IE%yQOʔ51绬W4dT63e͊N|X՜L͛{Wkg7{K-v?n&~j2ƥ@G.GJ]ٚRwyÿ965ڽb}&{\a;RE)wTJ_ar#m*l+תzМYY>$ =)}o?lNmxZK~kI?Lm`femhVrm\[Ky⇦Wnq\q-cYpgߊO\/YoP۵[-Yxſ||$M\~x&z0mx`qK/&gUۃTwaM)%ͱeګm{y9[^˹_Ϸu$ǧ#37qD +.ˎjHU}bR-?n{p(yUytaMU˽$ΚLnZyŻRuL:Vyutk(D&'?VY?n>sEQR_6pފB[kgkI#Vo_x3]Mksק}۲ +> /r*Qw=
u:^ޖTќ$D}1
8]{䯾Zw&( u^mDc,SGz-z"=|->hcnk]'dץ4D&%/߄0ovNS{Q9{Ed<"U\V[HMcn#F.R$=N>H9|KPhWJ2?[y?^wzYK`㹊?^v0{.w9̼Ć܂Y"rܛK<Ks۫ժ7wRl^_R+ߴ*_&)۟ئ8o?lmj<ڪ\۫_Jֶ7rO7kL:mڬɬ=ⱟ}ɼOoF\64mq
>LG7x2avҽe7ݒnn}7gnw{5F7F%Ȩ[ڒ6TFݿoj}79v( +|UUST(,Ϸ5L!Qhn
vOBo[R<Z*3| +>w78ӻ9yu6oؽ:,i|YHSF\ٮ@u+uv<ݞ4Px7?'6
O~\%-oaʁqkW}GCrL}BnyM={aLLC|_g[֮ N Q_{yNhoCޏȱ}Ib{ yoW'c۞Oq } +mn{#Zry]hSnJi*$ސxh_v^^e2xv7uV40,YYd53oя%yKL@voPTZLYneߎPJ̉&*Ox&U}.*H+,AXr",Z!~wڱN-F>/6VYk(231{2]f3<3~FYFd6KUaZW̔㘱1.3Dft/=f\)9qW2Mݘ5#7][OmݟݬFq&0V_ua99Qy1
Ea
wr1
SߏxE/Pܦ6=)d.̠dӁ?l +<4bQ ]äz~-68۽ Udѓ=F2: +Qyzhq27ՄOκ[ېX''s螎$?ۓU̴1ӈ#sCJ^f +=.lzsnHCEe~V_dFs\S/cڛK0GbEwSeU{RƏ~m9S|%.Ȝ7 +-|t+&м3CJoG2O"]hAm̩
9kӘj|]PH`
]2NÉOKbf>3df$fhYhM̌!7W˿DpĄSm凫mjc._YJ|
0>[oۼ2ӻ=3n
/`dFk
Jg<!^ 4>3zB˙1ÿgF\[̌Йnj36`3$]L^YjY@4n֣I.ҀN\*$ٝ"+:ͣЊ
!{67!ZSd`Fi1C{"?
&OXK&0#{Mc71p3z:imf%2_5Fx}#(Z|Ko=Ww'aPyw䝸uk|Hݫg]?&|D|/#|?(7d_Č]Č3V=3~)3f3f)3jzfwqS9faC`ΪsW2$gQr'65fٹ5j3s{Ebf=u^PFߺ.sJќp9C~/=>ghd03Rs434fй?1w0ّ3vȌ`FO3'0#Ggf\fE/7붵i=VWTx)!5 +Iϻq/PaPM~y5=*&5{rbS6zy{=!Ìό85lݡ[ƌ5|3bOMbMs]q̢m't55K?u?zjD[dd=
*|X1y Kĥ}kV010-!y5̹ZsAd>gk[YBs5IcV%nwk}f:_LW3m.jHȕٷ5^{nFȈ{ϰu{R"vQ{/(vAXkU ґARl ņ]cXc&{[v.9g=?Hu͙k˘?i^wȹpin\/F_4tӢw[=>3,g=n7{D*{A Q䜆{'!۶ߌ3,&Hj:̰K9iv,3nI3s53ֽ4qaFx2Ťq=cxcX!p5?:;\*|xj
74]!uÛytRÅJ#CLc)͵#eJb#N Ϗ$9f@r~VNLQ7GI@{|ǩ(f8f"fL`#3ʷuإՒÆ$}u<n熺5+t-ɹ}q_ɮܖ7OswܼEicoߥOdYOӗd\'c8>MF0Lc5ؠ3Y:f@f_fP7f(OfЌf2jYGMl|d0d-,[ϿE=^[uu}C'[vF>S{mA^wڿUM> +'yָ\Ha=qƌr!N|Rf`?IkÇ,b
]8reV0ffjfL_qxV3Z.8mˮfy4(8UQ~aM_^.zhK%[7~
S|l
:N+I_=/ a뚫ٌ_ϫ'Y^ҒfYC~d#x(fg3%;QŌq rT@̛: +f5CÌsKa0ӄ&yG&꾱aR
ˣgkķm길ڦ'C~c͛9_ʮk^9=29́ |ؙNec87\aɥ
!6?o~oZ<{/4L\~C/D?ܐe95OvRk7kxy混}^$߽mZaȌ7=ُLQ̔S>3seytzy97-j=kf]Ͳӆ-=hpr}:}a}C bw5"gy%sm +A,cx"'~be.3/_ҘA$d/Kfqcf7qg<ۆ9n-~on~fo.zm×6"_
!ޥK?]- {Wƫc+/K| +lI+HsP쏡W:1O`SUC{XL=*f%LU3SӘic<3~3a'3e媍楞5?ykaW5?^TKo~Mrs
RAP2&JK_]꣭//lUpmsJew6VVwF;7U=<#j3l\f fW<1|fa\k~jA4[borU?f~J_
+
*_
:
_^4x|2/+W}yʢէA7oc_hHS_i
T0bDXnvC^<JϴV1:uؒq1ȅ7UM5KEy뾲]nDloo˞4Ê,jlUnm݅`p:epiT<1^ٷi[+SXc_O}u>v.M'tO>6Pzh:w ^Sf)a6cZۦ ɳHg3f8/}ot)5+jH +!
\M!@ξ5}kS4HM/*ˎ:(:sVGb,C*N8ߟw-|NRob&Wwn +z~p^5ռЇTg
-n5NO>C\}wFq \?˒>&Ù;QÌLf\zѡ~۰'bs;,h|O6o_^1(6R$0UJ3Wo9+|\.=~<:GaòLc 76֖
5̓}^|/.]}FFKxp{qw!:S9U{F_B=H̘2eK +Y~A!!k]_J_ޕo3wL&0),+okEtd=iU]rસhf8凿y}%rb݇_b2>΄r/nwZ-8C^B6qUPVfE)̨!4Ft?ҌkBx6rPs)
K|fг}Tuo7ޟYpu/ſkūn<-Nr9mQ-2j<_^ʖkUln<S8Zn;-l,]9Nj(xA!7OO|=Wq{]5}1GhָfojCX;1ɝ,J:k>R_[ +B3]B1͵_>Nb@2j(`0f2\鲳⃕o$Of^=ѱBx9Xo<bYBKw'xenߗs_y)<|ytvLkh^3}<3OX}ޟ +ޖ`/Nܫ*i#%A-re;ef
ư^LXBeTNʡ:ً[R<v\q +GAG~P0ؘ̆3"̗dpފ-PU|77igq$68m7a%qy2pAGUw8(QtwVt@]{~"1O_n,wrx00*_TJF5fBI0+3MRoęlv9WW2WezKwVy:EwykW-3s!K+L,6Lޤ u +ƭJ_'DJ7+R]2AnfZ۰g'e + ` +z0s\,^!ʎ+0XVoAo-ùsړuGNjkt=
sG'm@0 +y&GԿU3x9a瓹"Ү0 +hy4E|]|/>R&pLUyc'3K&f1!pBaE}Ob|aw+G/ޗKNn>'7 +eb6?U[osq yBn +DBh:݉a7BˋlWwv?Uug&
NV?\mS}eJ-avcK̖.U0^J4Qoa3~:Շqx-^ޣE#'{z2n1>Z3}IhJmHIǘв
ҕG?c_k?[eF]?2W'V[~w]~*}%^̭=0+>_>:l<{5ߘvn`O\At?tAWC?]CE[+w2?jz>[?} +jakTe0@*h&2[5P;7!O%r~TRuO˄Sq.cS +i\և.¹w*c=]jy"#GS +OZ +Ɓm}N5gjUflH_i̿R{uü + ?yɟ~)+렎+P^`S6GR1w$[!G'з"t +-2k#$2P#kOLn.ڟ.|'Z>N>
"Tվ7B-=o?y!?UއM̷yzL8ꃱk3T-Pu:+0=~3~42f:p,f匏 ܛ6Iox{*o_%O4aA6X`|1"4q[/͕:,;%[ ? U{`btgZ +nޟ7ߝk(¯~Y8*H>"B}^ؚS\:^/PW7Sƾl2,ӔKhZg>3}8feR74"?AvS讁C^3A +zE
Cn880B(4H(mu`۞,}4Ui$xbF+hJOgRf
g FUǨw};}p;%'wn1'2TXmVYAA&9 +L=uH8{?+VvNLޟ#>|r-_d;R,;n4 }8|V;y;B>H?+Q` vE
NDo"ׄK_.hNm˖t
WUfb{Zb5
6rZnVw;d.[,2YЛإ TZnF2i5kN1%eDdY3SU0P;8:B#E뢳,;G76_m_w4h~U}R 4#OT꽯T\Ɵ~-h<NМ{B<RU\I,jzC{OeN{
F;R~5gvݱ|aulnn4gs?Oh}'=j}1e~歷N!..N|uX,grm|3MPvT4Lȩ[o/n8)mB:9$x鮑b!%8O̵K(og݇^`5J"}INN?'ZqF`%XIm0_~pWznt8)^au%y }ƊIo܄
Nwql +,㉯Xg'740 1p'XaKġ12J9UeyA,nxHsad
t(3*C>·K/@`xa6.3:|jk6$ƄmcJvybV{&8H$Q4{!MC>x!gA!+)g19JRfd3:e\T?Xa,NcI`Fi5^j)r$IUƐ})BRéyჩvv=6Ki> +>xKOِgcM叟W%O5<8BH(e+MwRj`65?nȟ?~4QEduI&>vuV\䐚dCRT35ṁ2jo>GwTk>Bمe-#ɧg*1xkqdMfJYCNkbNļ:-GƙI*} Pˌk*K/+T<z`i6J$Ò(tmrAJI4Nll\sp2=ڔ5}
}
;r30Cnva`j<Y9@䛯6G%tSL ++6"ZRza^*0#ne9*kwVUتBH~Bؿh,Ϩֻۉ͔6
tthuIޖjm!?0(nsV@yRXl<9]}OqStvR&3G<]'
f@R)\{hʰs{sBt>{\2g'O"#Ė[q+-4 \T9ka棐Hθ̟:p3Hgg kk*ΨA'>؏~,fRٶ(ܫP_4\ˋےaGj
]'X[@AF=t4qZF-MщyRRfFJ-WZ/.7~8v-D&n:_l/v ͵]Ƿ:aڲ[t +X|ڽwUoUlX)9>Z{2t=|W~4w=q,cĘ +K1В#OE\iT"XqQØa-
l"q^T1uX>O?s_xڟ;\-DMVT1,Cp|cˁNN7_+}\|뼫麭~"4%#tEuC5'fחɭ7܉mМ6BŮQ$M<*[hbm{IݯnuR𠅊4?fXF k;.چ[mC/g-\P m-w]ԵJfШ֟whivTs\m-qg-CRlzgvwj-ɟGP^ߑ ++^Gw!w= +Jztऋkov"RTYp6G}`|x +6NWI(fm?Yܚ^עC_,?`3˕LNӶ,uS=lO}=DvTg(i(X]vwԇ\G;jPmBhSPmxʥ3lO,m˩9Yb[Кkjȩ|ZS>I."zA_^|o|4^g#~4ëĚ}ƳΚ;JϽxZ/%1r;?o=u'_P@!H
P{$^oGkNL\pUd)X2usBIMDmZAs=^l]$W=TsM<Z{5PrrmZ,%hWof.Ľ?ĸr+Ƀ/E:Ya&RxXˢJk#qVbVB Loʽr~eqCgi<+B.8nvm< ,u}W<3%Y1=,n?|kBk|#S~4cѳM5>EЎ&Yhg58{a}PʀvQ*g:]0!m|YEyjgE;/7NK\mIurR
uбor +kD@="%I[kɲD{GCS6os-.|uv[$'~`v͆S5Mm#]w<k`qKvyOIwܐb
PSd+¾U9 +-TAH[ۇO4ZN$fv֎;λXKS;V?Yέ^쿵Jz3E|ѧ#1wC+6l8rxv/E^a ^u|h0frM.Wɫ3]==+v,Ifq=)J +#@;8!G~ץM/eSjF9"\H4$ .媮1@];Y˭%Rwd6u8BBn<9SM o!PJwo.es.G$ߺL_i.*$qHp홠%OLɃ|ۅjgQOvU,?EmTnMv/A?Y%=YJS;+cԒ9/㋶;
$eT8 +CVp|t3PdKz8ǻhɵYQ}^modBr!&_Γj5Z'ħCM}4'5yd~/<'P1j}ScuDox_a@w O!RWmn?OabLCN/+IܖOKj9ja?#=.BO?,T;4MYgmje_5jm+6\>&8[z<w`co8]ʲ^&o,l$1bE[B%|ŹЀka؟Ck8/Y`cߣjw7}4'8g_kIҶ닐Iїvֽ%o4OH* +z3jga} T_iMXm͡Qx`cg])Ю$Xc8MtmSz9>v1-x.bz\5'f-8XׇŘB:RY+=wh i6Ξj8ǭZխrU_]?pMŊG16B-Fƈ4?SdTaFbx..w`h'KLvdܫX>_JE,t&Ju{~HA.8+hAc$kTgkn4q6[5xzttٛS}fĝfǧkH +
oh +P<3ߐPMh\&@hzCyyq_EΏXj:MhX4Nn +:_z~_۲D'tO2:e;Fʫ*>DH̷})sH`w>wvD|H{GKT;Z!XZO쳲9_hghgɛ?#6]A[I69\}h uyW;FQ=cgH[/ΣZO_HhUuOn=jOLt>]wj.{!*L=8kwyEtm+\Dj,Zk;>EnDre$*Rh9|ɫmͲev|j
D.z(HIuضcauc`]`T4RW4]6hnGDŽ3t;W_&XEè%MɸA5Vۈ4G8:^zmD4坣-m!\Wԣ/ljReʮ3r=ۤFCOul;z<n#m86kڣbeRp)ܿ5E
CtސŗZϦurݩ;QQ,JTI>2GJjg9XזryߎZBzxTRy5x
WA|r۠;EXCj;K5ܵOM&ȇCO]]uLRϢo ]_.#3|Ձ%9rt^7)8Z37]Xn֯4HNM5Ww9BwK$gc
&Q1KKzQ-OY5Nз%IT}f9s ,֢83hmzjj?.|0kbLK62k*(^/4ZFBg
k%;<&$mСpiӅܒΣ=t? +c&q4ڵyϗiRJk!i2`_xECfCtk!՝>1Cl|횽㩦94oIGs>@Su4ggsM]oqeV4&?8 ~&S7Օ:{BĿMpOO:y&TS*Ze<
y2ŚDe6 DC^{&~
@ۿ\$n8sK\$x{= +b%g6DΊ>%^B h֫nth ^Xh=XNL +D2*8'VVȫ]s=zNӬ?0kQRbK5HF嶺i<73 +bi+M`O_`ɂ%yh'[aRnx4~
u0lz/4ȠWwd*[
NFw_NxZ/v^C=ԪnAo{~g!|f=%XT㱛v5tbpj}Wѽ'Xk +BrǝlMf{:$vv7nFb={6ɑG`͝=c,3Vts@NH̦A2?e췪;Vz0zləb%Ҧ.O/+F +v:r͟|bE0Ǥ.G~ \۵\ӕE,)gYhKᰇdJCW[iVZ$wzh2l\ָ)MRVQ_.$>u8]#57p/S1AjA.;$oJ Զ1Mm yZs@O li˻w +5[vFIu=GЂ`O={%>EnMg'T[:XͩOp&yŚ C1ǥzO:WЊ:;crJyl@Əgb +ϕk{zb0yNYُ+%94;N$6Vv8hQ2ܧ]g: u:?l8>^w`B7/EsrmtE#h\>:jsMjdD1=7{i|؊N2N%_J_H{AGn,tzHH#+mtx?x?x?x?x?ر CSCmDon>sUR#SlmOwKI] +δe~,Eg0vSW4]&ÒM5$.TX]Y{l:2=&[RYmI~HRJhEfo9j>VB>?mh$BR"DfM8M1HD +f&DǰbJ.4DJ4|L9B1oIy=U͆0#-yU&:F9Hc$@ϽrX:lWAG(g~^T)=J*h*U؊u:{!w.V)К2;)&(:]L9Ow@>5chcA`VE)UF~J-ڒ] R%9k +K_ioA紸_Pxo,R0l
2jHe֬hu }y;!F{FN7tIBTve.ZV767R[7;Yr;k27k;b˨a?R\aiJ0
[k7];+HvLtP*II+4g'R2tA+4UmABW<c"'Q"dlR`NJ}SKl5QTy5r\b9\>`*F&$^55Ǧ26|K9:@"BjH;JaڢxyrӍE` +z(BrZGƂ:N%Ѡ(P%<Ke]f+n)UVf]$x%F;̤ä+iK|'|ӥ$V%cjʑJ^i(3~bz>b"3Ȝ7RrzU|?Zň^+A( !ڈ9[YJo/EPRL RX[l"iJGblVo2+Vd?hYJq.e6>Je.T}v@)]p6.12%Oǒbqd*;tR^z +U쟠pjnZJq9Nn"k%>LthG*+167:a3~Z*)9]=357ukS+iRl^1ϭ]{kc@D;:Fj[_ۉ~ɅrQptNt#qS7A+12.59n&muo]WAwd,a!> 2o.7!`W +_ŀĊy&_ϰ2y=I|x2ā^f6t og?BdSh->dyi._-tGюq~e֚
څ&D?^3֠c&ؒI6PI#ٖ#hRJ72*̳@7r:`[r,O2p0yI+E䰌O_{&2'/hCЄkjߤ]s`|.c(XbT" +7JX#04':R5Mݶ
fcpB>C)9 TLF_:`kvYWw.a.IN*9vܖM0Wrs,ѥG}* 6pIdVP"0|tz|FPWcH\XY` j*eO"1FE1@`m<h'TaM +?)$"79etpӮLjh(=4$EۆS{;~>7c@BPu#ƐSIU7ҘjӫS
(S͠#>s(}
TVd(mI#:wi# Lc
%9 +:![ImYHm lz%w%6B8}բ~5c.{Km@bWµ\~/H|ՀiICtn +WD;J9̓N,9K5 +t&~TttkWs:AC(OƓ2k_d>js_#0K <!>5smi%GA7lt IY\2?(1BrB:O)> +RRGI㕶GR1y?:Ca3"Dώ||ڵLrHUv@%
ǧMUj6R_If? + ;2*̑RfK/eݣA? +Y.oEIUw9 + 5#~> +$J1 1I% 2˜'ZOiUu_F:k]ɒZ1ZB|7!u{J&uJcC aBL&$/f_u}(~Bh)dF$xe-*RL6!$Z!iJazdz#Œ:]
UԸdU5": +6vY$y`RR?l0q +sYJ?,[<A)%kJ]Ubuzww"^CG_u5^4ANVoe:P4q@I>#y5T
(5Plۤ>-}$qӏ!Yf(3͠ +E9pjFRゾ y՟oE
cq +*B7aÇS>E|lQ:%kOEA &z/m2@ZA4ԇ-+@*U'A[}$Xjsj{uChӉor1U=X뙔P4JS{9|sw~˨cӵy@3c+kn"Ua5g)o`BLnoDžm_/4Uk
[{r&[n̗?pۿtVHJqpԥZ%OJi~Lu'&Kg
de.؞zU u?ǯ|COѹI<XOʤnv{0?sQӁ@sk9/2 ]Nsn,X)kj" +z6AvD[UN@i.]̫:C@OrҾ($%Zp~dwvXc˂ƍ<koWպݍVlkY +"ȍK/ +&lФ!_85wg9( +Kק@~>&s6^W1ǟ`Ҵu'IX㝳FVn]R~(M<@$ԫr㑼7<csܓ*ԩd\F^r +}!`(:PB4>_[VEд&v~,u4u7A +7 +ܹqƱ+ +MM( +0>~J:`9U̟MVh5NY +hyqGSEy_7|!ryg%_(ly&N
"c(BSQk,Diط;85XERrNqh)؇ل +C><|(àHe[<%0.:LӃ\s<Ԏ/$cw߬1ɐdpN q!5PCgS2!0W2?m@
B|fNi1'Ĥ!5qt(I!Җ߿=&+,BFa˹rg.#bv}A%Z&sUq ؆NvWJGMuːǛN 2N__^o +{OˡF\r-㔡LvoQWs2#$kPǮi-)tݾ7r
1ǾK};En?NF0(xZN
Z? @
wh2?/5sQ&F2A&q~4N
wc<!}w&L5ǡ17L}&f3R#`^.؆ʏ}=2~/s +gMgȩo B\jRdl,AMA=8L8T
j6_jWFM|EvkcrA5}⓹ +TȨ]tPzP+߅njףUDD6Pt7U\p7P3%uLrS@܊(ALNcnMx&vO5蕀<p~ \>|Xxr&9OP&ڎEK3OQ ǀ?jĘ-~S.E4e;IM{ +rXb@ɳab*W+?9^MYM0chgԁ )>ۥD.
V0@z;s}N-gPH'URJAe9<{ qW5!585l72+́ی +8 +E<=q*J=.^S_$ıe\?@aZ/jR*y $\r +ypȭH4{8;6qo#y)9eI=r +LŇ)\=0Fl63.JD`nqVI(nIɟ'J:ba;Q>i+ %q^A
A7-c95SiL|:X&ub(ڑݼ*%ujBߏ.C_0=\MبMl +`v3]e^j\rSA}jC^k2W
P0O.MNxP>8#\vQsA
rRg|UKz#`f*=P>r= +\C!py芝3oN\}Z8F*(JdKКAIU{mK)1D攚rpAvJ05۠dīDXF.oQO<20p
(K=ozz\L/J3?\׀rkK1g +.ܤ|Ww6 +xm\\O7uA!f.=G>1uh>~hA8l&uP1ҕE[ +ꍢ~S5c_E.N + +iq땄"ԮἋ[!;Se "|ru 9h:J.s#zcu(G꩷8=rKӊ3&Nُ%[qP(aӁymp@yz7Sz})L@ơ{:b*:J^
Tc9>}+5/x)PX|=g5K@
K╳ )P0+0`%pqSO`/L`fP}]ʠj$.F<P~$/rP#p 32l/r+W:1TΠ/itPfPS#T8徐
(짹Xcto0-(LR\O4WBPjRT@F/DtQZ֭C6pR?uT̏@[~c 9G((Ll0&cwRq*:vj@%sحFlzX頷ˡiYTb>qJ!(sFx8<C%Uױܳ*NI@Г>/8rKq^I0uLB.n圹n&n~ҡn n~f0"_3%>r'}"~2m@%<k:,}]wVCBtk3v^[*;i;\*gr
LBFiQ.>TN'őE22$cwC~Fr3eqջA3}:Oou@sIͪ&6%"J4d=O2]| +v&˥rY+GR*z* +aԴJkg4Di
HxDLNk2]
zG=zf>D?֝Dw-yLaIVZt_wY5̽&^Z qv&K66
UNF44zt@ z
谢"^90o'@>x7߬jiV!*Pp^/qk1u\M5|!" +.|_dIun]AW,i>rpYdU)|CH&% +3Y +퀠Z\uF|*z2}'K]$51_"yYBjYJ
_])ӡc߳xkU6֑6-u\bIg15m(jǹR(xB#}G7a&E<o? ~VwXf)NZdl.aJsoiՈф0a)~y"?hHsj,~TiF'k jw]Rl +5"*ֳO9ѧK2$
w>>'2y/ysDy@|1$0Z$N> +O?SL¿/D$W^h)iVlHkc@, +GdG([$k1YlĵiT40'j_h%{Q~F|T+iK3ZIzJԬ'N6bҺ'A͊~9%/VD~_<x8]OAJd|QF'j\&˒Z_Quiѕ,,?@~vdO[QoY&?Җ#tQBz@}LR\o#{pN"ݫ13);yUⳒ2"0uf@SsK; 9cQzCƲjCe7vMl!Cd7skIsN +~7 Re¶c^h`ێyQ3Gɂo +_}鲮3Ҷʲ+TV%)ytP̣v$ɜyDZ('{\q[^M2nvQmzDGCNAf]$'bua:~w+>+;Α/$tN
|+&|"* ]rAa>X2'lfkXKEÆ~{Kj>x49_q)Wo]C!_|{I]E?^#]M_3M<#zUvue-c"k"]Jl%/kNH6&VIF8$bL":_UM1Q͂(W2X+Swsk͐nN +( +.qN9f咞_i{7Uq 3AK!a}DҀ4YAI1A~%o2k)6_i>).nYZTi--7ʿ*~|AmI^9U%$cIi~YIAYLMRv +.Gl&kB{Ⱥd}U뢾Fo悒?̈_,_R>^
6xJ?UZv=|j*>^u[<"*z2VQ'~`i,,4T4ʜ~&a~ϻU8$iQv"!hO˼jTT\V;ɚ*w{f·Q^E=%-+u,\d6`on;1g.F] +;y5#t /qrwCH~&vL[@&|Pn1z v ^,0NB1k`і'7-><`pQU軹CV]bHC=SDQ +b%uNݍИ|Qi}'5n3)h"fuy^?37w[gFm"byx
j:$T`H~NdFSag¥nuem!7|F7XsX2㻶0ktEYyӎD !U/Ѳ*/ˎQGڟFVZ\$tvV'LO~7>&w'oG:'ؓOեWLTo6NaQoɖr +(15b-FHTI>>~scB0'GAd8?-| (ؕb7[Ӵ:4MKS!o|Ԗj=iZQ"-p=1OR/\˗_O<$G><9"ZQ+Pa:Xr16ֽ7έ769ң7QR|k;oNpzxpKxZ<>vC^or0^>.GyQ%
'H/I7e7S̭fF/ k|v93\sjq'K;% +k~%*~56exI][S?kx8`wns.<E$ɘ` ~X#+?gh۽OC=Дl(I+w߫ɫ +,m_TeTPaTRcPYmP.*)SjVWy!)´uxۢ멜)'C@&ŏ;n|4jJntRo㢴Ot~pJ>R=.^ J9i'xxל5& +0+wx9=`0ioGw n v_e'/*h +|hX?YZ>]bsXrsX|KGOS?`EĒ]sf%Ůf
E!ⷍgDJ$I(yvHZxQRc/ҚZw +Djyyk\z ~-iXr1>D8"C$_vg4:EFԺƞiI`:꜏vdE6Ƹ{GF:oWÕUiӫ+baY%Rc홡->&YuUQ~III} +
yUla^^偑NUqn>ޱTgVodɯ3:#<-̮.W\\`coYCP,퓍|3}2ϴIgmDe]k`zUir>$)~͕Y/=Qg/?9? +]Vz24l}|crYOEs1@W%zAE/~9dTz)=tsFYz_y.~՚v;q7̢ܤ=n<xPm{As}
ӺQFOYMM7ohR?y#252%̲Wa'kjJY~+giA+{{V|r'|q0la%Mˍ%6fUZW&_M+siLvWv[q]\CJcUHMedװoC꣰~)/.ʎ]T{-=*a/nľ<ز;'d[FȈzH:ѧf/-0*b09OUw~?%ZwDR="GOɦˎ[!tScCtLchjNWBu[Gy;C8C||A_l}VIu`%*¾έ;BGGopNXjCDQ
P&x2_J情rA_jFvQov^a@C[vE*;M*hÖ=hڠC0+OhmVE~iwt(
+I~u-<ϰ7y`OUN7Gz{<M*eEb\X#2KX&
?n1/()oZk +f~4\7g%/F)Egh&9RƟ='F+ +2ŝ)l<4ƿ5MP&+A+y!iТy[ʕhz.*V$=(zY;y[oKm+w /VR#..wW#_?LNtruQt9?>Cď>:[SS&:ܟNY/o{ԋ[?(Gf-F35@84Ac5 #eh4e~ kvxZMڴe[8*ntp'joT[Ф|7BW9iaOob,Q(S\>8%,#/u&Ku/}?H6=ݪf;5ehHzi5(.]&h4F.E>)pxco_)rm$FHoբ"Wy<w<Sjhd|y./9N!O^;dyI1m>OzOOΗZ#^7z)01\5czTᵍ¯J[8|+
~oF
?ND#&fYS֠u[U38nG5oZ54K(1EAn~=O6:dsGoKUpc7761@g5^H + xMӱMşj0nuan,{3Y\Rjy]e4a:z<d~-/Bd]
xcYl`_*p
||Іb2bo,
:XƸ=!ZgvY)x_#E:hs$.VTf{$?Wؽ7nD<-rz1<79K<ЀwrRpkaiwo:ĕ^*w[(_f)Llp*OS"g?aG⟏9}a:BSF,As#$zVio79į+DKZ(3
aˀWb?Ԅm˔3;c^_c4uck#-_H/?{]_߃*uX|IC>HϵɛЪcwGhf +YC-U&^tCbhMK:EN1M.Mcj_u +9,#LnTqg۷߽#hn;S˅h%Y;lO;3kyεawss߸Dw)wI(-v{QSP./ ˥/wyx` +}jԻ<ȲBwN9cC|?콻\U_^&g9?2'}T4w4o4{^4w&^,@#hB]4o9]-%^:pK'Nr"gbۇ:95
vWXy'uyC>p$4Ca=v=e=;k/ZVo?mq4w]DY4kPDsp?[Gj'h~`C)OyZ-kLqKoJ"*"kl#jbkJb +)M]}J4sZ|vSw#ii{[a-EOzh;e+*FÇnx}YQ.!\{}R[CB[KxiԲ{_ZϦ*FS1Cj>s-@N״Qьk6U}c4Bs6*g}h7ZDdhLaw3TKٍToKn^!1k{! qam]mvy竣C4߲fOo_
=DJHiB9O{!1+?{7n=[ܙ{тEZ8Z +-rFK5Uk4_iVCԞ+yQWy!A]z_=9/)8gˮpyHWWKg^op)c߿)
_l38ۜߧ+[f[[}#&B3cX)5G,'В=hQZZUQ;n]vFqIiKI}CtWvk+s~Vb:Smtm|Y]l\kɏkV=_d5xgstşҤ^)6`;i>]#ZHiZ9_xր ;oU+M6lqݳ;8:j16\eYc[gJ\wcBG=-,ok?x-}_RGu:W
L|Y7{7;g/-)hQB&Gr6<vI?+`7iw<f[ӶGosyEO߸ J`Lb~Q +ܞ_,54s&RZa;h
qo*n;_5O%u_%.)*GY'oT:qg4?a7a|k1xR͊t|J60(ZAXT}^F俩30?n<l.ʞT䷜~'"DM8]{6uB7hj3x L廣5:к=G-$ZFX֨ +3l'vݗEZU^kb˚_;X~wU훕[Iu^|[R֘WR +\0pic>T;}n*L݇gǘV7ת؊R +pU
T +% k<"t+ᗳZN_\ܬg^%eKw2Wg3ϝdR>*f7l04TȚʼMn]]0W]x6H6{
3?XkENvn6]rd5-920a
m#*D=χOٯ{F + +=m_]SGfŃZwvd,2pԛ9|"6?kj|;s1u}[ߴmQnH)lCfvQ-~\V~ЛnV>([W1ngwjFzAP50xbMY?S6<8⌢ɁӊzhǪh߶mHgȃFM}_vs |oIϔ<ϤeR{o%x*|0v߲HUfSNaJsO(yA[$
J?\"yrPZY}wl{AB"BTϋ,bH[3c/eO:1^!|J)})T3kD;TѾf(LnRuk,!x&OxN5],uMT&U8sͥ k_*xD`0<F4 + +1jguxuﲛ
XPU!mc)2^V_/F{i })VEjk =hD}HW x$,N4sI\bvc%<$u 827̳*n|ڇ$R&Tʋ7§ N&k>OgRܳ.61J<3q"%f0O虵8nq
_UMJ7RTn|al3ԤowSgæ|A&DL^ +b,:4LMeڶtڳAsGu
ϲ,=%i0~?֊fJtQ@eRNs}\J4ѳkokfģ|o_Qz((}^ ymG1Lgq.3zj1tQgtQg.M6fϖI*-lG.7&)To=Rx5Yީƈ133`n1}pEEI:db0j/Ң3 dwn
j2>vK^ +i#.q>c@MWfa1KF蒒z͘DN_̿>8C|YEx~+}Vw̴}=+51k}D[pQZVOX?CZ4)8f3pمj4^?؈3 ++D/\:ӌ:j=8=\t `, :MyTArN*KR}:
wEϜV=6{YzJ׆g(y6Jj2jwo% +7Êכ؆N;1],89EC-n
S,'<lY-;-0
<SBqLLdS̕"BeLJsAdK\&f*^aB.*>2zHAjQyM2:"nWNκ߽ԵDxVN U{K0[O(w'\ +ZR!̹?4<0KsHKQ<(ȟ`[rŗ_:xn1O{f-'N8!ιq<`LZ>#Wqe}ԭ}ѭ۾eD=skjpY^){!oOZOCy_.LP'RL2R~RN[U"j7mK֣]6#]#_6LgD!-3FVSa xFf-¬?P 87*Lb~2YfvgQKMF;b]3}19fbn +9uRQv#?j_02zCsl;u nYAߴV}K'n3I)cY=a.'L2rmhOЮ0^r +i;-qD^]9t0/!8=L?}y,Hq+QqMۃDG-2Z&J̋/Lɳᛐ\AD
duGBAj^"Lj6&Rh/y|9EuDof=89tD?q3f#`$8Ҟ+Z/qO9IFe:P6ŦIrXFG#idi4-cq1w !JrJ!ޔ۾~㾵֩3#Ygʌ8֜}Z{U^{-:֞O: cG6rEӟtyw+Jų ۶ț56t'[x5<K>Nóx~-nM_3wwUج=0XâIQkHqZvT9tm_uSfͼe}B_kؐ +;}19+1F`ɡdhSL\^ZKrŮo+O=lT^YENXXc*'9iim=6ۄ2g7}^1NA1 +<Ű#_.dž_6ǰTwFÓWk^csu lyP?1?Ǝ`tspmПn<sޓn;hNYHY8}/ؔ{_ +<7|d/uI[;Ü>z0&Ptk;alI1cR,m=C_qFR{?y[~3|^;M|Ow>"
~<BY|Ϟ(QӂR:y_̿
[ag.: +s6a|(c#wn;r=S*a><uMŢ+0.Le71/Vsf''cb,:YL]yӃ_\rG
xhrpcׁ}K>;ػHwn,Ƽ7:'vN#~%AsvBbbn&1Z|k"o}˟FѸvB1xijh~z +<.g07H +a:#gBKws[,1uЦ@9vxc{<xSCm=;&W>aѲIladʱ Xڣc r?#rזs0.#yP1wdKWc:aZg/XwbF->bCP.X̧<]7H紆ۓoOiy12ʆ?TB˞\x[xc01!ԾD!ջ,rF]su0m1l?x1QE-wNE`Y|̉:Vdu%AGж`V/8Eao<{%7a3<ÏR̻U9;1w_OZ~Mr<ц?PG~EKL9=1k8;.{'EuהW+eoZPV߶|y`VR4$߮kOb@kf/hbgR^y m +<F:]K/_6o{;l3j0,pV0w^}ESz@hkNܜDxI*a|aLZ_,زH7u\ZBG^AqAtMxU9= +˅9B .gjdb|C-b1F`3B~[k`F_,GwD_]( +aMZbT\`ǦSz'aG(&1u ;N\Bx=Nw7e;XG|3>QNW| #0V39rwql+S)sȡĞ.9GḌ?3Gy|Jh*r7tzd2i` ߾/}G/yBlۼYs`>ʂK6cɷE;'N&ͫ
S]&_%RJ2.:.l_2tY'cRe[ΎݽskF0gT_e +c1o}A~x9zt@?V7ܹ
eR[=8NQ'o|˛bDWZ~?;b??{߭<8I,оo¸_K֓)04yal0@_{Sc\8ͧ7<盞<k1`n
|PI6xe(В5aC`\w*O}v}d2Ɗ<jgBy.<~{5.$}xOȁ?m< ͍1ڷ}rރb,C8Ťsl5ޗ"<+rO6ÿ>|s]Ƶ;.|x*j2d7={ȺiOs3BG1?|opI1tpKIw)>\%
Xm+N
'T07B c~3%][W*$:&aɔwٔ9"vÚ?S"k +i]\Xzo9e=sw(Tq!|37pYC4uq&keOTC"+]Haog͋9p]6|O`/BOF}Y$뇹f}SOo8&B+{qyƸMwmfg{\s=&<aeGuG>Fvv`/UFƺox +{[Ӣ2?rugknozm +o>zŷ{ڦ7}?>F3]m`b 19HbwEknQc̭4m|pgE]OnΟ b!|x=9z,[kjȝzT pMķqK&@mZe- +[-MD|fa21rɸ700﴿
8?[` +=NCy
eoˀ<?˗J'[&ҷr?TFrgaT>3wr=wVM;eotXfl;ƽƕ{79[=(Ϡ| 0/
Ų||y+Qgw%OkaC~~ƒGؖo_yIc~0e oc)`]~Q-rg)w'7Fѣ_܆>hP.uWXh$rIF,\__Sb_$ ?ҵG~ 4ny:]Z}uu<?(o̦<,ʎ(&xk<|>(oI0}xоOo5s\v +ܿHЦВ'B
mQv1Wl<N ֻNh|:ڷ+>s O?ڵC.8ȸW燻7ߘ>2}~@.rqKHF\< vz2\]o3qcQ~5{.FӼiضoO9pp}S)l-PSw#`7(' +]wq|u}<"`x̋y~B;ߚNcsˬ"YtU_ߌ3|_oءOǏ6yj`zK#==}`Sp_*K;ς
|y3 +=4<5/XAZs4ʝBp=N/κW˝ybhO +2qI9[P>=!|ƃQGv#xWtV0ߖS>n<
u=浘|`ַ+<?1)F쥏?$6[}]d/n<ԓjeLJO5؝yPcKNi\urxrܵ\`Vz`c0ucGQU4}B/vŘs# 3?zE+_WsiQKm^PrQnzk"э.ǜ(C0Q럸sV9/h6ʁ7(3
ꡑ FC}ؠ^'' +zc=vWhقzWwQcƘ#YhjiXއ +ބ#>7ܽ<\yr{Rto_O?-l|賿
p&5;&O}_M91-|n{u"{B́z{|?TwL]o
Lzkϣ.p)w?vA{='=Ǣ#}0:hD_A9U@۴}XA7$#`އ<59-<aO:ۍg羸s +?DJ{qh$pSgYˉ0 +{c;_p<ieǼs|Fxi$SN,Ѩ#a7:D761-7'"Աh汄('>}~y"<Aksí|<iy5/ |y;^@}Vc\6 +s唋g30w!{mUa?x.yP>zE畱?aܫ\_KM'ֈ>@[^z-7{OwG74x,فc昖xυ;5y%G3ҝ"\os +u Z0șG3V[iﰡ}~]郭wPc1Ȳ
x{~&c_s7{/9xx}E
V1C}'G{f +C{<rЎ{N=MU\ڠ=<kqLu!|SѮz6]C^D|`3F_v~}m>htG= [0fÝ+N9f.A_HSb(w-DW+lG?q.ȿQ[&# &|S+v9)0+C9c`[\k9xwYk&?Ə0)8sbqXtn=3OP/16uHaP?<N|YW +:g>x.mt0+}['pn?<kxKW+M/t-Ƶ{/! + 89-]xkޚZk2s<8(֢I'??/?<EN!AM<= |܃ͧSi^g~q?~q?~q?~q?~q?gʔٝmxy)fd˧4OJfxwgBjfyfxL<33r4Cu:xrgz}"L*Ϟd' +mW@Q۳.wDj^Nvf**4pUPO9vy 4E.L%2 +4Qǰ+Z;1{ Лw^ޕLt:-cY(ΞL|HZKRs
>N1UV)J[l!2v"udgYL[ӎ>CC{vJǚqū-qіCs,8dGD(jD;;"ls◙dfqG"|cdI.tE([Jrx,Ht/J %KO%*t.vGãQI8UөDbm:?"_.`V[d*|Lmu-1"%u;ޖuNENww-Nҋ1[/ws~Vȹ۸i#i.OCu8vڤX'9R[L/U#rp,@"8F1Z[&"%se$S)Gm& +;U2VIOwhiq7.+>)EJ;(Ջ㝝/JȖ_pԑw5T$8J[̂xW$cɻd.v+]97:G}vġ*[%sq\Bq&^o8DZTq>|8?3[^2wE w)Uc;ܧA:f?ڳ"|Y{z=R}qwdJg"pCk<պ AVxwr2iGzLw:Јvgdg"[w{赵%3ɥ gBۓT!>ש1LɎQ)wxqiufs%$یc73+WǙq8S-v:|rLgZǍ8bb8:ΰLjq +E>UƵWj|`J +\ڟ#%t
4._;_q#&o%Umv7 +bEZ]Zk{
K/8^`[2?=9O[2,AvE }gG--%g;Yܑ8JIEꔒ$ASǐ=8vsυ,%4wv&Y;3 Q*{7K9<{sv-s-ξg)ݦd{{oO: wWnvLº/Ш#חH˜bJ.Z+Z1xcs: /y<E5_ąL_s72oDs&dg[rQzd:NLw;$srr:w..).9tIঠ[WLjW$;z3dYO1[$ۿ1+5^zdt%ZA#;t"^J^KP,^ +E9"nx"sa-z9ߡ9eT2SOb>P"ԍpUl"tD]ѱJRndq٪nuZ
cۆqժr^oq-;z},sPڇms!AXE>W{݉uqZke#sDZP,3:/WF+0SeɶGwD!>Y㣎ej!=ʨV\=L[,}[ +QOi/(e9"G Xȥw.֙xNOY];؝i_&idn
Tgʌc7ގL|iًqxObNwDgs"Ԩ#gh"hij-lҡ*0LuO`~ +&5^/DqLڹN*n4%7Ҁȹє{hJ#қd80%p +vX`2nmq±F(.#lFXX
ëx!ӆ-^h{SoiOTA(hI[htW5QqO/|TzRSsJoJ}8:阤ӥ]5HX!Fd_k +t'wn6u{v[u_vQGnN_p~pBm(u$M?\l<v .8V"7J3dGo[45F֬$ښB6G(Z,?"t?W{(ua<!d-f/Jw& +Ee`+=iO4bʓOʥ=^֥SPjjKW8O5rsζd,sD5#"d'툰cH+E/;D3JIKgT2SOf"7]:GņH!5K1Zq%T&,bhYHtȵ4~S85Ґ6[stuRzLӮ+Lx|eGr8GӬ\(3`
:jH,F1%ո%u87W!E*Sb%QƩksTq;f|z;} +
8&s {3{}ȱ^JPwc6J[\<Ԟ螓.Ubgx'oI%jf>9Tí$nJtU +Cxػ;<E!*pMbd(i)h ,]9 ƫLk$C1mJ +)Xyvg +A"B; + +YTvQ%*Z_F&Rx*_h^${r+2H ++"p(H9)yYgy +,EdAd
endstream
endobj
29 0 obj
<</Length 65536>>stream
+TU喻Snn; +'qJVDp) 멀j*^xlI +k%hUYI)W<V/bT^ +ͭ +AQ=*<#JTq*A<:Udó^^Vw"I +aMZUxAF2!Y8Z7`E<x׀>0:YU"C0%Ydh-v1/Z h&{U +r}LD@<g1@#MAlwPR^ +iêH](N" +;<DfH,MC`jڌ +[06Aܦ47<7y(,nJs'S+;ӝDCUy$+CL"NS@ã0z$EE^h@U'^ +dթRI--XIsm}Z\~0gwz5\N6ͬ[sjӐL~L&ٜ^ +Vb65@Yp&Ƴ8 ð({q)ty~$((F7qk +F"ʳwN'AQ^PkjʍתZ,/w<0kID!ÕFGSelᐻS +#1e*r, + +G +l&!3*Wh5WkԔWlwYoK&`ӨS+T`v T-*GD +Bʲx@m$+Pu,qif],$)u,Qy/jU䪆9 ǔ +}e)MjkS*Uni'1o#>lH49c_Qe9
zJe_Yx_ne$mx9c6Ҭyױ8<XF?QŮj$6y"_H0QΎtN%3xw^,ˮڡWTZx45 +<܀ߒLfe
E ˵oo"Z5a՞fAQ:Vr$<A20/tͪ0&ZVBTm˻i$d6Frzm嶗d +>4&8XMp)_E"8I]T6E(F@pE!Ʊ
N&-(dK<ʪxU9S5HNeC"#`".fPqяmi]\۠DaINC҂#y4^-Ia?1[765T6 +QnSIADGnH9i_c/)rЀօmQtqel +s,#^ +Ί飡yMtM0CJ7jP?-ztƢk|q?V-ngdc־]o0eǻU9reN#{kSx +JW.UÄC0`[(8cenxfkxX鮶c:Z[YΘY Dt0QGƄ + +I"f{Cw0lm+0mxU_j.'\p"065mk]~2 +s9˲Xr$lapV*X0h?s~cgN%B~#G^"k~ +!9TDZM~cؽey[B]NF>tTcYԘY v0@( J^CbZ +)xRADgNez'`#YTAY@S@,kkmEEx0H ?b=Qz`B{Е5si;hY +8LquXGyH@ +F(c͋cp·hB +jj<cxW9/V
@
ЪaWex6Bd. bHб + Ԅ~hYq$*ґ
*Ê^
lP%$Ŏer;h3[Q( .6Zja^`p(J +VR2cw gf&g6F?^`/RD?N?;L^ +,qV
UȴXjn` 0BӬܤ;1j2B16
`r\j!
9qtA#*h#k5,_6u|>yLeEr +V,-gbc,|:J٤=j`0pWT +(X &z{B+\3Ne, + +E"C'2:Y~oȄ$AemN_㭶̢vQu(9q0lJX4bHQ|As_е_uXߪ2Xq~g
ǡ~f+zyjz Omw1 +m!nPɼ+wC@vh[+Pa{ndoRy,6K;~Ɂ`:M*
uCmt(1Qo`JI
tMI7j(jdfMv
fy۰AT߲jltsCby[:@:MZ
l-dowRO6Ⓑ·1ml!(5D7XP6aED7ֱdZ
XMlp![H6Z:F`kUȦl%Cc6,`ɦfv(M66Cl,eéj9u;(E·,u6Cؒ)YdʶT?rf3!lX,eʦfQv(W66az
)[MuH[[6}N!moY*oqipN7,N7,
N7,N73jd[dj'ddJj'd[ejce\Aӱ4K94K
5K+SM|LɁrӾ%ݑ8Kd"I^jJ9`<R^H <AdA(!WeY;9y4/d_Nr)qFcQmxV +(=c-u9Bxd%'=L6TƜ3'
KX4m'?HV53%(\V< =ͺlM<1ZɬQXAlFeT3VAmk^[s!U~K\{|~~F>CaU{<dU{-]Jef˚.Fq쪆;1]'S EkMW${hӯ©GOWk:f0'h*9U/0_O{iP^Uh#YU*XD</WU:].M#<V!ď6hYO;[yYa{W2K7 [^?]#i,UbHlvb0ɪCx]MhcvD
U PTK~.)n4n)5$`/u"I#R/HNmbSO(J6tzU9g]i0WcZF#"Շwvj!_]5t)s-feLnX1J`HI)`l6Vm,ʬ_r3J4jUÍ`4q1UY,pƠ5.mvƨ3F193(ʒ]Ҳ! +38EAr`0ױZhOkk4vUÎYIeZ(a9g&Cevj +`_aqAw^rũe`['n<kv$zU8r4B8U
;N0e٩:GQI+~8OeY +sf˛wJPY w>CvU$ONXx:02Nu7A.eDQB_0byé^*dЖNqHlAjLnΆ
.K`~"@*6/I6粄QEmEdm7uhiRێܦ <|*k[;Nr^U;`(;Q'-'hɸ1o
D-5m#;lU]noQE3x:aCjL'k;5ŴӿcBݨnۺc +" +M, +'[]F7^@xȽXsjZ=L{pGPpMY +_;o>_>#en1 +0Gi^=$).<%"y\(I*KNAu d=>f%xQk A;WD`c +t<TH#tS7 VsbYQO`S1xQ1;Ktùk9UQ-qu2/w +dlO9gpT%Rы/ܤAKedoLH@37Ve@ +#'*j@)a)(>iTA3NEsgt*:a^f'(ZN +HҘ*24ꑼNcxt181<GF~ETCrU~NdKQg\Mx1%m=j@/0%pj;Cx!N,065(H;>8aL +2-@
2NQ/8ZH
B;bqK +*I[ASEܚWQ
x@VD{P0'\4`ڀC?>> +F`i$$4x}{M*i}f +[ƽ$dn#ĵh +qkm6 +
nwp@Ud"Y2. 2,b\b!R[sXWi+~`G_iy:)gB]v1mV!-:S{ԨHZU-LOs@*bU4Urә;[cg~KjFt5Bcߦxsf
m!:B8jq +ڿ0[5ZUIF96xՏOCCHAe%UBS0'YPǪxT+ .a%Eai`~jf7·X%ڵw0JuԊwJ^p8%F +}fJ55vCby;onOks=0YND:'Ak]t1v8+inʬ'TٿXg# jZuSTw7u/z% +*e2: d7]~Xt +(0'|CuqTEb4NYEǻ[SlM)"@<su\w.MO5 @\_"1}ǫtoV_
siͰjo;btT<By*5WuìJ%dLj<)5c4bui%A*chitL餕M螶d+PCnth +U, lU*6LN<-<"R>}Dk/~yL؋?~6&tvYޓL^`^*Kcc&0őNYzj df({jmo,AĬdOW*ާNAn
5Sk#fTjsfװޱ(īJ6`DN'bţr+Z\Wh8+NH&y$%SR=DwpIJMIp̾`,:-gao]I闱(ŭg#D/32/q"uxNdJ,:;A#]PV5%GZLD=;9SQN\u% +FLLuJJ(~qL*՛,L3ĸȊ,'rxJ2'<+`MVgluH9,|=%1ϓlk,'2"fP;)YxPv䘮H eו&!X֖PSA5Ɏ'zJA&p*.ELdI6܍y;x]l@à|"1*z#qzCT<栣QH>L3\Z~dPyOYQ00yeEa)ωfplM_xLj
w^yXI Β,q"OGxmN^I|&gHd3MM3{xn g̵U]]]QY +#>,CBX$tbz.%<\ZB&[ +{)-ޮ7ioU)\aoKp`ED3KcܮMoxs_?b+a75a+tKs-OGuxDwpbo3@&Gvtmw+j\dN)5$ϨAgӠH_-t/!MD#j@H*H|l
E>'ڡ8(aH2;@1?Yz@'fvx=jZ)_lj38g`j/kؿDkc,N;d<6@rS
Y
>Y-F1Y_'J0~S\ڞ#o/M`PmG'8Ym}40!ht7/8t~7z>lGVh$Jl(?dsBn;~MCA1H)nH6~@K#G)A&}Lnϔ?*)e+գ +$>qO{QZ-G
$ 9ހ$Z? :1*\P"p'SVIobE-rQ* +0B/<V$Ec!L(F `ѵl +LL+Ai?10!E3UN +
Ez{/Aa^X?ꉢpG!AxA" H@rAPyD$ ^d!xIq|$*.UYWP)^Ac?>Tx}~ +Q +"Pa]hJ'G'`!nF p2xdG`skL֥BD|$^ + !Ų>^䌁KPddRay
+nǷ/XieNz}X3'Ë5Ff8 +!}.6 +.k_VBXh%NX1ȋ5ʢ +k +bO/%&,, +'']>0hO.X^A\{)I첸ע~Jh}D=.Hc CN;kЕ0`w/:HcФt{H"rJHy0TVؚ
PG* +ݠcGOXHLGHKZCl|[lܞ,/&A`]c4whe ڏb +p8 tX.^b +g͂, v +QSdr/F_ p%Y!GoSz{ۮ2 V>""(e)rCiD^K((Fe9f.\Jq.#6ș h;0QvȸgB&@ha{e +h9 +XDSeHd`Kd75Z,Rr
}_h(y4*ϋQ2oM2b4ȵzvs;. A6D~X@X|n;a_b{-yytyc<"QFLq4t}h,Xvwjୋ +h~~a.R[ NHv@jbۉD^aF;|S?xB!O)v +Mg5PgtG7p5o7
x7[3o{G]1oN!v"Q uKfM'<!^-$զ<T۞@hSˣ
|V_m;ڍnQ6mf8Oďcf)p +8v`c;' +0D@s!dL5w!
a's!rW=7r愶׀tg!x%(Mޣxf2vbN6٠7i)~,45T +tj/N]oEso]a@U^:;q[䷆4qjrNыX661T Jo?%;Q=
PM{
b7E=FHDS&Oj6̑+yQO`s>*5@ÒM,|5Ih^pKbw1.1?!/b0Uk#ҖX=|}|[p6ŎҟLTf-YQyPmٛvtV51}M3hXۮQ +Fi$fbAS%(%!9;ux /X3` +gՑ]w :1u?LMyd_6EP.*}DAC:?b6QPr?Ba +L|vaj8Ww.V_C.:(h<>n&L%)jZytZLL +mJ)a3iRmъOD,5p&\[pE.!Xwg>=WnBor^,Nz?Fr@v~a5^ߞ7WU}*mi=zSt{Q\/C*Aq{ZxҌfsCGm8)M.Uex3AViB6rX[zr{#ۖͲ(2fx6S)@@D3F$cZv8:5r +o{(ˏh7x#JI`U eȬxg q9ֽoNIWJ"%@dc|0QCz.cÉA}o 6d
)ĭ|\?.,Nqʩ81OyVaV
+%)I]jw<f^sqsVJw7嗔v)xd@ e4}䋨l&2WiVgJ +[1Rr`&jZ1qKv<gHr1S +?=Bt!X+&uX̛TpgFqPumЙ}z<Tl@sf毿lint֢3տ +$|0nfy,d|?z| +E5&Ze7!u&
|QߑD9O(ڼ1,>Q#}=g{IvMu>H4mGՕ}Yч Դ_X_gS_3fMB |g&=Z3e3I;eKzlɛҥI1a;ݔJ*m| -_qlB7]g*s%=eqM㘛<ԸΎw#
?aR~ώm!4;wxRe0$w뽛%;D}xgNf<|u<1+Wgҹ*7yX'l$[tiJ͢#S)Ȕxg^,(*
g|M|ӣygS;||aS vjO8R0t$]taXܘ\M}vP Z"@.nHfq.An]xO\n}zzJL<\y/Ftu:0ӥ>mJ+znl7vvx^bo!k]kl +ՍAWrrst53]=3]PG]={t +P^]ah~
6Fx+^77y2ҭkT^+z&Yћ:}z;Rw3}J/I{U^1K/o;_Դg%VX˯:Jo5yM,]_q@gҰyoj@[i5̾6
7N7{3D>cClH|yP{}QcYoЫѤ4|RaUOM
;sbbWF351:>~|}2J1&-Ƽ72VmEmw(g|j^bWJScw0n9I>DoL&}8X^M}oe )roĸaMJ)j35wL4Ѧi3,ͺaġfy1}/,a1std<W5??1'#ynm1.kyZonK!jI>KZj;K{fXi2;˲\x;XTguOV:HޚɆaaqS.m'Kz%9<)GƔ1]w;[=ڞc+f&@}-~)尻/;}s{k랽uE>6ysP#bwviw&g%vu䴾N*[wF\gf9yyoƹ.9wL1 WUZxmW\sw}:L #Rqn +|c"{6lS#*㚡>Z|Ef ZTlYPTC=9 +L^*,^[>ۇ7x֘d4ƽIoWϳO9|c;nFηw~ܾیޟKLGO&igh=yGȏ<Mztx +v̌G3A0,-=15g۵
nTہH> RI_9JϠԨT&ͭM!3?FFX3duB^2.ꌦЬMߘbrp:mمrx.ZюV?O}"^mGuIEc-zߛ-c))1gE}^?W`c|rw]uSv=՜'`xjŇ&,v&=N|ZzӤ%2LdiCldΐlS}oT9jԸkOaŪ~kOGcL2`e_u̾d-]6c_r-\vProA%mDʧkyiŤlbYx| +ܶh;P(6ӷ5])mSSSaIgFZ,4gT3;>Ag^~2+p5%~e5jC}?Ƈ@vAVOy?z+/
yFj4uWG3м4k!k9ُA|)m3<oQU*9ٶ%S;1?&}{;eSa
?MAad:~ +<zshZ{n:CeV~!;K;ث-:нL&{soLoPQ~[7Lף
ݪyܝ1wc`%a9=cڻڽx;jpV뷆Zk}4hȗT
is0 e6 }Np}
a<ǽ/-L
H +Okeko3߫-<{xVe>?ڠxu,?7Bt4/V:w"y,:r?-cx\vlqe-|]
<Dtdu1mXdCUæxج+=Vuq[kngr;ib'w:q1i\,9ZF[t9-#}<>yyq6u"
-#Wg_*|Ė} _Bbž4u}x}ʷna$NAAMO%PcLD>s6:NKd㦗ihM1͔=Mݾ=Bj.֜vvtn1v&>Voj +X :{iG-ޓ5W]6&+]1sZ-YϽ3ҏms.T~TÀfJD|إ?'an{<1eبONx7jf v[&cYSyӟoI߳˙X˻D:bMҾ1D/ٙ krۅy-ڹtZs +1=~k3hK)4NN)L +aNW<rgi +'"~B>vz# ljp&;`SW0w!&e1P_rzF0Τ1z,WÉ
Lh`%1ߘ4pPw}a`: ţ:ŝ $pK%>%,өG+ C-PiTgV$~ +׳x[!X.%G*]7>\B(;{]zpB[$i'd?IٶBR<^U7"5Emp]Y좃'Z{*,%?qN4&Rs5ᬾXYvg%po<}TLb3LsW<Hk- +:Q|D=jy>@sW̗7 Y[O:䐝h~.#YT*#Zs +/Ims<,0霉lK
M1/JZv~G]B,\? dϾ{<J.2@T!sG?R֝7:Lrgr]n o{sc sYh+I/MbנKLocU!4/&X{D5)
0X[Wdr<
'<ڛE~n-MNb=ځbBf9Ky@XJ1Hp4LayC6xdSI=oD3OH7I?f<l@P{46wpQmu'%[T˃~zwNA١0pǧ97ctvׇ.\3OgW)n\{M.A oYxǑ#!vig?،GWrY/Ak,0i93-8{x[GΔՕ[Cwn
֜S65 AX>gOӹi&Ffcm8k-ȹ|E2)=tpp.npgJ&f_G3>
=<*;3{e~W + +nfNg˫.MXӨda::1JCʗr]Xz"&iiZgEOwbJ]:[:|;ȠYFuqYhȣh䂉+mdhs\4R~b!r<SLX<#>H,z#+a@5D.h)'C1gz
<6 bc+_/cG9<^g[Hθd"8Rj2,r-tݎGYw_wh 3Ps,AZ!EP=3C(FI/$v{ZV|\£y cc_KlE"|7 !o&<C/ޚXi0fE`o7qhN/*>m +HX
m_`w4BZ^Vo>ҭǷVKSӗyXxMJiXcٞ9=<85 +c.LәXjkg}wÅjN0Q38iQ$&0` +1k=Qq9{/D"!~5Ir!&vzJ9uzyQcߘt<?u}:!9+OiP'ڱb~]Q;PwWyo +Vt6G9;hދ tnw1@KTpB4,DйݳkJ(5n +%D@h}ߢNtq.W}4o8a{HuHi\Np~z!A2o#^#FMnXjz +Z!*4@OD <}Zv(8|~*mہ9yܱOǖGhI}bbc%ri|+m{tO_ӡb?L](s_e[Tby6֕{Pze)_Ib(˽m֙v٧D?}Z<:h<L~s=u4vwӔ;"^GΞ8?|κKCK?z컟=$cވ<̔Uvl?n[VLSOo'^=y|[=e\<?G`^X\ⵁ<ފT+Oz3GNYoln@n73X +fu<ǟK-势eZ҆n橪?IkKúDԩ4a s#.ZhyXMYG2c[dsъOE,{IedY4qcYYs +y B[qR;G1AZ%5?3/1>Nv|7<_C>I +>k̟gX +gV-~$VugJYʽ~]OyIqq)O%EeK(zlQcka' eͬ葦]U+ ,3dp#WҴtb[nUx:bxpVF\&H"vFbQjn37b4PZ$%aw{|a3rOiirnȞђ8qoӵ#z'㠎tgΤt/]/u):Е=Aq.
t?/&[dfJR O(zD_$/ypB>'Y,
2N+rJ_q9%ÜUb`35UK7k7hzZÜPNK>+^wEY]Ysh1 +]\ᓳ$SnymT@<LP,A
#)>dHA1]@(-ђ{ۛղVP8 +Ɋ"$ˢЂqC04Bf0I%T7N^A +~)}]rO
)m'ռ [g!oE]%X47oRYSVy7:a퉳ts/G|M?뽓/љ`:%*`iZ)+;)|zcR_
r_'cD\N&RЗ@%nnhxOD0JdzD;zX%ڍ$%iUZ_h0kR\/.bl??h~vIiscJV[6Y[sGt_Ɩ/y12K:3NBg-UB$+f \-K<(0k&9 ޏ6^~~{qE;75%vpgfu!ρ6_]?yLw?ZY6l_e+`Qg?_tZ!K-}p?T/'UPYb cm(Ѕ}b~t$$$8])H:a[)ҩa'jQ:%s(=$"\5sгQ\iH$8GuySPK)G_A1Qɧlz|暌QHaqwmݜ=Z31\*F(\gb |wk,b-7&4r42t,毬$=npnsuV7s%]T7cOny
_]VЉ*]C\Ae/tՂW)WRC\A'~PC\A'A
rBU5ttZjq?X +g: +:l *j-/_ $Jvрd7mV/NM_HKZ:_Zm:v֊tUK1sR:S6>S<>Qrh'zd*U"WJ(I̡\U4IdD ܞ +Wc ׇdǫ:.n4 3!
bN9iĘ-v۶zIjnOZfAU3*u&L"/wlԗZ6^U))WڷƪCu%}.Cgjy`# +Iرɚ]U`<KI\jvi;_QW1ViE0ʦe%:5!*є?xYÜ0)9Z[%ff~Bt{L3uN1aS->ȳDń_*q zJ.mE:-tR^NԅLsP#KtAuICqu58{'Oٛ5;{rsх(0ϧS5}k ur4i*qS2(QUwJ5r7*e<<p"ߚk 9ogD?ׅw$\%RR):Q{Im
{uş=Y.#_f' 6*+!WFd( )JŬӊrvzQJ;ܢ/["0K
,aǧRc?#2ЁU!2Dd.Aeh\EZ}愈d<G[q*уUX.eq-Ʋa5d2y =X~'CYUrz}3_P|tjAf/3Tq]|=ź:^BzxHolKTY7X.;po)]wᤲk߮ApJW?\$N-~= 玲q]zJ v4Z~NmJW#k9Е0'RζԒZkʉT[f@ +'HT}h#텵p?Puɠ.Pq|@2ߙwLƳ~lR"<~GO!tx]e`#z_/J2n#C?Rhb ,s\ Ңm*>S>Iա>U&,w*R{w
E+R{J߯T~NE*7*RQ+RQ/Qv%Dԫl.nPT +'-~+fF)z)B)W?(A%pQA)t|LQ2~RT6WUˉB{,Vq&z"Ȩ3a.vsWѸt:/r)w^,{=GQ p^8<n4hӆ!
l3V={Hlkϙm<T=H9#B2qbIU!/Qz;<j| +?MZyR*vzdfRTUzr@?\APq-VȱEnit3LoU*v#"K +;`rn/JUgK +u)\Mr@G=<ܿdk\yr@vxVarƟ8>iljF-}eM=l5:˴!zbBPbRncŎEGT3G=CU5KJ}1ԒS<X7ovDL
%䮊½5HTC3!SDN1O;?\"';>{.:>4]Yj??Nzɍ/"Mwzr;tߋ\[M'j:N*&(^/?n|5T,^:'tpkVUIurB7龩ڧ9_SM'UK1j:X߫]j:)㆟;;tRt2ARn5qzcj:Ȇa5+;UM'g[n5vNԕxOEk~N: +2UrHP* +4.'ܘJbU.+$H!+apDZLݑŝ#͕#s۲.5ws4߹NvZ%Uri+Ӕ |gsl2t͝jDq6Ew?掭}SNѦ\yII^gQMlriO]tAj2:<+F5ihQ0O\_PH"Cԑ
9Y
[ +J%\s6t?9 +:Ӗѭ،e߯T>|+(p87tT/̮o@E%dz-;LSa결SQgr11V0.YR6Hz߫RrKU]fP+zr9ԣW*SN'_oI\vU>
&Ey?^uQxۋRV)l??N8.WQC!U;62li(d
wJ;%+{ou7)U>`WnS'vSON7|*p'KR{Ew]ÝSQk_f:S7sn:t+W>?Brι|Cn^z +SGVTtv.v"&(eL7eLZ,Ѯi1-eLAN]E)dT趟VeȪeUj)bDWb~UELrDDM{aT~a(qXS7j\SnSŐrtW]I)ou~h}T0U=ܔf+o}04T=Jj\2#
h|NFƍ)}h4
r5\ݗ}z)KLfb'A]TPwcZ?T%-z)ɢ2<.WGL&W*Ƣnc%rGYB=vz:x@i;c>#U9ڬw/)7&D`s2OR&6|sV\ +2 +(Zˣ}RL>(VtvQ.,erm0{ws{}ܴ03 +TSz4 +z}I&nKCMjOG_*R\gζ_igP2~nÒ]qzJoڼ״9okKF%qufolMq]<<MLMN.~|sgn%3l }{6LMiŃt{'qv[)yÓ8m8]_cfoZm?iLޏ/
oRIbzX:~c3S+:~S˜I،MoU~s/4ߌ*wl9-],eN}&]&r6Ӿ~Sw{ͺԁNlJXM].~[=c3yd]9~+YWYG7到t +/SfH{{ZVOC>qc;.l;\*/T^SdAOr;鱲#6HM-AيSm4H-b+Wmb9M:D:q+/}ȡmbIK[-Fda*yK"b_Dk堲a-}ŭU_<hpxv{ikڠp`Sh(fc}f~i4qGk-6=qM{4Vnw䀷+IK~ʽ-lTRܚO[L\(`+gcrFV7_2/뗲FŖ"ekfC%K`~(.]8D屳kK7.`;Jw`-n9s`}G-Ə,Kl߫eRqb)Rw0ˋRP1G,Q.+heDihUMXƷ}ln{G!JB߱biՎ"KVmQBZJf__]g-e?-+XG늌RR ));K\ݒu8_w0~|`OqoIJbcPbӻnsY]^6Tۻ̼K=v8XY,M\z:ST/\JRYűm+wwmڲHJ||ejJnl۩opA9Yg?`gl +<j9{[QsSK6m?v:ݕHLg8 cX/_v+wGΘ\Ol7۶V۴@`\ytY~ų~U^+:cMsZ8oO1:s7v>rڪZhU͕%Eumɴ-0}|dl!],)>+*>_.}$>ْ3{+9V(':5qN)"hMDIZBœؔB%I>Au>}my%MZ!KG1]Mݡ-n:>fV))Yy^Rlyɧѹ٣MRCլτ\~v65ƶ\J̥ɚ|XȔ,n]ߠ5^_ngoeqBVhՂ% +-V6]
,-T`֏)$Qs~'ִqWVe\v=h 7BZ]"ķV`Z*uY>ɰgyڋ>lHe"쨴 +/-mˌwn)b*;K蚦Fokj\V RB,_C'^ܶqM2Dh&wg8f{%mqUu[6=651ڲҍL3_g]_,5/rgytg.KiXwķsIߵۼoyzl{,評WEdJi[F)e# Qm]oŞ>HV[VQ|1SMc+.[Giء +;bW%2S/-Vy2<p8a$W0W[:c\=V |I}}I'~{?i\Ct-{zV:[VZ)Sf2yj}qEcK,L\ڳ65 f!ZF?JbB6ulYM饄|-rڸ:iv,I86,ZSRZYSS܉m-gNi,]M-'Nԝx'N4zy7YYq'<,jզf<b2k抓k!q#'KnL?\F]k.=xcE2}.-..rmI=l/+Nln,3Ek)ɕ{6Y^|cšM+w*/n-9oX,uLx-ċOV[eŁԛ_AEXS]ZP^NmǷ`w٦ǩ|y48Ҽ:%3W 34=fkMxZɓZأ6?Vx3~EX2,~CֿMo12e-!B~8RL^q{ybۍe{ݭK3&ְ5F.m\/lnhR.j妇~1ͧpoMB6>/=o&>m0 Od5lUkv]'*{
5Pj<Mr]E}x=¢
,6.tvXGJJքȻ衲
keZ=BzWMe\/P?gZA`cC_n!Fr,Z,rUcrrK롒R<;Rj;P4tgX+&%]{6xVR=%UJ!N\?ɩ:dEY7AO +g]4U$Y=K>R㽤l}܃ftζijk.'|$0~V'Nݛ)+7)0lםm2Nq>esoMn붷+c yg~;Pݮl
h u.}C7:f0XqKY|38u3\xWij!,z&-qt11S⩤KR>%5H͖T67|0IW}iq?1V(Iؔ8T^218?1VT0Ky5K
;.`d c,M(5olKyKJjWiFY_X7AUԧ=_:в 9;˦ه
ť'rhEkQcM=$pTaDr/؇q☴zٜFıeG#FжrՕLQ&}]{lEƆ#J͖U,[V'Ok[#0|--rCnۉ&pvYYcR-4-Rqz_M%1*T0g0ÜU7g}^<q0t\Zwt\윙'-%L4%LFrB&M IhMCRHd)!4&LB}iK +4%KڒB +'K !ǒISBE1mI!L +Օc0|o&n;Elf}SyİN/
AfeJBKtGJXw\4+.c,&6`zf nd
+,_dذFuNP!3#ؽ?6@,?8_r#c/!ڪLU.Wt =`BivbE^ڰE2V6V6z*2fֶV6n>qLS͑O,*JuZOiOʕJmEw=E%MM-g9ti:UCmM}³A#z.4&?*>ROџ +T>u|Kg.9zgˮ]7#kx9mQ~p仧ɇ*qՓOgq; 9wb$<rE'w؉fOE=%E%W^du隒UOMv횢kK^j5+<VYSYb5oE=]N +%2رUi=\钒558 += +.&;kEfa$ד(ʊS + +B +=2i<M6^# + +jLjyU]Ct-"vɣQ5ATيeuQbo* +19rC._K" +F3)7 +>?. +JKfX&m^9I͡ +|a| +a +ZNJ +uv.0]S1?|TE{ +훓V +>5 +cJ\L~ea"42 +,zG$pVi]ҀOA +-2$f+F-6Gc +d̀ ~`БVH2rt1YX/+&0W*:wEK +RiqDf$q\<0-y:4շb4&GvNAẌ09tQr$5 + +qHJ`栞;3$Ru
wH#]DFRU@}٣Qy+'Z<`rHq__+[&dӬL
B
$]Ԍs[㖢 +20NcQìH5/\y癤|O<*
yտwF\ѷ`ݲ뿲÷W,ďfJ]|94&*~Nh>;='+eo$OqYzg춆[=
4`2"j?>!>5ʕ'Hkz0$G$w&xb9RjYzL{}|k!4&_>Ue}$'K`AOc$iAY(<&Ɗ?C@Rx2SvK0iL]!/YveyriGEELj`JKG=o_x@n8d1E%8{r壚'wgqQWxIAKUA0Uo]E'ouKГGΰ&#SfX+ +'#O`V/`탞gC09rZv嘘TdI'L!8:xI'AL?
|F"CIpv_<( +4=ؚZQ +.r
eTg@3OI'@4+tJArf/6Z}{=wwP˛Г`J4˕Y|L3Ynci +ϳ&}V \n +%8Iߕ4 QnA++ `7t1Y*.ުA͖8y 1`3DLjh8&ӣzvk8I!Ch'A0A^/!1Y
RbW9I+YE=qX8}2qSj}sWBF)9uKIROolxne{L!8|M)>m $3^'& jn)&$]&8$f{XLZ4X$Xƾ] 8h+dߌ#twW(c s>so@|&&*V5-JFoDqP8zcH
rG"#yp| "$B(G\>V=)B-8݀ 8i9JW"ϡ¹IhzYfTNIɶ0YQL7Һ.sa(L&M+ܟkܿ,pEUs\!?_=v;3`21JA0 +=v` +na(4-|U5c +u_>#3wL6x +^M(ΆV-X'w> vĜ?-PQBOf5e!\bTf6]O2!n'uB~0dGP^(=wg:i꿡_;qрI犬 ?K4Br=a;Ѱ"P<6a&$3x_]/SL)1o,*L=WdOSUtg7*9>]&k¸ܿ,!1!|2-v&p"y͈&S'yD&փn n6XjaE(G$WDL~UP!Iev9.aPCFPBOfyaU9ߕBG攙Oe,rRd123M1@FE7 P$/z<%~z2&뗏{<fCL:NTDqi!ı{psaN^\'[21@ӇmQu&&G5v.LΖD60&L'ú#;*]s(G|!@}s3*g\90Z#qd8:a
AIDQ:kГ6I09r^|f+ʕω\0JAB=qdӇ hy9j^oUuEzpUۋJ;$)В{9ZU +t0q'Vî^'A&Ĝ +G<w]>`U͇U$0. &:qzRГ$Mכٲl?|ҰʅUDLI#{X$o1enz`l^Ü&tz_G$R6GcГ(&٩&_#`2S/CTD
:
Q="
&+ݟ:y2
==~7Tʯո:J'{M +mG}z*,@*I#`gO˟!Ji' =Aq:xQBpti$$Q|$;DL~iXW +q "B_Qf99(rh>{ۥ `2#mJ'9L]U\1'A0s>=#$)9<`rD&q&3Qaﱈnn\$_:}eݺ5oƜ'k7ې}qeaY=DvH9 = GބOyQ2y~]gR=I_wM"z$2NתVQLN-LkȚ0락|cI0ʯtH;Io(p{swʘ;Z^Ü&\fGP3;;e5ۮHI2j>sHz4VbҙЍ[
ΠNg띕$Wm1!0Gww `2s
zMZ5NL뮅B0N`>ˬ%$eN=˕P寞Iop2`/8NyyZ'm . +v~ׅ&Dzs8
znŭ[A (!Q7$e6'vKRLLF<%}VDd&S32qZ/0N:EZ U +g7:<#;H +D$Q +1{%Vv2 +=<EtY!":/*1 LJ09ɿh>ul!ġm-T6Ua -$j^o$dBJ3`{ 4L\Ǫh8ʘ;d$עL:G}*<`rXIzfʳTL*KqIGj8 '9YW;&b_>鋱rqg5VƼ3L#'L
/GEt<y + +rt=Yn'AL (3v19y(Wde̅(āqiH0x?qS$OyNa(FzCRf&_ +%/WNOp +F? A.F'}>Ĝ#{l'}0-W4`Q3Pg7N_A<]d +YIs2B̽9ϾwQp^ixmiݼ! pd[` i9JG[;Hp36'=˕h7Ήt6՞8N:zJ/ 7yWXF*fVS W5'pXu}
#`zR/; 10p=
.>Ayv)yUp/NOɟ˟9!&0!#'{ȊRq8 (?_7 e&PQM?ͲoP4"쓎^v#rbY)Ḿ$LPoN[r>@%_FEA @!"65Mv>"'y7=U4`f5v_71'vx
턥a7H]\a"]V9o]!wR*A&_? `stǢ` @"Ham?`VM<`?)}ZOg;Nc +e'B%كn3}eJMm3?:0 heT-"N, A7j in~
|DNw[*fD$j8zv; yFn}!ۤѨeȋɿ/B;Ep]%DCUc +co8ATD$H$ͽʁ|i4*Ƌ*IكKRMT4DN&ij^-FI3A^W%X=M{$ɅM<(JK5x+6`&L*2=A'*&_uJf$*'C1?Z ':(P1$p[t
= $(_]ndbz?%OE=TR!p=8hrGAǒwTQ&ӊI.cnu/HGcc.5k>St8MqZΔ$q C.&_ɓ҈6IwzE h:x8|
*+;GE'Z,mAd!jdTUhDE[x@J8EuII.((N_YF/0=Y;OhɁZ.J7)Y&z(m[cC(\^V!)HV%fV{qIɖ(> I=$PLr2mouju|s.)mcBF2t;zk;4 DuAc&'-ODyǏ,~1v.)%{6y缧M_[ ےX\%JdbDZ-ﲭ}I +m#8Sx7WU~&I + +;EKf[ҚWrQʿyȎnG6/sfX + +g'+]a>۠|V_v+eJo}2vV +$_~4m]h25
^"W$QcsR>#['Fes9d-n%} +[??ye. +Qf"v[D)7*= +ƀ +zcB&3s rQUFQxFɾؽÕLP܁+hn'3ZLjyn.7 +ɀmAd.Wb`&XՠɾXr8F +xs_.|%o!ƞUeaR)ELlnK^QTTFml]n)9 +-D-!EiѪA3iwUy[X6ind2"nSeS-֖>Z +dI"![xi +Np<>R?$vM%,U`5* ?QʘL4dŠ&SAe^4w}3SJtc C-Ehy+z
"{Q*7jwȇB +ypO +[%r>WֺG5Pz+ +zk20BWJ0l52 CEE3'.4yj,RnmHׯi@/ukg*L+t[EEjZ싥R6F9ϒ֊hIJ:x=UrB+<v;l"6j6 +Le&ODPRhD̢zMI|VM2mC r6@&MGyMBE#`UL! + _CcJa^rP +MTFΐK2 8X_by~݃ty2iz{8얮I/qJ!{5f%((A)*J龕mh @eGBfd/c8cJyABRJ.*ͬL%v%h[&7I +e>ɖ->RjUF+7*3AȤI!pYw.z^%XJ٨|>[,|OXJ!&視7:EJ}eM)i2 4%%NyTKpPGW؎98D \ V\#VVpR]> +{QAas#6~=X*nWN[aGs>nWz?*:;& +½2ǀiGnmSYgFowK9 VH.2YH@<t[
aP/N?'*e\J9o
x~JT`N {/"Ei +:ϕrPUڦX ʠ7HQu|[2 +Zj z!` +%o?PX-LA,yƋ/ƔRPGhC'oFfm]QC_iU1p(% +"<W[¸јR5fRJ^.|\QwZ'BKVҽ
|K~ +Qxa7^eGӱ
y_8?Y'eˬ2 +@&p쨲t$/D9- w0A9WʷaEYy5zZJf`/%ker(DLb"IދqM~qs^<#ѓ7oE-d'Z=b9ݢ[Ƞ%IH~*g-y㬷uh8Ӑ#ydUR&)3J7" nTLTXɓy2 ҂^4^mȱQGYo=#ן
G7$V<hv:6T'B-#-u͓ ?!<`HG+-h8cʛߊnd=qh@&9s[C>)+o27C46ǀ +CGNlC'=t29c{)J=V;4s74 2I^Ӹ2 2f)^X<lPH㎆K@su*M^['8{K/a_"Z.|݀8Q +0qYO>r?ͭFOqN)e[7]Ɓ[C36Q LwLz/ +031-]i+\)ŔRc6K:ˬ*kB E|wE̦zCW"0 Vj<uyS^^m/@rORHhr+{i.o +G%&Mqnި-R=Vk9IHIvQdqAe:1މ-)Ca-loWk.4&9#~ud=kRaCSB_N^\Y +z/0/>U76g/K9mt=j{Jp +B) L~>z +Q@'mxMzp~;17j!+e?ǻX]J(%2LB&A%oi ~? _ +;
x+xhA} +`2Ȑ:{QKޗmd^mvI$Bf)+XV B=]UOc8{E2ug_Qa@Db]2Đ=>uǁ16W_AIe)&?L^.c`0'm^DwX(Z:fh]J(%ȘD `h!O-I JAD){O"RzD)y!f.27B/y.\o>熞."O'_n*TYJ"4~gC&P8\kIFoCXPͻ>B +ɣhi S^2 +^T
Z|2 .<#ҩK י1TPT.\m +@v!uaCFE=ˣ{@d'x+RB)`{&c p!>9Ʃ ʍp7QPR"K d[)U +yA(E?SãZP0HK|E;.T9N4PN2'n<v>;S_pxsJGlyо$|O/Y_8Ĩۻ!UK +O"r&OxVW#%!8ΤTdՙ;gxL .ϳa4܄g; +&HQ(Dc2 + +NBXNӍvAf^◯4Xn$"u:'ȋ9 `ne!FՠtIZ>ȍ6H]2v[ǯ7aA'NtEF۔ ҕᐳ?Z|2DX"% +] |2c" +C9>yaAQQ'擣I%&`:z\kp(2
U`Wh1wvQIث] Yc ++f>++=R)b!:&
=$/(; jQ0~*g/3Ƌ?BF'IWÂI<pb9Ή''KNvDP)( +/D)/AxPhs|ȂEjkkc)J,y#tqD; +(H +.(7v]'Kag%OmJ,E DDHN] +/D LZ5(,_D|aM%{֮-^)JҍnǺA +=K<,y#
*_tKHN|$H72ӋJ<ƹs9H]=BMCx,ulamLNxRHqDؑZO[&#jLU'XqNqWi)o[ 'Xr/ǻO +Q.^UA|_F1m|^gw .51UYop1 +Do}v:zK;%l<3{?a܂;q0qd1v'o+nj^\H|rŲ'){' Jߊ:$s.yRDL~
o{f:I 薆CRvŃ䒵sٽUغg)J2T/breyؖuU5|E]kK'q*1m|Э$0{)=C*Ҡ;'s婭6)2BI>4Xk҉l<1tHS\SM _K=?yFYJzʓ=
4>-ˏ֎F9VEݵ2z*'>vNrD^O33U)\%-.wՠp1 +++%y4*Ƿ.|{l&ڶq-˳94~gVI@.3pɛ(>Wo_[Gnϼ +{|ɛɹy[(-xO9MN3d+<Wyzh(@V/>UjdR>Q} L#ח6>OrU̖O'$fRtQ\1ٳC^gʋJ%|[(O78_z'qVҺ.%o' J/<Lp2U@zִ178Ϊ;!j|-!9d\GCˡaeKءF7!o=wmshdQ
oCqKeS+Oa>yʺ/be!+;9 +dMNrOMRriD{LmnNi-^_ɞ9irǦ5KI1d +s]FW[뺅Owg|;GWJv՛w^'ɗ+O^[v]\zw
_'( +; =7]T[;sD;6!yj[tr46Oj0%v6<9߽,!8v;α}>4{G:l+xn\n$vNI(^mƻw% +Gd~8!-[dR.amߴG7O֧?H.79Yhuk=MHO#7|@F=vN>RA0{U.G~ۜm+O8} ݸ_^G/hvCګϒtwx'L'ݱd?}V8OHq'Mך]g]eeOuUuZ~2"|NNm{( +yk tw, +snt$z,`20Y],s$u{n(z9UR29Ds`Fn +3$=dR\ow֭D,}v35M?}io*>L)4emWmeO<?1 +wet34f}Wֻ7[J2Tkm҄'/9H7γ#Uy&Z'*}_|jeOsFغh'ͦ6+ͮ/q7-]Wbf 'MEѓ8R$'Cne)oJۧvKmsVOpu-w~[/EI;v{gR)H=Lsg,/>㧝|%ҺG/(w4/&mj϶P_Kܛ{<? 4s.2>DI +vny$ +K+}r!y1~jvT<\R3Nrʇ7(e\ab\]<+y=~E59N'#$ǢHiΎؕ-[K~yuLF͢~rE?bzr!_Гqxg+682udshcR_<ԾF@<~{W)Ma?X2 +՜}`zr߽go[y'RS%rHAyg3=y_O + SOZ-&er"3q +:q +ԓk"I/>Y_& +)w:+v[mXjv +>ݲ-Wtk[y
'x[F tUM7տ-UH=3z\;/榠dz7_jY+Kvj[G-TjʓG=>LQOx4v0rf]~
Г^\O>- HR2i4/iKH
CuAks}`~=Ϋn麋Fi.Bk^J#D)'-l Mj,8>ޛURXRalϕd`GB_~J]zE'b8N\|mƎoyp2^/ +!mYHyϸ:(g_i䤾~`O.+̳y=97۳Vʟ/qU-OvrxHyR9>A,adla'sDfJT'.|/V 8 @b)oԵ-R귛T*w/ +/9;^P[|m7]pQxn_KerZf@I_Xцf]9>pQs3Qv=8Ɉt:-IbΓ\]J?S +o,?; +(`j>GSHU^6m
M&)Wb
ax2;:ՆIN\OX-\zҩ'Oj` +CRVT?גPUtR&,r6M2]i +A4$¯&jޖM]~bv/|0 +{3q'xdAj?D4 +ER!]y%|!ԓ-YLOj
zҁ`= +(|%lɌd21>ȦZa=Xⶓr +-O` +E +f )ÅI,y,S/]F xvyR,NI/<Uc& +a%oHG]?d06_c=.*w|d_rD> 6 +Im2C8-Fʹ[hVp./ГvZ<8ei2d7 +?L +(с>~:w4ۭ4VHv +/v
>N +'X2=tcHʘU)/t5Г,G3Q' +208Zvcq;P{ܖhHNH|Q.P{$5ۡ'X|,w +Nkaޜ
wtӑG{*"JH ڨ=7]!~Q| +w|@D)ྔEXa/n_ww+ +]Œ, +c"P@mH:!'|do<a~+o2jХ2*Yr*9jFZ-H00KΝ +g'U:Rn۔x;0.&0D65gS=}QO|'{<IP5_o[ +6XH +7>psߐZ_[w7potF.+$%3a)Zس?Ii&aϦz&JGN +R +oɦb=2eGȹo!;=ֿf|D܂sڱ8$%G|['ћc
Uk0]DCXf(MjKڑ&j +q$AE)icl.KR[93v:(09=N(aa5=A1:!)}|7]z`E)uMГ +#[=JI痔Es˧+m<ٍ7 +B +N/k<8V;H;-box;cM9\R,{Cs*)rȈg +RZ:@ʗ2i3'Rܢl' + J&>(ٝ<7 +djx0yM,^C +Z51zlxMYjO2I0
n<stQBjp#
LGǗIi00 nueGD`i_Ib6]YzC%3_rlrʗG侇IJ'lFJ|(~I)^'nP7|9 +o2ѣ3̱%R
~4+,>Bқ-}J~wKi7'}i%Iuz3<4̙$#'qCLUY9kR`D =H%Sp2
+[@*m,!!)k2)ɮty`=V^9z==$ +:XYy<(aTΚYUTC>]aamoL +x;З<^g +3-%'+bIOcz7/z +eCeܙOCBRX7'(47+AARR4)D +
9{JOkQtp[{otS9QKѥ%Wmnc|a1}~I?.R(h|ӈ1qMT⨢qzr*zRmQdg>It +I ѵnrKIIt$A2}f.1~ϐ}?V?jkJybU-7Js:#Ì*'''&]Ib$&
J}.;krSPE е굱.^6Hwwk~iH)i&)%}CT<@Yzy8?o>WK*1gRF뵜GOF|%R)S5n +uyZ]ZM//_!!8 ĈӢcJ'u֞EF/L.\L~r)?[+Tݤb}B/:F3;n
'CII̙VI!S9ҨHy&QwmD!nehiӾ{1fCڇoOQ6wŕ{-凶3h +F}712`r'Gד<8iԍw( +&t禇ڥgPˆ^>beR/]R{^)fϗkݣ8x#%@Oɘ{Nx}$#J1 +=I[gi)R +Np6fǴL
gOd41
܌uȏbƚ':̗ݛ+Њd߽@Q2}T"ӍΛ?"t#D#H4' IxWNWK+ +Zr +fk=]Q|fd +N +2S9<?lK~{D=}e,`o + +:CH_sT%[dzwogb_W>O=>ܷ/~. +>β9K>]>]tͷn٨tv55mww]L[ncb?:^`û?{}q +tas۶mzJ*xo^O$/?9"mvfƙ, +]r˽߰ +ٴ._5dB\B\#톶m=B+ +ʏ$ҏ-,%17r!o +ƣF6eTK#2I'Β;yF;Q SYk4Ŷ^#xeI9j%Ӗ];|]POc2SLC(8cӧvw_O?=y,M榦̿-cmOnܓ;/MT܊ .n#ݜͫ3ty`<D{Q2#=+x˄7HXX(=_:z`sVOikfR38NLR}Vοҋo_莕6/m哥<ػ{}%;8okݢ{eƹ>ۖ%;WqM]QA2 +#@'EbFFt pAVnjAzxꞭb0Qy@OO"m}駟~}/*s5_rǍW7XM>A|˟^d ~E:{k/=c5ײԳ{ks&8,/owk,{gN6w }g+]_EU3Qkmg4<Q\0VD\O]fLҌ<~LșӧN.to~uW^xQֳ|7^|)$Tί9GFg맯ޭ#x xWOAO"AQH[P}n)\r/8С!c;IU6Gӧ[C&yN + +W?Yҟ^̻;cH.Pz0'J7g?:k~c{g.r#n?y[6F>]i}Cθ_5 =sGv=oW{{ÃlRݢ8ό +%@Bc܀GI@dl5wclq{l|3wSI73=!}m#:eN}129!1wY\tW]=>k/Lzfx{KGl!\GL7sǥkbny~D˨OoDr7D-V+_~gRD
0-$nXsH= ,zY,7.6)'^3'ŬƊynn,XX[5IY.+G:՛usG?᧿O?rLsy\W!_̇\VUz9顱&%DBVaUOQ𧼇Uxѕ% +cTFJbSi
}E3sF+?(\4T2i!nxKXXm$E|rMƭxt<0μvʼnOg̷x*Z0}1#5v+1g2hNfeх;<5s%;zQ'qkJ.i՞ +#Uրtuy9{gQΛrLo]Q틼id4_`^{tB¸λ庫ͯS{Rud?Y4].2ﱞZǚcxN&S1K2g&W0>9,)Fv^?=ܿrVΛY7p}ˀnqwyḎo%tyok~uO=O:&s0.xНtֱMJ9oP'i`pPaƚ[;XQxLx yʋ$kzYFjNK.La\>>ǒ?ǦnA⹛0D+,i9n?&X^Ͷl1q~fu$Z{0EcOw]%?uN 6]!//<ondJj>^QHKsSk]`.a3%fn:?CQwC-6,7#pU:Q^ŜܬH} ʊ8+gSObJ!]]Iޤ< ABJzk8vL7pF˛wv/Ʈ^ȇC̴Dfb{Ug`ʒ{|l[*'G|=C{@XZ`7m^N27r[J _{>0s~@-DZeq2fsֳ)9qZ9oEsIDu
QT϶`Ğ=P<!2^̜I{p'ѕ2xԤbX7%n䑟?3q +V')'sj +azy SXa_{ɉ܍ ww=3'K@2EuEiAb)*k
Hok^-.n.u,l/'ֱ-RqVs]9XQx+MЇsȸޡLJ粃P^.Uw2jH +QeUGO[+4'Y*2JgNA림w2\(^]}Ըm>9,,@uXM
OfGfOod%8 -+--%%K.?3YX]^M
rҍN˝&Nn&B;/LG&|?J}"E_M=sO_7hF̠=๙U҃>^D}ˠggeCԱZi},na}g_校ۼ^nWAX
/wOjմkDCVoQ 5@sΖ8+<t@z!eKzxZ/8UX-\5fԾ]s2dsGG#SC!r@+!wOL79mr~N\k#.jwt#^9o& +hg=2 Z{leM3nKrGF+ݸً +F{?" NH8\\m +1 }ypܐ=۷QCW.1D]垙fќeYZJX*($FM#&WD_ªkm~NMb>6Ѹ]!j_wU,gÇթYc^i^oYܳfD|PU^D/]17\f3aj}"R:?{K$wOJ<\s&>s#yad}ț 2-~䧕*W͟(g;Nz#^RTd=[|Sq!X/VfUnҗU(~YKx'#[&H~̺7#sR|5$H +/`FzcV
ryA6s?,˜E+xf3K&t}\t Cx#t[[Ar#_{Rh$?J;i7n!BH ++>yXg,\?j2&wtI RMqWQBk`[jv/F|z;x{+ + +̙'{?qDqx
s2yF;q +bs݊Jn^0yG-Ju'82$y09ϫо]iy/qIN6A|!ݒy +#y`Jaq ćFNg^<&Bw +kW$PDž +sMg]4GYA%yKjwoqqskK6xD>LDm}vsLJ5[Cniy6YIȝx8 +o,g}Z>"ҟv#+Jo|uk# +# +ObԤCdāܖZu⮉|)I0b;4tsYWÍ +GYbewi^VSExn[SZ{jߓ9_VEJT^%%˿&xa70 + +o)XfKϙ{ۄJΤ4xѣp/wzzj;M +}Oi:t(+u#EI{'Ή "P}"u艇@s
& +# +(}/L.^_7 +pH=e".e{)vn,9ŀM^WvR.f_ծod88]ynj,?ԗYV7pFQrrZrQbU k5S +M_{oUNNa&?_TQ +nAL +b& +ڏ@pý(z>[ɞGJkQZ#8>u_J >ܨ@f.m16]aw +?aQP2=`ܸ+ +NaSǩ_OGb<r¡tᢞ~qϮtf[t(03%`SVxQ߯ M<la<+JI0M7{υn^5DZ (ϢEnHn\p1< + H|P< +LjU^n.I:5\ub⺂ 'cSx!*IՊJKEŅ##/OGd}({
RFڐ+}{aq6.K>m +n_(0=GjyOfeQ}ZƮ5[U'6d,1[8߽~SZGbߏ{rf@pz/7y U6@USM=, fW 1{^)?j1#OnܜM\Ra]KlV
~!_\6=s8]H;_/jC9L=0cPIc6bVw.Nm:r٤Y+ubӯyȀ9\VG/kM*}3ImYUEvd͝v]qCx탯ʹjM|(Ο&SԶ5\ubBo߯NC{a֣F6yVAXW_&Iwp +a=`.o:{k-;S[LփE?N ؘqMs*g1rHVvD0P +Iد%Oŝjn1EWƮۍhex.Tql/#SدWM1~*[>ycrS"hm2w7Bc-vwpՉM +ɗ9{{jn1,w-,u,'!$3>vS}-ši<$uW6 +~"DC}M=>:ȓm-}goM2L~~WhVYCJ!vq[+݈4{7<$?rf^:5c=o,!.{kĢ34+Wgwq)n5$?WW תP>n9[YXtqoXV0~cn[!2bFjeL*rpoy 0l_{#C*Hfo+#7Qؕ4CH]^U\Lf7C+2IP~{N7 k|ߗ~|&6ݠSCFPn?W
t0cU1gs H_^H=db)5 +`.NZ:!Ups60YMt&5U٧+ɰBmZ.j{6^hC!B^o3w[4dc Pr.0q:;C:vcvqh.{u&M^}hđ.q<1g(KxHcpdGT3>Fe}2Eп0G=Ƽ2{XMgŢ{¥l`cv+DпXt(-^J}^BO N0kA,-6D )8P6Ʀޠ!]_U]nn[=0mZW8l]ƽ/0:Z+?AZuث?
<4CP:T#<c{e:_U(ׇcXH88w?# +GۯA{H|%uD
<DϾ?C+56}TkxūeY]T>HK~rWkbApѳOv{mX7MҘIUcٿ}d"ݵhf'{?٪rQItC4v2b[-
!mycNGLb(r/7$k 'R^іaONS~wY7*>_&Jߘ&lw;hfTJ{&1On"Hq2n7~:v?E=ĘBu(T[CI/^^xj'¢)!!^TnD5x:JuܯUWvZbHL=jĦSp잣耸4H]yHWMJ&Ʊ4b&3oS[wǦk:({C=$nr$ݏko<Gd2$ :pՉM+oSO~"N;l#C=]
IƬVqdӰsTCŐد4L +]XtH'rܰu'fl+?(Ħy/юhܜ<ͨ&x6oHcӯTRo|ߡ>vggXZd=||mM% +G<2^ +{rR$#6(C')
&4Zb7*M1v3@Ul9AdA:f¼U/+-9;D'8ZrRIeކBtEyc
*6c +*-U*Z!Z0o@Qؓt%♃9 +@Ul?^%w2\
PTj
`_ؽUa +ɍN@2 +SIԉdJ7.QǼz亁0w2^fO.ơ3q'
rQ@]Q!7aހ8Al#qyuIG? +#jTq'=dwNTro@afie̛IdutMsIǧp'8*rc6PnW&f"$82\0U-rʁCTƢU&PGb"-!#BpEvVžn亁qUjLҩr*c/P%Zb,
s'&,'8*$C2 +$?Fy :PprQ߀~Mߣs)c$K(wz"Np쵼t7p? tŦÑX(&Vt +rGqMYxnkVm\l1Y=JթJ?}߳Ak d++on3L]o[سh*irg.
D33VOI*;C&B$J{vE^Ξchao~Bv Ozyt9< ٽƁ?_OMl2W܅Yb2F"wۨcí+a@b}?ks6gjQo :,zӛ.a`D%m78r +J!4|d\ǿDn +h{S#-:W> +m@p@IZyj֍twp$Py#\p(E7&yџ*j8P ?)0~LbBp@+ 2!@Z6DaoR`3!8 +:yǑ +
2I!$ش#&ewpO-cӣō<wNT5piC
+CչSE&(Ǣ#յse
Aބ܀tQCGQgPnOL6o]b=v xHpl<lpݺtfzSGP8r耞ݺvn!}d + +fj 8 tqқI~tšOg-Н2鍹p'tcD\9%@_l@fp'شh<z3HϕŢw{maހXuCהForƗ +֛Ia +lzl\!@T`ӣGXontĐ_D65z3H4*Gv||$FD̲DУ4I75 +AFm "w)1w2r/#%8G2.l<<2,zA:D#1^prQ7uTG`gP[n>0BbVo&qeIzBo :qlp@ +C1x ZaІ3ygmXJi[Nٚ]eA; fmh+1w2
D56m.ReX6
wD7][x 辋 +C~N +MLo + + +Wp^cL` +}MbFF|7vn%P<ܨ]98sp0߆Ș܍1y2ǧ$ಾES\_7ްzw7$#龻7@;c7=w@ +%?}sYȖ`JO#6%LI7\@wդ]KOu|pB9B
c׀ p\<h<<ޘo]𬨣N{KXic4pmtdmz6|}Gǜ!&phglB14\Hm>U
Fp_{%̏yȏlⴓC.(YPQ,|9w`b@m16t=9J|ppΐ_ +ct,+ +_l}8rr'_j-AUoif~tn=R], +GUV!prׂu/cp0?r讵`2z+y,*|Wm%` HN~a!dl4L)\9,[ȃx78g1!Ot9zK?f +oH5 Xb=-%=}h6-ePsX>{ 01rlB7VڟBC:dX +?gOBP涋mL=C) +~КVQGKf
ytETmTdE8Y?^MՓ$8qJ\02&VQG_<26OGݴoX &9&Lz<9p(dl^K/r3"]0^2܋|%?~ЛN-"ŖΟc#Ǣ +G&ǀx47L:0{[ixgyrlKs4?yx9r4.| +ل39xSp"'Tn덷j8f)ްl(D +oH}+OnSuR?] +u_p]kW 0,k&3%n})R
R<P`^W];Ƣ)BkܥK(oSMv&?Վ4:dC )_n.+8y]gBpzCmC] +Q*(-Z_۲rh2r> +WE>!pr@͍˿<su,o=zI;afE577t1f6ŃGѫl +5Ĵ09)0ɯC7vlv]gOYVvbtOxT$AYw\ww)L)FmlxG'O?>r1H^ҋ/P6dd2T\6#ܥ*#FƏI]ç/?/0iml3R +Mڐr#rM7AԱc}m߸᧫V2(&C@S +_ZvXnt8<]>xG~~݆9_u!ԍd~Z@.]Zaz1M8/xs3ݵ
1qK]17|s|@ +G#_o/-m5YQgMңKf}]\{Ւ>X"
Lƈ0y0psDB9z|(1/ bJS_JCqZ3V6 /+ՆgF/-)6*t5fMsRs"Kc(N4S_>U$P&)+mE<LQ>GceQk\~']6Tgmm6Լ^Wc5g*cbE Ćx:)>n%ӶZפmЬ7g袲YzGsW`.#-ɘ7+Fkmj}|֛#W?k¦FCAQ]rCF˺ڼx}A`lN6}[Mb[fLQe5E&;Őܬ.]ї9یMٵm%re|v`-Ԣj-2R樾 +C2l27kʲB]_zuEkFV\L6ڠQw>j0#ݽ˴5EZr%}n\bj{RuD93rx G^r0&ϤꯏfΡ@ݗѮhrfENMݘST4 +mIT:VQ +}Vowö^5ݍ{cs"fjZL}wQc02c;ӝ>Rl[)qW6z]P"~`s+[EbZBDQnSMC(ΑXߝ:؝[d2csbm֖ ߥ,mj0QoӒX3@ggT*+
kkʛu9jg8^ng2R' )UUҟѕnHoo.p4$GMΠFh2s\^m1e^#<tNZ`TimYPG>t~TP:-Jԓ+\e6ZU*SSuԷv)6Z,EI}v^ұ/МߝC40w#d*]Ѣn+ڢWeO16UeFVk]Uݥ+Փf!r&ܙs͚5GPF[SRBY.ĤkY-Ime)U&92ͨn.6W-3[ZcuZmkIF[\e'yP'+RB兆h&+ٸ^Zҥ5e)v}JSTȭv9QNhlNXƩ]ָ3Ra6%SJ][i[M9efBJwŕJ +"'9h,dRLQZJӔjcIAǪ|٘֘ڣcIi)-2IUV:<QA}Ue!}&k
#5`\S<GԍivU)_eeLd0S舰FTsVSs͊eiφ5mQVnPNc5l +p*EwmlعU{ +xTs4> +LL<m*9|yd.T,u<Ʋ +x_Ts4Ur>*\q3<WnwvK-= +p(mC鱙Tke_mo*bC"OmZU r7;ʖ +cި +p'Hc<WR6zlfX.xV9U r}<s`ǦS +\rnvl +k__괭>Q5J,(I
endstream
endobj
15 0 obj
[/ICCBased 19 0 R]
endobj
6 0 obj
[5 0 R]
endobj
32 0 obj
<</CreationDate(D:20160615142312-04'00')/Creator(Adobe Illustrator CC 2015 \(Macintosh\))/ModDate(D:20160615142312-04'00')/Producer(Adobe PDF library 15.00)/Title(metamask_icon)>>
endobj
xref
0 33
0000000000 65535 f
+0000000016 00000 n
+0000000144 00000 n
+0000047649 00000 n
+0000000000 00000 f
+0000163121 00000 n
+0000593503 00000 n
+0000047700 00000 n
+0000048109 00000 n
+0000048283 00000 n
+0000163420 00000 n
+0000139682 00000 n
+0000163307 00000 n
+0000049181 00000 n
+0000048344 00000 n
+0000593468 00000 n
+0000048620 00000 n
+0000048668 00000 n
+0000139717 00000 n
+0000160473 00000 n
+0000163191 00000 n
+0000163222 00000 n
+0000163494 00000 n
+0000163800 00000 n
+0000165099 00000 n
+0000187851 00000 n
+0000253439 00000 n
+0000319027 00000 n
+0000384615 00000 n
+0000450203 00000 n
+0000515791 00000 n
+0000581379 00000 n
+0000593526 00000 n
+trailer
<</Size 33/Root 1 0 R/Info 32 0 R/ID[<858D18969ABF4CF88593CFB9A20C1759><B33F39DA517C42B9A50D10EC91C85574>]>>
startxref
593722
%%EOF
\ No newline at end of file diff --git a/ui/responsive/design/chromeStorePics/promo1400560.png b/ui/responsive/design/chromeStorePics/promo1400560.png Binary files differnew file mode 100644 index 000000000..d3637ecc8 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/promo1400560.png diff --git a/ui/responsive/design/chromeStorePics/promo440280.png b/ui/responsive/design/chromeStorePics/promo440280.png Binary files differnew file mode 100644 index 000000000..c1f92b1c0 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/promo440280.png diff --git a/ui/responsive/design/chromeStorePics/promo920680.png b/ui/responsive/design/chromeStorePics/promo920680.png Binary files differnew file mode 100644 index 000000000..726bd810a --- /dev/null +++ b/ui/responsive/design/chromeStorePics/promo920680.png diff --git a/ui/responsive/design/chromeStorePics/screen_dao_accounts.png b/ui/responsive/design/chromeStorePics/screen_dao_accounts.png Binary files differnew file mode 100644 index 000000000..1a2e8052c --- /dev/null +++ b/ui/responsive/design/chromeStorePics/screen_dao_accounts.png diff --git a/ui/responsive/design/chromeStorePics/screen_dao_locked.png b/ui/responsive/design/chromeStorePics/screen_dao_locked.png Binary files differnew file mode 100644 index 000000000..6592c17e4 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/screen_dao_locked.png diff --git a/ui/responsive/design/chromeStorePics/screen_dao_notification.png b/ui/responsive/design/chromeStorePics/screen_dao_notification.png Binary files differnew file mode 100644 index 000000000..baeb2ec39 --- /dev/null +++ b/ui/responsive/design/chromeStorePics/screen_dao_notification.png diff --git a/ui/responsive/design/chromeStorePics/screen_wei_account.png b/ui/responsive/design/chromeStorePics/screen_wei_account.png Binary files differnew file mode 100644 index 000000000..23301e4bf --- /dev/null +++ b/ui/responsive/design/chromeStorePics/screen_wei_account.png diff --git a/ui/responsive/design/chromeStorePics/screen_wei_notification.png b/ui/responsive/design/chromeStorePics/screen_wei_notification.png Binary files differnew file mode 100644 index 000000000..7a763e5df --- /dev/null +++ b/ui/responsive/design/chromeStorePics/screen_wei_notification.png diff --git a/ui/responsive/design/metamask-logo-eyes.png b/ui/responsive/design/metamask-logo-eyes.png Binary files differnew file mode 100644 index 000000000..c29331b28 --- /dev/null +++ b/ui/responsive/design/metamask-logo-eyes.png diff --git a/ui/responsive/design/wireframes/1st_time_use.png b/ui/responsive/design/wireframes/1st_time_use.png Binary files differnew file mode 100644 index 000000000..c18ced5e2 --- /dev/null +++ b/ui/responsive/design/wireframes/1st_time_use.png diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf b/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf Binary files differnew file mode 100644 index 000000000..c77c9274a --- /dev/null +++ b/ui/responsive/design/wireframes/metamask_wfs_jan_13.pdf diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_13.png b/ui/responsive/design/wireframes/metamask_wfs_jan_13.png Binary files differnew file mode 100644 index 000000000..d71d7bdb4 --- /dev/null +++ b/ui/responsive/design/wireframes/metamask_wfs_jan_13.png diff --git a/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf b/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf Binary files differnew file mode 100644 index 000000000..592ba8532 --- /dev/null +++ b/ui/responsive/design/wireframes/metamask_wfs_jan_18.pdf diff --git a/ui/responsive/example.js b/ui/responsive/example.js new file mode 100644 index 000000000..4627c0e9c --- /dev/null +++ b/ui/responsive/example.js @@ -0,0 +1,123 @@ +const injectCss = require('inject-css') +const MetaMaskUi = require('./index.js') +const MetaMaskUiCss = require('./css.js') +const EventEmitter = require('events').EventEmitter + +// account management + +var identities = { + '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111': { + name: 'Walrus', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + balance: 220, + txCount: 4, + }, + '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222': { + name: 'Tardus', + img: 'QmQYaRdrf2EhRhJWaHnts8Meu1mZiXrNib5W1P6cYmXWRL', + address: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + balance: 10.005, + txCount: 16, + }, + '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333': { + name: 'Gambler', + img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd', + address: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + balance: 0.000001, + txCount: 1, + }, +} + +var unapprovedTxs = {} +addUnconfTx({ + from: '0x222462427bcc9133bb46e88bcbe39cd7ef0e7222', + to: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + value: '0x123', +}) +addUnconfTx({ + from: '0x1113462427bcc9133bb46e88bcbe39cd7ef0e111', + to: '0x333462427bcc9133bb46e88bcbe39cd7ef0e7333', + value: '0x0000', + data: '0x000462427bcc9133bb46e88bcbe39cd7ef0e7000', +}) + +function addUnconfTx (txParams) { + var time = (new Date()).getTime() + var id = createRandomId() + unapprovedTxs[id] = { + id: id, + txParams: txParams, + time: time, + } +} + +var isUnlocked = false +var selectedAccount = null + +function getState () { + return { + isUnlocked: isUnlocked, + identities: isUnlocked ? identities : {}, + unapprovedTxs: isUnlocked ? unapprovedTxs : {}, + selectedAccount: selectedAccount, + } +} + +var accountManager = new EventEmitter() + +accountManager.getState = function (cb) { + cb(null, getState()) +} + +accountManager.setLocked = function () { + isUnlocked = false + this._didUpdate() +} + +accountManager.submitPassword = function (password, cb) { + if (password === 'test') { + isUnlocked = true + cb(null, getState()) + this._didUpdate() + } else { + cb(new Error('Bad password -- try "test"')) + } +} + +accountManager.setSelectedAccount = function (address, cb) { + selectedAccount = address + cb(null, getState()) + this._didUpdate() +} + +accountManager.signTransaction = function (txParams, cb) { + alert('signing tx....') +} + +accountManager._didUpdate = function () { + this.emit('update', getState()) +} + +// start app + +var container = document.getElementById('app-content') + +var css = MetaMaskUiCss() +injectCss(css) + +MetaMaskUi({ + container: container, + accountManager: accountManager, +}) + +// util + +function createRandomId () { + // 13 time digits + var datePart = new Date().getTime() * Math.pow(10, 3) + // 3 random digits + var extraPart = Math.floor(Math.random() * Math.pow(10, 3)) + // 16 digits + return datePart + extraPart +} diff --git a/ui/responsive/index.html b/ui/responsive/index.html new file mode 100644 index 000000000..9dfaefbb3 --- /dev/null +++ b/ui/responsive/index.html @@ -0,0 +1,20 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <title>MetaMask</title> + </head> + <body> + + <!-- app content --> + <div id="app-content"></div> + <script src="./bundle.js" type="text/javascript" charset="utf-8"></script> + + <!-- design reference --> + <link rel="stylesheet" type="text/css" href="./app/css/debug.css"> + <div id="design-container"> + <img id="design-img" src="./design/metamask_wfs_jan_13.png"> + </div> + + </body> +</html> diff --git a/ui/responsive/index.js b/ui/responsive/index.js new file mode 100644 index 000000000..a729138d3 --- /dev/null +++ b/ui/responsive/index.js @@ -0,0 +1,58 @@ +const render = require('react-dom').render +const h = require('react-hyperscript') +const Root = require('./app/root') +const actions = require('./app/actions') +const configureStore = require('./app/store') +const txHelper = require('./lib/tx-helper') +global.log = require('loglevel') + +module.exports = launchMetamaskUi + + +log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn') + +function launchMetamaskUi (opts, cb) { + var accountManager = opts.accountManager + actions._setBackgroundConnection(accountManager) + // check if we are unlocked first + accountManager.getState(function (err, metamaskState) { + if (err) return cb(err) + const store = startApp(metamaskState, accountManager, opts) + cb(null, store) + }) +} + +function startApp (metamaskState, accountManager, opts) { + // parse opts + const store = configureStore({ + + // metamaskState represents the cross-tab state + metamask: metamaskState, + + // appState represents the current tab's popup state + appState: {}, + + // Which blockchain we are using: + networkVersion: opts.networkVersion, + }) + + // if unconfirmed txs, start on txConf page + const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network) + if (unapprovedTxsAll.length > 0) { + store.dispatch(actions.showConfTxPage()) + } + + accountManager.on('update', function (metamaskState) { + store.dispatch(actions.updateMetamaskState(metamaskState)) + }) + + // start app + render( + h(Root, { + // inject initial state + store: store, + } + ), opts.container) + + return store +} diff --git a/ui/responsive/lib/account-link.js b/ui/responsive/lib/account-link.js new file mode 100644 index 000000000..d061d0ad1 --- /dev/null +++ b/ui/responsive/lib/account-link.js @@ -0,0 +1,26 @@ +module.exports = function (address, network) { + const net = parseInt(network) + let link + switch (net) { + case 1: // main net + link = `http://etherscan.io/address/${address}` + break + case 2: // morden test net + link = `http://morden.etherscan.io/address/${address}` + break + case 3: // ropsten test net + link = `http://ropsten.etherscan.io/address/${address}` + break + case 4: // rinkeby test net + link = `http://rinkeby.etherscan.io/address/${address}` + break + case 42: // kovan test net + link = `http://kovan.etherscan.io/address/${address}` + break + default: + link = '' + break + } + + return link +} diff --git a/ui/responsive/lib/contract-namer.js b/ui/responsive/lib/contract-namer.js new file mode 100644 index 000000000..f05e770cc --- /dev/null +++ b/ui/responsive/lib/contract-namer.js @@ -0,0 +1,33 @@ +/* CONTRACT NAMER + * + * Takes an address, + * Returns a nicname if we have one stored, + * otherwise returns null. + */ + +const contractMap = require('eth-contract-metadata') +const ethUtil = require('ethereumjs-util') + +module.exports = function (addr, identities = {}) { + const checksummed = ethUtil.toChecksumAddress(addr) + if (contractMap[checksummed] && contractMap[checksummed].name) { + return contractMap[checksummed].name + } + + const address = addr.toLowerCase() + const ids = hashFromIdentities(identities) + return addrFromHash(address, ids) +} + +function hashFromIdentities (identities) { + const result = {} + for (const key in identities) { + result[key] = identities[key].name + } + return result +} + +function addrFromHash (addr, hash) { + const address = addr.toLowerCase() + return hash[address] || null +} diff --git a/ui/responsive/lib/etherscan-prefix-for-network.js b/ui/responsive/lib/etherscan-prefix-for-network.js new file mode 100644 index 000000000..2c1904f1c --- /dev/null +++ b/ui/responsive/lib/etherscan-prefix-for-network.js @@ -0,0 +1,21 @@ +module.exports = function (network) { + const net = parseInt(network) + let prefix + switch (net) { + case 1: // main net + prefix = '' + break + case 3: // ropsten test net + prefix = 'ropsten.' + break + case 4: // rinkeby test net + prefix = 'rinkeby.' + break + case 42: // kovan test net + prefix = 'kovan.' + break + default: + prefix = '' + } + return prefix +} diff --git a/ui/responsive/lib/explorer-link.js b/ui/responsive/lib/explorer-link.js new file mode 100644 index 000000000..3b82ecd5f --- /dev/null +++ b/ui/responsive/lib/explorer-link.js @@ -0,0 +1,6 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + +module.exports = function (hash, network) { + const prefix = prefixForNetwork(network) + return `http://${prefix}etherscan.io/tx/${hash}` +} diff --git a/ui/responsive/lib/icon-factory.js b/ui/responsive/lib/icon-factory.js new file mode 100644 index 000000000..27a74de66 --- /dev/null +++ b/ui/responsive/lib/icon-factory.js @@ -0,0 +1,65 @@ +var iconFactory +const isValidAddress = require('ethereumjs-util').isValidAddress +const toChecksumAddress = require('ethereumjs-util').toChecksumAddress +const contractMap = require('eth-contract-metadata') + +module.exports = function (jazzicon) { + if (!iconFactory) { + iconFactory = new IconFactory(jazzicon) + } + return iconFactory +} + +function IconFactory (jazzicon) { + this.jazzicon = jazzicon + this.cache = {} +} + +IconFactory.prototype.iconForAddress = function (address, diameter) { + const addr = toChecksumAddress(address) + if (iconExistsFor(addr)) { + return imageElFor(addr) + } + + return this.generateIdenticonSvg(address, diameter) +} + +// returns svg dom element +IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { + var cacheId = `${address}:${diameter}` + // check cache, lazily generate and populate cache + var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) + // create a clean copy so you can modify it + var cleanCopy = identicon.cloneNode(true) + return cleanCopy +} + +// creates a new identicon +IconFactory.prototype.generateNewIdenticon = function (address, diameter) { + var numericRepresentation = jsNumberForAddress(address) + var identicon = this.jazzicon(diameter, numericRepresentation) + return identicon +} + +// util + +function iconExistsFor (address) { + return contractMap[address] && isValidAddress(address) && contractMap[address].logo +} + +function imageElFor (address) { + const contract = contractMap[address] + const fileName = contract.logo + const path = `images/contract/${fileName}` + const img = document.createElement('img') + img.src = path + img.style.width = '75%' + return img +} + +function jsNumberForAddress (address) { + var addr = address.slice(2, 10) + var seed = parseInt(addr, 16) + return seed +} + diff --git a/ui/responsive/lib/lost-accounts-notice.js b/ui/responsive/lib/lost-accounts-notice.js new file mode 100644 index 000000000..948b13db6 --- /dev/null +++ b/ui/responsive/lib/lost-accounts-notice.js @@ -0,0 +1,23 @@ +const summary = require('../app/util').addressSummary + +module.exports = function (lostAccounts) { + return { + date: new Date().toDateString(), + title: 'Account Problem Caught', + body: `MetaMask has fixed a bug where some accounts were previously mis-generated. This was a rare issue, but you were affected! + +We have successfully imported the accounts that were mis-generated, but they will no longer be recovered with your normal seed phrase. + +We have marked the affected accounts as "Loose", and recommend you transfer ether and tokens away from those accounts, or export & back them up elsewhere. + +Your affected accounts are: +${lostAccounts.map(acct => ` - ${summary(acct)}`).join('\n')} + +These accounts have been marked as "Loose" so they will be easy to recognize in the account list. + +For more information, please read [our blog post.][1] + +[1]: https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd#.7d8ktj4h3 + `, + } +} diff --git a/ui/responsive/lib/persistent-form.js b/ui/responsive/lib/persistent-form.js new file mode 100644 index 000000000..d4dc20b03 --- /dev/null +++ b/ui/responsive/lib/persistent-form.js @@ -0,0 +1,61 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const defaultKey = 'persistent-form-default' +const eventName = 'keyup' + +module.exports = PersistentForm + +function PersistentForm () { + Component.call(this) +} + +inherits(PersistentForm, Component) + +PersistentForm.prototype.componentDidMount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + const store = this.getPersistentStore() + + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + const key = field.getAttribute('data-persistent-formid') + const cached = store[key] + if (cached !== undefined) { + field.value = cached + } + + field.addEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } +} + +PersistentForm.prototype.getPersistentStore = function () { + let store = window.localStorage[this.persistentFormParentId || defaultKey] + if (store && store !== 'null') { + store = JSON.parse(store) + } else { + store = {} + } + return store +} + +PersistentForm.prototype.setPersistentStore = function (newStore) { + window.localStorage[this.persistentFormParentId || defaultKey] = JSON.stringify(newStore) +} + +PersistentForm.prototype.persistentFieldDidUpdate = function (event) { + const field = event.target + const store = this.getPersistentStore() + const key = field.getAttribute('data-persistent-formid') + const val = field.value + store[key] = val + this.setPersistentStore(store) +} + +PersistentForm.prototype.componentWillUnmount = function () { + const fields = document.querySelectorAll('[data-persistent-formid]') + for (var i = 0; i < fields.length; i++) { + const field = fields[i] + field.removeEventListener(eventName, this.persistentFieldDidUpdate.bind(this)) + } + this.setPersistentStore({}) +} + diff --git a/ui/responsive/lib/tx-helper.js b/ui/responsive/lib/tx-helper.js new file mode 100644 index 000000000..ec19daf64 --- /dev/null +++ b/ui/responsive/lib/tx-helper.js @@ -0,0 +1,17 @@ +const valuesFor = require('../app/util').valuesFor + +module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { + log.debug('tx-helper called with params:') + log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) + + const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs) + log.debug(`tx helper found ${txValues.length} unapproved txs`) + const msgValues = valuesFor(unapprovedMsgs) + log.debug(`tx helper found ${msgValues.length} unsigned messages`) + let allValues = txValues.concat(msgValues) + const personalValues = valuesFor(personalMsgs) + log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) + allValues = allValues.concat(personalValues) + + return allValues.sort(txMeta => txMeta.time) +} |