Compare commits

..
Author SHA1 Message Date
Siddarth Kumar 89d7f4a950 fix: make linter happy 2024-12-10 15:26:52 +05:30
Siddarth Kumar c96d3f427a fix: update SENTRY_DSN_STATUS_GO config 2024-12-10 15:08:07 +05:30
Icaro Motta 1d3e0be2e2 TEMP FOR PR: Show if MixPanel token is set 2024-12-09 14:33:47 -03:00
marko 52cbdf2aaa ci: enable sentry for releases
referenced issue: https://github.com/status-im/status-mobile/issues/21706

Signed-off-by: markoburcul <marko@status.im>
2024-12-09 13:03:16 -03:00
Icaro Motta 32fe1a04e1 TEMP for PR bump VERSION 2024-12-09 13:03:16 -03:00
Icaro Motta 0dab5b47ad Add SENTRY_DSN_STATUS_GO to SECRETS_ENV_VARS 2024-12-09 13:03:16 -03:00
Icaro Motta ec58e7e3c4 Warn in UI if Sentry DSN isn't set 2024-12-09 13:03:16 -03:00
Icaro Motta 7d60849b13 TEMPORARILY enable intentional crash flag
To work in all builds, including release
2024-12-09 13:03:16 -03:00
Icaro Motta cb1f807841 Configure Sentry 2024-12-09 13:03:16 -03:00
710 changed files with 27239 additions and 8150 deletions
+185
View File
@@ -0,0 +1,185 @@
legacy.status-im.utils.build/warning-handler
legacy.status-im.utils.build/get-current-sha
status-im.constants/spacing-char
status-im.constants/arg-wrapping-char
legacy.status-im.ios.core/init
legacy.status-im.ui.components.camera/aspects
legacy.status-im.ui.components.camera/capture-targets
legacy.status-im.ui.components.camera/set-torch
legacy.status-im.ui.components.camera/request-access-ios
legacy.status-im.chat.models.message-content/stylings
legacy.status-im.chat.models.message-content/actions
legacy.status-im.chat.models.message-content/blank-string
legacy.status-im.chat.models.message-content/sorted-ranges
legacy.status-im.ethereum.mnemonic/words->passphrase
native-module.core/listener
native-module.core/multiaccount-reset
native-module.core/extract-group-membership-signatures
native-module.core/sign-group-membership
native-module.core/update-mailservers
legacy.status-im.ethereum.abi-spec/bytes-to-hex
legacy.status-im.android.core/init
legacy.status-im.chat.models.message/transport-keys
legacy.status-im.utils.datetime/week
legacy.status-im.utils.datetime/time-ago
legacy.status-im.utils.datetime/format-date
legacy.status-im.utils.datetime/get-ordinal-date
reagent.dom/render
reagent.dom/unmount-component-at-node
reagent.dom/dom-node
reagent.dom/force-update-all
legacy.status-im.multiaccounts.create.core/get-new-key-code
legacy.status-im.ethereum.decode/string
legacy.status-im.ui.screens.network.views/mainnet?
legacy.status-im.data-store.chats/event->string
legacy.status-im.mailserver.constants/seven-days
legacy.status-im.mailserver.constants/connection-timeout
legacy.status-im.hardwallet.card/install-applet
legacy.status-im.hardwallet.card/install-cash-applet
legacy.status-im.multiaccount.login.data-test/all-contacts
legacy.status-im.multiaccount.login.data-test/multiaccounts
legacy.status-im.multiaccount.login.data-test/get-chats
legacy.status-im.multiaccount.login.data-test/transport
legacy.status-im.multiaccount.login.data-test/topics
legacy.status-im.utils.utils/clear-timeout
legacy.status-im.utils.handlers/logged-in
legacy.status-im.multiaccounts.model/credentials
legacy.status-im.multiaccounts.login.core/contract-fleet?
legacy.status-im.multiaccounts.login.core/fetch-nodes
status-im.config/rpc-networks-only?
status-im.config/waku-enabled?
legacy.status-im.utils.pairing/has-paired-installations?
legacy.status-im.tribute-to-talk.core-test/user-cofx
mocks.js-dependencies/action-button
mocks.js-dependencies/camera
mocks.js-dependencies/dismiss-keyboard
mocks.js-dependencies/emoji-picker
mocks.js-dependencies/i18n
mocks.js-dependencies/qr-code
mocks.js-dependencies/svg
mocks.js-dependencies/icons
mocks.js-dependencies/webview
mocks.js-dependencies/desktop-linking
mocks.js-dependencies/desktop-shortcuts
mocks.js-dependencies/snoopy
mocks.js-dependencies/snoopy-filter
mocks.js-dependencies/snoopy-bars
mocks.js-dependencies/snoopy-buffer
mocks.js-dependencies/desktop-menu
mocks.js-dependencies/desktop-config
mocks.js-dependencies/react-native-navigation-twopane
mocks.js-dependencies/react-native-screens
mocks.js-dependencies/react-native-shake
mocks.js-dependencies/back-handler
mocks.js-dependencies/react
mocks.js-dependencies/react-navigation-native
mocks.js-dependencies/react-navigation-stack
mocks.js-dependencies/react-navigation-bottom-tabs
mocks.js-dependencies/react-native-haptic-feedback
mocks.js-dependencies/mock
quo.previews.main/init
legacy.status-im.chat.models.message-list-test/descending-range
legacy.status-im.chat.models.message-list-test/random-range
legacy.status-im.ethereum.eip165/supports?
legacy.status-im.utils.hex/valid-hex?
legacy.status-im.multiaccounts.core/chat-send?
legacy.status-im.hardwallet.simulated-keycard/install-cash-applet
legacy.status-im.hardwallet.simulated-keycard/sign-typed-data
legacy.status-im.utils.core/safe-read-message-content
legacy.status-im.ui.components.react/native-modules
legacy.status-im.ui.components.react/progress-bar
react-native.fs/move-file
react-native.fs/read-dir
react-native.fs/mkdir
react-native.fs/unlink
react-native.fs/file-exists?
legacy.status-im.ui.components.colors/white
legacy.status-im.ui.components.colors/black
legacy.status-im.ui.components.core/animated-header
legacy.status-im.ui.components.core/safe-area-provider
legacy.status-im.ui.components.core/safe-area-consumer
legacy.status-im.ui.components.core/safe-area-view
legacy.status-im.utils.universal-links.core/open!
legacy.status-im.transport.filters.core-test/member-2
legacy.status-im.ui.screens.chat.ttt/one-to-one-chat-description-container
legacy.status-im.ethereum.ens/ttl
legacy.status-im.ethereum.ens/addr-hash
legacy.status-im.ethereum.ens/name-hash
legacy.status-im.ethereum.ens/ABI-hash
legacy.status-im.ethereum.ens/pubkey-hash
legacy.status-im.network.core/get-network
status-im.constants/desktop-content-types
status-im.constants/blocks-per-hour
status-im.constants/one-earth-day
status-im.constants/left-pane-min-width
status-im.constants/system
status-im.constants/contact-discovery
status-im.constants/send-transaction-failed-parse-response
status-im.constants/send-transaction-failed-parse-params
status-im.constants/send-transaction-no-account-selected
status-im.constants/send-transaction-invalid-tx-sender
status-im.constants/web3-get-logs
status-im.constants/web3-transaction-receipt
status-im.constants/web3-new-filter
status-im.constants/web3-new-pending-transaction-filter
status-im.constants/web3-new-block-filter
status-im.constants/web3-uninstall-filter
status-im.constants/web3-get-filter-changes
status-im.constants/web3-shh-post
status-im.constants/web3-shh-new-identity
status-im.constants/web3-shh-has-identity
status-im.constants/web3-shh-new-group
status-im.constants/web3-shh-add-to-group
status-im.constants/web3-shh-new-filter
status-im.constants/web3-shh-uninstall-filter
status-im.constants/web3-shh-get-filter-changes
status-im.constants/web3-shh-get-messages
status-im.constants/status-create-address
status-im.constants/event-transfer-hash
status-im.constants/regx-rtl-characters
status-im.constants/desktop-msg-chars-hard-limit
status-im.constants/debug-metrics
status-im.constants/scan-qr-code
status-im.constants/ipfs-proto-code
status-im.constants/swarm-proto-code
legacy.status-im.multiaccounts.update.publisher/publish-update!
legacy.status-im.utils.async/task-queue
legacy.status-im.utils.async/async-periodic-run!
legacy.status-im.desktop.core/app-root
legacy.status-im.desktop.core/init
legacy.status-im.chat.models-test/test-db
legacy.status-im.ui.components.spacing/padding-vertical
legacy.status-im.utils.money/percent-change
utils.debounce/clear-all
legacy.status-im.transport.db/create-chat
legacy.status-im.utils.priority-map/priority-map
legacy.status-im.utils.priority-map/priority-map-by
legacy.status-im.utils.priority-map/priority-map-keyfn
legacy.status-im.utils.priority-map/empty-message-map
legacy.status-im.wallet.choose-recipient.core/use-default-eth-gas
legacy.status-im.ui.components.animation/decay
legacy.status-im.ui.components.animation/remove-all-listeners
legacy.status-im.ui.components.animation/stop-animation
legacy.status-im.ui.components.animation/animated
legacy.status-im.ui.components.animation/add
legacy.status-im.ui.components.animation/subtract
legacy.status-im.ui.components.animation/get-layout
legacy.status-im.wallet.transactions-test/uri-query-data
legacy.status-im.utils.name/shortened-name
legacy.status-im.ui.components.button.haptic/trigger
legacy.status-im.contact.db/filter-dapps
legacy.status-im.contact.db/filter-group-contacts
legacy.status-im.contact.db/legacy-pending?
legacy.status-im.utils.transducers-test/preview-call-1
legacy.status-im.utils.transducers-test/preview-call-2
legacy.status-im.utils.types/to-string
legacy.status-im.chat.db/datemark?
legacy.status-im.chat.db/gap?
legacy.status-im.chat.db/map->sorted-seq
legacy.status-im.utils.snoopy/status-module-filter
legacy.status-im.utils.snoopy/websocket-filter
legacy.status-im.utils.snoopy/touches-filter
legacy.status-im.utils.snoopy/native-animation-filter
legacy.status-im.utils.snoopy/keyboard-observer-filter
legacy.status-im.utils.multihash/hex
legacy.status-im.utils.multihash/decode-array
+3 -28
View File
@@ -4,7 +4,7 @@
legacy.status-im.utils.views/letsubs clojure.core/let
reagent.core/with-let clojure.core/let
legacy.status-im.utils.fx/defn clj-kondo.lint-as/def-catch-all
utils.re-frame/defn clojure.core/defn
utils.re-frame/defn clj-kondo.lint-as/def-catch-all
quo.react/with-deps-check clojure.core/fn
quo.previews.preview/list-comp clojure.core/for
legacy.status-im.utils.styles/def clojure.core/def
@@ -13,29 +13,7 @@
test-helpers.unit/deftest-event clojure.core/defn
taoensso.tufte/defnp clojure.core/defn}
:linters {:clojure-lsp/unused-public-var {:exclude-when-defined-by #{utils.re-frame/defn}
:exclude #{
;; even if we are not
;; using color now, we
;; will need it later
quo.foundations.colors
;; same for funcs from
;; native-module
native-module.core
;; some funcs used
;; temporarily during
;; development
test-helpers.component
test-helpers.unit
;; not sure if unused
;; functions here should
;; be removed:
keycard.keycard
react-native.fs}}
:case-duplicate-test {:level :error}
:linters {:case-duplicate-test {:level :error}
:case-quoted-test {:level :error}
:case-symbol-test {:level :error}
:clj-kondo-config {:level :error}
@@ -62,8 +40,7 @@
:equals-true {:level :error}
:inline-def {:level :error}
:invalid-arity {:skip-args [legacy.status-im.utils.fx/defn
utils.re-frame/defn
utils.re-frame/merge]}
utils.re-frame/defn]}
:loop-without-recur {:level :error}
:minus-one {:level :error}
:misplaced-docstring {:level :error}
@@ -76,8 +53,6 @@
:redundant-do {:level :error}
:redundant-let {:level :error}
:refer-all {:level :error}
;; ignore error in config itself saying that `:shadowed-fn-param` is unknown
#_{:clj-kondo/ignore [:clj-kondo-config]}
:shadowed-fn-param {:level :error}
:shadowed-var {:level :error
;; We temporarily use :include to define an
+1
View File
@@ -36,3 +36,4 @@ SHOW_NOT_IMPLEMENTED_FEATURES=0
ENABLE_ALERT_BANNER=0
FLAG_WALLET_CONNECT_ENABLED=1
API_LOGGING_ENABLED=1
SENTRY_ENVIRONMENT=ci-main
+1
View File
@@ -40,3 +40,4 @@ ENABLE_ALERT_BANNER=0
FLAG_WALLET_CONNECT_ENABLED=1
MOBILE_DATA_SYNCING_TOGGLE_ENABLE=0
API_LOGGING_ENABLED=1
SENTRY_ENVIRONMENT=ci-main
+1
View File
@@ -37,3 +37,4 @@ TEST_NETWORKS_ENABLED=1
ENABLE_ALERT_BANNER=1
FLAG_WALLET_CONNECT_ENABLED=1
API_LOGGING_ENABLED=1
SENTRY_ENVIRONMENT=ci-main
+3 -2
View File
@@ -2,7 +2,7 @@ DEBUG_WEBVIEW=1
ETHEREUM_DEV_CLUSTER=1
EXTENSIONS=0
GROUP_CHATS_ENABLED=1
LOG_LEVEL=debug
LOG_LEVEL=info
MAILSERVER_CONFIRMATIONS_ENABLED=1
MAINNET_WARNING_ENABLED=1
PFS_ENCRYPTION_ENABLED=1
@@ -23,4 +23,5 @@ FAST_CREATE_COMMUNITY_ENABLED=0
TEST_NETWORKS_ENABLED=0
ENABLE_ALERT_BANNER=1
FLAG_WALLET_CONNECT_ENABLED=1
API_LOGGING_ENABLED=1
API_LOGGING_ENABLED=0
SENTRY_ENVIRONMENT=ci-nightly
+1
View File
@@ -21,3 +21,4 @@ TEST_NETWORKS_ENABLED=0
STATUS_PROXY_STAGE_NAME=prod
FLAG_WALLET_CONNECT_ENABLED=1
API_LOGGING_ENABLED=0
SENTRY_ENVIRONMENT=production
+34 -16
View File
@@ -1,26 +1,39 @@
[comment]: # (Please replace ... with your information. Remove < and >)
[comment]: # (To auto-close issue on merge, please insert the related issue number after # i.e fixes #566)
If you submit PR for issue with bounty then write here Fixes #NN where NN is issue number
*otherwise*
fixes #...
## Summary
### Summary
[comment]: # (Summarise the problem and how the pull request solves it)
...
## Review notes
[comment]: # (Optional. Specify if something in particular should be looked at, or ignored, during review)
<!-- (Optional, remove if no changes to documentation) -->
Documentation change PR (review please): https://github.com/status-im/status.im/pull/xxx
## Testing notes
[comment]: # (Optional)
### Review notes
<!-- (Optional. Specify if something in particular should be looked at, or ignored, during review) -->
### Platforms
[comment]: # (Optional. Specify which platforms should be tested)
### Testing notes
<!-- (Optional) -->
#### Platforms
<!-- (Optional. Specify which platforms should be tested) -->
- Android
- iOS
- macOS
- Linux
- Windows
### Areas that may be impacted
[comment]: # (Optional. Specify if some specific areas need to be tested, such as 1-1 chats)
#### Areas that maybe impacted
<!-- (Optional. Specify if some specific areas has to be tested, for example 1-1 chats) -->
#### Functional
##### Functional
- 1-1 chats
- public chats
@@ -35,27 +48,32 @@ fixes #...
- fleet
- bootnodes
#### Non-functional
##### Non-functional
- battery performance
- CPU performance / speed of the app
- network consumption
## Steps to test
[comment]: # (Specify exact steps to test if there are such)
### Steps to test
<!-- (Specify exact steps to test if there are such) -->
- Open Status
- ...
- Step 3, etc.
<!-- (PRs will only be accepted if squashed into single commit.) -->
[comment]: # (Can be ready or wip)
status: ready
### Before and after screenshots comparison
| Figma (if available) | iOS (if available) | Android (if available)
| --- | --- | --- |
| Please embed Image/Video here of the before and after. | Please embed Image/Video here of the before and after. | Please embed Image/Video here of the before and after. |
status: ready <!-- Can be ready or wip -->
<!-- Uncomment this section for status-go upgrade/dogfooding pull requests
- Specify potentially impacted user flows in _Areas that may be impacted*.
- Specify potentially impacted user flows in _Areas that maybe impacted*.
- Ensure that _Steps to test_ is filled in.
### Risk
+4 -4
View File
@@ -43,6 +43,8 @@ export NODE_OPTIONS += --openssl-legacy-provider
# The path can be anything, but home is usually safest.
export KEYSTORE_PATH ?= $(HOME)/.gradle/status-im.keystore
# Our custom config is located in nix/nix.conf
export NIX_USER_CONF_FILES = $(PWD)/nix/nix.conf
# Location of symlinks to derivations that should not be garbage collected
export _NIX_GCROOTS = ./.nix-gcroots
# Defines which variables will be kept for Nix pure shell, use semicolon as divider
@@ -267,11 +269,9 @@ run-metro: export TARGET := clojure
run-metro: ##@run Start Metro to build React Native changes
@scripts/run-metro.sh
export RE_FRISK_PORT ?= 4567
run-re-frisk: export TARGET := clojure
run-re-frisk: ##@run Start re-frisk server
yarn shadow-cljs run re-frisk-remote.core/start ${RE_FRISK_PORT}
yarn shadow-cljs run re-frisk-remote.core/start
# TODO: Migrate this to a Nix recipe, much the same way as nix/mobile/android/targets/release-android.nix
run-android: export TARGET := android
@@ -445,7 +445,7 @@ android-ports: export RCT_METRO_PORT ?= 8081
android-ports: ##@other Add proxies to Android Device/Simulator
adb reverse tcp:$(RCT_METRO_PORT) tcp:$(RCT_METRO_PORT) && \
adb reverse tcp:3449 tcp:3449 && \
adb reverse tcp:$(RE_FRISK_PORT) tcp:$(RE_FRISK_PORT) && \
adb reverse tcp:4567 tcp:4567 && \
adb reverse tcp:$(FLOWSTORM_PORT) tcp:$(FLOWSTORM_PORT) && \
adb forward tcp:5561 tcp:5561
+1 -1
View File
@@ -1 +1 @@
2.32.0
2.32.21682
@@ -0,0 +1,40 @@
package im.status.ethereum;
import java.io.File;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/** @author Kevin Kowalewski */
public class RootUtil {
public static boolean isDeviceRooted() {
return checkRootMethod1() || checkRootMethod2() || checkRootMethod3();
}
private static boolean checkRootMethod1() {
String buildTags = android.os.Build.TAGS;
return buildTags != null && buildTags.contains("test-keys");
}
private static boolean checkRootMethod2() {
String[] paths = { "/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
"/system/bin/failsafe/su", "/data/local/su" };
for (String path : paths) {
if (new File(path).exists()) return true;
}
return false;
}
private static boolean checkRootMethod3() {
Process process = null;
try {
process = Runtime.getRuntime().exec(new String[] { "/system/xbin/which", "su" });
BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
return in.readLine() != null;
} catch (Throwable t) {
return false;
} finally {
if (process != null) process.destroy();
}
}
}
@@ -1,42 +0,0 @@
package im.status.ethereum
import android.os.Build
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
object RootUtil {
fun isDeviceRooted(): Boolean =
checkRootMethod1() || checkRootMethod2() || checkRootMethod3()
private fun checkRootMethod1(): Boolean =
Build.TAGS?.contains("test-keys") ?: false
private fun checkRootMethod2(): Boolean {
val paths = arrayOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su"
)
return paths.any { File(it).exists() }
}
private fun checkRootMethod3(): Boolean {
var process: Process? = null
return try {
process = Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su"))
BufferedReader(InputStreamReader(process.inputStream)).readLine() != null
} catch (t: Throwable) {
false
} finally {
process?.destroy()
}
}
}
+3 -2
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env groovy
library 'status-jenkins-lib@v1.9.13'
library 'status-jenkins-lib@v1.9.15'
/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
pipeline {
agent { label 'linux && x86_64 && nix-2.24' }
agent { label 'linux && x86_64 && nix-2.19' }
options {
timestamps()
@@ -41,6 +41,7 @@ pipeline {
BUILD_ENV = 'prod'
NIX_CONF_DIR = "${env.WORKSPACE}/nix"
FASTLANE_DISABLE_COLORS = 1
SENTRY_PRODUCTION = "${utils.isReleaseBuild() ? 'true' : 'false'}"
}
stages {
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env groovy
library 'status-jenkins-lib@v1.9.13'
library 'status-jenkins-lib@v1.9.15'
import groovy.json.JsonBuilder
+3 -2
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env groovy
library 'status-jenkins-lib@v1.9.13'
library 'status-jenkins-lib@v1.9.15'
/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
pipeline {
agent { label 'macos && arm64 && nix-2.24 && xcode-16.2' }
agent { label 'macos && arm64 && nix-2.19 && xcode-15.1' }
parameters {
string(
@@ -41,6 +41,7 @@ pipeline {
NIX_CONF_DIR = "${env.WORKSPACE}/nix"
FASTLANE_DISABLE_COLORS = 1
BUNDLE_PATH = "${HOME}/.bundle"
SENTRY_PRODUCTION = "${utils.isReleaseBuild() ? 'true' : 'false'}"
}
stages {
+1 -1
View File
@@ -5,7 +5,7 @@ library 'status-jenkins-lib@v1.9.13'
def isPRBuild = utils.isPRBuild()
pipeline {
agent { label 'linux && x86_64 && nix-2.24' }
agent { label 'linux && x86_64 && nix-2.19' }
options {
timestamps()
+20 -4
View File
@@ -73,9 +73,9 @@ pipeline {
passwordVariable: 'TESTRAIL_PASS'
),
usernamePassword(
credentialsId: 'lambda-test-api',
usernameVariable: 'LAMBDA_TEST_USERNAME',
passwordVariable: 'LAMBDA_TEST_ACCESS_KEY'
credentialsId: 'sauce-labs-api',
usernameVariable: 'SAUCE_USERNAME',
passwordVariable: 'SAUCE_ACCESS_KEY'
),
string(
credentialsId: 'etherscan-api-key',
@@ -95,7 +95,8 @@ pipeline {
sh 'cp -f $TEST_ETH_ACCOUNTS_FILE users.py'
sh """
python3 -m pytest \
--numprocesses 9 \
--numprocesses 8 \
--rerun_count=2 \
--testrail_report=True \
-m testrail_id \
-m \"nightly\" \
@@ -109,6 +110,21 @@ pipeline {
}
post {
always {
script {
sauce('sauce-labs-cred') {
saucePublisher()
}
}
}
success {
script {
junit(
testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']],
testResults: 'test/appium/tests/*.xml'
)
}
}
cleanup {
sh 'make purge'
}
+5 -4
View File
@@ -111,9 +111,9 @@ pipeline {
passwordVariable: 'TESTRAIL_PASS'
),
usernamePassword(
credentialsId: 'lambda-test-api',
usernameVariable: 'LAMBDA_TEST_USERNAME',
passwordVariable: 'LAMBDA_TEST_ACCESS_KEY'
credentialsId: 'sauce-labs-api',
usernameVariable: 'SAUCE_USERNAME',
passwordVariable: 'SAUCE_ACCESS_KEY'
),
string(
credentialsId: 'etherscan-api-key',
@@ -133,7 +133,8 @@ pipeline {
sh 'cp -f $TEST_ETH_ACCOUNTS_FILE users.py'
sh """
python3 -m pytest \
--numprocesses 9 \
--numprocesses 8 \
--rerun_count=2 \
--testrail_report=True \
-k \"${params.KEYWORD_EXPRESSION}\" \
--apk=${params.APK_URL ?: apk_path} \
-1
View File
@@ -16,7 +16,6 @@
- [Patching](patching.md)
- [Creating a pixel perfect UI](pixel-perfection.md)
- [Contributing to status-go](status-go-changes.md)
- [Status Backend Server](use-status-backend-server.md)
- [Malli schemas (recorded demo)](https://www.youtube.com/watch?v=SlRio70aYVI) ([slides](files/forging-code-with-schemas-sep-2023-slides.pdf))
## Testing
+1 -1
View File
@@ -150,7 +150,7 @@ This one is straightforward, just evaluate a navigation dispatch form from anywh
#### REPL-ing a component
This is also straightforward, but there are some small differences. Just like above, we only have to evaluate a dispatch form, as follows:
This is also straighforward, but there are some small differences. Just like above, we only have to evaluate a dispatch form, as follows:
```cljs
;; example debugging a quo component, although it could be any other component e.g. from the `status-im2` ns.
+1 -1
View File
@@ -1 +1 @@
Status: محفظة إيثريوم كريبتو
Status: محفظة تشفير اجتماعية
+1 -1
View File
@@ -1 +1 @@
Status: Ethereum Crypto Wallet
Status: Social Crypto Wallet
+1 -1
View File
@@ -1 +1 @@
Status: Billetera Ethereum
Status: Billetera social cripto
+1 -1
View File
@@ -1 +1 @@
Status: Portefeuille Ethereum
Status : Portef. crypto social
+1 -1
View File
@@ -1 +1 @@
Status: イーサリアム クリプト ウォレット
Status: ソーシャル クリプト ウォレット
+1 -1
View File
@@ -1 +1 @@
Status: 이더리움 크립토 지갑
Status: 소셜 크립토 지갑
+1 -1
View File
@@ -1 +1 @@
Status: Carteira Ethereum
Status: Carteira social cripto
+1 -1
View File
@@ -1 +1 @@
Status: Ethereum Криптокошелек
Status: Криптокошелек
+1 -1
View File
@@ -1 +1 @@
Status Ethereum Kripto Cüzdanı
Status: Sosyal Kripto Cüzdanı
+1 -1
View File
@@ -1 +1 @@
Status Ethereum Криптогаманець
Status: Криптогаманець
+1 -1
View File
@@ -1 +1 @@
Status: 以太坊加密钱包
Status: 可以社交的加密货币钱包
+18 -21
View File
@@ -23,18 +23,15 @@ PODS:
- secp256k1
- SSZipArchive
- libevent (2.1.12)
- libwebp (1.3.2):
- libwebp/demux (= 1.3.2)
- libwebp/mux (= 1.3.2)
- libwebp/sharpyuv (= 1.3.2)
- libwebp/webp (= 1.3.2)
- libwebp/demux (1.3.2):
- libwebp (1.2.4):
- libwebp/demux (= 1.2.4)
- libwebp/mux (= 1.2.4)
- libwebp/webp (= 1.2.4)
- libwebp/demux (1.2.4):
- libwebp/webp
- libwebp/mux (1.3.2):
- libwebp/mux (1.2.4):
- libwebp/demux
- libwebp/sharpyuv (1.3.2)
- libwebp/webp (1.3.2):
- libwebp/sharpyuv
- libwebp/webp (1.2.4)
- RCT-Folly (2022.05.16.00):
- boost
- DoubleConversion
@@ -1155,15 +1152,15 @@ PODS:
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- RNImageCropPicker (0.41.2):
- RNImageCropPicker (0.40.0):
- React-Core
- React-RCTImage
- RNImageCropPicker/QBImagePickerController (= 0.41.2)
- TOCropViewController (~> 2.7.4)
- RNImageCropPicker/QBImagePickerController (0.41.2):
- RNImageCropPicker/QBImagePickerController (= 0.40.0)
- TOCropViewController
- RNImageCropPicker/QBImagePickerController (0.40.0):
- React-Core
- React-RCTImage
- TOCropViewController (~> 2.7.4)
- TOCropViewController
- RNKeychain (8.1.2):
- React-Core
- RNLinearGradient (3.0.0-alpha.1):
@@ -1192,7 +1189,7 @@ PODS:
- secp256k1 (0.1.6)
- SocketRocket (0.6.1)
- SSZipArchive (2.4.3)
- TOCropViewController (2.7.4)
- TOCropViewController (2.6.1)
- Yoga (1.14.0)
DEPENDENCIES:
@@ -1492,7 +1489,7 @@ CHECKOUT OPTIONS:
:git: https://github.com/status-im/Keycard.swift.git
:tag: 3.1.1
secp256k1:
:commit: 4ab977cc2b2d7319be858bcb30a5d189bb149884
:commit: 46a1fa30d9b8babeae85ff519050f42394ab5fcc
:git: https://github.com/status-im/secp256k1.swift.git
:submodules: true
@@ -1509,7 +1506,7 @@ SPEC CHECKSUMS:
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
Keycard: 3bd24405b9a875b3b3cf1ab1be69b5c30c5f28bc
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0
RCTRequired: 2544c0f1081a5fa12e108bb8cb40e5f4581ccd87
RCTTypeSafety: 50efabe2b115c11ed03fbf3fd79e2f163ddb5d7c
@@ -1580,7 +1577,7 @@ SPEC CHECKSUMS:
RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7
RNFS: 2bd9eb49dc82fa9676382f0585b992c424cd59df
RNGestureHandler: 15c6ef51acba34c49ff03003806cf5dd6098f383
RNImageCropPicker: 771e2ca319d2cf92e04ebf334ece892ee9a6728f
RNImageCropPicker: 486e2f7e2b0461ce24321f751410dce1b3b49e6d
RNKeychain: a65256b6ca6ba6976132cc4124b238a5b13b3d9c
RNLinearGradient: ccaaebce083b54180da61d992e7fa56abfe000d6
RNPermissions: 08e619529cced22695f4b6d0efcc0a233e278903
@@ -1593,9 +1590,9 @@ SPEC CHECKSUMS:
secp256k1: f61d67e6fdcb85fd727acf1bf35ace6036db540c
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
TOCropViewController: edfd4f25713d56905ad1e0b9f5be3fbe0f59c863
Yoga: a716eea57d0d3430219c0a5a233e1e93ee931eb7
PODFILE CHECKSUM: 56d9f695324306d6db368d1172d211dfab55b7cb
PODFILE CHECKSUM: 81a66b45adddb083171ade882c343deb4faf2a8b
COCOAPODS: 1.15.2
-6
View File
@@ -572,7 +572,6 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm-StatusImTests/Pods-Status-StatusIm-StatusImTests-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNPermissions/RNPermissionsPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
@@ -580,7 +579,6 @@
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNPermissionsPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
@@ -616,7 +614,6 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Status-StatusIm/Pods-Status-StatusIm-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNPermissions/RNPermissionsPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
@@ -624,7 +621,6 @@
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNPermissionsPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
@@ -732,7 +728,6 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Status-StatusImPR/Pods-Status-StatusImPR-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNPermissions/RNPermissionsPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
@@ -740,7 +735,6 @@
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNPermissionsPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
+1 -1
View File
@@ -26,4 +26,4 @@ These log files are uploaded to Jenkins as CI job artifacts on failure using `ar
* `set_xcode_version.log` - Output from `node_modules/react-native/scripts/react-native-xcode.sh`.
- Created by redirecting output of `shellScript` in `ios/StatusIm.xcodeproj/project.pbxproj` for "Run Script" build Phase.
* `Status PR-StatusImPR.log - Created by [Fastlane Gym](https://docs.fastlane.tools/actions/gym/).
- Configured via [`Fastfile`](../Fastfile) using `buildlog_path` argument for `build_ios_app`.
- Configred via [`Fastfile`](../Fastfile) using `buildlog_path` argument for `build_ios_app`.
@@ -203,6 +203,24 @@ class AccountManager(private val reactContext: ReactApplicationContext) : ReactC
)
}
@ReactMethod
fun verify(address: String, password: String, callback: Callback) {
val absRootDirPath = utils.getNoBackupDirectory()
val newKeystoreDir = utils.pathCombine(absRootDirPath, "keystore")
val jsonParams = JSONObject()
jsonParams.put("keyStoreDir", newKeystoreDir)
jsonParams.put("address", address)
jsonParams.put("password", password)
StatusBackendClient.executeStatusGoRequestWithCallback(
endpoint = "VerifyAccountPasswordV2",
requestBody = jsonParams.toString(),
statusgoFunction = { Statusgo.verifyAccountPasswordV2(jsonParams.toString()) },
callback
)
}
@ReactMethod
fun verifyDatabasePassword(keyUID: String, password: String, callback: Callback) {
val jsonParams = JSONObject()
@@ -0,0 +1,297 @@
package im.status.ethereum.module;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.Callback;
import android.util.Log;
import statusgo.Statusgo;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.function.Function;
import android.app.Activity;
import android.view.WindowManager;
import android.os.Build;
import android.view.Window;
import android.preference.PreferenceManager;
import android.content.SharedPreferences;
public class EncryptionUtils extends ReactContextBaseJavaModule {
private static final String TAG = "EncryptionUtils";
private ReactApplicationContext reactContext;
private Utils utils;
public EncryptionUtils(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
this.utils = new Utils(reactContext);
}
@Override
public String getName() {
return "EncryptionUtils";
}
@ReactMethod
private void initKeystore(final String keyUID, final Callback callback) throws JSONException {
Log.d(TAG, "initKeystore");
final String commonKeydir = this.utils.pathCombine(this.utils.getNoBackupDirectory(), "/keystore");
final String keydir = this.utils.pathCombine(commonKeydir, keyUID);
StatusBackendClient.executeStatusGoRequestWithCallback(
"InitKeystore",
keydir,
() -> Statusgo.initKeystore(keydir),
callback
);
}
@ReactMethod
public void reEncryptDbAndKeystore(final String keyUID, final String password, final String newPassword, final Callback callback) throws JSONException {
JSONObject params = new JSONObject();
params.put("keyUID", keyUID);
params.put("oldPassword", password);
params.put("newPassword", newPassword);
String jsonParams = params.toString();
StatusBackendClient.executeStatusGoRequestWithCallback(
"ChangeDatabasePasswordV2",
jsonParams,
() -> Statusgo.changeDatabasePasswordV2(jsonParams),
callback
);
}
@ReactMethod
public void convertToKeycardAccount(final String keyUID, final String accountData, final String options, final String keycardUID, final String password,
final String newPassword, final Callback callback) throws JSONException {
final String keyStoreDir = this.utils.getKeyStorePath(keyUID);
JSONObject params = new JSONObject();
params.put("keyUID", keyUID);
params.put("account", new JSONObject(accountData));
params.put("settings", new JSONObject(options));
params.put("keycardUID", keycardUID);
params.put("oldPassword", password);
params.put("newPassword", newPassword);
final String jsonParams = params.toString();
StatusBackendClient.executeStatusGoRequest(
"InitKeystore",
keyStoreDir,
() -> Statusgo.initKeystore(keyStoreDir)
);
StatusBackendClient.executeStatusGoRequestWithCallback(
"ConvertToKeycardAccountV2",
jsonParams,
() -> Statusgo.convertToKeycardAccountV2(jsonParams),
callback
);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String encodeTransfer(final String to, final String value) {
try {
JSONObject params = new JSONObject();
params.put("to", to);
params.put("value", value);
String jsonParams = params.toString();
return StatusBackendClient.executeStatusGoRequestWithResult(
"EncodeTransferV2",
jsonParams,
() -> Statusgo.encodeTransferV2(jsonParams)
);
} catch (JSONException e) {
Log.e(TAG, "Error creating JSON for encodeTransfer: " + e.getMessage());
return null;
}
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String encodeFunctionCall(final String method, final String paramsJSON) {
try {
JSONObject params = new JSONObject();
params.put("method", method);
params.put("paramsJSON", new JSONObject(paramsJSON));
String jsonString = params.toString();
return StatusBackendClient.executeStatusGoRequestWithResult(
"EncodeFunctionCallV2",
jsonString,
() -> Statusgo.encodeFunctionCallV2(jsonString)
);
} catch (JSONException e) {
Log.e(TAG, "Error creating JSON for encodeFunctionCall: " + e.getMessage());
return null;
}
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String decodeParameters(final String decodeParamJSON) {
return StatusBackendClient.executeStatusGoRequestWithResult(
"DecodeParameters",
decodeParamJSON,
() -> Statusgo.decodeParameters(decodeParamJSON)
);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String hexToNumber(final String hex) {
return StatusBackendClient.executeStatusGoRequestWithResult(
"HexToNumber",
hex,
() -> Statusgo.hexToNumber(hex)
);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String numberToHex(final String numString) {
return StatusBackendClient.executeStatusGoRequestWithResult(
"NumberToHex",
numString,
() -> Statusgo.numberToHex(numString)
);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String sha3(final String str) {
return StatusBackendClient.executeStatusGoRequestWithResult(
"Sha3",
str,
() -> Statusgo.sha3(str)
);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String utf8ToHex(final String str) {
return StatusBackendClient.executeStatusGoRequestWithResult(
"Utf8ToHex",
str,
() -> Statusgo.utf8ToHex(str)
);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String hexToUtf8(final String str) {
return StatusBackendClient.executeStatusGoRequestWithResult(
"HexToUtf8",
str,
() -> Statusgo.hexToUtf8(str)
);
}
@ReactMethod(isBlockingSynchronousMethod = true)
public String serializeLegacyKey(final String publicKey) {
return StatusBackendClient.executeStatusGoRequestWithResult(
"SerializeLegacyKey",
publicKey,
() -> Statusgo.serializeLegacyKey(publicKey)
);
}
@ReactMethod
public void setBlankPreviewFlag(final Boolean blankPreview) {
final SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this.reactContext);
sharedPrefs.edit().putBoolean("BLANK_PREVIEW", blankPreview).commit();
setSecureFlag();
}
private void setSecureFlag() {
final SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this.reactContext);
final boolean setSecure = sharedPrefs.getBoolean("BLANK_PREVIEW", true);
final Activity activity = this.reactContext.getCurrentActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
final Window window = activity.getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && setSecure) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
}
}
});
}
}
@ReactMethod
public void hashTransaction(final String txArgsJSON, final Callback callback) throws JSONException {
StatusBackendClient.executeStatusGoRequestWithCallback(
"HashTransaction",
txArgsJSON,
() -> Statusgo.hashTransaction(txArgsJSON),
callback
);
}
@ReactMethod
public void hashMessage(final String message, final Callback callback) throws JSONException {
StatusBackendClient.executeStatusGoRequestWithCallback(
"HashMessage",
message,
() -> Statusgo.hashMessage(message),
callback
);
}
@ReactMethod
public void multiformatDeserializePublicKey(final String multiCodecKey, final String base58btc, final Callback callback) throws JSONException {
JSONObject params = new JSONObject();
params.put("key", multiCodecKey);
params.put("outBase", base58btc);
String jsonParams = params.toString();
StatusBackendClient.executeStatusGoRequestWithCallback(
"MultiformatDeserializePublicKeyV2",
jsonParams,
() -> Statusgo.multiformatDeserializePublicKeyV2(jsonParams),
callback
);
}
@ReactMethod
public void deserializeAndCompressKey(final String desktopKey, final Callback callback) throws JSONException {
StatusBackendClient.executeStatusGoRequestWithCallback(
"DeserializeAndCompressKey",
desktopKey,
() -> Statusgo.deserializeAndCompressKey(desktopKey),
callback
);
}
@ReactMethod
public void hashTypedData(final String data, final Callback callback) throws JSONException {
this.utils.executeRunnableStatusGoMethod(() -> Statusgo.hashTypedData(data), callback);
}
@ReactMethod
public void hashTypedDataV4(final String data, final Callback callback) throws JSONException {
this.utils.executeRunnableStatusGoMethod(() -> Statusgo.hashTypedDataV4(data), callback);
}
@ReactMethod
public void signMessage(final String rpcParams, final Callback callback) throws JSONException {
StatusBackendClient.executeStatusGoRequestWithCallback(
"SignMessage",
rpcParams,
() -> Statusgo.signMessage(rpcParams),
callback
);
}
@ReactMethod
public void signTypedData(final String data, final String account, final String password, final Callback callback) throws JSONException {
this.utils.executeRunnableStatusGoMethod(() -> Statusgo.signTypedData(data, account, password), callback);
}
@ReactMethod
public void signTypedDataV4(final String data, final String account, final String password, final Callback callback) throws JSONException {
this.utils.executeRunnableStatusGoMethod(() -> Statusgo.signTypedDataV4(data, account, password), callback);
}
@ReactMethod
public void signGroupMembership(final String content, final Callback callback) throws JSONException {
this.utils.executeRunnableStatusGoMethod(() -> Statusgo.signGroupMembership(content), callback);
}
}
@@ -1,258 +0,0 @@
package im.status.ethereum.module
import android.app.Activity
import android.os.Build
import android.preference.PreferenceManager
import android.util.Log
import android.view.WindowManager
import com.facebook.react.bridge.*
import org.json.JSONException
import org.json.JSONObject
import statusgo.Statusgo
import java.lang.Exception
import java.lang.RuntimeException
class EncryptionUtils(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private val utils = Utils(reactContext)
companion object {
private const val TAG = "EncryptionUtils"
}
override fun getName(): String = "EncryptionUtils"
@ReactMethod
@Throws(Exception::class)
private fun initKeystore(keyUID: String, callback: Callback) {
Log.d(TAG, "initKeystore")
val commonKeydir = utils.pathCombine(utils.getNoBackupDirectory(), "/keystore")
val keydir = utils.pathCombine(commonKeydir, keyUID)
StatusBackendClient.executeStatusGoRequestWithCallback(
"InitKeystore",
keydir,
{ Statusgo.initKeystore(keydir) },
callback
)
}
@ReactMethod
@Throws(Exception::class)
fun reEncryptDbAndKeystore(keyUID: String, password: String, newPassword: String, callback: Callback) {
val params = JSONObject().apply {
put("keyUID", keyUID)
put("oldPassword", password)
put("newPassword", newPassword)
}
StatusBackendClient.executeStatusGoRequestWithCallback(
"ChangeDatabasePasswordV2",
params.toString(),
{ Statusgo.changeDatabasePasswordV2(params.toString()) },
callback
)
}
@ReactMethod
@Throws(Exception::class)
fun convertToKeycardAccount(
keyUID: String,
accountData: String,
options: String,
keycardUID: String,
password: String,
newPassword: String,
callback: Callback
) {
val keyStoreDir = utils.getKeyStorePath(keyUID)
val params = JSONObject().apply {
put("keyUID", keyUID)
put("account", JSONObject(accountData))
put("settings", JSONObject(options))
put("keycardUID", keycardUID)
put("oldPassword", password)
put("newPassword", newPassword)
}
val jsonParams = params.toString()
StatusBackendClient.executeStatusGoRequest(
"InitKeystore",
keyStoreDir,
{ Statusgo.initKeystore(keyStoreDir) }
)
StatusBackendClient.executeStatusGoRequestWithCallback(
"ConvertToKeycardAccountV2",
jsonParams,
{ Statusgo.convertToKeycardAccountV2(jsonParams) },
callback
)
}
@ReactMethod(isBlockingSynchronousMethod = true)
fun decodeParameters(decodeParamJSON: String): String =
StatusBackendClient.executeStatusGoRequestWithResult(
"DecodeParameters",
decodeParamJSON,
{ Statusgo.decodeParameters(decodeParamJSON) }
)
@ReactMethod(isBlockingSynchronousMethod = true)
fun hexToNumber(hex: String): String =
StatusBackendClient.executeStatusGoRequestWithResult(
"HexToNumber",
hex,
{ Statusgo.hexToNumber(hex) }
)
@ReactMethod(isBlockingSynchronousMethod = true)
fun numberToHex(numString: String): String =
StatusBackendClient.executeStatusGoRequestWithResult(
"NumberToHex",
numString,
{ Statusgo.numberToHex(numString) }
)
@ReactMethod(isBlockingSynchronousMethod = true)
fun sha3(str: String): String =
StatusBackendClient.executeStatusGoRequestWithResult(
"Sha3",
str,
{ Statusgo.sha3(str) }
)
@ReactMethod(isBlockingSynchronousMethod = true)
fun utf8ToHex(str: String): String =
StatusBackendClient.executeStatusGoRequestWithResult(
"Utf8ToHex",
str,
{ Statusgo.utf8ToHex(str) }
)
@ReactMethod(isBlockingSynchronousMethod = true)
fun hexToUtf8(str: String): String =
StatusBackendClient.executeStatusGoRequestWithResult(
"HexToUtf8",
str,
{ Statusgo.hexToUtf8(str) }
)
@ReactMethod(isBlockingSynchronousMethod = true)
fun serializeLegacyKey(publicKey: String): String =
StatusBackendClient.executeStatusGoRequestWithResult(
"SerializeLegacyKey",
publicKey,
{ Statusgo.serializeLegacyKey(publicKey) }
)
@ReactMethod
fun setBlankPreviewFlag(blankPreview: Boolean) {
PreferenceManager.getDefaultSharedPreferences(reactContext)
.edit()
.putBoolean("BLANK_PREVIEW", blankPreview)
.commit()
setSecureFlag()
}
private fun setSecureFlag() {
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(reactContext)
val setSecure = sharedPrefs.getBoolean("BLANK_PREVIEW", true)
reactContext.currentActivity?.run {
runOnUiThread {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && setSecure) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
}
@ReactMethod
@Throws(Exception::class)
fun hashTransaction(txArgsJSON: String, callback: Callback) {
StatusBackendClient.executeStatusGoRequestWithCallback(
"HashTransaction",
txArgsJSON,
{ Statusgo.hashTransaction(txArgsJSON) },
callback
)
}
@ReactMethod
@Throws(Exception::class)
fun hashMessage(message: String, callback: Callback) {
StatusBackendClient.executeStatusGoRequestWithCallback(
"HashMessage",
message,
{ Statusgo.hashMessage(message) },
callback
)
}
@ReactMethod
@Throws(Exception::class)
fun multiformatDeserializePublicKey(multiCodecKey: String, base58btc: String, callback: Callback) {
val params = JSONObject().apply {
put("key", multiCodecKey)
put("outBase", base58btc)
}
StatusBackendClient.executeStatusGoRequestWithCallback(
"MultiformatDeserializePublicKeyV2",
params.toString(),
{ Statusgo.multiformatDeserializePublicKeyV2(params.toString()) },
callback
)
}
@ReactMethod
@Throws(Exception::class)
fun deserializeAndCompressKey(desktopKey: String, callback: Callback) {
StatusBackendClient.executeStatusGoRequestWithCallback(
"DeserializeAndCompressKey",
desktopKey,
{ Statusgo.deserializeAndCompressKey(desktopKey) },
callback
)
}
@ReactMethod
@Throws(Exception::class)
fun hashTypedData(data: String, callback: Callback) {
utils.executeRunnableStatusGoMethod({ Statusgo.hashTypedData(data) }, callback)
}
@ReactMethod
@Throws(Exception::class)
fun hashTypedDataV4(data: String, callback: Callback) {
utils.executeRunnableStatusGoMethod({ Statusgo.hashTypedDataV4(data) }, callback)
}
@ReactMethod
@Throws(Exception::class)
fun signMessage(rpcParams: String, callback: Callback) {
StatusBackendClient.executeStatusGoRequestWithCallback(
"SignMessage",
rpcParams,
{ Statusgo.signMessage(rpcParams) },
callback
)
}
@ReactMethod
@Throws(Exception::class)
fun signTypedData(data: String, account: String, password: String, callback: Callback) {
utils.executeRunnableStatusGoMethod({ Statusgo.signTypedData(data, account, password) }, callback)
}
@ReactMethod
@Throws(Exception::class)
fun signTypedDataV4(data: String, account: String, password: String, callback: Callback) {
utils.executeRunnableStatusGoMethod({ Statusgo.signTypedDataV4(data, account, password) }, callback)
}
@ReactMethod
@Throws(Exception::class)
fun signGroupMembership(content: String, callback: Callback) {
utils.executeRunnableStatusGoMethod({ Statusgo.signGroupMembership(content) }, callback)
}
}
@@ -108,6 +108,15 @@ class StatusModule(private val reactContext: ReactApplicationContext, private va
)
}
@ReactMethod
fun intendedPanic(message: String) {
StatusBackendClient.executeStatusGoRequest(
endpoint = "IntendedPanic",
requestBody = "",
statusgoFunction = { Statusgo.intendedPanic(message) },
)
}
@ReactMethod
fun addCentralizedMetric(request: String, callback: Callback) {
StatusBackendClient.executeStatusGoRequestWithCallback(
@@ -0,0 +1,124 @@
package im.status.ethereum.module;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import android.app.Activity;
import android.util.Log;
import android.os.Build;
import android.webkit.WebView;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.WebStorage;
import android.view.WindowManager;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import android.view.inputmethod.InputMethodManager;
import android.view.View;
import android.widget.EditText;
import android.content.Context;
public class UIHelper extends ReactContextBaseJavaModule {
private static final String TAG = "UIHelper";
private ReactApplicationContext reactContext;
public UIHelper(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "UIHelper";
}
@ReactMethod
public void setSoftInputMode(final int mode) {
Log.d(TAG, "setSoftInputMode");
final Activity activity = getCurrentActivity();
if (activity == null) {
return;
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().setSoftInputMode(mode);
}
});
}
@SuppressWarnings("deprecation")
@ReactMethod
public void clearCookies() {
Log.d(TAG, "clearCookies");
final Activity activity = getCurrentActivity();
if (activity == null) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().removeAllCookies(null);
CookieManager.getInstance().flush();
} else {
CookieSyncManager cookieSyncManager = CookieSyncManager.createInstance(activity);
cookieSyncManager.startSync();
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
cookieManager.removeSessionCookie();
cookieSyncManager.stopSync();
cookieSyncManager.sync();
}
}
@ReactMethod
public void toggleWebviewDebug(final boolean val) {
Log.d(TAG, "toggleWebviewDebug");
final Activity activity = getCurrentActivity();
if (activity == null) {
return;
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
WebView.setWebContentsDebuggingEnabled(val);
}
});
}
@ReactMethod
public void clearStorageAPIs() {
Log.d(TAG, "clearStorageAPIs");
final Activity activity = getCurrentActivity();
if (activity == null) {
return;
}
WebStorage storage = WebStorage.getInstance();
if (storage != null) {
storage.deleteAllData();
}
}
@ReactMethod
public void resetKeyboardInputCursor(final int reactTagToReset, final int selection) {
UIManagerModule uiManager = getReactApplicationContext().getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
InputMethodManager imm = (InputMethodManager) getReactApplicationContext().getBaseContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
View viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset);
imm.restartInput(viewToReset);
try {
EditText textView = (EditText) viewToReset;
textView.setSelection(selection);
} catch (Exception e) {}
}
}
});
}
}
@@ -1,97 +0,0 @@
package im.status.ethereum.module
import android.app.Activity
import android.content.Context
import android.os.Build
import android.util.Log
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.webkit.CookieManager
import android.webkit.CookieSyncManager
import android.webkit.WebStorage
import android.webkit.WebView
import android.widget.EditText
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.uimanager.NativeViewHierarchyManager
import com.facebook.react.uimanager.UIBlock
import com.facebook.react.uimanager.UIManagerModule
class UIHelper(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
companion object {
private const val TAG = "UIHelper"
}
override fun getName(): String = "UIHelper"
@ReactMethod
fun setSoftInputMode(mode: Int) {
Log.d(TAG, "setSoftInputMode")
getCurrentActivity()?.run {
runOnUiThread {
window.setSoftInputMode(mode)
}
}
}
@Suppress("DEPRECATION")
@ReactMethod
fun clearCookies() {
Log.d(TAG, "clearCookies")
getCurrentActivity()?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().apply {
removeAllCookies(null)
flush()
}
} else {
CookieSyncManager.createInstance(activity).also { cookieSyncManager ->
cookieSyncManager.startSync()
CookieManager.getInstance().apply {
removeAllCookie()
removeSessionCookie()
}
cookieSyncManager.stopSync()
cookieSyncManager.sync()
}
}
}
}
@ReactMethod
fun toggleWebviewDebug(enabled: Boolean) {
Log.d(TAG, "toggleWebviewDebug")
getCurrentActivity()?.run {
runOnUiThread {
WebView.setWebContentsDebuggingEnabled(enabled)
}
}
}
@ReactMethod
fun clearStorageAPIs() {
Log.d(TAG, "clearStorageAPIs")
getCurrentActivity()?.let {
WebStorage.getInstance()?.deleteAllData()
}
}
@ReactMethod
fun resetKeyboardInputCursor(reactTagToReset: Int, selection: Int) {
reactApplicationContext.getNativeModule(UIManagerModule::class.java)?.addUIBlock(object : UIBlock {
override fun execute(nativeViewHierarchyManager: NativeViewHierarchyManager) {
(reactApplicationContext.baseContext.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)?.let { imm ->
val viewToReset = nativeViewHierarchyManager.resolveView(reactTagToReset)
imm.restartInput(viewToReset)
try {
(viewToReset as? EditText)?.setSelection(selection)
} catch (e: Exception) {
// Ignore exceptions during selection setting
}
}
}
})
}
}
@@ -0,0 +1,109 @@
package im.status.ethereum.pushnotifications;
import android.content.Context;
import android.content.Intent;
import android.app.Service;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.provider.Settings;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import android.os.Build;
import im.status.ethereum.module.R;
public class ForegroundService extends Service {
private static final String CHANNEL_ID = "status-service";
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent i, int flags, int startId) {
// NOTE: recent versions of Android require the service to display
// a sticky notification to inform the user that the service is running
Context context = getApplicationContext();
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
Intent intent = null;
String notificationContentText = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager =
context.getSystemService(NotificationManager.class);
NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
context.getResources().getString(R.string.status_service),
NotificationManager.IMPORTANCE_HIGH);
channel.setShowBadge(false);
notificationManager.createNotificationChannel(channel);
// Create intent that takes the user to the notification channel settings so they can hide it
intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
intent.putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID);
notificationContentText = context.getResources().getString(R.string.tap_to_hide_notification);
} else {
// For older versions of android intent takes the user to the Status app
Class intentClass;
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
String className = launchIntent.getComponent().getClassName();
try {
intentClass = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return 0;
}
intent = new Intent(context, intentClass);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setAction(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
notificationContentText = context.getResources().getString(R.string.keep_status_running);
}
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
Intent stopIntent = new Intent(PushNotificationHelper.ACTION_TAP_STOP);
PendingIntent stopPendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_notify_status)
.setContentTitle(context.getResources().getString(R.string.background_service_opened))
.setContentText(notificationContentText)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setContentIntent(pendingIntent)
.setNumber(0)
.addAction(R.drawable.ic_stat_notify_status,
context.getResources().getString(R.string.stop),
stopPendingIntent)
.build();
// the id of the foreground notification MUST NOT be 0
if (Build.VERSION.SDK_INT >= 33) {
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else {
startForeground(1, notification);
}
return START_STICKY;
}
}
@@ -1,122 +0,0 @@
package im.status.ethereum.pushnotifications
import android.content.Context
import android.content.Intent
import android.app.Service
import android.content.pm.ServiceInfo
import android.os.IBinder
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.provider.Settings
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import android.os.Build
import im.status.ethereum.module.R
class ForegroundService : Service() {
companion object {
private const val CHANNEL_ID = "status-service"
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(i: Intent?, flags: Int, startId: Int): Int {
// NOTE: recent versions of Android require the service to display
// a sticky notification to inform the user that the service is running
val context = applicationContext
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
// Create notification channel for Android O and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = context.getSystemService(NotificationManager::class.java)
val channel = NotificationChannel(
CHANNEL_ID,
context.resources.getString(R.string.status_service),
NotificationManager.IMPORTANCE_HIGH
).apply {
setShowBadge(false)
}
notificationManager.createNotificationChannel(channel)
}
// Initialize intent and notification text based on Android version
val notificationIntent = createNotificationIntent(context)
val notificationText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.resources.getString(R.string.tap_to_hide_notification)
} else {
context.resources.getString(R.string.keep_status_running)
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE
)
val stopIntent = Intent(PushNotificationHelper.ACTION_TAP_STOP)
val stopPendingIntent = PendingIntent.getBroadcast(
context,
0,
stopIntent,
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_notify_status)
.setContentTitle(context.resources.getString(R.string.background_service_opened))
.setContentText(notificationText)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setContentIntent(pendingIntent)
.setNumber(0)
.addAction(
R.drawable.ic_stat_notify_status,
context.resources.getString(R.string.stop),
stopPendingIntent
)
.build()
// the id of the foreground notification MUST NOT be 0
if (Build.VERSION.SDK_INT >= 33) {
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(1, notification)
}
return START_STICKY
}
private fun createNotificationIntent(context: Context): Intent {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Create intent that takes the user to the notification channel settings
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID)
}
} else {
// For older versions of android, intent takes the user to the Status app
val packageName = context.packageName
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
val className = launchIntent?.component?.className
try {
val intentClass = Class.forName(className!!)
Intent(context, intentClass).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
} catch (e: ClassNotFoundException) {
e.printStackTrace()
// Return an empty intent as fallback
Intent()
}
}
}
}
@@ -0,0 +1,146 @@
package im.status.ethereum.pushnotifications;
import android.app.Activity;
import android.app.Application;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationManagerCompat;
import java.security.SecureRandom;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import android.util.Log;
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery;
public class PushNotification extends ReactContextBaseJavaModule implements ActivityEventListener {
public static final String LOG_TAG = "PushNotification";
private final SecureRandom mRandomNumberGenerator = new SecureRandom();
private PushNotificationHelper pushNotificationHelper;
private PushNotificationJsDelivery delivery;
private ReactApplicationContext reactContext;
private boolean started;
public PushNotification(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
reactContext.addActivityEventListener(this);
Application applicationContext = (Application) reactContext.getApplicationContext();
IntentFilter intentFilter = new IntentFilter();
pushNotificationHelper = new PushNotificationHelper(applicationContext, intentFilter);
delivery = new PushNotificationJsDelivery(reactContext);
}
@Override
public String getName() {
return "PushNotification";
}
// removed @Override temporarily just to get it working on different versions of RN
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
onActivityResult(requestCode, resultCode, data);
}
// removed @Override temporarily just to get it working on different versions of RN
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// Ignored, required to implement ActivityEventListener for RN 0.33
}
private Bundle getBundleFromIntent(Intent intent) {
Bundle bundle = null;
if (intent.hasExtra("notification")) {
bundle = intent.getBundleExtra("notification");
} else if (intent.hasExtra("google.message_id")) {
bundle = new Bundle();
bundle.putBundle("data", intent.getExtras());
}
if(null != bundle && !bundle.getBoolean("foreground", false) && !bundle.containsKey("userInteraction")) {
bundle.putBoolean("userInteraction", true);
}
return bundle;
}
@Override
public void onNewIntent(Intent intent) {
Bundle bundle = this.getBundleFromIntent(intent);
if (bundle != null) {
delivery.notifyNotification(bundle);
}
}
@ReactMethod
/**
* Creates a channel if it does not already exist. Returns whether the channel was created.
*/
public void createChannel(ReadableMap channelInfo, Callback callback) {
boolean created = pushNotificationHelper.createChannel(channelInfo);
if(callback != null) {
callback.invoke(created);
}
}
@ReactMethod
public void presentLocalNotification(ReadableMap details) {
if (!this.started) {
return;
}
Bundle bundle = Arguments.toBundle(details);
// If notification ID is not provided by the user, generate one at random
if (bundle.getString("id") == null) {
bundle.putString("id", String.valueOf(mRandomNumberGenerator.nextInt()));
}
pushNotificationHelper.sendToNotificationCentre(bundle);
}
@ReactMethod
public void clearMessageNotifications(String conversationId) {
if (this.started) {
pushNotificationHelper.clearMessageNotifications(conversationId);
}
}
@ReactMethod
public void clearAllMessageNotifications() {
pushNotificationHelper.clearAllMessageNotifications();
}
@ReactMethod
public void enableNotifications() {
this.started = true;
this.pushNotificationHelper.start();
}
@ReactMethod
public void disableNotifications() {
if (this.started) {
this.started = false;
this.pushNotificationHelper.stop();
}
}
}
@@ -1,118 +0,0 @@
package im.status.ethereum.pushnotifications
import android.app.Activity
import android.app.Application
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.util.Log
import androidx.annotation.NonNull
import androidx.core.app.NotificationManagerCompat
import com.facebook.react.bridge.*
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery
import java.security.SecureRandom
class PushNotification(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext), ActivityEventListener {
companion object {
const val LOG_TAG = "PushNotification"
}
private val mRandomNumberGenerator = SecureRandom()
private val pushNotificationHelper: PushNotificationHelper
private val delivery: PushNotificationJsDelivery
private val reactContext: ReactApplicationContext = reactContext
private var started: Boolean = false
init {
reactContext.addActivityEventListener(this)
val applicationContext = reactContext.applicationContext as Application
val intentFilter = IntentFilter()
pushNotificationHelper = PushNotificationHelper(applicationContext, intentFilter)
delivery = PushNotificationJsDelivery(reactContext)
}
override fun getName(): String = "PushNotification"
// Primary implementation of ActivityEventListener interface
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
// Call the helper method with the correct parameter order
handleActivityResult(requestCode, resultCode, data)
}
// Private helper method for handling activity results
private fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// Ignored, required to implement ActivityEventListener for RN 0.33
}
private fun getBundleFromIntent(intent: Intent): Bundle? {
return when {
intent.hasExtra("notification") -> intent.getBundleExtra("notification")
intent.hasExtra("google.message_id") -> Bundle().apply {
putBundle("data", intent.extras)
}
else -> null
}?.apply {
if (!getBoolean("foreground", false) && !containsKey("userInteraction")) {
putBoolean("userInteraction", true)
}
}
}
override fun onNewIntent(intent: Intent) {
getBundleFromIntent(intent)?.let { bundle ->
delivery.notifyNotification(bundle)
}
}
@ReactMethod
fun createChannel(channelInfo: ReadableMap, callback: Callback?) {
val created = pushNotificationHelper.createChannel(channelInfo)
callback?.invoke(created)
}
@ReactMethod
fun presentLocalNotification(details: ReadableMap) {
if (!started) return
Arguments.toBundle(details)?.apply {
// If notification ID is not provided by the user, generate one at random
if (getString("id") == null) {
putString("id", mRandomNumberGenerator.nextInt().toString())
}
pushNotificationHelper.sendToNotificationCentre(this)
}
}
@ReactMethod
fun clearMessageNotifications(conversationId: String) {
if (started) {
pushNotificationHelper.clearMessageNotifications(conversationId)
}
}
@ReactMethod
fun clearAllMessageNotifications() {
pushNotificationHelper.clearAllMessageNotifications()
}
@ReactMethod
fun enableNotifications() {
started = true
pushNotificationHelper.start()
}
@ReactMethod
fun disableNotifications() {
if (started) {
started = false
pushNotificationHelper.stop()
}
}
}
@@ -0,0 +1,93 @@
package im.status.ethereum.pushnotifications;
import android.os.Build;
import android.app.Application;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery;
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
public class PushNotificationActions extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
String intentActionPrefix = context.getPackageName() + ".ACTION_";
Log.i(LOG_TAG, "PushNotificationBootEventReceiver loading scheduled notifications");
if (null == intent.getAction() || !intent.getAction().startsWith(intentActionPrefix)) {
return;
}
final Bundle bundle = intent.getBundleExtra("notification");
// Dismiss the notification popup.
NotificationManager manager = (NotificationManager) context.getSystemService(context.NOTIFICATION_SERVICE);
int notificationID = Integer.parseInt(bundle.getString("id"));
boolean autoCancel = bundle.getBoolean("autoCancel", true);
if(autoCancel) {
if (bundle.containsKey("tag")) {
String tag = bundle.getString("tag");
manager.cancel(tag, notificationID);
} else {
manager.cancel(notificationID);
}
}
boolean invokeApp = bundle.getBoolean("invokeApp", true);
// Notify the action.
if(invokeApp) {
IntentFilter intentFilter = new IntentFilter();
PushNotificationHelper helper = new PushNotificationHelper((Application) context.getApplicationContext(), intentFilter);
helper.invokeApp(bundle);
} else {
// We need to run this on the main thread, as the React code assumes that is true.
// Namely, DevServerHelper constructs a Handler() without a Looper, which triggers:
// "Can't create handler inside thread that has not called Looper.prepare()"
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
public void run() {
// Construct and load our normal React JS code bundle
final ReactInstanceManager mReactInstanceManager = ((ReactApplication) context.getApplicationContext()).getReactNativeHost().getReactInstanceManager();
ReactContext context = mReactInstanceManager.getCurrentReactContext();
PushNotificationJsDelivery delivery = new PushNotificationJsDelivery(context);
// If it's constructed, send a notification
if (context != null) {
delivery.notifyNotificationAction(bundle);
} else {
// Otherwise wait for construction, then send the notification
mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
public void onReactContextInitialized(ReactContext context) {
PushNotificationJsDelivery delivery = new PushNotificationJsDelivery(context);
delivery.notifyNotificationAction(bundle);
mReactInstanceManager.removeReactInstanceEventListener(this);
}
});
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
// Construct it in the background
mReactInstanceManager.createReactContextInBackground();
}
}
}
});
}
}
}
@@ -1,100 +0,0 @@
package im.status.ethereum.pushnotifications
import android.os.Build
import android.app.Application
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.facebook.react.ReactApplication
import com.facebook.react.ReactInstanceManager
import com.facebook.react.bridge.ReactContext
import im.status.ethereum.pushnotifications.PushNotificationJsDelivery
import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG
/**
* BroadcastReceiver responsible for handling push notification actions.
* This class processes notification interactions and manages their lifecycle.
*/
class PushNotificationActions : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val intentActionPrefix = "${context.packageName}.ACTION_"
Log.i(LOG_TAG, "PushNotificationBootEventReceiver loading scheduled notifications")
if (intent.action == null || !intent.action!!.startsWith(intentActionPrefix)) {
return
}
val bundle = intent.getBundleExtra("notification") ?: return
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationID = bundle.getString("id")?.toInt() ?: return
// Dismiss the notification popup.
val autoCancel = bundle.getBoolean("autoCancel", true)
if (autoCancel) {
bundle.getString("tag")?.let { tag ->
manager.cancel(tag, notificationID)
} ?: run {
manager.cancel(notificationID)
}
}
// Notify the action.
val invokeApp = bundle.getBoolean("invokeApp", true)
if (invokeApp) {
val intentFilter = IntentFilter()
val helper = PushNotificationHelper(
context.applicationContext as Application,
intentFilter
)
helper.invokeApp(bundle)
} else {
// We need to run this on the main thread, as the React code assumes that is true.
// Namely, DevServerHelper constructs a Handler() without a Looper, which triggers:
// "Can't create handler inside thread that has not called Looper.prepare()"
Handler(Looper.getMainLooper()).post {
// Construct and load our normal React JS code bundle
val reactInstanceManager = (context.applicationContext as ReactApplication)
.reactNativeHost
.reactInstanceManager
val currentContext = reactInstanceManager.currentReactContext
when {
// If context exists, deliver notification immediately
currentContext != null -> {
PushNotificationJsDelivery(currentContext)
.notifyNotificationAction(bundle)
}
// Otherwise wait for construction, then send the notification
else -> {
val listener = object : ReactInstanceManager.ReactInstanceEventListener {
override fun onReactContextInitialized(reactContext: ReactContext) {
PushNotificationJsDelivery(reactContext)
.notifyNotificationAction(bundle)
reactInstanceManager.removeReactInstanceEventListener(this)
}
}
// Add the listener and create context if needed
reactInstanceManager.addReactInstanceEventListener(listener)
if (!reactInstanceManager.hasStartedCreatingInitialContext()) {
// Construct it in the background
reactInstanceManager.createReactContextInBackground()
}
}
}
}
}
}
}
@@ -0,0 +1,890 @@
// https://github.com/zo0r/react-native-push-notification/blob/bedc8f646aab67d594f291449fbfa24e07b64fe8/android/src/main/java/com/dieam/reactnativepushnotification/modules/RNPushNotificationHelper.java Copy-Paste with removed firebase
package im.status.ethereum.pushnotifications;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.AlarmManager;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.media.AudioAttributes;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.Base64;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.app.Person;
import androidx.core.graphics.drawable.IconCompat;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import im.status.ethereum.module.R;
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
public class PushNotificationHelper {
private Context context;
private static final long DEFAULT_VIBRATION = 300L;
private static final String CHANNEL_ID = "status-im-notifications";
public static final String ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION";
public static final String ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP";
final int flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_CANCEL_CURRENT;
private NotificationManager notificationManager;
private HashMap<String, Person> persons;
private HashMap<String, StatusMessageGroup> messageGroups;
private IntentFilter intentFilter;
public PushNotificationHelper(Application context, IntentFilter intentFilter) {
this.context = context;
this.intentFilter = intentFilter;
this.persons = new HashMap<String, Person>();
this.messageGroups = new HashMap<String, StatusMessageGroup>();
this.notificationManager = context.getSystemService(NotificationManager.class);
this.registerBroadcastReceiver();
}
public Intent getOpenAppIntent(String deepLink) {
Class intentClass;
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
String className = launchIntent.getComponent().getClassName();
try {
intentClass = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
Intent intent = new Intent(context, intentClass);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setAction(Intent.ACTION_VIEW);
//NOTE: you might wonder, why the heck did he decide to set these flags in particular. Well,
//the answer is a simple as it can get in the Android native development world. I noticed
//that my initial setup was opening the app but wasn't triggering any events on the js side, like
//the links do from the browser. So I compared both intents and noticed that the link from
//the browser produces an intent with the flag 0x14000000. I found out that it was the following
//flags in this link:
//https://stackoverflow.com/questions/52390129/android-intent-setflags-issue
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (deepLink != null) {
intent.setData(Uri.parse(deepLink));
}
return intent;
}
//NOTE: we use a dynamically created BroadcastReceiver here so that we can capture
//intents from notifications and act on them. For instance when tapping/dismissing
//a chat notification we want to clear the chat so that next messages don't show
//the messages that we have seen again
private final BroadcastReceiver notificationActionReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == ACTION_DELETE_NOTIFICATION) {
String groupId = intent.getExtras().getString("im.status.ethereum.groupId");
if (groupId != null) {
cleanGroup(groupId);
}
}
if (intent.getAction() == ACTION_TAP_STOP) {
stop();
System.exit(0);
}
Log.e(LOG_TAG, "intent received: " + intent.getAction());
}
};
public void registerBroadcastReceiver() {
this.intentFilter.addAction(ACTION_DELETE_NOTIFICATION);
this.intentFilter.addAction(ACTION_TAP_STOP);
context.registerReceiver(notificationActionReceiver, this.intentFilter, Context.RECEIVER_EXPORTED);
Log.e(LOG_TAG, "Broadcast Receiver registered");
}
private NotificationManager notificationManager() {
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
public void invokeApp(Bundle bundle) {
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
String className = launchIntent.getComponent().getClassName();
try {
Class<?> activityClass = Class.forName(className);
Intent activityIntent = new Intent(context, activityClass);
if(bundle != null) {
activityIntent.putExtra("notification", bundle);
}
activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(activityIntent);
} catch(Exception e) {
Log.e(LOG_TAG, "Class not found", e);
return;
}
}
public void sendToNotificationCentre(final Bundle bundle) {
PushNotificationPicturesAggregator aggregator = new PushNotificationPicturesAggregator(new PushNotificationPicturesAggregator.Callback() {
public void call(Bitmap largeIconImage, Bitmap bigPictureImage) {
sendToNotificationCentreWithPicture(bundle, largeIconImage, bigPictureImage);
}
});
aggregator.setLargeIconUrl(context, bundle.getString("largeIconUrl"));
aggregator.setBigPictureUrl(context, bundle.getString("bigPictureUrl"));
}
public void handleConversation(final Bundle bundle) {
if (bundle.getBoolean("deleted")){
this.removeStatusMessage(bundle);
} else {
this.addStatusMessage(bundle);
}
}
public void clearMessageNotifications(String conversationId) {
notificationManager.cancel(conversationId.hashCode());
cleanGroup(conversationId);
}
public void clearAllMessageNotifications() {
notificationManager.cancelAll();
}
public void sendToNotificationCentreWithPicture(final Bundle bundle, Bitmap largeIconBitmap, Bitmap bigPictureBitmap) {
try {
Class intentClass = getMainActivityClass();
if (intentClass == null) {
Log.e(LOG_TAG, "No activity class found for the notification");
return;
}
if (bundle.getBoolean("isConversation")) {
this.handleConversation(bundle);
return;
}
if (bundle.getString("message") == null) {
// this happens when a 'data' notification is received - we do not synthesize a local notification in this case
Log.d(LOG_TAG, "Ignore this message if you sent data-only notification. Cannot send to notification centre because there is no 'message' field in: " + bundle);
return;
}
String notificationIdString = bundle.getString("id");
if (notificationIdString == null) {
Log.e(LOG_TAG, "No notification ID specified for the notification");
return;
}
Resources res = context.getResources();
String packageName = context.getPackageName();
String title = bundle.getString("title");
if (title == null) {
ApplicationInfo appInfo = context.getApplicationInfo();
title = context.getPackageManager().getApplicationLabel(appInfo).toString();
}
int priority = NotificationCompat.PRIORITY_HIGH;
final String priorityString = bundle.getString("priority");
if (priorityString != null) {
switch (priorityString.toLowerCase()) {
case "max":
priority = NotificationCompat.PRIORITY_MAX;
break;
case "high":
priority = NotificationCompat.PRIORITY_HIGH;
break;
case "low":
priority = NotificationCompat.PRIORITY_LOW;
break;
case "min":
priority = NotificationCompat.PRIORITY_MIN;
break;
case "default":
priority = NotificationCompat.PRIORITY_DEFAULT;
break;
default:
priority = NotificationCompat.PRIORITY_HIGH;
}
}
int visibility = NotificationCompat.VISIBILITY_PRIVATE;
final String visibilityString = bundle.getString("visibility");
if (visibilityString != null) {
switch (visibilityString.toLowerCase()) {
case "private":
visibility = NotificationCompat.VISIBILITY_PRIVATE;
break;
case "public":
visibility = NotificationCompat.VISIBILITY_PUBLIC;
break;
case "secret":
visibility = NotificationCompat.VISIBILITY_SECRET;
break;
default:
visibility = NotificationCompat.VISIBILITY_PRIVATE;
}
}
String channel_id = bundle.getString("channelId");
if(channel_id == null) {
channel_id = this.getNotificationDefaultChannelId();
}
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, channel_id)
.setContentTitle(title)
.setTicker(bundle.getString("ticker"))
.setVisibility(visibility)
.setPriority(priority)
.setAutoCancel(bundle.getBoolean("autoCancel", true))
.setOnlyAlertOnce(bundle.getBoolean("onlyAlertOnce", false));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // API 24 and higher
// Restore showing timestamp on Android 7+
// Source: https://developer.android.com/reference/android/app/Notification.Builder.html#setShowWhen(boolean)
boolean showWhen = bundle.getBoolean("showWhen", true);
notification.setShowWhen(showWhen);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // API 26 and higher
// Changing Default mode of notification
notification.setDefaults(Notification.DEFAULT_LIGHTS);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { // API 20 and higher
String group = bundle.getString("group");
if (group != null) {
notification.setGroup(group);
}
if (bundle.containsKey("groupSummary") || bundle.getBoolean("groupSummary")) {
notification.setGroupSummary(bundle.getBoolean("groupSummary"));
}
}
String numberString = bundle.getString("number");
if (numberString != null) {
notification.setNumber(Integer.parseInt(numberString));
}
// Small icon
int smallIconResId = 0;
String smallIcon = bundle.getString("smallIcon");
if (smallIcon != null && !smallIcon.isEmpty()) {
smallIconResId = res.getIdentifier(smallIcon, "mipmap", packageName);
} else if(smallIcon == null) {
smallIconResId = res.getIdentifier("ic_stat_notify_status", "drawable", packageName);
}
if (smallIconResId == 0) {
smallIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
if (smallIconResId == 0) {
smallIconResId = android.R.drawable.ic_dialog_info;
}
}
notification.setSmallIcon(smallIconResId);
// Large icon
if(largeIconBitmap == null) {
int largeIconResId = 0;
String largeIcon = bundle.getString("largeIcon");
if (largeIcon != null && !largeIcon.isEmpty()) {
largeIconResId = res.getIdentifier(largeIcon, "mipmap", packageName);
} else if(largeIcon == null) {
largeIconResId = res.getIdentifier("ic_launcher", "mipmap", packageName);
}
// Before Lolipop there was no large icon for notifications.
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
}
}
Bundle author = bundle.getBundle("notificationAuthor");
if (largeIconBitmap == null && author != null) {
String base64Image = author.getString("icon").split(",")[1];
byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
notification.setLargeIcon(getCircleBitmap(decodedByte));
} else if (largeIconBitmap != null){
notification.setLargeIcon(largeIconBitmap);
}
String message = bundle.getString("message");
notification.setContentText(message);
String subText = bundle.getString("subText");
if (subText != null) {
notification.setSubText(subText);
}
String bigText = bundle.getString("bigText");
if (bigText == null) {
bigText = message;
}
NotificationCompat.Style style;
if(bigPictureBitmap != null) {
style = new NotificationCompat.BigPictureStyle()
.bigPicture(bigPictureBitmap)
.setBigContentTitle(title)
.setSummaryText(message);
} else {
style = new NotificationCompat.BigTextStyle().bigText(bigText);
}
notification.setStyle(style);
Intent intent = new Intent(context, intentClass);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
bundle.putBoolean("foreground", this.isApplicationInForeground());
bundle.putBoolean("userInteraction", true);
intent.putExtra("notification", bundle);
Uri soundUri = null;
if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) {
String soundName = bundle.getString("soundName");
if (soundName == null) {
soundName = "default";
}
soundUri = getSoundUri(soundName);
notification.setSound(soundUri);
}
if (soundUri == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setSound(null);
}
if (bundle.containsKey("ongoing") || bundle.getBoolean("ongoing")) {
notification.setOngoing(bundle.getBoolean("ongoing"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
notification.setCategory(NotificationCompat.CATEGORY_CALL);
String color = bundle.getString("color");
int defaultColor = -1;
if (color != null) {
notification.setColor(Color.parseColor(color));
} else if (defaultColor != -1) {
notification.setColor(defaultColor);
}
}
int notificationID = notificationIdString.hashCode();
notification.setContentIntent(createOnTapIntent(context, notificationID, bundle.getString("deepLink")))
.setDeleteIntent(createOnDismissedIntent(context, notificationID, bundle.getString("deepLink")));
NotificationManager notificationManager = notificationManager();
long[] vibratePattern = new long[]{0};
if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) {
long vibration = bundle.containsKey("vibration") ? (long) bundle.getDouble("vibration") : DEFAULT_VIBRATION;
if (vibration == 0)
vibration = DEFAULT_VIBRATION;
vibratePattern = new long[]{0, vibration};
notification.setVibrate(vibratePattern);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Define the shortcutId
String shortcutId = bundle.getString("shortcutId");
if (shortcutId != null) {
notification.setShortcutId(shortcutId);
}
Long timeoutAfter = (long) bundle.getDouble("timeoutAfter");
if (timeoutAfter != null && timeoutAfter >= 0) {
notification.setTimeoutAfter(timeoutAfter);
}
}
Long when = (long) bundle.getDouble("when");
if (when != null && when >= 0) {
notification.setWhen(when);
}
notification.setUsesChronometer(bundle.getBoolean("usesChronometer", false));
notification.setChannelId(channel_id);
JSONArray actionsArray = null;
try {
actionsArray = bundle.getString("actions") != null ? new JSONArray(bundle.getString("actions")) : null;
} catch (JSONException e) {
Log.e(LOG_TAG, "Exception while converting actions to JSON object.", e);
}
if (actionsArray != null) {
// No icon for now. The icon value of 0 shows no icon.
int icon = 0;
// Add button for each actions.
for (int i = 0; i < actionsArray.length(); i++) {
String action;
try {
action = actionsArray.getString(i);
} catch (JSONException e) {
Log.e(LOG_TAG, "Exception while getting action from actionsArray.", e);
continue;
}
Intent actionIntent = new Intent(context, PushNotificationActions.class);
actionIntent.setAction(packageName + ".ACTION_" + i);
actionIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
// Add "action" for later identifying which button gets pressed.
bundle.putString("action", action);
actionIntent.putExtra("notification", bundle);
actionIntent.setPackage(packageName);
PendingIntent pendingActionIntent = PendingIntent.getBroadcast(context, notificationID, actionIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
notification.addAction(new NotificationCompat.Action.Builder(icon, action, pendingActionIntent).build());
} else {
notification.addAction(icon, action, pendingActionIntent);
}
}
}
if (!(this.isApplicationInForeground() && bundle.getBoolean("ignoreInForeground"))) {
Notification info = notification.build();
info.defaults |= Notification.DEFAULT_LIGHTS;
if (bundle.containsKey("tag")) {
String tag = bundle.getString("tag");
notificationManager.notify(tag, notificationID, info);
} else {
notificationManager.notify(notificationID, info);
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "failed to send push notification", e);
}
}
private boolean checkOrCreateChannel(NotificationManager manager, String channel_id, String channel_name, String channel_description, Uri soundUri, int importance, long[] vibratePattern, boolean showBadge) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return false;
if (manager == null)
return false;
NotificationChannel channel = manager.getNotificationChannel(channel_id);
if (
channel == null && channel_name != null && channel_description != null ||
channel != null &&
(
channel_name != null && !channel.getName().equals(channel_name) ||
channel_description != null && !channel.getDescription().equals(channel_description)
)
) {
// If channel doesn't exist create a new one.
// If channel name or description is updated then update the existing channel.
channel = new NotificationChannel(channel_id, channel_name, importance);
channel.setDescription(channel_description);
channel.enableLights(true);
channel.enableVibration(vibratePattern != null);
channel.setVibrationPattern(vibratePattern);
channel.setShowBadge(showBadge);
if (soundUri != null) {
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build();
channel.setSound(soundUri, audioAttributes);
} else {
channel.setSound(null, null);
}
manager.createNotificationChannel(channel);
return true;
}
return false;
}
public boolean createChannel(ReadableMap channelInfo) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
return false;
String channelId = channelInfo.getString("channelId");
String channelName = channelInfo.getString("channelName");
String channelDescription = channelInfo.hasKey("channelDescription") ? channelInfo.getString("channelDescription") : "";
String soundName = channelInfo.hasKey("soundName") ? channelInfo.getString("soundName") : "default";
int importance = channelInfo.hasKey("importance") ? channelInfo.getInt("importance") : 4;
boolean vibrate = channelInfo.hasKey("vibrate") && channelInfo.getBoolean("vibrate");
long[] vibratePattern = vibrate ? new long[] { DEFAULT_VIBRATION } : null;
boolean showBadge = channelInfo.hasKey("showBadge") && channelInfo.getBoolean("showBadge");
NotificationManager manager = notificationManager();
Uri soundUri = getSoundUri(soundName);
return checkOrCreateChannel(manager, channelId, channelName, channelDescription, soundUri, importance, vibratePattern, showBadge);
}
public String getNotificationDefaultChannelId() {
return this.CHANNEL_ID;
}
private Uri getSoundUri(String soundName) {
if (soundName == null || "default".equalsIgnoreCase(soundName)) {
return RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
} else {
// sound name can be full filename, or just the resource name.
// So the strings 'my_sound.mp3' AND 'my_sound' are accepted
// The reason is to make the iOS and android javascript interfaces compatible
int resId;
if (context.getResources().getIdentifier(soundName, "raw", context.getPackageName()) != 0) {
resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName());
} else {
soundName = soundName.substring(0, soundName.lastIndexOf('.'));
resId = context.getResources().getIdentifier(soundName, "raw", context.getPackageName());
}
return Uri.parse("android.resource://" + context.getPackageName() + "/" + resId);
}
}
public Class getMainActivityClass() {
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
String className = launchIntent.getComponent().getClassName();
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
public boolean isApplicationInForeground() {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
if (processInfos != null) {
for (RunningAppProcessInfo processInfo : processInfos) {
if (processInfo.processName.equals(context.getPackageName())
&& processInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND
&& processInfo.pkgList.length > 0) {
return true;
}
}
}
return false;
}
private Bitmap getCircleBitmap(Bitmap bitmap) {
final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
bitmap.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(output);
final int color = Color.RED;
final Paint paint = new Paint();
final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
final RectF rectF = new RectF(rect);
paint.setAntiAlias(true);
canvas.drawARGB(0, 0, 0, 0);
paint.setColor(color);
canvas.drawOval(rectF, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
bitmap.recycle();
return output;
}
private Person getPerson(Bundle bundle) {
String name = bundle.getString("name");
return new Person.Builder().setName(name).build();
}
private StatusMessage createMessage(Bundle data) {
Person author = getPerson(data.getBundle("notificationAuthor"));
long timeStampLongValue = (long) data.getDouble("timestamp");
return new StatusMessage(data.getString("id"), author, timeStampLongValue, data.getString("message"));
}
private PendingIntent createGroupOnDismissedIntent(Context context, int notificationId, String groupId, String deepLink) {
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
intent.putExtra("im.status.ethereum.deepLink", deepLink);
intent.putExtra("im.status.ethereum.groupId", groupId);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag);
}
private PendingIntent createGroupOnTapIntent(Context context, int notificationId, String groupId, String deepLink) {
Intent intent = getOpenAppIntent(deepLink);
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag);
}
private PendingIntent createOnTapIntent(Context context, int notificationId, String deepLink) {
Intent intent = getOpenAppIntent(deepLink);
return PendingIntent.getActivity(context.getApplicationContext(), notificationId, intent, flag);
}
private PendingIntent createOnDismissedIntent(Context context, int notificationId, String deepLink) {
Intent intent = new Intent(ACTION_DELETE_NOTIFICATION);
intent.putExtra("im.status.ethereum.deepLink", deepLink);
return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, flag);
}
public void removeStatusMessage(Bundle bundle) {
String conversationId = bundle.getString("conversationId");
StatusMessageGroup group = this.messageGroups.get(conversationId);
NotificationManager notificationManager = notificationManager();
if (group == null) {
group = new StatusMessageGroup(conversationId);
}
this.messageGroups.put(conversationId, group);
String id = bundle.getString("id");
group.removeMessage(id);
this.showMessages(bundle);
}
public StatusMessageGroup getMessageGroup(String conversationId) {
return this.messageGroups.get(conversationId);
}
public void addStatusMessage(Bundle bundle) {
String conversationId = bundle.getString("conversationId");
StatusMessageGroup group = this.messageGroups.get(conversationId);
NotificationManager notificationManager = notificationManager();
if (group == null) {
group = new StatusMessageGroup(conversationId);
}
this.messageGroups.put(conversationId, group);
group.addMessage(createMessage(bundle));
this.showMessages(bundle);
}
public void showMessages(Bundle bundle) {
String conversationId = bundle.getString("conversationId");
StatusMessageGroup group = this.messageGroups.get(conversationId);
NotificationManager notificationManager = notificationManager();
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle("Me");
ArrayList<StatusMessage> messages = group.getMessages();
if (messages.size() == 0) {
notificationManager.cancel(conversationId.hashCode());
return;
}
for (int i = 0; i < messages.size(); i++) {
StatusMessage message = messages.get(i);
messagingStyle.addMessage(message.getText(),
message.getTimestamp(),
message.getAuthor());
}
if (bundle.getString("title") != null) {
messagingStyle.setConversationTitle(bundle.getString("title"));
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_notify_status)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setStyle(messagingStyle)
.setGroup(conversationId)
.setOnlyAlertOnce(true)
.setGroupSummary(true)
.setContentIntent(createGroupOnTapIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
.setDeleteIntent(createGroupOnDismissedIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
.setNumber(messages.size() + 1)
.setAutoCancel(true);
if (Build.VERSION.SDK_INT >= 21) {
builder.setVibrate(new long[0]);
}
notificationManager.notify(conversationId.hashCode(), builder.build());
}
class StatusMessageGroup {
private ArrayList<StatusMessage> messages;
private String id;
StatusMessageGroup(String id) {
this.id = id;
this.messages = new ArrayList<StatusMessage>();
}
public ArrayList<StatusMessage> getMessages() {
return messages;
}
public void addMessage(StatusMessage message) {
this.messages.add(message);
}
public void removeMessage(String id) {
ArrayList<StatusMessage> newMessages = new ArrayList<StatusMessage>();
for(StatusMessage message: this.messages) {
if(!message.id.equals(id)) {
newMessages.add(message);
}
}
this.messages = newMessages;
}
public String getId() {
return this.id;
}
}
class StatusMessage {
public Person getAuthor() {
return author;
}
public long getTimestamp() {
return timestamp;
}
public String getText() {
return text;
}
private String id;
private Person author;
private long timestamp;
private String text;
StatusMessage(String id, Person author, long timestamp, String text) {
this.id = id;
this.author = author;
this.timestamp = timestamp;
this.text = text;
}
}
private void removeGroup(String groupId) {
this.messageGroups.remove(groupId);
}
private void cleanGroup(String groupId) {
removeGroup(groupId);
if (messageGroups.size() == 0) {
notificationManager.cancelAll();
}
}
public void start() {
Log.e(LOG_TAG, "Starting Foreground Service");
Intent serviceIntent = new Intent(context, ForegroundService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
this.registerBroadcastReceiver();
}
public void stop() {
Log.e(LOG_TAG, "Stopping Foreground Service");
//NOTE: we cancel all the current notifications, because the intents can't be used anymore
//since the broadcast receiver will be killed as well and won't be able to handle any intent
notificationManager.cancelAll();
Intent serviceIntent = new Intent(context, ForegroundService.class);
context.stopService(serviceIntent);
context.unregisterReceiver(notificationActionReceiver);
}
}
@@ -1,750 +0,0 @@
// https://github.com/zo0r/react-native-push-notification/blob/bedc8f646aab67d594f291449fbfa24e07b64fe8/android/src/main/
// java/com/dieam/reactnativepushnotification/modules/RNPushNotificationHelper.java Copy-Paste with removed firebase
package im.status.ethereum.pushnotifications
import android.app.ActivityManager
import android.app.ActivityManager.RunningAppProcessInfo
import android.app.AlarmManager
import android.app.Application
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.BroadcastReceiver
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.graphics.RectF
import android.media.AudioAttributes
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.service.notification.StatusBarNotification
import android.util.Log
import android.util.Base64
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.graphics.drawable.IconCompat
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import org.json.JSONArray
import org.json.JSONException
import im.status.ethereum.module.R
import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG
class PushNotificationHelper(private val context: Application, private val intentFilter: IntentFilter) {
companion object {
private const val DEFAULT_VIBRATION: Long = 300L
private const val CHANNEL_ID = "status-im-notifications"
const val ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION"
const val ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP"
}
private val flag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_CANCEL_CURRENT
}
private val notificationManager: NotificationManager by lazy {
context.getSystemService(NotificationManager::class.java)
}
private val persons: MutableMap<String, Person> = mutableMapOf()
private val messageGroups: MutableMap<String, StatusMessageGroup> = mutableMapOf()
init {
registerBroadcastReceiver()
}
private val notificationActionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_DELETE_NOTIFICATION -> {
intent.extras?.getString("im.status.ethereum.groupId")?.let { groupId ->
cleanGroup(groupId)
}
}
ACTION_TAP_STOP -> {
stop()
System.exit(0)
}
}
Log.e(LOG_TAG, "intent received: ${intent.action}")
}
}
private fun registerBroadcastReceiver() {
intentFilter.apply {
addAction(ACTION_DELETE_NOTIFICATION)
addAction(ACTION_TAP_STOP)
}
context.registerReceiver(notificationActionReceiver, intentFilter, Context.RECEIVER_EXPORTED)
Log.e(LOG_TAG, "Broadcast Receiver registered")
}
fun getOpenAppIntent(deepLink: String?): Intent? {
val packageName = context.packageName
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
val className = launchIntent?.component?.className ?: return null
return try {
val intentClass = Class.forName(className)
Intent(context, intentClass).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
deepLink?.let { data = Uri.parse(it) }
}
} catch (e: ClassNotFoundException) {
e.printStackTrace()
null
}
}
fun invokeApp(bundle: Bundle?) {
val packageName = context.packageName
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
val className = launchIntent?.component?.className ?: return
try {
val activityClass = Class.forName(className)
Intent(context, activityClass).apply {
bundle?.let { putExtra("notification", it) }
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(this)
}
} catch (e: ClassNotFoundException) {
Log.e(LOG_TAG, "Class not found", e)
}
}
fun getMainActivityClass(): Class<*>? {
val packageName = context.packageName
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
val className = launchIntent?.component?.className ?: return null
return try {
Class.forName(className)
} catch (e: ClassNotFoundException) {
e.printStackTrace()
null
}
}
fun isApplicationInForeground(): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val processInfos = activityManager.runningAppProcesses ?: return false
return processInfos.any { processInfo ->
processInfo.processName == context.packageName &&
processInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
processInfo.pkgList.isNotEmpty()
}
}
private fun getCircleBitmap(bitmap: Bitmap): Bitmap {
val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)
val paint = Paint().apply {
isAntiAlias = true
color = Color.RED
}
val rect = Rect(0, 0, bitmap.width, bitmap.height)
val rectF = RectF(rect)
canvas.drawARGB(0, 0, 0, 0)
canvas.drawOval(rectF, paint)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
canvas.drawBitmap(bitmap, rect, rect, paint)
bitmap.recycle()
return output
}
// Represents a message in a Status conversation
data class StatusMessage(
val id: String,
val author: Person,
val timestamp: Long,
val text: String
)
// Represents a group of messages in a Status conversation
inner class StatusMessageGroup(val id: String) {
private val _messages = mutableListOf<StatusMessage>()
val messages: List<StatusMessage> get() = _messages.toList()
fun addMessage(message: StatusMessage) {
_messages.add(message)
}
fun removeMessage(id: String) {
_messages.removeAll { it.id == id }
}
}
private fun getPerson(bundle: Bundle): Person {
return Person.Builder()
.setName(bundle.getString("name"))
.build()
}
private fun createMessage(data: Bundle): StatusMessage {
val author = getPerson(data.getBundle("notificationAuthor") ?: Bundle())
val timeStampLongValue = data.getDouble("timestamp").toLong()
return StatusMessage(
id = data.getString("id") ?: "",
author = author,
timestamp = timeStampLongValue,
text = data.getString("message") ?: ""
)
}
private fun createGroupOnDismissedIntent(
context: Context,
notificationId: Int,
groupId: String,
deepLink: String?
): PendingIntent {
return Intent(ACTION_DELETE_NOTIFICATION).apply {
putExtra("im.status.ethereum.deepLink", deepLink)
putExtra("im.status.ethereum.groupId", groupId)
}.let { intent ->
PendingIntent.getBroadcast(
context.applicationContext,
notificationId,
intent,
flag
)
}
}
private fun createGroupOnTapIntent(
context: Context,
notificationId: Int,
groupId: String,
deepLink: String?
): PendingIntent {
return getOpenAppIntent(deepLink)?.let { intent ->
PendingIntent.getActivity(
context.applicationContext,
notificationId,
intent,
flag
)
} ?: throw IllegalStateException("Could not create open app intent")
}
private fun createOnTapIntent(
context: Context,
notificationId: Int,
deepLink: String?
): PendingIntent {
return getOpenAppIntent(deepLink)?.let { intent ->
PendingIntent.getActivity(
context.applicationContext,
notificationId,
intent,
flag
)
} ?: throw IllegalStateException("Could not create open app intent")
}
private fun createOnDismissedIntent(
context: Context,
notificationId: Int,
deepLink: String?
): PendingIntent {
return Intent(ACTION_DELETE_NOTIFICATION).apply {
putExtra("im.status.ethereum.deepLink", deepLink)
}.let { intent ->
PendingIntent.getBroadcast(
context.applicationContext,
notificationId,
intent,
flag
)
}
}
fun removeStatusMessage(bundle: Bundle) {
val conversationId = bundle.getString("conversationId") ?: return
val group = messageGroups.getOrPut(conversationId) {
StatusMessageGroup(conversationId)
}
bundle.getString("id")?.let { id ->
group.removeMessage(id)
}
showMessages(bundle)
}
fun getMessageGroup(conversationId: String): StatusMessageGroup? {
return messageGroups[conversationId]
}
fun addStatusMessage(bundle: Bundle) {
val conversationId = bundle.getString("conversationId") ?: return
val group = messageGroups.getOrPut(conversationId) {
StatusMessageGroup(conversationId)
}
group.addMessage(createMessage(bundle))
showMessages(bundle)
}
fun showMessages(bundle: Bundle) {
val conversationId = bundle.getString("conversationId") ?: return
val group = messageGroups[conversationId] ?: return
val messagingStyle = NotificationCompat.MessagingStyle("Me")
// If there are no messages, cancel the notification and return
if (group.messages.isEmpty()) {
notificationManager.cancel(conversationId.hashCode())
return
}
group.messages.forEach { message ->
messagingStyle.addMessage(
message.text,
message.timestamp,
message.author
)
}
// Set conversation title if available
bundle.getString("title")?.let { title ->
messagingStyle.conversationTitle = title
}
val builder = NotificationCompat.Builder(context, CHANNEL_ID).apply {
setSmallIcon(R.drawable.ic_stat_notify_status)
setPriority(NotificationCompat.PRIORITY_HIGH)
setCategory(NotificationCompat.CATEGORY_MESSAGE)
setStyle(messagingStyle)
setGroup(conversationId)
setOnlyAlertOnce(true)
setGroupSummary(true)
setContentIntent(createGroupOnTapIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
setDeleteIntent(createGroupOnDismissedIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink")))
setNumber(group.messages.size + 1)
setAutoCancel(true)
// Set empty vibration pattern for Android 5.0 and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setVibrate(LongArray(0))
}
}
// Show the notification
notificationManager.notify(conversationId.hashCode(), builder.build())
}
fun checkOrCreateChannel(
manager: NotificationManager?,
channelId: String,
channelName: String?,
channelDescription: String?,
soundUri: Uri?,
importance: Int,
vibratePattern: LongArray?,
showBadge: Boolean
): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || manager == null) {
return false
}
val existingChannel = manager.getNotificationChannel(channelId)
val shouldCreateOrUpdate = when {
// Channel doesn't exist and we have required info
existingChannel == null && channelName != null && channelDescription != null -> true
// Channel exists but needs update
existingChannel != null && (
(channelName != null && existingChannel.name != channelName) ||
(channelDescription != null && existingChannel.description != channelDescription)
) -> true
else -> false
}
if (shouldCreateOrUpdate) {
NotificationChannel(channelId, channelName, importance).apply {
description = channelDescription
enableLights(true)
enableVibration(vibratePattern != null)
vibrationPattern = vibratePattern
setShowBadge(showBadge)
// Configure sound
if (soundUri != null) {
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
setSound(soundUri, audioAttributes)
} else {
setSound(null, null)
}
manager.createNotificationChannel(this)
}
return true
}
return false
}
fun createChannel(channelInfo: ReadableMap): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false
}
return checkOrCreateChannel(
manager = notificationManager,
channelId = channelInfo.getString("channelId") ?: return false,
channelName = channelInfo.getString("channelName"),
channelDescription = if (channelInfo.hasKey("channelDescription")) {
channelInfo.getString("channelDescription")
} else "",
soundUri = getSoundUri(
if (channelInfo.hasKey("soundName")) {
channelInfo.getString("soundName")
} else "default"
),
importance = if (channelInfo.hasKey("importance")) {
channelInfo.getInt("importance")
} else 4,
vibratePattern = if (channelInfo.hasKey("vibrate") && channelInfo.getBoolean("vibrate")) {
longArrayOf(DEFAULT_VIBRATION)
} else null,
showBadge = channelInfo.hasKey("showBadge") && channelInfo.getBoolean("showBadge")
)
}
fun getNotificationDefaultChannelId(): String = CHANNEL_ID
private fun getSoundUri(soundName: String?): Uri {
return when {
soundName == null || soundName.equals("default", ignoreCase = true) -> {
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
}
else -> {
val resources = context.resources
val packageName = context.packageName
val resId = when {
// Try to find the resource as is
resources.getIdentifier(soundName, "raw", packageName) != 0 -> {
resources.getIdentifier(soundName, "raw", packageName)
}
// Try without file extension
else -> {
val nameWithoutExtension = soundName.substringBeforeLast('.')
resources.getIdentifier(nameWithoutExtension, "raw", packageName)
}
}
Uri.parse("android.resource://$packageName/$resId")
}
}
}
private fun removeGroup(groupId: String) {
messageGroups.remove(groupId)
}
private fun cleanGroup(groupId: String) {
removeGroup(groupId)
if (messageGroups.isEmpty()) {
notificationManager.cancelAll()
}
}
fun start() {
Log.e(LOG_TAG, "Starting Foreground Service")
Intent(context, ForegroundService::class.java).also { serviceIntent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}
registerBroadcastReceiver()
}
fun stop() {
Log.e(LOG_TAG, "Stopping Foreground Service")
// Cancel all current notifications since their intents won't be valid
// after the broadcast receiver is killed
notificationManager.cancelAll()
Intent(context, ForegroundService::class.java).also { serviceIntent ->
context.stopService(serviceIntent)
}
context.unregisterReceiver(notificationActionReceiver)
}
fun sendToNotificationCentre(bundle: Bundle) {
val aggregator = PushNotificationPicturesAggregator { largeIconImage, bigPictureImage ->
sendToNotificationCentreWithPicture(bundle, largeIconImage, bigPictureImage)
}
aggregator.apply {
setLargeIconUrl(context, bundle.getString("largeIconUrl"))
setBigPictureUrl(context, bundle.getString("bigPictureUrl"))
}
}
fun handleConversation(bundle: Bundle) {
if (bundle.getBoolean("deleted")) {
removeStatusMessage(bundle)
} else {
addStatusMessage(bundle)
}
}
fun clearMessageNotifications(conversationId: String) {
notificationManager.cancel(conversationId.hashCode())
cleanGroup(conversationId)
}
fun clearAllMessageNotifications() {
notificationManager.cancelAll()
}
fun sendToNotificationCentreWithPicture(
bundle: Bundle,
largeIconBitmap: Bitmap?,
bigPictureBitmap: Bitmap?
) {
try {
val intentClass = getMainActivityClass() ?: run {
Log.e(LOG_TAG, "No activity class found for the notification")
return
}
if (bundle.getBoolean("isConversation")) {
handleConversation(bundle)
return
}
if (bundle.getString("message") == null) {
Log.d(LOG_TAG, "Ignore this message if you sent data-only notification. " +
"Cannot send to notification centre because there is no 'message' field in: $bundle")
return
}
val notificationIdString = bundle.getString("id") ?: run {
Log.e(LOG_TAG, "No notification ID specified for the notification")
return
}
val resources = context.resources
val packageName = context.packageName
val title = bundle.getString("title") ?: run {
val appInfo = context.applicationInfo
context.packageManager.getApplicationLabel(appInfo).toString()
}
val priority = when (bundle.getString("priority")?.toLowerCase()) {
"max" -> NotificationCompat.PRIORITY_MAX
"high" -> NotificationCompat.PRIORITY_HIGH
"low" -> NotificationCompat.PRIORITY_LOW
"min" -> NotificationCompat.PRIORITY_MIN
"default" -> NotificationCompat.PRIORITY_DEFAULT
else -> NotificationCompat.PRIORITY_HIGH
}
val visibility = when (bundle.getString("visibility")?.toLowerCase()) {
"private" -> NotificationCompat.VISIBILITY_PRIVATE
"public" -> NotificationCompat.VISIBILITY_PUBLIC
"secret" -> NotificationCompat.VISIBILITY_SECRET
else -> NotificationCompat.VISIBILITY_PRIVATE
}
val channelId = bundle.getString("channelId") ?: getNotificationDefaultChannelId()
NotificationCompat.Builder(context, channelId).apply {
setContentTitle(title)
setTicker(bundle.getString("ticker"))
setVisibility(visibility)
setPriority(priority)
setAutoCancel(bundle.getBoolean("autoCancel", true))
setOnlyAlertOnce(bundle.getBoolean("onlyAlertOnce", false))
// Configure notification based on Android version
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setShowWhen(bundle.getBoolean("showWhen", true))
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setDefaults(Notification.DEFAULT_LIGHTS)
}
// Handle group settings for Android KitKat Watch and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
bundle.getString("group")?.let { group ->
setGroup(group)
}
if (bundle.containsKey("groupSummary") || bundle.getBoolean("groupSummary")) {
setGroupSummary(bundle.getBoolean("groupSummary"))
}
}
// Set notification number if provided
bundle.getString("number")?.toIntOrNull()?.let { number ->
setNumber(number)
}
val smallIconResId = findSmallIconResourceId(resources, packageName, bundle)
setSmallIcon(smallIconResId)
configureLargeIcon(this, largeIconBitmap, bundle, resources, packageName)
val message = bundle.getString("message") ?: ""
setContentText(message)
bundle.getString("subText")?.let { subText ->
setSubText(subText)
}
val bigText = bundle.getString("bigText") ?: message
val style = when {
bigPictureBitmap != null -> NotificationCompat.BigPictureStyle()
.bigPicture(bigPictureBitmap)
.setBigContentTitle(title)
.setSummaryText(message)
else -> NotificationCompat.BigTextStyle().bigText(bigText)
}
setStyle(style)
val notificationId = notificationIdString.hashCode()
setContentIntent(createOnTapIntent(context, notificationId, bundle.getString("deepLink")))
setDeleteIntent(createOnDismissedIntent(context, notificationId, bundle.getString("deepLink")))
configureSoundAndVibration(this, bundle)
setOngoing(bundle.getBoolean("ongoing", false))
// Handle color for Lollipop and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setCategory(NotificationCompat.CATEGORY_CALL)
bundle.getString("color")?.let { color ->
setColor(Color.parseColor(color))
}
}
// Finally show the notification if conditions are met
if (!(isApplicationInForeground() && bundle.getBoolean("ignoreInForeground"))) {
val notification = build()
notification.defaults = notification.defaults or Notification.DEFAULT_LIGHTS
if (bundle.containsKey("tag")) {
notificationManager.notify(bundle.getString("tag"), notificationId, notification)
} else {
notificationManager.notify(notificationId, notification)
}
}
}
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to send push notification", e)
}
}
private fun findSmallIconResourceId(resources: Resources, packageName: String, bundle: Bundle): Int {
val smallIcon = bundle.getString("smallIcon")
return when {
!smallIcon.isNullOrEmpty() ->
resources.getIdentifier(smallIcon, "mipmap", packageName)
smallIcon == null ->
resources.getIdentifier("ic_stat_notify_status", "drawable", packageName)
else -> {
val defaultIcon = resources.getIdentifier("ic_launcher", "mipmap", packageName)
defaultIcon.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info
}
}
}
private fun configureLargeIcon(
builder: NotificationCompat.Builder,
largeIconBitmap: Bitmap?,
bundle: Bundle,
resources: Resources,
packageName: String
) {
var finalLargeIcon = largeIconBitmap
if (finalLargeIcon == null) {
val largeIcon = bundle.getString("largeIcon")
val largeIconResId = when {
!largeIcon.isNullOrEmpty() ->
resources.getIdentifier(largeIcon, "mipmap", packageName)
largeIcon == null ->
resources.getIdentifier("ic_launcher", "mipmap", packageName)
else -> 0
}
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
finalLargeIcon = BitmapFactory.decodeResource(resources, largeIconResId)
}
}
bundle.getBundle("notificationAuthor")?.getString("icon")?.let { icon ->
val base64Image = icon.split(",")[1]
val decodedString = Base64.decode(base64Image, Base64.DEFAULT)
val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size)
builder.setLargeIcon(getCircleBitmap(decodedByte))
} ?: finalLargeIcon?.let { icon ->
builder.setLargeIcon(icon)
}
}
private fun configureSoundAndVibration(
builder: NotificationCompat.Builder,
bundle: Bundle
) {
var soundUri: Uri? = null
if (!bundle.containsKey("playSound") || bundle.getBoolean("playSound")) {
val soundName = bundle.getString("soundName") ?: "default"
soundUri = getSoundUri(soundName)
builder.setSound(soundUri)
}
if (soundUri == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setSound(null)
}
if (!bundle.containsKey("vibrate") || bundle.getBoolean("vibrate")) {
val vibration = if (bundle.containsKey("vibration")) {
bundle.getDouble("vibration").toLong()
} else DEFAULT_VIBRATION
val finalVibration = if (vibration == 0L) DEFAULT_VIBRATION else vibration
builder.setVibrate(longArrayOf(0, finalVibration))
}
}
}
@@ -0,0 +1,86 @@
package im.status.ethereum.pushnotifications;
import android.os.Build;
import android.app.Application;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Set;
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
class PushNotificationJsDelivery {
private ReactContext reactContext;
PushNotificationJsDelivery(ReactContext context){
reactContext = context;
}
String convertJSON(Bundle bundle) {
try {
JSONObject json = convertJSONObject(bundle);
return json.toString();
} catch (JSONException e) {
return null;
}
}
// a Bundle is not a map, so we have to convert it explicitly
private JSONObject convertJSONObject(Bundle bundle) throws JSONException {
JSONObject json = new JSONObject();
Set<String> keys = bundle.keySet();
for (String key : keys) {
Object value = bundle.get(key);
if (value instanceof Bundle) {
json.put(key, convertJSONObject((Bundle)value));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
json.put(key, JSONObject.wrap(value));
} else {
json.put(key, value);
}
}
return json;
}
void notifyNotification(Bundle bundle) {
String bundleString = convertJSON(bundle);
WritableMap params = Arguments.createMap();
params.putString("dataJSON", bundleString);
sendEvent("remoteNotificationReceived", params);
}
void notifyNotificationAction(Bundle bundle) {
String bundleString = convertJSON(bundle);
WritableMap params = Arguments.createMap();
params.putString("dataJSON", bundleString);
sendEvent("notificationActionReceived", params);
}
void sendEvent(String eventName, Object params) {
if (reactContext.hasActiveCatalystInstance()) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
}
@@ -1,93 +0,0 @@
package im.status.ethereum.pushnotifications
import android.os.Build
import android.app.Application
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule
import org.json.JSONException
import org.json.JSONObject
import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG
class PushNotificationJsDelivery(
private val reactContext: ReactContext
) {
/**
* Converts a Bundle to its JSON string representation.
* Returns null if conversion fails.
*/
fun convertJSON(bundle: Bundle): String? {
return try {
convertJSONObject(bundle).toString()
} catch (e: JSONException) {
null
}
}
/**
* Converts a Bundle to a JSONObject, handling nested bundles and different value types.
* This explicit conversion is necessary because Bundle is not a standard map.
*/
@Throws(JSONException::class)
private fun convertJSONObject(bundle: Bundle): JSONObject {
return JSONObject().apply {
// Using Kotlin's for loop syntax and 'apply' scope function
bundle.keySet().forEach { key ->
bundle.get(key)?.let { value ->
when (value) {
is Bundle -> put(key, convertJSONObject(value))
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
put(key, JSONObject.wrap(value))
} else {
put(key, value)
}
}
}
}
}
}
/**
* Notifies about a received remote notification by sending an event to JavaScript
*/
fun notifyNotification(bundle: Bundle) {
convertJSON(bundle)?.let { bundleString ->
Arguments.createMap().apply {
putString("dataJSON", bundleString)
sendEvent("remoteNotificationReceived", this)
}
}
}
/**
* Notifies about a notification action by sending an event to JavaScript
*/
fun notifyNotificationAction(bundle: Bundle) {
convertJSON(bundle)?.let { bundleString ->
Arguments.createMap().apply {
putString("dataJSON", bundleString)
sendEvent("notificationActionReceived", this)
}
}
}
/**
* Sends an event to JavaScript if there's an active Catalyst instance
*/
fun sendEvent(eventName: String, params: Any) {
if (reactContext.hasActiveCatalystInstance()) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}
}
}
@@ -0,0 +1,27 @@
package im.status.ethereum.pushnotifications;
import im.status.ethereum.pushnotifications.PushNotification;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Collections;
import java.util.List;
public class PushNotificationPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.<NativeModule>singletonList(new PushNotification(reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
@@ -1,24 +0,0 @@
package im.status.ethereum.pushnotifications
import im.status.ethereum.pushnotifications.PushNotification
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.JavaScriptModule
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class PushNotificationPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(PushNotification(reactContext))
}
// Note: This function is deprecated in newer versions of React Native
// but kept for backwards compatibility
fun createJSModules(): List<Class<out JavaScriptModule>> {
return emptyList()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
@@ -0,0 +1,136 @@
package im.status.ethereum.pushnotifications;
import androidx.annotation.Nullable;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.common.Priority;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import android.util.Log;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import java.util.concurrent.atomic.AtomicInteger;
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
public class PushNotificationPicturesAggregator {
interface Callback {
public void call(Bitmap largeIconImage, Bitmap bigPictureImage);
}
private AtomicInteger count = new AtomicInteger(0);
private Bitmap largeIconImage;
private Bitmap bigPictureImage;
private Callback callback;
public PushNotificationPicturesAggregator(Callback callback) {
this.callback = callback;
}
public void setBigPicture(Bitmap bitmap) {
this.bigPictureImage = bitmap;
this.finished();
}
public void setBigPictureUrl(Context context, String url) {
if(null == url) {
this.setBigPicture(null);
return;
}
Uri uri = null;
try {
uri = Uri.parse(url);
} catch(Exception ex) {
Log.e(LOG_TAG, "Failed to parse bigPictureUrl", ex);
this.setBigPicture(null);
return;
}
final PushNotificationPicturesAggregator aggregator = this;
this.downloadRequest(context, uri, new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
aggregator.setBigPicture(bitmap);
}
@Override
public void onFailureImpl(DataSource dataSource) {
aggregator.setBigPicture(null);
}
});
}
public void setLargeIcon(Bitmap bitmap) {
this.largeIconImage = bitmap;
this.finished();
}
public void setLargeIconUrl(Context context, String url) {
if(null == url) {
this.setLargeIcon(null);
return;
}
Uri uri = null;
try {
uri = Uri.parse(url);
} catch(Exception ex) {
Log.e(LOG_TAG, "Failed to parse largeIconUrl", ex);
this.setLargeIcon(null);
return;
}
final PushNotificationPicturesAggregator aggregator = this;
this.downloadRequest(context, uri, new BaseBitmapDataSubscriber() {
@Override
public void onNewResultImpl(@Nullable Bitmap bitmap) {
aggregator.setLargeIcon(bitmap);
}
@Override
public void onFailureImpl(DataSource dataSource) {
aggregator.setLargeIcon(null);
}
});
}
private void downloadRequest(Context context, Uri uri, BaseBitmapDataSubscriber subscriber) {
ImageRequest imageRequest = ImageRequestBuilder
.newBuilderWithSource(uri)
.setRequestPriority(Priority.HIGH)
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.FULL_FETCH)
.build();
if(!Fresco.hasBeenInitialized()) {
Fresco.initialize(context);
}
DataSource<CloseableReference<CloseableImage>> dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context);
dataSource.subscribe(subscriber, CallerThreadExecutor.getInstance());
}
private void finished() {
synchronized(this.count) {
int val = this.count.incrementAndGet();
if(val >= 2 && this.callback != null) {
this.callback.call(this.largeIconImage, this.bigPictureImage);
}
}
}
}
@@ -1,109 +0,0 @@
package im.status.ethereum.pushnotifications
import androidx.annotation.Nullable
import com.facebook.common.executors.CallerThreadExecutor
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.common.Priority
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import android.util.Log
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import java.util.concurrent.atomic.AtomicInteger
import im.status.ethereum.pushnotifications.PushNotification.Companion.LOG_TAG
class PushNotificationPicturesAggregator(
private val callback: (Bitmap?, Bitmap?) -> Unit
) {
private val count = AtomicInteger(0)
private var largeIconImage: Bitmap? = null
private var bigPictureImage: Bitmap? = null
fun setBigPicture(bitmap: Bitmap?) {
bigPictureImage = bitmap
finished()
}
fun setBigPictureUrl(context: Context, url: String?) {
if (url == null) {
setBigPicture(null)
return
}
val uri = try {
Uri.parse(url)
} catch (ex: Exception) {
Log.e(LOG_TAG, "Failed to parse bigPictureUrl", ex)
setBigPicture(null)
return
}
downloadRequest(context, uri, object : BaseBitmapDataSubscriber() {
override fun onNewResultImpl(@Nullable bitmap: Bitmap?) {
setBigPicture(bitmap)
}
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
setBigPicture(null)
}
})
}
fun setLargeIcon(bitmap: Bitmap?) {
largeIconImage = bitmap
finished()
}
fun setLargeIconUrl(context: Context, url: String?) {
if (url == null) {
setLargeIcon(null)
return
}
val uri = try {
Uri.parse(url)
} catch (ex: Exception) {
Log.e(LOG_TAG, "Failed to parse largeIconUrl", ex)
setLargeIcon(null)
return
}
downloadRequest(context, uri, object : BaseBitmapDataSubscriber() {
override fun onNewResultImpl(@Nullable bitmap: Bitmap?) {
setLargeIcon(bitmap)
}
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
setLargeIcon(null)
}
})
}
private fun downloadRequest(context: Context, uri: Uri, subscriber: BaseBitmapDataSubscriber) {
val imageRequest = ImageRequestBuilder
.newBuilderWithSource(uri)
.setRequestPriority(Priority.HIGH)
.setLowestPermittedRequestLevel(ImageRequest.RequestLevel.FULL_FETCH)
.build()
if (!Fresco.hasBeenInitialized()) {
Fresco.initialize(context)
}
val dataSource = Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
dataSource.subscribe(subscriber, CallerThreadExecutor.getInstance())
}
private fun finished() {
synchronized(count) {
if (count.incrementAndGet() >= 2) {
callback(largeIconImage, bigPictureImage)
}
}
}
}
@@ -0,0 +1,54 @@
package im.status.ethereum.pushnotifications;
import org.junit.Test;
import static org.junit.Assert.*;
import android.app.Application;
import android.app.NotificationManager;
import android.os.Bundle;
import android.content.Intent;
import android.app.PendingIntent;
import android.content.IntentFilter;
import static org.mockito.Mockito.*;
import org.mockito.Spy;
public class PushNotificationHelperTest {
PushNotificationHelper helper;
@Test
public void testAddStatusMessage() {
final Application context = mock(Application.class);
final NotificationManager manager = mock(NotificationManager.class);
final IntentFilter filter = mock(IntentFilter.class);
when(context.getSystemService(NotificationManager.class)).thenReturn(manager);
// Create a spy for PushNotificationHelper
PushNotificationHelper realHelper = new PushNotificationHelper(context, filter);
helper = spy(realHelper);
Bundle bundle = mock(Bundle.class);
Bundle personBundle = mock(Bundle.class);
String conversationId = "conversation-id";
String deepLink = "deep-link";
when(bundle.getString("conversationId")).thenReturn(conversationId);
when(bundle.getString("id")).thenReturn("id");
when(bundle.getString("message")).thenReturn("message");
when(bundle.getDouble("timestamp")).thenReturn(100000.0);
when(bundle.getString("deepLink")).thenReturn(deepLink);
when(personBundle.getString("name")).thenReturn("name");
when(bundle.getBundle("notificationAuthor")).thenReturn(personBundle);
doNothing().when(helper).showMessages(bundle);
helper.addStatusMessage(bundle);
assertNotNull(helper.getMessageGroup(conversationId));
}
}
@@ -1,49 +0,0 @@
package im.status.ethereum.pushnotifications
import android.app.Application
import android.app.NotificationManager
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.mockito.Mockito.*
import org.mockito.Spy
class PushNotificationHelperTest {
private lateinit var helper: PushNotificationHelper
@Test
fun testAddStatusMessage() {
val context = mock(Application::class.java)
val manager = mock(NotificationManager::class.java)
val filter = mock(IntentFilter::class.java)
`when`(context.getSystemService(NotificationManager::class.java)).thenReturn(manager)
// Create a spy for PushNotificationHelper
val realHelper = PushNotificationHelper(context, filter)
helper = spy(realHelper)
val bundle = mock(Bundle::class.java)
val personBundle = mock(Bundle::class.java)
val conversationId = "conversation-id"
val deepLink = "deep-link"
`when`(bundle.getString("conversationId")).thenReturn(conversationId)
`when`(bundle.getString("id")).thenReturn("id")
`when`(bundle.getString("message")).thenReturn("message")
`when`(bundle.getDouble("timestamp")).thenReturn(100000.0)
`when`(bundle.getString("deepLink")).thenReturn(deepLink)
`when`(personBundle.getString("name")).thenReturn("name")
`when`(bundle.getBundle("notificationAuthor")).thenReturn(personBundle)
doNothing().`when`(helper).showMessages(bundle)
helper.addStatusMessage(bundle)
assertNotNull(helper.getMessageGroup(conversationId))
}
}
@@ -195,6 +195,36 @@ RCT_EXPORT_METHOD(loginAccount:(NSString *)request) {
}];
}
RCT_EXPORT_METHOD(verify:(NSString *)address
password:(NSString *)password
callback:(RCTResponseSenderBlock)callback) {
#if DEBUG
NSLog(@"VerifyAccountPasswordV2() method called");
#endif
NSURL *rootUrl = [Utils getRootUrl];
NSString *keystorePath = [rootUrl.path stringByAppendingPathComponent:@"keystore"];
NSDictionary *params = @{
@"keyStoreDir": keystorePath,
@"address": address,
@"password": password
};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error];
if (error) {
NSLog(@"Error creating JSON: %@", error);
return;
}
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[StatusBackendClient executeStatusGoRequestWithCallback:@"VerifyAccountPasswordV2"
body:jsonString
statusgoFunction:^NSString *{
return StatusgoVerifyAccountPasswordV2(jsonString);
}
callback:callback];
}
RCT_EXPORT_METHOD(verifyDatabasePassword:(NSString *)keyUID
password:(NSString *)password
callback:(RCTResponseSenderBlock)callback) {
@@ -105,6 +105,48 @@ RCT_EXPORT_METHOD(convertToKeycardAccount:(NSString *)keyUID
callback:callback];
}
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(encodeTransfer:(NSString *)to
value:(NSString *)value) {
NSDictionary *params = @{
@"to": to,
@"value": value
};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error];
if (error) {
NSLog(@"Error creating JSON: %@", [error localizedDescription]);
return nil;
}
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
return [StatusBackendClient executeStatusGoRequestWithResult:@"EncodeTransferV2"
body:jsonString
statusgoFunction:^NSString *{
return StatusgoEncodeTransferV2(jsonString);
}];
}
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(encodeFunctionCall:(NSString *)method
paramsJSON:(NSString *)paramsJSON) {
NSDictionary *params = @{
@"method": method,
@"paramsJSON": paramsJSON
};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:params options:0 error:&error];
if (error) {
NSLog(@"Error creating JSON: %@", [error localizedDescription]);
return nil;
}
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
return [StatusBackendClient executeStatusGoRequestWithResult:@"EncodeFunctionCallV2"
body:jsonString
statusgoFunction:^NSString *{
return StatusgoEncodeFunctionCallV2(jsonString);
}];
}
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(decodeParameters:(NSString *)decodeParamJSON) {
return [StatusBackendClient executeStatusGoRequestWithResult:@"DecodeParameters"
body:decodeParamJSON
@@ -109,6 +109,13 @@ RCT_EXPORT_METHOD(getNodeConfig:(RCTResponseSenderBlock)callback) {
callback:callback];
}
RCT_EXPORT_METHOD(intendedPanic:(NSString *)message) {
#if DEBUG
NSLog(@"IntendedPanic() method called");
#endif
StatusgoIntendedPanic(message);
}
RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(fleets) {
return [StatusBackendClient executeStatusGoRequestWithResult:@"Fleets"
body:@""
@@ -389,6 +389,86 @@ void _StopCPUProfiling(const FunctionCallbackInfo<Value>& args) {
}
void _EncodeTransfer(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.Length() != 2) {
// Throw an Error that is passed back to JavaScript
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong number of arguments for EncodeTransfer")));
return;
}
// Check the argument types
if (!args[0]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong argument type for 'to'")));
return;
}
if (!args[1]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong argument type for 'value'")));
return;
}
String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked());
char *arg0 = *arg0Obj;
String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked());
char *arg1 = *arg1Obj;
// Call exported Go function, which returns a C string
char *c = EncodeTransfer(arg0, arg1);
Local<String> ret = String::NewFromUtf8(isolate, c).ToLocalChecked();
args.GetReturnValue().Set(ret);
delete c;
}
void _EncodeFunctionCall(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.Length() != 2) {
// Throw an Error that is passed back to JavaScript
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong number of arguments for EncodeFunctionCall")));
return;
}
// Check the argument types
if (!args[0]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong argument type for 'method'")));
return;
}
if (!args[1]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong argument type for 'paramsJSON'")));
return;
}
String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked());
char *arg0 = *arg0Obj;
String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked());
char *arg1 = *arg1Obj;
// Call exported Go function, which returns a C string
char *c = EncodeFunctionCall(arg0, arg1);
Local<String> ret = String::NewFromUtf8(isolate, c).ToLocalChecked();
args.GetReturnValue().Set(ret);
delete c;
}
void _DecodeParameters(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
@@ -1300,6 +1380,53 @@ void _CallPrivateRPC(const FunctionCallbackInfo<Value>& args) {
}
void _VerifyAccountPassword(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.Length() != 3) {
// Throw an Error that is passed back to JavaScript
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong number of arguments for VerifyAccountPassword")));
return;
}
// Check the argument types
if (!args[0]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong argument type for 'keyStoreDir'")));
return;
}
if (!args[1]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong argument type for 'address'")));
return;
}
if (!args[2]->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8Literal(isolate, "Wrong argument type for 'password'")));
return;
}
String::Utf8Value arg0Obj(isolate, args[0]->ToString(context).ToLocalChecked());
char *arg0 = *arg0Obj;
String::Utf8Value arg1Obj(isolate, args[1]->ToString(context).ToLocalChecked());
char *arg1 = *arg1Obj;
String::Utf8Value arg2Obj(isolate, args[2]->ToString(context).ToLocalChecked());
char *arg2 = *arg2Obj;
// Call exported Go function, which returns a C string
char *c = VerifyAccountPassword(arg0, arg1, arg2);
Local<String> ret = String::NewFromUtf8(isolate, c).ToLocalChecked();
args.GetReturnValue().Set(ret);
delete c;
}
void _SendTransactionWithSignature(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
@@ -1865,6 +1992,8 @@ void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "acceptTerms", _AcceptTerms);
NODE_SET_METHOD(exports, "fleets", _Fleets);
NODE_SET_METHOD(exports, "stopCPUProfiling", _StopCPUProfiling);
NODE_SET_METHOD(exports, "encodeTransfer", _EncodeTransfer);
NODE_SET_METHOD(exports, "encodeFunctionCall", _EncodeFunctionCall);
NODE_SET_METHOD(exports, "decodeParameters", _DecodeParameters);
NODE_SET_METHOD(exports, "hexToNumber", _HexToNumber);
NODE_SET_METHOD(exports, "numberToHex", _NumberToHex);
@@ -1888,6 +2017,7 @@ void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "openAccounts", _OpenAccounts);
NODE_SET_METHOD(exports, "extractGroupMembershipSignatures", _ExtractGroupMembershipSignatures);
NODE_SET_METHOD(exports, "callPrivateRPC", _CallPrivateRPC);
NODE_SET_METHOD(exports, "verifyAccountPassword", _VerifyAccountPassword);
NODE_SET_METHOD(exports, "sendTransactionWithSignature", _SendTransactionWithSignature);
NODE_SET_METHOD(exports, "writeHeapProfile", _WriteHeapProfile);
NODE_SET_METHOD(exports, "addPeer", _AddPeer);
+1 -1
View File
@@ -51,7 +51,7 @@ in {
yarn = super.yarn.override { nodejs = super.nodejs_20; };
openjdk = super.openjdk17_headless;
xcodeWrapper = callPackage ./pkgs/xcodeenv/compose-xcodewrapper.nix { } {
versions = ["16.0" "16.1" "16.2"];
versions = ["15.1" "15.2" "15.3" "15.4"];
};
go = super.go_1_21;
clang = super.clang_15;
+2 -2
View File
@@ -38,8 +38,8 @@ nix_install_type() {
"${UID}") echo "single";;
"(${UID})") echo "single";;
"") echo "none";
echo "No Nix installation detected!" >&2;;
*) echo "Unknown Nix installation type!" >&2; exit 1;;
echo "No Nix installtion detected!" >&2;;
*) echo "Unknown Nix installtion type!" >&2; exit 1;;
esac
fi
}
+3 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
export NIX_VERSION="2.24.11"
export NIX_PACKAGE="nixVersions.nix_2_24"
export NIX_VERSION="2.19.3"
export NIX_PACKAGE="nixVersions.nix_2_19"
export NIX_INSTALL_URL="https://nixos.org/releases/nix/nix-${NIX_VERSION}/install"
export NIX_INSTALL_SHA256="5c610a0e97c8a2dd48c1a630608502e43fdecd7d2572463dd4166126106d9188"
export NIX_INSTALL_SHA256="73d47b0ab783fddca1b2d44a03d52a74c97c28a1fc8ff5a29419079302ef9c3d"
export NIX_INSTALL_PATH="/tmp/nix-install-${NIX_VERSION}"
-3
View File
@@ -38,9 +38,6 @@ in mkShell {
LANG="en_US.UTF-8";
LANGUAGE="en_US.UTF-8";
# Important to load our own Nix binary cache.
NIX_CONFIG = builtins.readFile ./nix.conf;
# just a nicety for easy access to node scripts
shellHook = ''
export STATUS_MOBILE_HOME=$(git rev-parse --show-toplevel)
+5 -1
View File
@@ -11,7 +11,7 @@
, outputFileName ? "status-go-${source.shortRev}-${platform}.aar" }:
let
inherit (lib) concatStringsSep optionalString optional splitString;
inherit (lib) concatStringsSep optionalString optional splitString fileContents;
isIOS = platform == "ios";
isAndroid = platform == "android";
enforceXCodeAvailable = callPackage ./enforceXCodeAvailable.nix { };
@@ -46,6 +46,10 @@ in buildGoPackage rec {
# TODO: try removing when go is upgraded to 1.22
GODEBUG = "netdns=cgo+2";
# Sentry for status-go
SENTRY_CONTEXT_NAME = "status-mobile";
SENTRY_CONTEXT_VERSION = fileContents ../../../VERSION;
preBuild = ''
echo 'Generate static files'
pushd go/src/$goPackagePath
+4 -4
View File
@@ -20,8 +20,8 @@
"@react-native-community/netinfo": "^4.4.0",
"@react-native-community/push-notification-ios": "^1.4.1",
"@react-native-community/slider": "^3.0.0",
"@walletconnect/react-native-compat": "2.17.3",
"@reown/walletkit": "^1.0.0",
"@walletconnect/react-native-compat": "2.11.2",
"@walletconnect/web3wallet": "1.10.2",
"base-64": "^1.0.0",
"big-integer": "1.6.51",
"bignumber.js": "git+https://github.com/status-im/bignumber.js.git#refs/tags/v4.0.2-status",
@@ -51,7 +51,7 @@
"react-native-get-random-values": "^1.11.0",
"react-native-gifted-charts": "^1.3.2",
"react-native-hole-view": "^3.0.0-alpha4",
"react-native-image-crop-picker": "0.41.2",
"react-native-image-crop-picker": "0.40.0",
"react-native-image-resizer": "^1.2.3",
"react-native-keychain": "8.1.2",
"react-native-linear-gradient": "3.0.0-alpha.1",
@@ -66,7 +66,7 @@
"react-native-shake": "^3.3.1",
"react-native-share": "10.0.2",
"react-native-static-safe-area-insets": "^2.2.0",
"react-native-status-keycard": "git+https://github.com/status-im/react-native-status-keycard.git#refs/tags/v2.6.5",
"react-native-status-keycard": "git+https://github.com/status-im/react-native-status-keycard.git#refs/tags/v2.6.3",
"react-native-svg": "13.10.0",
"react-native-webview": "13.6.3",
"react-syntax-highlighter": "^15.5.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

+41 -52
View File
@@ -1,10 +1,9 @@
Status Terms of Use
Last updated: 10 December 2024
Last updated: 14 August 2024
1. Introduction
1.1 Who we are
Status is developing a set of open source projects that use peer-to-peer technologies to help people transact securely, communicate freely, and organise with confidence. Anyone participating in these projects helps to build technology and tools that empowers people to advance their own sovereign communities.
@@ -15,7 +14,7 @@ Status does not provide any services, such as financial services, to users of St
1.2 About these terms
These terms are a contract between you and Status and apply to your use of Status Software. The term “Status Software” as used herein means a software developed by Status and is composed of a secure messaging tool and a crypto wallet integrated together.
These terms are a contract between you and Status and apply to your use of Status Software. The term “Status Software” as used herein means a software developed by Status and is composed of a secure messaging tool, a crypto wallet, and a Web 3 browser integrated together.
Please read these terms carefully before using Status Software. By installing or using Status Software on any device, you agree to these terms. If you do not agree, you should not install or use Status Software.
@@ -49,56 +48,57 @@ If a dispute arises between you and Status, we will seek to first solve it amica
At Status, we strive to develop open source software that can serve as a secure communication tool that helps to uphold human rights. Status Software is specifically designed to facilitate the free flow of information, protect the right to private, secure communications and promote the sovereignty of individuals.
Status Software is composed of a secure messaging tool and a crypto wallet integrated together. The software developed by Status in this regard is simply called “Status Software” and you can find more details about its development on Status GitHub page.
Status Software is composed of a secure messaging tool, a crypto wallet, and a Web 3 browser integrated together. The software developed by Status in this regard is simply called “Status Software” and you can find more details about its development on Status GitHub page.
Status Software is open source client software that is designed to be user-friendly and can be installed or run in a web browser on your local desktop or mobile device. You can use Status Software to interact with and participate in Web 3 protocols, applications and peer to peer networks (referred to in these terms collectively, as “Web 3”). Please note that Status Software is not a blockchain protocol, or a blockchain network, a platform or a web-based platform.
Status develops Status Software and makes it available for you to run in a web browser or for your local use on your desktop or mobile device. This means that you are solely responsible for your activity and any potential risk or loss you may incur through using the Status Software as further detailed below in these terms.
As a result of the privacy-first design of Status Software:
1. Status does not store nor have access to user data, messages, or content;
2. Status cannot associate content or actions with individual users. However, users may choose to disclose certain limited information to Status voluntarily, such as when opting in to share usage analytics. This data, shared only through explicit consent of the user, is processed in accordance with our Privacy Policy;
3. Status cannot block or ban users from using Status Software features;
4. Status has no means of monitoring, moderating, removing, modifying or regulating users content or activity, except in Community Spaces created and directly managed by Status.
Status develops Status Software and makes it available for you to run in a web browser or for your local use on your desktop or mobile device. This means that you are solely responsible for your activity and any potential risk or loss you may incur through using the Status Software as further detailed below in these terms.
2.1.1 Wallet
Status Software includes a crypto wallet. We will refer to such wallet functionality in these terms as “Wallet”.
The Wallet is your own personal crypto wallet. This type of wallet is sometimes also called “non-custodial”, “self-custodial” or “self-hosted”, which means you are in complete control of the crypto tokens (the “Digital Assets”) in the Wallet. Status will never have access to or control over your seed phrase, private keys or Digital Assets in your Wallet given its design.
Using the Wallet, you are able to send, receive and store Digital Assets and connect to and interact with third-party services that enable the exchange of Digital Assets for fiat currency and Web 3, including certain blockchain bridges and decentralised exchanges.
The Wallet is your own personal crypto wallet. This type of wallet is sometimes also called “non-custodial”, “self-custodial” or “self-hosted”, which means you are in complete control of the crypto tokens (the “Digital Assets”) in the Wallet. Status will never have access to or control over your seed phrase, private keys or Digital Assets in your Wallet given its design.
The Wallet contains the latest security standards, anti-phishing mechanisms and locally generated and stored public and private keys to ensure that your Digital Assets are secured to the highest standards.
Using the Wallet, you are able to send, receive and store Digital Assets and connect to and interact with third-party services that enable the exchange of Digital Assets for fiat currency and Web 3, including certain blockchain bridges and decentralised exchanges.
When generating an account, a randomisation process is started on your own device that generates a key pair. Status does not have visibility of or access to your private keys. You are solely responsible for storing and securing your seed phrase and private keys, and creating, storing and securing their backups. If you fail to maintain the appropriate security measures of your private keys, it may result in you losing access to your Wallet and control of any Digital Assets in your Wallet. If you lose or forget your seed phrase or private keys, Status will not be able to help you recover or replace them and further, Status will not be able to assist you with the recovery of your Digital Assets.
The Wallet contains the latest security standards, anti-phishing mechanisms and locally generated and stored public and private keys to ensure that your Digital Assets are secured to the highest standards.
In order to further enhance the security of storing your private keys, you may also choose to store your keys on a keycard, such as the Status Keycard.
When generating an account, a randomisation process is started on your own device that generates a key pair. Status does not have visibility of or access to your private keys. You are solely responsible for storing and securing your seed phrase and private keys, and creating, storing and securing their backups. If you fail to maintain the appropriate security measures of your private keys, it may result in you losing access to your Wallet and control of any Digital Assets in your Wallet. If you lose or forget your seed phrase or private keys, Status will not be able to help you recover or replace them and further, Status will not be able to assist you with the recovery of your Digital Assets.
In order to further enhance the security of storing your private keys, you may also choose to store your keys on a keycard, such as the Status Keycard.
2.1.2 Messaging
Status Software includes messaging functionality that enables secure communication between users. We will refer to the messaging functionality of Status Software and the Community Spaces collectively in these terms, as “Messaging”.
Status Software includes messaging functionality that enables secure communication between users. We will refer to the messaging functionality of Status Software and the Status Community Spaces collectively in these terms, as “Messaging”.
Messaging uses an open source, peer-to-peer protocol with end-to-end encryption and metadata suppression to protect your messages from third parties (including Status). End-to-end encryption means that only the sender and recipient of a message can read its contents, and no one else. Peer-to-peer communication is accomplished using the Waku peer-to-peer messaging protocol, which relies on a distributed network of nodes instead of a centralised server. This means that there is no central party or server from which messages can be intercepted, modified or blocked. Learn more about Waku and how it works on Wakus relevant website.
Messaging uses an open source, peer-to-peer protocol with end-to-end encryption and metadata suppression to protect your messages from third parties (including Status). End-to-end encryption means that only the sender and recipient of a message can read its contents, and no one else. Peer-to-peer communication is accomplished using the Waku peer-to-peer messaging protocol, which relies on a distributed network of nodes instead of a centralised server. This means that there is no central party or server from which messages can be intercepted, modified or blocked. Learn more about Waku and how it works on Wakus relevant website.
Within Messaging, you will also be able to participate in Community Spaces. This feature allows users to create and join communities with varying access and discoverability options, which may include token-gated communities, private communities and open and public communities, where you can create group chats and engage with your communities. We note that content in all Community Spaces will be accessible to the members of those spaces. For open and public communities, content will be accessible to all other users of Status Software. Nonetheless, all content, regardless of the type of community, will still be protected by the same end-to-end encryption utilised in Status Software.
Within Messaging, you will also be able to participate in Status Community Spaces. This feature allows users to create their own token-gated communities where you can create group chats and engage with your communities, while leveraging the possibilities, functionality and benefits of blockchain technology. You will also be able to create (and join) open and public communities on Status Software that are accessible to other users. We note that content in these open and public communities will be accessible to all other users of Status Software. Nonetheless, all content will still be protected by the same end-to-end encryption utilised in Status Software.
2.1.2 Web 3 browser
Status Software also includes a Web 3 browser through which you may access and interact with Web 3, exchanges, marketplaces, games and more. We will refer to the Web 3 browser in these terms as the “Web 3 Browser”.
3. Privacy
Status processes a limited amount of user data to ensure the technical operation of the Status Software, enhance its functionality, and improve the user experience. For more details on this limited process of user data, please refer to our Privacy Policy.
Status uses an open source, peer-to-peer protocol with end-to-end encryption and metadata suppression, which by design, means that Status (and any third party) is unable to and does not collect, store, own, control or have any visibility or means of access to your identity, Wallet, browsing information, any user private keys, Digital Assets, messages, content, history of interactions, user accounts or any other user information.
Therefore, Status does not (and cannot) monetise any users data or content, such as messages or browser information and Status does not have any interest in doing so.
Status however processes a limited amount of user data to ensure the technical operation of the Status Software, enhance its functionality, and improve the user experience. For more details on this limited process of user data, please refer to our Privacy Policy.
If you use third party services offered through Status Software, your privacy may not be protected and you will be subject to the privacy terms and conditions of such third parties. Please refer to our full terms regarding third party services below.
4. Using third party services and interacting with Web 3
When using Status Software, you may access or use third party products, services or tools (“Third Party Services”) and interact with Web 3. Status Software is only one means of interacting with these Third Party Services and Web 3 and you can independently utilise other means and tools to do so as well.
Your interactions with Web 3 could include many aspects, such as interacting with smart contracts that reside on blockchain networks, for example to swap Digital Assets or use (crypto) bridges, or deploying your own smart contracts through Community Spaces. You may also interact with and participate in peer-to-peer networks which could vary in their level of decentralisation, ranging from more centralised networks controlled by a single party to networks governed by decentralised communities.
Your interactions with Web 3 could include many aspects, such as interacting with smart contracts that reside on blockchain networks, for example to swap Digital Assets or use (crypto) bridges, or deploying your own smart contracts through Status Community Spaces. You may also interact with and participate in peer-to-peer networks which could vary in their level of decentralisation, ranging from more centralised networks controlled by a single party to networks governed by decentralised communities.
When you use Third Party Services or interact with Web 3, you acknowledge that Status does not control or operate any Third Party Service or Web 3. These products, services, or tools are either controlled, operated, owned, or licensed by the third party providing them, or they may operate autonomously, independently of Status or any other party.
@@ -127,7 +127,7 @@ Although security is a top priority for us and many functions of Status Software
7. Content
At Status, we believe in the sovereignty of individuals and we stand for the cause of personal liberty. We aim to promote social, political, and economic freedoms through Status Software, which is built on the principles of the free flow of information and censorship resistance. Through Status Software, particularly when using Messenger and Community Spaces, you will be able to create and interact with various content. We consider content to include anything you (or any other user) create, post, distribute or exchange through Status Software (as well as any future functions that may be added to Status Software), whether its text, links, GIFs, emoji, photos or any other media formats.
At Status, we believe in the sovereignty of individuals and we stand for the cause of personal liberty. We aim to promote social, political, and economic freedoms through Status Software, which is built on the principles of the free flow of information and censorship resistance. Through Status Software, particularly when using Messenger and Status Community Spaces, you will be able to create and interact with various content. We consider content to include anything you (or any other user) create, post, distribute or exchange through Status Software (as well as any future functions that may be added to Status Software), whether its text, links, GIFs, emoji, photos or any other media formats.
You are solely responsible for any content that you create, post, distribute or submit, messages you exchange or any other activity you may engage in related to your content on or through Status Software and any harm or liability that may result from such content.
@@ -145,35 +145,25 @@ You acknowledge that Status has no means of monitoring, moderating, removing, mo
8.1 Adherence to Status Principles
One of Status goals is the widespread adoption of the decentralised web while also staying true to our Principles. These terms are designed to reflect our Principles, while providing important legal protections for Status and setting out guidelines and terms for the Status Software community of users. We ask that you use Status Software in a way that promotes and aligns with the Status Principles, and avoid actions that may harm or disrupt the community of users.
One of Status goals is the widespread adoption of the decentralised web while also staying true to our Principles. These terms are designed to reflect our Principles, while providing important legal protections for Status and setting out guidelines and terms for the Status Software community of users. We ask that you use Status Software in a way that promotes and aligns with the Status Principles.
We strongly encourage users to act responsibly and ethically in their interactions and refrain from (i) inciting harm, violence or illegal activity, (ii) engaging in harassment, abuse or discrimination, and (iii) spreading false or misleading information.
8.2 Limitation on certain uses of and acts in Status Software
If you are a Community Space creator or admin, we encourage you to establish your own guidelines to foster healthy and respectful community environments for your members.
8.2 Community Governance
Community Spaces are self-sovereign environments managed independently by their creators and appointed admins. These users are solely responsible for:
1. establishing and enforcing their own community guidelines;
2. configuring access controls and permissions;
3. managing community membership, including accepting or rejecting requests;
4. moderating content and interactions within their community; and
5. taking actions against community members who violate their community guidelines or behave disruptively, including the ability to exclude or ban members.
By participating in Community Spaces, you acknowledge that community governance lies solely with community creators and their appointed admins. Status cannot monitor, moderate or intervene in community interactions or governance decisions within any Community Space, except in cases where a Community Space is created and directly managed by Status.
8.3 Limitation on certain uses of and acts in Status Software
You agree not to use Status Software, or attempt or assist others to use it, in a manner that:
1. uses unauthorised methods to create or gain access to an account on Status Software;
2. interferes with or disrupts the integrity, safety, security, availability or performance of Status Software;
3. installs or transmits on or through Status Software, any viruses, worms, malware, Trojan horses, or other harmful or destructive code or content;
4. use Status Software, if you are subject to sanctions, or otherwise designated on a list of prohibited or restricted parties as maintained by, the United Nations, the European Union or any of its member states or the Singaporean, Swiss or US government, or intend to interact or transact with such parties;
5. violate, misappropriate or infringe the rights of Status, other users of Status Software, or any other third parties in any way, including but not limited to privacy, publicity, intellectual property, confidentiality, property or other rights;
6. engage in any activity through Status Software that may be in violation of applicable law, rules or regulations in your jurisdiction;
7. interfere, disrupt, negatively affect, or inhibit other users from their use of Status Software; or
8. engage in any attack, hack, denial-of-service attack, interference, or exploit of any smart contract in connection with use of Status Software.
You shall not, nor try to, encourage or assist others using Status Software to:
1. gain or try to gain unauthorised access to Status Software;
2. interfere with or disrupt the integrity, safety, security, availability or performance of Status Software;
3. generate accounts on Status Software through unauthorised means;
4. install or transmit on or through Status Software, any viruses, worms, malware, Trojan horses, or other harmful or destructive code or content;
5. use spam, data mining, or other unsolicited promotional methods, machine generated content, or unethical or unwanted commercial content;
6. engage in phishing, spoofing, or similar fraudulent acts;
7. knowingly spread misinformation or impersonate another person or legal entity;
8. incite, commit or threaten, harm or engage in illegal or inappropriate conduct including stalking, bullying, harassment, intimidation or abuse another individual or group;
9. engage in or organise child sexual exploitation of any kind;
10. use Status Software, if you are subject to sanctions, or otherwise designated on a list of prohibited or restricted parties as maintained by, the United Nations, the European Union or any of its member states or the Singaporean, Swiss or US government, or intend to interact or transact with such parties;
11. violate, misappropriate or infringe the rights of Status, other users of Status Software, or any other third parties in any way, including but not limited to privacy, publicity, intellectual property, confidentiality, property or other rights;
12. engage in any activity through Status Software that may be in violation of applicable law, rules or regulations in your jurisdiction;
13. interfere, disrupt, negatively affect, or inhibit other users from their use of Status Software; or
14. engage in any attack, hack, denial-of-service attack, interference, or exploit of any smart contract in connection with use of Status Software.
9. Fees
@@ -262,7 +252,6 @@ You release Status from all liability related to any claim, cause of action, con
14. Limitation of liability
Status will not be held liable to you under any contract, negligence, strict liability, or other legal or equitable theory for any lost profits, cost of procurement for substitute services, or any special, incidental, or consequential damages related to, arising from, or in any way connected with these terms, Status, your use of Status Software, particularly where Status has indicated that it is not responsible. This includes without limitation, your use of SNT or other Digital Assets (creating, distributing, transacting in or otherwise) even if Status has been advised of the possibility of such damages.
In any event, Status aggregate liability for any claims is limited to EUR 100 (one hundred Euros). This limitation of liability will apply to the extent permitted by applicable law.
+1 -2
View File
@@ -37,8 +37,6 @@ SECRETS_ENV_VARS=(
'ALCHEMY_ARBITRUM_SEPOLIA_TOKEN'
'ALCHEMY_OPTIMISM_MAINNET_TOKEN'
'ALCHEMY_OPTIMISM_SEPOLIA_TOKEN'
'ALCHEMY_BASE_MAINNET_TOKEN'
'ALCHEMY_BASE_SEPOLIA_TOKEN'
'RARIBLE_MAINNET_API_KEY'
'RARIBLE_TESTNET_API_KEY'
'INFURA_TOKEN'
@@ -47,6 +45,7 @@ SECRETS_ENV_VARS=(
'MIXPANEL_APP_ID'
'MIXPANEL_TOKEN'
'POKT_TOKEN'
'SENTRY_DSN_STATUS_GO'
)
# Secrets like this can't be passed via args or they end up in derivation.
+3 -8
View File
@@ -58,7 +58,7 @@
:preloads [;; The official recommendation is to
;; load the debugger preload first.
flow-storm.api
dev.re-frisk-preload
re-frisk-remote.preload
status-im.setup.schema-preload
;; In order to use component test helpers in the REPL we need to
;; preload namespaces that are not normally required by
@@ -87,9 +87,7 @@
status-im.config/ALCHEMY_ARBITRUM_MAINNET_TOKEN #shadow/env "ALCHEMY_ARBITRUM_MAINNET_TOKEN"
status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN"
status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN"
status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN"
status-im.config/ALCHEMY_BASE_MAINNET_TOKEN #shadow/env "ALCHEMY_BASE_MAINNET_TOKEN"
status-im.config/ALCHEMY_BASE_SEPOLIA_TOKEN #shadow/env "ALCHEMY_BASE_SEPOLIA_TOKEN"}
status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN"}
:compiler-options {:output-feature-set :es5
;; We disable `:fn-deprecated` warnings because we
;; are managing deprecation via clj-kondo and we
@@ -126,8 +124,7 @@
status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN"
status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN"
status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN"
status-im.config/ALCHEMY_BASE_MAINNET_TOKEN #shadow/env "ALCHEMY_BASE_MAINNET_TOKEN"
status-im.config/ALCHEMY_BASE_SEPOLIA_TOKEN #shadow/env "ALCHEMY_BASE_SEPOLIA_TOKEN"}
status-im.config/SENTRY_DSN_STATUS_GO #shadow/env "SENTRY_DSN_STATUS_GO"}
:compiler-options {:output-feature-set :es6
;;disable for android build as there
;;is an intermittent warning with deftype
@@ -169,8 +166,6 @@
status-im.config/ALCHEMY_ARBITRUM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_ARBITRUM_SEPOLIA_TOKEN"
status-im.config/ALCHEMY_OPTIMISM_MAINNET_TOKEN #shadow/env "ALCHEMY_OPTIMISM_MAINNET_TOKEN"
status-im.config/ALCHEMY_OPTIMISM_SEPOLIA_TOKEN #shadow/env "ALCHEMY_OPTIMISM_SEPOLIA_TOKEN"
status-im.config/ALCHEMY_BASE_MAINNET_TOKEN #shadow/env "ALCHEMY_BASE_MAINNET_TOKEN"
status-im.config/ALCHEMY_BASE_SEPOLIA_TOKEN #shadow/env "ALCHEMY_BASE_SEPOLIA_TOKEN"
status-im.config/WALLET_CONNECT_PROJECT_ID #shadow/env "WALLET_CONNECT_PROJECT_ID"}
:compiler-options
{;; needed because we override require and it
-5
View File
@@ -1,5 +0,0 @@
(ns dev.re-frisk-preload
(:require [re-frisk-remote.core :as re-frisk-remote]
[status-im.config]))
(re-frisk-remote/enable {:host (str status-im.config/re-frisk-host ":" status-im.config/re-frisk-port)})
-4
View File
@@ -71,10 +71,6 @@
[callback]
(.addListener ^js event-emitter "keyCardOnDisconnected" callback))
(defn on-card-new-pairing
[callback]
(.addListener ^js event-emitter "keyCardNewPairing" callback))
(defn on-nfc-user-cancelled
[callback]
(.addListener ^js event-emitter "keyCardOnNFCUserCancelled" callback))
+55 -7
View File
@@ -19,6 +19,8 @@
[status-im.constants :as constants]
[status-im.contexts.chat.events :as chat.events]
[status-im.navigation.events :as navigation]
[taoensso.timbre :as log]
[utils.address :as address]
[utils.debounce :as debounce]
[utils.ens.core :as utils.ens]
[utils.ethereum.chain :as chain]
@@ -137,11 +139,13 @@
{:events [:browser/delete-bookmark]}
[{:keys [db] :as cofx}
url]
(rf/merge cofx
{:db (update db :bookmarks/bookmarks dissoc url)
:json-rpc/call [{:method "wakuext_removeBookmark"
:params [url]
:on-success #()}]}))
(let [old-bookmark (get-in db [:bookmarks/bookmarks url])
removed-bookmark (merge old-bookmark {:removed true})]
(rf/merge cofx
{:db (update db :bookmarks/bookmarks dissoc url)
:json-rpc/call [{:method "wakuext_removeBookmark"
:params [url]
:on-success #()}]})))
(defn can-go-back?
[{:keys [history-index]}]
@@ -353,11 +357,43 @@
:id (int id)
:result result}}})
(defn utf8-to-hex
[s]
(let [hex (native-module/utf8-to-hex (str s))]
(if (empty? hex)
nil
hex)))
(defn normalize-message
"NOTE (andrey) there is no spec for this, so this implementation just to be compatible with MM"
[message]
(if (string/starts-with? message "0x")
message
(utf8-to-hex message)))
(defn normalize-sign-message-params
"NOTE (andrey) we need this function, because params may be mixed up"
[params typed?]
(let [[first-param second-param] params]
(when (and (string? first-param) (string? second-param))
(cond
(address/address? first-param)
[first-param (if typed? second-param (normalize-message second-param))]
(address/address? second-param)
[second-param (if typed? first-param (normalize-message first-param))]))))
(rf/defn send-to-bridge
{:events [:browser.callback/call-rpc]}
[_ message]
{:browser/send-to-bridge message})
(defn web3-sign-message?
[method]
(#{constants/web3-sign-typed-data constants/web3-sign-typed-data-v3 constants/web3-sign-typed-data-v4
constants/web3-personal-sign
constants/web3-eth-sign constants/web3-keycard-sign-typed-data}
method))
(rf/defn handle-no-permissions
[cofx {:keys [method id]} message-id]
(if (= method "eth_accounts")
@@ -448,6 +484,18 @@
(when (and message webview)
(.injectJavaScript webview msg)))))
(re-frame/reg-fx
:browser/call-rpc
(fn [[payload callback]]
(native-module/call-rpc
(types/clj->json payload)
(fn [response]
(if (= "" response)
(do
(log/warn :web3-response-error)
(callback "web3-response-error" nil))
(callback nil (.parse js/JSON response)))))))
(re-frame/reg-fx
:browser/show-browser-selection
(fn [link]
@@ -500,8 +548,8 @@
(rf/defn lock-pressed
{:events [:browser.ui/lock-pressed]}
[cofx is-secure?]
(update-browser-option cofx :show-tooltip (if is-secure? :secure :not-secure)))
[cofx secure?]
(update-browser-option cofx :show-tooltip (if secure? :secure :not-secure)))
(rf/defn close-tooltip-pressed
{:events [:browser.ui/close-tooltip-pressed]}
+120
View File
@@ -0,0 +1,120 @@
(ns legacy.status-im.browser.core-test
(:require
[utils.url :as url]))
(defn has-wrong-properties?
[result dapp-url expected-browser]
(let [browser (get-in result [:db :browser/browsers dapp-url])]
(reduce (fn [acc k]
(if (= (k browser)
(k expected-browser))
acc
(conj acc [k (str "was expecting " (k expected-browser) " got " (k browser))])))
nil
(keys expected-browser))))
(defn get-dapp-id
[result dapp-url]
(some #(when (= (url/normalize-and-decode-url dapp-url) (first (:history %))) (:browser-id %))
(vals (get-in result [:db :browser/browsers]))))
#_(deftest browser-test
(let [dapp1-url "cryptokitties.co"
dapp2-url "http://test2.com"]
(testing "user opens a dapp"
(let [result-open (browser/open-url {:db {} :now 1} dapp1-url)
dapp1-id (get-dapp-id result-open dapp1-url)]
(is (= dapp1-id (get-in result-open [:db :browser/options :browser-id]))
"browser-id should be dapp1-url")
(is (not (has-wrong-properties? result-open
dapp1-id
{:browser-id dapp1-id
:history-index 0
:history ["https://cryptokitties.co"]
:dapp? false
:name (i18n/label :t/browser)}))
"some properties of the browser are not correct")
(testing "then a second dapp"
(let [result-open-2 (browser/open-url {:db (:db result-open)
:now 2}
dapp2-url)
dapp2-id (get-dapp-id result-open-2 dapp2-url)]
(is (= dapp2-id (get-in result-open-2 [:db :browser/options :browser-id]))
"browser-id should be dapp2 host")
(is (not (has-wrong-properties? result-open-2
dapp2-id
{:browser-id dapp2-id
:history-index 0
:history ["http://test2.com"]
:dapp? false}))
"some properties of the browser are not correct")
(testing "then removes the second dapp"
(let [result-remove-2 (browser/remove-browser {:db (:db result-open-2)} dapp2-id)]
(is (= #{dapp1-id}
(set (keys (get-in result-remove-2 [:db :browser/browsers]))))
"the second dapp shouldn't be in the browser list anymore")))))
(testing "then opens the dapp again"
(let [result-open-existing (browser/open-existing-browser {:db (:db result-open)
:now 2}
dapp1-id)
dapp1-url2 (str "https://" dapp1-url "/nav2")]
(is (not (has-wrong-properties? result-open-existing
dapp1-id
{:browser-id dapp1-id
:history-index 0
:history ["https://cryptokitties.co"]
:dapp? false
:name (i18n/label :t/browser)}))
"some properties of the browser are not correct")
(is (nil? (browser/navigate-to-next-page result-open-existing))
"nothing should happen if user tries to navigate to next page")
(is (nil? (browser/navigate-to-previous-page result-open-existing))
"nothing should happen if user tries to navigate to previous page")
(testing "then navigates to a new url in the dapp"
(let [result-navigate (browser/navigation-state-changed
{:db (:db result-open-existing)
:now 4}
(clj->js {"url" dapp1-url2
"loading" false})
false)]
(is (not (has-wrong-properties? result-navigate
dapp1-id
{:browser-id dapp1-id
:history-index 1
:history ["https://cryptokitties.co" dapp1-url2]
:dapp? false
:name (i18n/label :t/browser)}))
"some properties of the browser are not correct")
(testing "then navigates to previous page"
(let [result-previous (browser/navigate-to-previous-page {:db (:db result-navigate)
:now 5})]
(is
(not (has-wrong-properties? result-previous
dapp1-id
{:browser-id dapp1-id
:history-index 0
:history ["https://cryptokitties.co"
dapp1-url2]
:dapp? false
:name (i18n/label :t/browser)}))
"some properties of the browser are not correct")
(testing "then navigates to next page")
(let [result-next (browser/navigate-to-next-page {:db (:db result-previous)
:now 6})]
(is (not
(has-wrong-properties? result-next
dapp1-id
{:browser-id dapp1-id
:history-index 1
:history ["https://cryptokitties.co"
dapp1-url2]
:dapp? false
:name (i18n/label :t/browser)}))
"some properties of the browser are not correct"))))))))))))
@@ -22,7 +22,7 @@
:icon :main-icons/wallet}})
(rf/defn permission-yield-control
[cofx dapp-name permission message-id]
[{:keys [db] :as cofx} dapp-name permission message-id params]
(cond
(= permission constants/dapp-permission-qr-code)
(rf/merge (assoc-in cofx [:db :browser/options :yielding-control?] true)
@@ -33,7 +33,7 @@
:message-id message-id}}))))
(rf/defn permission-show-permission
[{:keys [db]} dapp-name permission message-id yield-control?]
[{:keys [db] :as cofx} dapp-name permission message-id yield-control?]
{:db (assoc-in db
[:browser/options :show-permission]
{:requested-permission permission
@@ -51,7 +51,7 @@
(rf/defn send-response-to-bridge
"Send response to the bridge. If the permission is allowed, send data associated
with the permission"
[_cofx permission message-id allowed? data]
[{:keys [db] :as cofx} permission message-id allowed? data]
{:browser/send-to-bridge (cond-> {:type constants/api-response
:isAllowed allowed?
:permission permission
@@ -108,13 +108,13 @@
(let [pending-permissions (get-in db [:browser/options :pending-permissions])
next-permission (last pending-permissions)
new-cofx (update-in cofx [:db :browser/options :pending-permissions] butlast)]
(when-let [{:keys [yield-control? permission message-id allowed?]} next-permission]
(when-let [{:keys [yield-control? permission message-id allowed? params]} next-permission]
(if (and yield-control? allowed?)
(permission-yield-control new-cofx dapp-name permission message-id)
(permission-yield-control new-cofx dapp-name permission message-id params)
(permission-show-permission new-cofx dapp-name permission message-id yield-control?)))))))
(rf/defn send-response-and-process-next-permission
[cofx dapp-name requested-permission message-id]
[{:keys [db] :as cofx} dapp-name requested-permission message-id]
(rf/merge cofx
(send-response-to-bridge requested-permission
message-id
@@ -126,12 +126,12 @@
"Add permission to set of allowed permission and process next permission"
{:events [:browser.permissions.ui/dapp-permission-allowed]}
[{:keys [db] :as cofx}]
(let [{:keys [requested-permission message-id dapp-name yield-control?]}
(let [{:keys [requested-permission message-id dapp-name yield-control? params]}
(get-in db [:browser/options :show-permission])]
(rf/merge (assoc-in cofx [:db :browser/options :show-permission] nil)
(update-dapp-permissions dapp-name requested-permission true)
(if yield-control?
(permission-yield-control dapp-name requested-permission message-id)
(permission-yield-control dapp-name requested-permission message-id params)
(send-response-and-process-next-permission dapp-name requested-permission message-id)))))
(rf/defn deny-permission
@@ -0,0 +1,90 @@
(ns legacy.status-im.browser.permissions-test)
#_(deftest permissions-test
(let [dapp-name "test.com"
dapp-name2 "test2.org"
cofx {:db (assoc-in (:db (browser/open-url {:db {}} dapp-name))
[:profile/profile :public-key]
"public-key")}
dapp-id (core.tests/get-dapp-id cofx dapp-name)]
(testing "dapps permissions are initialized"
(is (zero? (count (get-in cofx [:db :dapps/permissions]))))
(is (= dapp-id (get-in cofx [:db :browser/options :browser-id]))))
(testing "receiving an unsupported permission"
(let [result-ask (browser/process-bridge-message cofx
(types/clj->json
{:type "api-request"
:host dapp-name
:messageId 0
:permission "FAKE_PERMISSION"}))]
(is (not (get-in result-ask [:browser/send-to-bridge :isAllowed])))))
(testing "receiving a supported permission"
(let [result-ask (browser/process-bridge-message cofx
(types/clj->json {:type "api-request"
:host dapp-name
:messageId 1
:permission "contact-code"}))]
(is (= (get-in result-ask [:db :browser/options :show-permission])
{:requested-permission "contact-code"
:dapp-name "test.com"
:message-id 1
:yield-control? nil}))
(is (zero? (count (get-in result-ask [:db :dapps/permissions]))))
(testing "then user accepts the supported permission"
(let [accept-result (permissions/allow-permission {:db (:db result-ask)})]
(is (= (get accept-result :browser/send-to-bridge)
{:type "api-response"
:messageId 1
:isAllowed true
:data "public-key"
:permission "contact-code"})
"the data should have been sent to the bridge")
(is (= (get-in accept-result [:db :dapps/permissions])
{"test.com" {:dapp "test.com" :permissions ["contact-code"]}})
"the dapp should now have CONTACT_CODE permission")
(testing "then dapp asks for permission again"
(let [result-ask-again (browser/process-bridge-message {:db (:db accept-result)}
(types/clj->json
{:type "api-request"
:host dapp-name
:messageId 2
:permission "contact-code"}))]
(is (= (get result-ask-again :browser/send-to-bridge)
{:type "api-response"
:isAllowed true
:messageId 2
:data "public-key"
:permission "contact-code"})
"the response should be immediatly sent to the bridge")))
(testing "then user switch to another dapp that asks for permissions"
(let [new-dapp (browser/open-url {:db (:db accept-result)} dapp-name2)
result-ask2 (browser/process-bridge-message {:db (:db new-dapp)}
(types/clj->json
{:type "api-request"
:host dapp-name2
:messageId 3
:permission "contact-code"}))]
(is (= (get-in result-ask2 [:db :dapps/permissions])
{"test.com" {:dapp "test.com" :permissions ["contact-code"]}})
"there should only be permissions for dapp-name at that point")
(is (nil? (get result-ask2 :browser/send-to-bridge))
"no message should be sent to the bridge")
(testing "then user accepts permission for dapp-name2"
(let [accept-result2 (permissions/allow-permission {:db (:db result-ask2)})]
(is (= (get-in accept-result2 [:db :dapps/permissions])
{"test.com" {:dapp "test.com" :permissions ["contact-code"]}
"test2.org" {:dapp "test2.org" :permissions ["contact-code"]}})
"there should be permissions for both dapps now")
(is (= (get accept-result2 :browser/send-to-bridge)
{:type "api-response"
:isAllowed true
:messageId 3
:data "public-key"
:permission "contact-code"})
"the response should be sent to the bridge")))))))))))
@@ -156,7 +156,7 @@
(rf/defn select-mention
{:events [:chat.ui/select-mention]}
[{:keys [db]} {:keys [primary-name searched-text match public-key] :as _user}]
[{:keys [db]} {:keys [primary-name searched-text match public-key] :as user}]
(let [chat-id (:current-chat-id db)
text (get-in db [:chat/inputs chat-id :input-text])
method "wakuext_chatMentionSelectMention"
+4 -23
View File
@@ -5,6 +5,7 @@
[legacy.status-im.data-store.messages :as data-store.messages]
[legacy.status-im.utils.deprecated-types :as types]
[react-native.platform :as platform]
[status-im.contexts.chat.messenger.messages.delete-message.events :as delete-message]
[status-im.contexts.chat.messenger.messages.list.events :as message-list]
[status-im.contexts.chat.messenger.messages.list.state :as view.state]
[utils.re-frame :as rf]))
@@ -118,35 +119,15 @@
(when (get-in db [:messages chat-id message-id])
{:db (assoc-in db [:messages chat-id message-id :outgoing-status] status)}))
(defn- update-db-delete-locally-without-time-limit
"Delete message in re-frame db, used to handle received removed-messages"
[db chat-id message-id deleted-by]
(when (get-in db [:messages chat-id message-id])
(update-in db [:messages chat-id message-id] assoc :deleted? true :deleted-by deleted-by)))
(rf/defn delete-messages-locally
"Mark messages :deleted? localy in client"
{:events [:chat.ui/delete-messages-localy]}
[{:keys [db]} messages chat-id]
(let [new-db (->> messages
(filter #(get-in db [:messages chat-id (:message-id %)]))
(reduce #(update-db-delete-locally-without-time-limit %1
chat-id
(:message-id %2)
(:deleted-by %2))
db))]
(when new-db
(message-list/rebuild-message-list {:db new-db} chat-id))))
(rf/defn handle-removed-messages
[cofx removed-messages]
[{:keys [db] :as cofx} removed-messages]
(let [mark-as-deleted-fx (->> removed-messages
(map #(assoc %
:message-id (:messageId %)
:deleted-by (:deletedBy %)))
(group-by :chatId)
(mapv (fn [[chat-id messages]]
(delete-messages-locally messages chat-id))))
(delete-message/delete-messages-localy messages chat-id))))
mark-as-seen-fx (mapv
(fn [removed-message]
(let [chat-id (:chatId removed-message)
@@ -155,7 +136,7 @@
[message-id]
nil)))
removed-messages)
remove-messages-fx (fn [_cofx]
remove-messages-fx (fn [{:keys [db]}]
{:dispatch [:activity-center.notifications/fetch-unread-count]})]
(apply rf/merge
cofx
+3 -3
View File
@@ -43,7 +43,7 @@
(rf/defn member-ban
{:events [::member-ban]}
[_cofx community-id public-key]
[cofx community-id public-key]
{:json-rpc/call [{:method "wakuext_banUserFromCommunity"
:params [{:communityId community-id
:user public-key}]
@@ -63,7 +63,7 @@
(rf/defn member-kick
{:events [::member-kick]}
[_cofx community-id public-key]
[cofx community-id public-key]
{:json-rpc/call [{:method "wakuext_removeUserFromCommunity"
:params [community-id public-key]
:js-response true
@@ -97,7 +97,7 @@
(rf/defn add-role-to-member
{:events [:community.member/add-role]}
[_cofx community-id public-key role-id]
[cofx community-id public-key role-id]
{:json-rpc/call [{:method "wakuext_addRoleToMember"
:params [{:communityId community-id
:user public-key
@@ -4,6 +4,12 @@
[status-im.constants :as constants]
[utils.transforms :as transforms]))
(defn <-revealed-accounts-rpc
[accounts]
(mapv
#(set/rename-keys % {:isAirdropAddress :airdrop-address?})
(js->clj accounts :keywordize-keys true)))
(defn <-request-to-join-community-rpc
[r]
(set/rename-keys r
@@ -16,7 +16,7 @@
:community-id :communityId
:clock-value :clock})))
(defn <-status-link-previews-rpc
(defn- <-status-link-previews-rpc
[preview]
(-> preview
(update :community
@@ -40,7 +40,7 @@
(update-in [:community :banner] set/rename-keys {:data-uri :dataUri})
(update-in [:community :icon] set/rename-keys {:data-uri :dataUri})))
(defn <-link-preview-rpc
(defn- <-link-preview-rpc
[preview]
(-> preview
(update :thumbnail set/rename-keys {:dataUri :data-uri})
@@ -160,17 +160,17 @@
:on-error #(log/error "failed to delete messages by chat-id" % chat-id)}]})
(rf/defn delete-message
[_cofx id]
[cofx id]
(delete-message-rpc id))
(rf/defn delete-messages-from
[_cofx author]
[cofx author]
(delete-messages-from-rpc author))
(rf/defn mark-messages-seen
[_cofx chat-id ids on-success]
[cofx chat-id ids on-success]
(mark-seen-rpc chat-id ids on-success))
(rf/defn delete-messages-by-chat-id
[_cofx chat-id]
[cofx chat-id]
(delete-messages-by-chat-id-rpc chat-id))
@@ -29,7 +29,7 @@
:on-error on-error}]})
(rf/defn send-pin-message
[_cofx pin-message]
[cofx pin-message]
{:json-rpc/call [{:method "wakuext_sendPinMessage"
:params [(messages/->rpc pin-message)]
:js-response true
+11 -4
View File
@@ -62,7 +62,7 @@
(rf/defn update-ens-tx-state-and-redirect
{:events [:update-ens-tx-state-and-redirect]}
[cofx new-state username custom-domain? tx-hash]
[{:keys [db] :as cofx} new-state username custom-domain? tx-hash]
(rf/merge cofx
(update-ens-tx-state new-state username custom-domain? tx-hash)
(redirect-to-ens-summary)))
@@ -178,6 +178,13 @@
:else
(re-frame/dispatch [::name-resolved username :taken]))))
(defn registration-cost
[chain-id]
(case chain-id
3 50
5 10
1 10))
(rf/defn register-name
{:events [::register-name-pressed]}
[{:keys [db]} address]
@@ -232,14 +239,14 @@
(let [{:keys [custom-domain?]} (:ens/registration db)
chain-id (chain/chain-id db)
usernames (into #{} (keys (get-in db [:ens/names chain-id])))
next-state (state custom-domain? username usernames)]
state (state custom-domain? username usernames)]
(reset! resolve-last-id (random/id))
(merge
{:db (update db
:ens/registration assoc
:username username
:state next-state)}
(when (= next-state :searching)
:state state)}
(when (= state :searching)
(let [{:profile/keys [profile]} db
{:keys [public-key]} profile
addresses (addresses-without-watch db)
+23 -1
View File
@@ -1,6 +1,28 @@
(ns legacy.status-im.ethereum.macros
(:require
[clojure.java.io :as io]))
[clojure.java.io :as io]
[clojure.string :as string]))
(defn token-icon-path
[path]
(fn [el]
(let [el (string/replace el ".png" "")
s (str path el ".png")
s-js (str "." s)]
(when (.exists (io/file s))
[el `(js/require ~s-js)]))))
(defmacro resolve-icons
"In react-native arguments to require must be static strings.
Resolve all icons at compilation time so no variable is used."
[network]
(let [path (str "./resources/images/tokens/" (name network) "/")
files (->> (io/file path)
file-seq
(filter #(string/ends-with? % "png"))
(map #(first (string/split (.getName %) #"@")))
distinct)]
(into {} (map (token-icon-path path) files))))
(defn network->icon
[network]
+50 -1
View File
@@ -1,7 +1,8 @@
(ns legacy.status-im.ethereum.tokens
(:require
[clojure.string :as string]
[utils.ethereum.chain :as chain])
(:require-macros [legacy.status-im.ethereum.macros :as ethereum.macros]))
(:require-macros [legacy.status-im.ethereum.macros :as ethereum.macros :refer [resolve-icons]]))
(def default-native-currency
(memoize
@@ -12,6 +13,8 @@
:decimals 18
:icon {:source (js/require "../resources/images/tokens/default-token.png")}})))
(def snt-icon-source (js/require "../resources/images/tokens/mainnet/SNT.png"))
(def all-native-currencies
(ethereum.macros/resolve-native-currency-icons
{:mainnet {:name "Ether"
@@ -35,8 +38,54 @@
:symbol-display :BNBtest
:decimals 18}}))
(def native-currency-symbols
(set (map #(-> % val :symbol) all-native-currencies)))
(defn native-currency
[{sym :symbol :as current-network}]
(let [chain (chain/network->chain-keyword current-network)]
(get all-native-currencies chain (default-native-currency sym))))
(defn ethereum?
[sym]
(native-currency-symbols sym))
(def token-icons
{:mainnet (resolve-icons :mainnet)
:xdai (resolve-icons :xdai)
:custom []})
(def default-token (js/require "../resources/images/tokens/default-token.png"))
(defn update-icon
[network token]
(-> token
(assoc-in [:icon :source] (get-in token-icons [network (name (:symbol token))] default-token))
(update :address string/lower-case)))
(defn nfts-for
[all-tokens]
(filter :nft? (vals all-tokens)))
(defn sorted-tokens-for
[all-tokens]
(->> (vals all-tokens)
(filter #(not (:hidden? %)))
(sort #(compare (string/lower-case (:name %1))
(string/lower-case (:name %2))))))
(defn symbol->token
[all-tokens sym]
(some #(when (= sym (:symbol %)) %) (vals all-tokens)))
(defn address->token
[all-tokens address]
(get all-tokens (string/lower-case address)))
(defn asset-for
[all-tokens current-network sym]
(let [native-coin (native-currency current-network)]
(if (or (= (:symbol-display native-coin) sym)
(= (:symbol native-coin) sym))
native-coin
(symbol->token all-tokens sym))))
@@ -7,9 +7,37 @@
[re-frame.core :as re-frame]
[status-im.common.json-rpc.events :as json-rpc]
[taoensso.timbre :as log]
[utils.ethereum.chain :as chain]
[utils.ethereum.eip.eip55 :as eip55]
[utils.re-frame :as rf]))
(def confirmations-count-threshold 12)
(def etherscan-supported?
#{(chain/chain-keyword->chain-id :mainnet)
(chain/chain-keyword->chain-id :sepolia)})
(def binance-mainnet-chain-id (chain/chain-keyword->chain-id :bsc))
(def binance-testnet-chain-id (chain/chain-keyword->chain-id :bsc-testnet))
(def network->subdomain {11155111 "sepolia"})
(defn get-transaction-details-url
[chain-id tx-hash]
{:pre [(number? chain-id) (string? tx-hash)]
:post [(or (nil? %) (string? %))]}
(cond
(etherscan-supported? chain-id)
(let [network-subdomain (when-let [subdomain (network->subdomain chain-id)]
(str subdomain "."))]
(str "https://" network-subdomain "etherscan.io/tx/" tx-hash))
(= chain-id binance-mainnet-chain-id)
(str "https://bscscan.com/tx/" tx-hash)
(= chain-id binance-testnet-chain-id)
(str "https://testnet.bscscan.com/tx/" tx-hash)))
(def default-erc20-token
{:symbol :ERC20
:decimals 18
@@ -93,16 +121,16 @@
(rf/defn check-transaction
"Check if the transaction has been triggered and applies the effects returned
by `on-trigger` if that is the case"
[{:keys [db] :as cofx} {tx-hash :hash :as transaction}]
[{:keys [db] :as cofx} {:keys [hash] :as transaction}]
(when-let [watch-params
(get-in db [:ethereum/watched-transactions tx-hash])]
(get-in db [:ethereum/watched-transactions hash])]
(let [{:keys [trigger-fn on-trigger]} watch-params]
(when (trigger-fn db transaction)
(rf/merge cofx
{:db (update db
:ethereum/watched-transactions
dissoc
tx-hash)}
hash)}
(on-trigger transaction))))))
(rf/defn check-watched-transactions
@@ -124,14 +152,14 @@
"We determine a unique id for the transfer before adding it because some
transaction can contain multiple transfers and they would overwrite each other
in the transfer map if identified by hash"
[{:keys [db] :as cofx} {tx-hash :hash :keys [id address] :as transfer}]
(let [transfer-by-hash (get-in db [:wallet-legacy :accounts address :transactions tx-hash])]
[{:keys [db] :as cofx} {:keys [hash id address] :as transfer}]
(let [transfer-by-hash (get-in db [:wallet-legacy :accounts address :transactions hash])]
(when-let [unique-id (when-not (= transfer transfer-by-hash)
(if (and transfer-by-hash
(not (= :pending
(:type transfer-by-hash))))
id
tx-hash))]
hash))]
(rf/merge cofx
{:db (assoc-in db
[:wallet-legacy :accounts address :transactions unique-id]
@@ -142,7 +170,11 @@
[db address]
(get-in db [:wallet-legacy :accounts (eip55/address->checksum address) :min-block]))
(defn count-min-block-transfers
(defn get-max-block-with-transfers
[db address]
(get-in db [:wallet-legacy :accounts (eip55/address->checksum address) :max-block]))
(defn min-block-transfers-count
[db address]
(get-in db
[:wallet-legacy :accounts
@@ -155,14 +187,14 @@
{:keys [min-block min-block-transfers-count]}
(reduce
(fn [{:keys [min-block] :as acc}
{tx-hash :hash block :block}]
{:keys [block hash]}]
(cond
(or (nil? min-block) (> min-block (js/parseInt block)))
{:min-block (js/parseInt block)
:min-block-transfers-count 1}
(and (= min-block block)
(nil? (get-in db [:wallet-legacy :accounts checksum :transactions tx-hash])))
(nil? (get-in db [:wallet-legacy :accounts checksum :transactions hash])))
(update acc :min-block-transfers-count inc)
:else acc))
@@ -171,7 +203,7 @@
(js/parseInt min-block-string))
:min-block-transfers-count
(count-min-block-transfers db address)}
(min-block-transfers-count db address)}
transfers)]
(log/debug "[transactions] set-lowest-fetched-block"
"address" address
@@ -206,7 +238,7 @@
{:db (update-fetching-status db addresses :history? false)})
(rf/defn tx-history-end-reached
[{:keys [db]} address]
[{:keys [db] :as cofx} address]
(let [syncing-allowed? (utils.mobile-sync/syncing-allowed? db)]
{:db (assoc-in db
[:wallet-legacy :fetching address :all-fetched?]
@@ -215,22 +247,23 @@
:all-preloaded))}))
(rf/defn handle-new-transfer
[cofx transfers {:keys [address limit]}]
[{:keys [db] :as cofx} transfers {:keys [address limit]}]
(log/debug "[transfers] new-transfers"
"address" address
"count" (count transfers)
"limit" limit)
(let [checksum (eip55/address->checksum address)
effects (cond-> [(when (seq transfers)
(set-lowest-fetched-block checksum transfers))]
(let [checksum (eip55/address->checksum address)
max-known-block (get-max-block-with-transfers db address)
effects (cond-> [(when (seq transfers)
(set-lowest-fetched-block checksum transfers))]
(seq transfers)
(concat
[]
(mapv add-transfer transfers))
(seq transfers)
(concat
[]
(mapv add-transfer transfers))
(< (count transfers) limit)
(conj (tx-history-end-reached checksum)))]
(< (count transfers) limit)
(conj (tx-history-end-reached checksum)))]
(apply rf/merge cofx (tx-fetching-ended [checksum]) effects)))
(rf/defn new-transfers
@@ -278,12 +311,16 @@
:limit limit)])
:on-error #(re-frame/dispatch [::tx-fetching-failed address])})))))
(defn some-transactions-loaded?
[db address]
(not-empty (get-in db [:wallet-legacy :accounts address :transactions])))
(rf/defn fetch-more-tx
{:events [:transactions/fetch-more]}
[{:keys [db] :as cofx} address]
(let [min-known-block (or (get-min-known-block db address)
(:ethereum/current-block db))
min-count (or (count-min-block-transfers db address) 0)]
(let [min-known-block (or (get-min-known-block db address)
(:ethereum/current-block db))
min-block-transfers-count (or (min-block-transfers-count db address) 0)]
(rf/merge
cofx
{:transactions/get-transfers
@@ -296,7 +333,7 @@
;; the whole `default-transfers-limit` of transfers the number of transfers already received
;; for `min-known-block` is added to the page size.
:limit-per-address {address (+ default-transfers-limit
min-count)}}}
min-block-transfers-count)}}}
(tx-fetching-in-progress [address]))))
(rf/defn get-fetched-transfers
+19 -2
View File
@@ -7,7 +7,6 @@
legacy.status-im.chat.models.loading
legacy.status-im.data-store.chats
legacy.status-im.data-store.visibility-status-updates
legacy.status-im.ens.core
legacy.status-im.fleet.core
legacy.status-im.group-chats.core
legacy.status-im.log-level.core
@@ -60,11 +59,21 @@
(fn [options]
(permissions/request-notifications options)))
(re-frame/reg-fx
:ui/show-error
(fn [content]
(utils/show-popup "Error" content)))
(re-frame/reg-fx
:ui/show-confirmation
(fn [options]
(utils/show-confirmation options)))
(re-frame/reg-fx
:ui/close-application
(fn [_]
(native-module/close-application)))
(re-frame/reg-fx
::app-state-change-fx
(fn [state]
@@ -74,6 +83,11 @@
(theme/change-device-theme (rn/get-color-scheme)))
(native-module/app-state-change state)))
(re-frame/reg-fx
:ui/listen-to-window-dimensions-change
(fn []
(dimensions/add-event-listener)))
(rf/defn dismiss-keyboard
{:events [:dismiss-keyboard]}
[_]
@@ -219,4 +233,7 @@
:on-success (fn [on-ramps]
(re-frame/dispatch [::crypto-loaded on-ramps]))}]})
(re-frame/reg-event-fx :buy-crypto.ui/open-screen
(fn []
{:fx [[:dispatch [:wallet-legacy/keep-watching]]
[:dispatch [:open-modal :buy-crypto nil]]]}))
+1 -1
View File
@@ -84,7 +84,7 @@
(rf/defn save
{:events [:fleet.ui/save-fleet-confirmed]}
[{:keys [db] :as cofx} fleet]
[{:keys [db now] :as cofx} fleet]
(let [old-fleet (get-in db [:profile/profile :fleet])]
(when (not= fleet old-fleet)
(multiaccounts.update/multiaccount-update

Some files were not shown because too many files have changed in this diff Show More