diff --git a/.gitguardian.yml b/.gitguardian.yml index 0337ea8db..aabcbae59 100644 --- a/.gitguardian.yml +++ b/.gitguardian.yml @@ -2,7 +2,10 @@ # This file configures which files and secrets to ignore during scanning # Ignore specific file patterns -paths-ignore: +paths_ignore: + # Gitleaks configuration file (contains example secrets/patterns for detection) + - ".gitleaks.toml" + # Mock certificates for testing (these are intentionally committed test data) - "**/mock_certificates/**/*.key" - "**/mock_certificates/**/*.crt" @@ -46,7 +49,7 @@ paths-ignore: - "**/packages/mobile-sdk-alpha/ios/Frameworks/**" - "**/packages/mobile-sdk-alpha/ios/SelfSDK/**" # Ignore specific secret types for mock files -secrets-ignore: +secrets_ignore: - "Generic Private Key" # For mock certificate keys - "Generic Certificate" # For mock certificates - "RSA Private Key" # For mock RSA keys @@ -57,6 +60,7 @@ secret: - match: 2036b4e50ad3042969b290e354d9864465107a14de6f5a36d49f81ea8290def8 name: prebuilt-ios-arm64-apple-ios.private.swiftinterface ignored_paths: + - ".gitleaks.toml" - "**/*.swiftinterface" - "**/*.xcframework/**" - "**/packages/mobile-sdk-alpha/ios/Frameworks/**" diff --git a/.github/workflows/mobile-deploy.yml b/.github/workflows/mobile-deploy.yml index 25ca53707..ece367ef1 100644 --- a/.github/workflows/mobile-deploy.yml +++ b/.github/workflows/mobile-deploy.yml @@ -681,6 +681,9 @@ jobs: SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }} SELFXYZ_INTERNAL_REPO_PAT: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }} + TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }} + TURNKEY_ORGANIZATION_ID: ${{ secrets.TURNKEY_ORGANIZATION_ID }} timeout-minutes: 90 run: | cd ${{ env.APP_PATH }} @@ -1121,6 +1124,9 @@ jobs: NODE_OPTIONS: "--max-old-space-size=6144" SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }} + TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }} + TURNKEY_ORGANIZATION_ID: ${{ secrets.TURNKEY_ORGANIZATION_ID }} run: | cd ${{ env.APP_PATH }} diff --git a/.gitleaks.toml b/.gitleaks.toml index 32d34f472..db2ddd650 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,8 +1,11 @@ # This file has been auto-generated. Do not edit manually. # If you would like to contribute new rules, please use # cmd/generate/config/main.go and follow the contributing guidelines -# at https://github.com/zricethezav/gitleaks/blob/master/CONTRIBUTING.md - +# at https://github.com/gitleaks/gitleaks/blob/master/CONTRIBUTING.md +# +# How the hell does secret scanning work? Read this: +# https://lookingatcomputer.substack.com/p/regex-is-almost-all-you-need +# # This is the default gitleaks configuration file. # Rules and allowlists are defined within this file. # Rules instruct gitleaks on what should be considered a secret. @@ -10,2798 +13,3179 @@ title = "gitleaks config" +# minVersion indicates the minimum Gitleaks version required to use this config. +# If the running version is older, a warning will be logged and not all +# config-enabled features are guaranteed to work. +minVersion = "v8.25.0" + +# TODO: change to [[allowlists]] [allowlist] description = "global allow lists" paths = [ - '''gitleaks.toml''', - '''.*\.(jpg|gif|doc|docx|zip|xls|pdf|bin|svg|socket)$''', - '''(go.mod|go.sum)$''', - '''gradle.lockfile''', - '''node_modules''', - '''package-lock.json''', - '''pnpm-lock.yaml''', - '''Podfile.lock''', - '''common/src/mock_certificates/.*''', - '''common/dist/.*/mock_certificates/.*''', - '''common/src/constants/mockCertificates.ts''', - '''common/src/utils/passports/genMockIdDoc.ts''', + '''gitleaks\.toml''', + '''(?i)\.(?:bmp|gif|jpe?g|png|svg|tiff?)$''', + '''(?i)\.(?:eot|[ot]tf|woff2?)$''', + '''(?i)\.(?:docx?|xlsx?|pdf|bin|socket|vsidx|v2|suo|wsuo|.dll|pdb|exe|gltf)$''', + '''go\.(?:mod|sum|work(?:\.sum)?)$''', + '''(?:^|/)vendor/modules\.txt$''', + '''(?:^|/)vendor/(?:github\.com|golang\.org/x|google\.golang\.org|gopkg\.in|istio\.io|k8s\.io|sigs\.k8s\.io)(?:/.*)?$''', + '''(?:^|/)gradlew(?:\.bat)?$''', + '''(?:^|/)gradle\.lockfile$''', + '''(?:^|/)mvnw(?:\.cmd)?$''', + '''(?:^|/)\.mvn/wrapper/MavenWrapperDownloader\.java$''', + '''(?:^|/)node_modules(?:/.*)?$''', + '''(?:^|/)(?:deno\.lock|npm-shrinkwrap\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$''', + '''(?:^|/)bower_components(?:/.*)?$''', + '''(?:^|/)(?:angular|bootstrap|jquery(?:-?ui)?|plotly|swagger-?ui)[a-zA-Z0-9.-]*(?:\.min)?\.js(?:\.map)?$''', + '''(?:^|/)javascript\.json$''', + '''(?:^|/)(?:Pipfile|poetry)\.lock$''', + '''(?i)(?:^|/)(?:v?env|virtualenv)/lib(?:64)?(?:/.*)?$''', + '''(?i)(?:^|/)(?:lib(?:64)?/python[23](?:\.\d{1,2})+|python/[23](?:\.\d{1,2})+/lib(?:64)?)(?:/.*)?$''', + '''(?i)(?:^|/)[a-z0-9_.]+-[0-9.]+\.dist-info(?:/.+)?$''', + '''(?:^|/)vendor/(?:bundle|ruby)(?:/.*?)?$''', + '''\.gem$''', + '''verification-metadata\.xml''', '''Database.refactorlog''', - '''vendor''', - '''.*tamagui-components\.config\.cjs$''', - '''packages/mobile-sdk-alpha/src/animations/.*\.json$''', + '''(?:^|/)\.git$''', +] +regexes = [ + '''(?i)^true|false|null$''', + '''^(?i:a+|b+|c+|d+|e+|f+|g+|h+|i+|j+|k+|l+|m+|n+|o+|p+|q+|r+|s+|t+|u+|v+|w+|x+|y+|z+|\*+|\.+)$''', + '''^\$(?:\d+|{\d+})$''', + '''^\$(?:[A-Z_]+|[a-z_]+)$''', + '''^\${(?:[A-Z_]+|[a-z_]+)}$''', + '''^\{\{[ \t]*[\w ().|]+[ \t]*}}$''', + '''^\$\{\{[ \t]*(?:(?:env|github|secrets|vars)(?:\.[A-Za-z]\w+)+[\w "'&./=|]*)[ \t]*}}$''', + '''^%(?:[A-Z_]+|[a-z_]+)%$''', + '''^%[+\-# 0]?[bcdeEfFgGoOpqstTUvxX]$''', + '''^\{\d{0,2}}$''', + '''^@(?:[A-Z_]+|[a-z_]+)@$''', + '''^/Users/(?i)[a-z0-9]+/[\w .-/]+$''', + '''^/(?:bin|etc|home|opt|tmp|usr|var)/[\w ./-]+$''', +] +stopwords = [ + "014df517-39d1-4453-b7b3-9930c563627c", + "abcdefghijklmnopqrstuvwxyz", ] [[rules]] -description = "Adafruit API Key" +id = "1password-secret-key" +description = "Uncovered a possible 1Password secret key, potentially compromising access to secrets in vaults." +regex = '''\bA3-[A-Z0-9]{6}-(?:(?:[A-Z0-9]{11})|(?:[A-Z0-9]{6}-[A-Z0-9]{5}))-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}\b''' +entropy = 3.8 +keywords = ["a3-"] + +[[rules]] +id = "1password-service-account-token" +description = "Uncovered a possible 1Password service account token, potentially compromising access to secrets in vaults." +regex = '''ops_eyJ[a-zA-Z0-9+/]{250,}={0,3}''' +entropy = 4 +keywords = ["ops_"] + +[[rules]] id = "adafruit-api-key" -regex = '''(?i)(?:adafruit)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9_-]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "adafruit", -] +description = "Identified a potential Adafruit API Key, which could lead to unauthorized access to Adafruit services and sensitive data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:adafruit)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["adafruit"] [[rules]] -description = "Adobe Client ID (OAuth Web)" id = "adobe-client-id" -regex = '''(?i)(?:adobe)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "adobe", -] +description = "Detected a pattern that resembles an Adobe OAuth Web Client ID, posing a risk of compromised Adobe integrations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:adobe)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["adobe"] [[rules]] -description = "Adobe Client Secret" id = "adobe-client-secret" -regex = '''(?i)\b((p8e-)(?i)[a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -keywords = [ - "p8e-", -] +description = "Discovered a potential Adobe Client Secret, which, if exposed, could allow unauthorized Adobe service access and data manipulation." +regex = '''\b(p8e-(?i)[a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["p8e-"] [[rules]] -description = "Age secret key" -id = "age secret key" +id = "age-secret-key" +description = "Discovered a potential Age encryption tool secret key, risking data decryption and unauthorized access to sensitive information." regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}''' -keywords = [ - "age-secret-key-1", -] +keywords = ["age-secret-key-1"] [[rules]] -description = "Airtable API Key" id = "airtable-api-key" -regex = '''(?i)(?:airtable)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{17})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "airtable", -] +description = "Uncovered a possible Airtable API Key, potentially compromising database access and leading to data leakage or alteration." +regex = '''(?i)[\w.-]{0,50}?(?:airtable)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{17})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["airtable"] [[rules]] -description = "Algolia API Key" id = "algolia-api-key" -regex = '''(?i)(?:algolia)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -keywords = [ - "algolia", -] +description = "Identified an Algolia API Key, which could result in unauthorized search operations and data exposure on Algolia-managed platforms." +regex = '''(?i)[\w.-]{0,50}?(?:algolia)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["algolia"] [[rules]] -description = "Alibaba AccessKey ID" id = "alibaba-access-key-id" -regex = '''(?i)\b((LTAI)(?i)[a-z0-9]{20})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -keywords = [ - "ltai", -] +description = "Detected an Alibaba Cloud AccessKey ID, posing a risk of unauthorized cloud resource access and potential data compromise." +regex = '''\b(LTAI(?i)[a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["ltai"] [[rules]] -description = "Alibaba Secret Key" id = "alibaba-secret-key" -regex = '''(?i)(?:alibaba)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{30})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "alibaba", -] +description = "Discovered a potential Alibaba Cloud Secret Key, potentially allowing unauthorized operations and data access within Alibaba Cloud." +regex = '''(?i)[\w.-]{0,50}?(?:alibaba)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["alibaba"] + +[[rules]] +id = "anthropic-admin-api-key" +description = "Detected an Anthropic Admin API Key, risking unauthorized access to administrative functions and sensitive AI model configurations." +regex = '''\b(sk-ant-admin01-[a-zA-Z0-9_\-]{93}AA)(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["sk-ant-admin01"] + +[[rules]] +id = "anthropic-api-key" +description = "Identified an Anthropic API Key, which may compromise AI assistant integrations and expose sensitive data to unauthorized access." +regex = '''\b(sk-ant-api03-[a-zA-Z0-9_\-]{93}AA)(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["sk-ant-api03"] + +[[rules]] +id = "artifactory-api-key" +description = "Detected an Artifactory api key, posing a risk unauthorized access to the central repository." +regex = '''\bAKCp[A-Za-z0-9]{69}\b''' +entropy = 4.5 +keywords = ["akcp"] + +[[rules]] +id = "artifactory-reference-token" +description = "Detected an Artifactory reference token, posing a risk of impersonation and unauthorized access to the central repository." +regex = '''\bcmVmd[A-Za-z0-9]{59}\b''' +entropy = 4.5 +keywords = ["cmvmd"] [[rules]] -description = "Asana Client ID" id = "asana-client-id" -regex = '''(?i)(?:asana)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9]{16})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "asana", -] +description = "Discovered a potential Asana Client ID, risking unauthorized access to Asana projects and sensitive task information." +regex = '''(?i)[\w.-]{0,50}?(?:asana)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["asana"] [[rules]] -description = "Asana Client Secret" id = "asana-client-secret" -regex = '''(?i)(?:asana)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "asana", -] +description = "Identified an Asana Client Secret, which could lead to compromised project management integrity and unauthorized access." +regex = '''(?i)[\w.-]{0,50}?(?:asana)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["asana"] [[rules]] -description = "Atlassian API token" id = "atlassian-api-token" -regex = '''(?i)(?:atlassian|confluence|jira)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{24})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Detected an Atlassian API token, posing a threat to project management and collaboration tool security and data confidentiality." +regex = '''(?i)[\w.-]{0,50}?(?:(?-i:ATLASSIAN|[Aa]tlassian)|(?-i:CONFLUENCE|[Cc]onfluence)|(?-i:JIRA|[Jj]ira))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20}[a-f0-9]{4})(?:[\x60'"\s;]|\\[nr]|$)|\b(ATATT3[A-Za-z0-9_\-=]{186})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 keywords = [ - "atlassian","confluence","jira", + "atlassian", + "confluence", + "jira", + "atatt3", ] [[rules]] -description = "Authress Service Client Access Key" id = "authress-service-client-access-key" -regex = '''(?i)\b((?:sc|ext|scauth|authress)_[a-z0-9]{5,30}\.[a-z0-9]{4,6}\.acc_[a-z0-9-]{10,32}\.[a-z0-9+/_=-]{30,120})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Uncovered a possible Authress Service Client Access Key, which may compromise access control services and sensitive data." +regex = '''\b((?:sc|ext|scauth|authress)_(?i)[a-z0-9]{5,30}\.[a-z0-9]{4,6}\.(?-i:acc)[_-][a-z0-9-]{10,32}\.[a-z0-9+/_=-]{30,120})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 keywords = [ - "sc_","ext_","scauth_","authress_", + "sc_", + "ext_", + "scauth_", + "authress_", ] [[rules]] -description = "AWS" id = "aws-access-token" -regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' +description = "Identified a pattern that may indicate AWS credentials, risking unauthorized cloud resource access and data breaches on AWS platforms." +regex = '''\b((?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA)[A-Z2-7]{16})\b''' +entropy = 3 keywords = [ - "akia","agpa","aida","aroa","aipa","anpa","anva","asia", + "a3t", + "akia", + "asia", + "abia", + "acca", +] +[[rules.allowlists]] +regexes = [ + '''.+EXAMPLE$''', ] [[rules]] -description = "Beamer API token" +id = "aws-amazon-bedrock-api-key-long-lived" +description = "Identified a pattern that may indicate long-lived Amazon Bedrock API keys, risking unauthorized Amazon Bedrock usage" +regex = '''\b(ABSK[A-Za-z0-9+/]{109,269}={0,2})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["absk"] + +[[rules]] +id = "aws-amazon-bedrock-api-key-short-lived" +description = "Identified a pattern that may indicate short-lived Amazon Bedrock API keys, risking unauthorized Amazon Bedrock usage" +regex = '''bedrock-api-key-YmVkcm9jay5hbWF6b25hd3MuY29t''' +entropy = 3 +keywords = ["bedrock-api-key-"] + +[[rules]] +id = "azure-ad-client-secret" +description = "Azure AD Client Secret" +regex = '''(?:^|[\\'"\x60\s>=:(,)])([a-zA-Z0-9_~.]{3}\dQ~[a-zA-Z0-9_~.-]{31,34})(?:$|[\\'"\x60\s<),])''' +entropy = 3 +keywords = ["q~"] + +[[rules]] id = "beamer-api-token" -regex = '''(?i)(?:beamer)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(b_[a-z0-9=_\-]{44})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "beamer", -] +description = "Detected a Beamer API token, potentially compromising content management and exposing sensitive notifications and updates." +regex = '''(?i)[\w.-]{0,50}?(?:beamer)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(b_[a-z0-9=_\-]{44})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["beamer"] [[rules]] -description = "Bitbucket Client ID" id = "bitbucket-client-id" -regex = '''(?i)(?:bitbucket)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "bitbucket", -] +description = "Discovered a potential Bitbucket Client ID, risking unauthorized repository access and potential codebase exposure." +regex = '''(?i)[\w.-]{0,50}?(?:bitbucket)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["bitbucket"] [[rules]] -description = "Bitbucket Client Secret" id = "bitbucket-client-secret" -regex = '''(?i)(?:bitbucket)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "bitbucket", -] +description = "Discovered a potential Bitbucket Client Secret, posing a risk of compromised code repositories and unauthorized access." +regex = '''(?i)[\w.-]{0,50}?(?:bitbucket)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["bitbucket"] [[rules]] -description = "Bittrex Access Key" id = "bittrex-access-key" -regex = '''(?i)(?:bittrex)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "bittrex", -] +description = "Identified a Bittrex Access Key, which could lead to unauthorized access to cryptocurrency trading accounts and financial loss." +regex = '''(?i)[\w.-]{0,50}?(?:bittrex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["bittrex"] [[rules]] -description = "Bittrex Secret Key" id = "bittrex-secret-key" -regex = '''(?i)(?:bittrex)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "bittrex", -] +description = "Detected a Bittrex Secret Key, potentially compromising cryptocurrency transactions and financial security." +regex = '''(?i)[\w.-]{0,50}?(?:bittrex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["bittrex"] + +[[rules]] +id = "cisco-meraki-api-key" +description = "Cisco Meraki is a cloud-managed IT solution that provides networking, security, and device management through an easy-to-use interface." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Mm]eraki|MERAKI))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["meraki"] + +[[rules]] +id = "clickhouse-cloud-api-secret-key" +description = "Identified a pattern that may indicate clickhouse cloud API secret key, risking unauthorized clickhouse cloud api access and data breaches on ClickHouse Cloud platforms." +regex = '''\b(4b1d[A-Za-z0-9]{38})\b''' +entropy = 3 +keywords = ["4b1d"] [[rules]] -description = "Clojars API token" id = "clojars-api-token" -regex = '''(?i)(CLOJARS_)[a-z0-9]{60}''' +description = "Uncovered a possible Clojars API token, risking unauthorized access to Clojure libraries and potential code manipulation." +regex = '''(?i)CLOJARS_[a-z0-9]{60}''' +entropy = 2 +keywords = ["clojars_"] + +[[rules]] +id = "cloudflare-api-key" +description = "Detected a Cloudflare API Key, potentially compromising cloud application deployments and operational security." +regex = '''(?i)[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["cloudflare"] + +[[rules]] +id = "cloudflare-global-api-key" +description = "Detected a Cloudflare Global API Key, potentially compromising cloud application deployments and operational security." +regex = '''(?i)[\w.-]{0,50}?(?:cloudflare)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{37})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["cloudflare"] + +[[rules]] +id = "cloudflare-origin-ca-key" +description = "Detected a Cloudflare Origin CA Key, potentially compromising cloud application deployments and operational security." +regex = '''\b(v1\.0-[a-f0-9]{24}-[a-f0-9]{146})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 keywords = [ - "clojars", + "cloudflare", + "v1.0-", ] [[rules]] -description = "Codecov Access Token" id = "codecov-access-token" -regex = '''(?i)(?:codecov)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Found a pattern resembling a Codecov Access Token, posing a risk of unauthorized access to code coverage reports and sensitive data." +regex = '''(?i)[\w.-]{0,50}?(?:codecov)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["codecov"] + +[[rules]] +id = "cohere-api-token" +description = "Identified a Cohere Token, posing a risk of unauthorized access to AI services and data manipulation." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:cohere|CO_API_KEY)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-zA-Z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 keywords = [ - "codecov", + "cohere", + "co_api_key", ] [[rules]] -description = "Coinbase Access Token" id = "coinbase-access-token" -regex = '''(?i)(?:coinbase)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9_-]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "coinbase", -] +description = "Detected a Coinbase Access Token, posing a risk of unauthorized access to cryptocurrency accounts and financial transactions." +regex = '''(?i)[\w.-]{0,50}?(?:coinbase)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["coinbase"] [[rules]] -description = "Confluent Access Token" id = "confluent-access-token" -regex = '''(?i)(?:confluent)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{16})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "confluent", -] +description = "Identified a Confluent Access Token, which could compromise access to streaming data platforms and sensitive data flow." +regex = '''(?i)[\w.-]{0,50}?(?:confluent)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["confluent"] [[rules]] -description = "Confluent Secret Key" id = "confluent-secret-key" -regex = '''(?i)(?:confluent)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "confluent", -] +description = "Found a Confluent Secret Key, potentially risking unauthorized operations and data access within Confluent services." +regex = '''(?i)[\w.-]{0,50}?(?:confluent)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["confluent"] [[rules]] -description = "Contentful delivery API token" id = "contentful-delivery-api-token" -regex = '''(?i)(?:contentful)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{43})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "contentful", +description = "Discovered a Contentful delivery API token, posing a risk to content management systems and data integrity." +regex = '''(?i)[\w.-]{0,50}?(?:contentful)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{43})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["contentful"] + +[[rules]] +id = "curl-auth-header" +description = "Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource." +regex = '''\bcurl\b(?:.*?|.*?(?:[\r\n]{1,2}.*?){1,5})[ \t\n\r](?:-H|--header)(?:=|[ \t]{0,5})(?:"(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))"|'(?i)(?:Authorization:[ \t]{0,5}(?:Basic[ \t]([a-z0-9+/]{8,}={0,3})|(?:Bearer|(?:Api-)?Token)[ \t]([\w=~@.+/-]{8,})|([\w=~@.+/-]{8,}))|(?:(?:X-(?:[a-z]+-)?)?(?:Api-?)?(?:Key|Token)):[ \t]{0,5}([\w=~@.+/-]{8,}))')(?:\B|\s|\z)''' +entropy = 2.75 +keywords = ["curl"] + +[[rules]] +id = "curl-auth-user" +description = "Discovered a potential basic authorization token provided in a curl command, which could compromise the curl accessed resource." +regex = '''\bcurl\b(?:.*|.*(?:[\r\n]{1,2}.*){1,5})[ \t\n\r](?:-u|--user)(?:=|[ \t]{0,5})("(:[^"]{3,}|[^:"]{3,}:|[^:"]{3,}:[^"]{3,})"|'([^:']{3,}:[^']{3,})'|((?:"[^"]{3,}"|'[^']{3,}'|[\w$@.-]+):(?:"[^"]{3,}"|'[^']{3,}'|[\w${}@.-]+)))(?:\s|\z)''' +entropy = 2 +keywords = ["curl"] +[[rules.allowlists]] +regexes = [ + '''[^:]+:(?:change(?:it|me)|pass(?:word)?|pwd|test|token|\*+|x+)''', + '''['"]?<[^>]+>['"]?:['"]?<[^>]+>|<[^:]+:[^>]+>['"]?''', + '''[^:]+:\[[^]]+]''', + '''['"]?[^:]+['"]?:['"]?\$(?:\d|\w+|\{(?:\d|\w+)})['"]?''', + '''\$\([^)]+\):\$\([^)]+\)''', + '''['"]?\$?{{[^}]+}}['"]?:['"]?\$?{{[^}]+}}['"]?''', ] [[rules]] -description = "Databricks API token" id = "databricks-api-token" -regex = '''(?i)\b(dapi[a-h0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -keywords = [ - "dapi", -] +description = "Uncovered a Databricks API token, which may compromise big data analytics platforms and sensitive data processing." +regex = '''\b(dapi[a-f0-9]{32}(?:-\d)?)(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["dapi"] [[rules]] -description = "Datadog Access Token" id = "datadog-access-token" -regex = '''(?i)(?:datadog)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "datadog", -] +description = "Detected a Datadog Access Token, potentially risking monitoring and analytics data exposure and manipulation." +regex = '''(?i)[\w.-]{0,50}?(?:datadog)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["datadog"] [[rules]] -description = "Defined Networking API token" id = "defined-networking-api-token" -regex = '''(?i)(?:dnkey)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(dnkey-[a-z0-9=_\-]{26}-[a-z0-9=_\-]{52})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "dnkey", -] +description = "Identified a Defined Networking API token, which could lead to unauthorized network operations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:dnkey)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(dnkey-[a-z0-9=_\-]{26}-[a-z0-9=_\-]{52})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dnkey"] [[rules]] -description = "DigitalOcean OAuth Access Token" id = "digitalocean-access-token" -regex = '''(?i)\b(doo_v1_[a-f0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "doo_v1_", -] +description = "Found a DigitalOcean OAuth Access Token, risking unauthorized cloud resource access and data compromise." +regex = '''\b(doo_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["doo_v1_"] [[rules]] -description = "DigitalOcean Personal Access Token" id = "digitalocean-pat" -regex = '''(?i)\b(dop_v1_[a-f0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "dop_v1_", -] +description = "Discovered a DigitalOcean Personal Access Token, posing a threat to cloud infrastructure security and data privacy." +regex = '''\b(dop_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["dop_v1_"] [[rules]] -description = "DigitalOcean OAuth Refresh Token" id = "digitalocean-refresh-token" -regex = '''(?i)\b(dor_v1_[a-f0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "dor_v1_", -] +description = "Uncovered a DigitalOcean OAuth Refresh Token, which could allow prolonged unauthorized access and resource manipulation." +regex = '''(?i)\b(dor_v1_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dor_v1_"] [[rules]] -description = "Discord API key" id = "discord-api-token" -regex = '''(?i)(?:discord)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "discord", -] +description = "Detected a Discord API key, potentially compromising communication channels and user data privacy on Discord." +regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["discord"] [[rules]] -description = "Discord client ID" id = "discord-client-id" -regex = '''(?i)(?:discord)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9]{18})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "discord", -] +description = "Identified a Discord client ID, which may lead to unauthorized integrations and data exposure in Discord applications." +regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{18})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["discord"] [[rules]] -description = "Discord client secret" id = "discord-client-secret" -regex = '''(?i)(?:discord)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "discord", -] +description = "Discovered a potential Discord client secret, risking compromised Discord bot integrations and data leaks." +regex = '''(?i)[\w.-]{0,50}?(?:discord)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["discord"] [[rules]] -description = "Doppler API token" id = "doppler-api-token" -regex = '''(dp\.pt\.)(?i)[a-z0-9]{43}''' -keywords = [ - "doppler", -] +description = "Discovered a Doppler API token, posing a risk to environment and secrets management security." +regex = '''dp\.pt\.(?i)[a-z0-9]{43}''' +entropy = 2 +keywords = ["dp.pt."] [[rules]] -description = "Droneci Access Token" id = "droneci-access-token" -regex = '''(?i)(?:droneci)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "droneci", -] +description = "Detected a Droneci Access Token, potentially compromising continuous integration and deployment workflows." +regex = '''(?i)[\w.-]{0,50}?(?:droneci)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["droneci"] [[rules]] -description = "Dropbox API secret" id = "dropbox-api-token" -regex = '''(?i)(?:dropbox)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{15})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "dropbox", -] +description = "Identified a Dropbox API secret, which could lead to unauthorized file access and data breaches in Dropbox storage." +regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{15})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dropbox"] [[rules]] -description = "Dropbox long lived API token" id = "dropbox-long-lived-api-token" -regex = '''(?i)(?:dropbox)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -keywords = [ - "dropbox", -] +description = "Found a Dropbox long-lived API token, risking prolonged unauthorized access to cloud storage and sensitive data." +regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dropbox"] [[rules]] -description = "Dropbox short lived API token" id = "dropbox-short-lived-api-token" -regex = '''(?i)(?:dropbox)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(sl\.[a-z0-9\-=_]{135})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -keywords = [ - "dropbox", -] +description = "Discovered a Dropbox short-lived API token, posing a risk of temporary but potentially harmful data access and manipulation." +regex = '''(?i)[\w.-]{0,50}?(?:dropbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(sl\.[a-z0-9\-=_]{135})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["dropbox"] [[rules]] -description = "Duffel API token" id = "duffel-api-token" -regex = '''duffel_(test|live)_(?i)[a-z0-9_\-=]{43}''' -keywords = [ - "duffel", -] +description = "Uncovered a Duffel API token, which may compromise travel platform integrations and sensitive customer data." +regex = '''duffel_(?:test|live)_(?i)[a-z0-9_\-=]{43}''' +entropy = 2 +keywords = ["duffel_"] [[rules]] -description = "Dynatrace API token" id = "dynatrace-api-token" +description = "Detected a Dynatrace API token, potentially risking application performance monitoring and data exposure." regex = '''dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}''' -keywords = [ - "dynatrace", -] +entropy = 4 +keywords = ["dt0c01."] [[rules]] -description = "EasyPost API token" id = "easypost-api-token" -regex = '''\bEZAK(?i)[a-z0-9]{54}''' -keywords = [ - "ezak", -] +description = "Identified an EasyPost API token, which could lead to unauthorized postal and shipment service access and data exposure." +regex = '''\bEZAK(?i)[a-z0-9]{54}\b''' +entropy = 2 +keywords = ["ezak"] [[rules]] -description = "EasyPost test API token" id = "easypost-test-api-token" -regex = '''\bEZTK(?i)[a-z0-9]{54}''' -keywords = [ - "eztk", -] +description = "Detected an EasyPost test API token, risking exposure of test environments and potentially sensitive shipment data." +regex = '''\bEZTK(?i)[a-z0-9]{54}\b''' +entropy = 2 +keywords = ["eztk"] [[rules]] -description = "Etsy Access Token" id = "etsy-access-token" -regex = '''(?i)(?:etsy)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{24})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "etsy", -] +description = "Found an Etsy Access Token, potentially compromising Etsy shop management and customer data." +regex = '''(?i)[\w.-]{0,50}?(?:(?-i:ETSY|[Ee]tsy))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["etsy"] [[rules]] -description = "Facebook Access Token" -id = "facebook" -regex = '''(?i)(?:facebook)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +id = "facebook-access-token" +description = "Discovered a Facebook Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure." +regex = '''(?i)\b(\d{15,16}(\||%)[0-9a-z\-_]{27,40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["facebook"] + +[[rules]] +id = "facebook-page-access-token" +description = "Discovered a Facebook Page Access Token, posing a risk of unauthorized access to Facebook accounts and personal data exposure." +regex = '''\b(EAA[MC](?i)[a-z0-9]{100,})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 keywords = [ - "facebook", + "eaam", + "eaac", ] [[rules]] -description = "Fastly API key" +id = "facebook-secret" +description = "Discovered a Facebook Application secret, posing a risk of unauthorized access to Facebook accounts and personal data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:facebook)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["facebook"] + +[[rules]] id = "fastly-api-token" -regex = '''(?i)(?:fastly)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "fastly", -] +description = "Uncovered a Fastly API key, which may compromise CDN and edge cloud services, leading to content delivery and security issues." +regex = '''(?i)[\w.-]{0,50}?(?:fastly)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["fastly"] [[rules]] -description = "Finicity API token" id = "finicity-api-token" -regex = '''(?i)(?:finicity)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "finicity", -] +description = "Detected a Finicity API token, potentially risking financial data access and unauthorized financial operations." +regex = '''(?i)[\w.-]{0,50}?(?:finicity)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["finicity"] [[rules]] -description = "Finicity Client Secret" id = "finicity-client-secret" -regex = '''(?i)(?:finicity)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{20})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "finicity", -] +description = "Identified a Finicity Client Secret, which could lead to compromised financial service integrations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:finicity)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["finicity"] [[rules]] -description = "Finnhub Access Token" id = "finnhub-access-token" -regex = '''(?i)(?:finnhub)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{20})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "finnhub", -] +description = "Found a Finnhub Access Token, risking unauthorized access to financial market data and analytics." +regex = '''(?i)[\w.-]{0,50}?(?:finnhub)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["finnhub"] [[rules]] -description = "Flickr Access Token" id = "flickr-access-token" -regex = '''(?i)(?:flickr)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "flickr", -] +description = "Discovered a Flickr Access Token, posing a risk of unauthorized photo management and potential data leakage." +regex = '''(?i)[\w.-]{0,50}?(?:flickr)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["flickr"] [[rules]] -description = "Flutterwave Encryption Key" id = "flutterwave-encryption-key" +description = "Uncovered a Flutterwave Encryption Key, which may compromise payment processing and sensitive financial information." regex = '''FLWSECK_TEST-(?i)[a-h0-9]{12}''' -keywords = [ - "flwseck_test", -] +entropy = 2 +keywords = ["flwseck_test"] [[rules]] -description = "Finicity Public Key" id = "flutterwave-public-key" +description = "Detected a Finicity Public Key, potentially exposing public cryptographic operations and integrations." regex = '''FLWPUBK_TEST-(?i)[a-h0-9]{32}-X''' -keywords = [ - "flwpubk_test", -] +entropy = 2 +keywords = ["flwpubk_test"] [[rules]] -description = "Flutterwave Secret Key" id = "flutterwave-secret-key" +description = "Identified a Flutterwave Secret Key, risking unauthorized financial transactions and data breaches." regex = '''FLWSECK_TEST-(?i)[a-h0-9]{32}-X''' +entropy = 2 +keywords = ["flwseck_test"] + +[[rules]] +id = "flyio-access-token" +description = "Uncovered a Fly.io API key" +regex = '''\b((?:fo1_[\w-]{43}|fm1[ar]_[a-zA-Z0-9+\/]{100,}={0,3}|fm2_[a-zA-Z0-9+\/]{100,}={0,3}))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 keywords = [ - "flwseck_test", + "fo1_", + "fm1", + "fm2_", ] [[rules]] -description = "Frame.io API token" id = "frameio-api-token" +description = "Found a Frame.io API token, potentially compromising video collaboration and project management." regex = '''fio-u-(?i)[a-z0-9\-_=]{64}''' -keywords = [ - "fio-u-", -] +keywords = ["fio-u-"] + +[[rules]] +id = "freemius-secret-key" +description = "Detected a Freemius secret key, potentially exposing sensitive information." +regex = '''(?i)["']secret_key["']\s*=>\s*["'](sk_[\S]{29})["']''' +path = '''(?i)\.php$''' +keywords = ["secret_key"] [[rules]] -description = "Freshbooks Access Token" id = "freshbooks-access-token" -regex = '''(?i)(?:freshbooks)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "freshbooks", -] +description = "Discovered a Freshbooks Access Token, posing a risk to accounting software access and sensitive financial data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:freshbooks)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["freshbooks"] [[rules]] -description = "GCP API key" id = "gcp-api-key" -regex = '''(?i)\b(AIza[0-9A-Za-z\\-_]{35})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "aiza", +description = "Uncovered a GCP API key, which could lead to unauthorized access to Google Cloud services and data breaches." +regex = '''\b(AIza[\w-]{35})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["aiza"] +[[rules.allowlists]] +regexes = [ + '''AIzaSyabcdefghijklmnopqrstuvwxyz1234567''', + '''AIzaSyAnLA7NfeLquW1tJFpx_eQCxoX-oo6YyIs''', + '''AIzaSyCkEhVjf3pduRDt6d1yKOMitrUEke8agEM''', + '''AIzaSyDMAScliyLx7F0NPDEJi1QmyCgHIAODrlU''', + '''AIzaSyD3asb-2pEZVqMkmL6M9N6nHZRR_znhrh0''', + '''AIzayDNSXIbFmlXbIE6mCzDLQAqITYefhixbX4A''', + '''AIzaSyAdOS2zB6NCsk1pCdZ4-P6GBdi_UUPwX7c''', + '''AIzaSyASWm6HmTMdYWpgMnjRBjxcQ9CKctWmLd4''', + '''AIzaSyANUvH9H9BsUccjsu2pCmEkOPjjaXeDQgY''', + '''AIzaSyA5_iVawFQ8ABuTZNUdcwERLJv_a_p4wtM''', + '''AIzaSyA4UrcGxgwQFTfaI3no3t7Lt1sjmdnP5sQ''', + '''AIzaSyDSb51JiIcB6OJpwwMicseKRhhrOq1cS7g''', + '''AIzaSyBF2RrAIm4a0mO64EShQfqfd2AFnzAvvuU''', + '''AIzaSyBcE-OOIbhjyR83gm4r2MFCu4MJmprNXsw''', + '''AIzaSyB8qGxt4ec15vitgn44duC5ucxaOi4FmqE''', + '''AIzaSyA8vmApnrHNFE0bApF4hoZ11srVL_n0nvY''', ] [[rules]] -description = "Generic API Key" id = "generic-api-key" -regex = '''(?i)(?:key|api|token|secret|client|passwd|password|auth|access)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9a-z\-_.=]{10,150})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Detected a Generic API Key, potentially exposing access to various services and sensitive operations." +regex = '''(?i)[\w.-]{0,50}?(?:access|auth|(?-i:[Aa]pi|API)|credential|creds|key|passw(?:or)?d|secret|token)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([\w.=-]{10,150}|[a-z0-9][a-z0-9+/]{11,}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' entropy = 3.5 keywords = [ - "key","api","token","secret","client","passwd","password","auth","access", -] -[rules.allowlist] -stopwords= [ - "client", - "endpoint", - "vpn", - "_ec2_", - "aws_", - "authorize", - "author", - "define", - "config", + "access", + "api", + "auth", + "key", "credential", - "setting", - "sample", - "xxxxxx", + "creds", + "passwd", + "password", + "secret", + "token", +] +[[rules.allowlists]] +regexes = [ + '''^[a-zA-Z_.-]+$''', +] +[[rules.allowlists]] +description = "Allowlist for Generic API Keys" +regexTarget = "match" +regexes = [ + '''(?i)(?:access(?:ibility|or)|access[_.-]?id|random[_.-]?access|api[_.-]?(?:id|name|version)|rapid|capital|[a-z0-9-]*?api[a-z0-9-]*?:jar:|author|X-MS-Exchange-Organization-Auth|Authentication-Results|(?:credentials?[_.-]?id|withCredentials)|(?:bucket|foreign|hot|idx|natural|primary|pub(?:lic)?|schema|sequence)[_.-]?key|(?:turkey)|key[_.-]?(?:alias|board|code|frame|id|length|mesh|name|pair|press(?:ed)?|ring|selector|signature|size|stone|storetype|word|up|down|left|right)|key[_.-]?vault[_.-]?(?:id|name)|keyVaultToStoreSecrets|key(?:store|tab)[_.-]?(?:file|path)|issuerkeyhash|(?-i:[DdMm]onkey|[DM]ONKEY)|keying|(?:secret)[_.-]?(?:length|name|size)|UserSecretsId|(?:csrf)[_.-]?token|(?:io\.jsonwebtoken[ \t]?:[ \t]?[\w-]+)|(?:api|credentials|token)[_.-]?(?:endpoint|ur[il])|public[_.-]?token|(?:key|token)[_.-]?file|(?-i:(?:[A-Z_]+=\n[A-Z_]+=|[a-z_]+=\n[a-z_]+=)(?:\n|\z))|(?-i:(?:[A-Z.]+=\n[A-Z.]+=|[a-z.]+=\n[a-z.]+=)(?:\n|\z)))''', +] +stopwords = [ "000000", - "buffer", - "delete", + "6fe4476ee5a1832882e326b506d14126", + "_ec2_", "aaaaaa", - "fewfwef", - "getenv", - "env_", - "system", - "example", - "ecdsa", - "sha256", - "sha1", - "sha2", - "md5", - "alert", - "wizard", - "target", - "onboard", - "welcome", - "page", - "exploit", - "experiment", - "expire", - "rabbitmq", - "scraper", - "widget", - "music", - "dns_", - "dns-", - "yahoo", - "want", - "json", + "about", + "abstract", + "academy", + "acces", + "account", + "act-", + "act.", + "act_", "action", - "script", - "fix_", - "fix-", - "develop", - "compas", - "stripe", - "service", - "master", - "metric", - "tech", - "gitignore", - "rich", - "open", - "stack", - "irc_", - "irc-", - "sublime", - "kohana", - "has_", - "has-", - "fabric", - "wordpres", - "role", - "osx_", - "osx-", - "boost", + "active", + "actively", + "activity", + "adapter", + "add-", + "add-on", + "add.", + "add_", + "addon", "addres", - "queue", - "working", - "sandbox", - "internet", - "print", - "vision", - "tracking", - "being", - "generator", - "traffic", - "world", - "pull", - "rust", - "watcher", - "small", + "admin", + "adobe", + "advanced", + "adventure", + "agent", + "agile", + "air-", + "air.", + "air_", + "ajax", + "akka", + "alert", + "alfred", + "algorithm", + "all-", + "all.", + "all_", + "alloy", + "alpha", + "amazon", + "amqp", + "analysi", + "analytic", + "analyzer", + "android", + "angular", + "angularj", + "animate", + "animation", + "another", + "ansible", + "answer", + "ant-", + "ant.", + "ant_", + "any-", + "any.", + "any_", + "apache", + "app-", + "app.", + "app_", + "apple", + "arch", + "archive", + "archived", + "arduino", + "array", + "art-", + "art.", + "art_", + "article", + "asp-", + "asp.", + "asp_", + "asset", + "async", + "atom", + "attention", + "audio", + "audit", + "aura", "auth", - "full", - "hash", - "more", - "install", + "author", + "authorize", "auto", - "complete", - "learn", - "paper", - "installer", - "research", - "acces", - "last", + "automated", + "automatic", + "awesome", + "aws_", + "azure", + "back", + "backbone", + "backend", + "backup", + "bar-", + "bar.", + "bar_", + "base", + "based", + "bash", + "basic", + "batch", + "been", + "beer", + "behavior", + "being", + "benchmark", + "best", + "beta", + "better", + "big-", + "big.", + "big_", + "binary", "binding", - "spine", - "into", + "bit-", + "bit.", + "bit_", + "bitcoin", + "block", + "blog", + "board", + "book", + "bookmark", + "boost", + "boot", + "bootstrap", + "bosh", + "bot-", + "bot.", + "bot_", + "bower", + "box-", + "box.", + "box_", + "boxen", + "bracket", + "branch", + "bridge", + "browser", + "brunch", + "buffer", + "bug-", + "bug.", + "bug_", + "build", + "builder", + "building", + "buildout", + "buildpack", + "built", + "bundle", + "busines", + "but-", + "but.", + "but_", + "button", + "cache", + "caching", + "cakephp", + "calendar", + "call", + "camera", + "campfire", + "can-", + "can.", + "can_", + "canva", + "captcha", + "capture", + "card", + "carousel", + "case", + "cassandra", + "cat-", + "cat.", + "cat_", + "category", + "center", + "cento", + "challenge", + "change", + "changelog", + "channel", + "chart", "chat", - "algorithm", - "resource", - "uploader", - "video", - "maker", - "next", - "proc", - "lock", - "robot", - "snake", - "patch", - "matrix", - "drill", - "terminal", - "term", - "stuff", - "genetic", - "generic", - "identity", - "audit", - "pattern", - "audio", - "web_", - "web-", - "crud", - "problem", - "statu", + "cheat", + "check", + "checker", + "chef", + "ches", + "chinese", + "chosen", + "chrome", + "ckeditor", + "clas", + "classe", + "classic", + "clean", + "cli-", + "cli.", + "cli_", + "client", + "clojure", + "clone", + "closure", + "cloud", + "club", + "cluster", "cms-", "cms_", - "arch", + "coco", + "code", + "coding", "coffee", - "workflow", - "changelog", - "another", - "uiview", - "content", - "kitchen", - "gnu_", - "gnu-", - "gnu.", - "conf", - "couchdb", - "client", - "opencv", - "rendering", - "update", - "concept", - "varnish", - "gui_", - "gui-", - "gui.", - "version", - "shared", - "extra", - "product", - "still", - "not_", - "not-", - "not.", - "drop", - "ring", - "png_", - "png-", - "png.", - "actively", - "import", - "output", - "backup", - "start", - "embedded", - "registry", - "pool", - "semantic", - "instagram", - "bash", - "system", - "ninja", - "drupal", - "jquery", - "polyfill", - "physic", - "league", - "guide", - "pack", - "synopsi", - "sketch", - "injection", - "svg_", - "svg-", - "svg.", - "friendly", - "wave", - "convert", - "manage", - "camera", - "link", - "slide", - "timer", - "wrapper", - "gallery", - "url_", - "url-", - "url.", - "todomvc", - "requirej", - "party", - "http", - "payment", - "async", - "library", - "home", - "coco", - "gaia", - "display", - "universal", - "func", - "metadata", - "hipchat", - "under", - "room", + "color", + "combination", + "combo", + "command", + "commander", + "comment", + "commit", + "common", + "community", + "compas", + "compiler", + "complete", + "component", + "composer", + "computer", + "computing", + "con-", + "con.", + "con_", + "concept", + "conf", "config", - "personal", - "realtime", - "resume", + "connect", + "connector", + "console", + "contact", + "container", + "contao", + "content", + "contest", + "context", + "control", + "convert", + "converter", + "conway'", + "cookbook", + "cookie", + "cool", + "copy", + "cordova", + "core", + "couchbase", + "couchdb", + "countdown", + "counter", + "course", + "craft", + "crawler", + "create", + "creating", + "creator", + "credential", + "crm-", + "crm.", + "crm_", + "cros", + "crud", + "csv-", + "csv.", + "csv_", + "cube", + "cucumber", + "cuda", + "current", + "currently", + "custom", + "daemon", + "dark", + "dart", + "dash", + "dashboard", + "data", "database", - "testing", - "tiny", - "basic", - "forum", - "meetup", - "yet_", - "yet-", - "yet.", - "cento", + "date", + "day-", + "day.", + "day_", "dead", - "fluentd", - "editor", - "utilitie", - "run_", - "run-", - "run.", - "box_", - "box-", - "box.", - "bot_", - "bot-", - "bot.", - "making", - "sample", - "group", - "monitor", - "ajax", - "parallel", - "cassandra", - "ultimate", - "site", - "get_", - "get-", - "get.", - "gen_", - "gen-", - "gen.", - "gem_", - "gem-", - "gem.", - "extended", - "image", - "knife", - "asset", - "nested", - "zero", - "plugin", - "bracket", - "mule", - "mozilla", - "number", - "act_", - "act-", - "act.", - "map_", - "map-", - "map.", - "micro", + "debian", "debug", - "openshift", - "chart", - "expres", - "backend", - "task", - "source", - "translate", - "jbos", - "composer", - "sqlite", - "profile", - "mustache", - "mqtt", - "yeoman", - "have", - "builder", - "smart", - "like", - "oauth", - "school", - "guideline", - "captcha", - "filter", - "bitcoin", - "bridge", - "color", - "toolbox", + "debugger", + "deck", + "define", + "del-", + "del.", + "del_", + "delete", + "demo", + "deploy", + "design", + "designer", + "desktop", + "detection", + "detector", + "dev-", + "dev.", + "dev_", + "develop", + "developer", + "device", + "devise", + "diff", + "digital", + "directive", + "directory", "discovery", - "new_", - "new-", - "new.", - "dashboard", - "when", - "setting", - "level", - "post", - "standard", - "port", - "platform", - "yui_", - "yui-", - "yui.", - "grunt", - "animation", - "haskell", - "icon", - "latex", - "cheat", - "lua_", - "lua-", - "lua.", - "gulp", - "case", - "author", - "without", - "simulator", - "wifi", - "directory", - "lisp", - "list", - "flat", - "adventure", - "story", - "storm", - "gpu_", - "gpu-", - "gpu.", - "store", - "caching", - "attention", - "solr", - "logger", - "demo", - "shortener", - "hadoop", - "finder", - "phone", - "pipeline", - "range", - "textmate", - "showcase", - "app_", - "app-", - "app.", - "idiomatic", - "edit", - "our_", - "our-", - "our.", - "out_", - "out-", - "out.", - "sentiment", - "linked", - "why_", - "why-", - "why.", - "local", - "cube", - "gmail", - "job_", - "job-", - "job.", - "rpc_", - "rpc-", - "rpc.", - "contest", - "tcp_", - "tcp-", - "tcp.", - "usage", - "buildout", - "weather", - "transfer", - "automated", - "sphinx", - "issue", - "sas_", - "sas-", - "sas.", - "parallax", - "jasmine", - "addon", - "machine", - "solution", - "dsl_", + "display", + "django", + "dns-", + "dns_", + "doc-", + "doc.", + "doc_", + "docker", + "docpad", + "doctrine", + "document", + "doe-", + "doe.", + "doe_", + "dojo", + "dom-", + "dom.", + "dom_", + "domain", + "don't", + "done", + "dot-", + "dot.", + "dot_", + "dotfile", + "download", + "draft", + "drag", + "drill", + "drive", + "driven", + "driver", + "drop", + "dropbox", + "drupal", "dsl-", "dsl.", + "dsl_", + "dynamic", + "easy", + "ecdsa", + "eclipse", + "edit", + "editing", + "edition", + "editor", + "element", + "emac", + "email", + "embed", + "embedded", + "ember", + "emitter", + "emulator", + "encoding", + "endpoint", + "engine", + "english", + "enhanced", + "entity", + "entry", + "env_", "episode", - "menu", - "theme", - "best", - "adapter", - "debugger", - "chrome", - "tutorial", - "life", - "step", - "people", - "joomla", - "paypal", - "developer", - "solver", - "team", - "current", - "love", - "visual", - "date", - "data", - "canva", - "container", - "future", - "xml_", - "xml-", - "xml.", - "twig", - "nagio", - "spatial", - "original", - "sync", - "archived", - "refinery", - "science", - "mapping", - "gitlab", - "play", - "ext_", + "erlang", + "error", + "espresso", + "event", + "evented", + "example", + "exchange", + "exercise", + "experiment", + "expire", + "exploit", + "explorer", + "export", + "exporter", + "expres", "ext-", "ext.", - "session", - "impact", - "set_", - "set-", - "set.", - "see_", - "see-", - "see.", - "migration", - "commit", - "community", - "shopify", - "what'", - "cucumber", - "statamic", - "mysql", - "location", - "tower", - "line", - "code", - "amqp", - "hello", - "send", - "index", - "high", - "notebook", - "alloy", - "python", + "ext_", + "extended", + "extension", + "external", + "extra", + "extractor", + "fabric", + "facebook", + "factory", + "fake", + "fast", + "feature", + "feed", + "fewfwef", + "ffmpeg", "field", - "document", - "soap", - "edition", - "email", - "php_", - "php-", - "php.", - "command", - "transport", - "official", - "upload", - "study", - "secure", - "angularj", - "akka", - "scalable", - "package", - "request", - "con_", - "con-", - "con.", - "flexible", - "security", - "comment", - "module", - "flask", - "graph", - "flash", - "apache", - "change", - "window", - "space", - "lambda", - "sheet", - "bookmark", - "carousel", - "friend", - "objective", - "jekyll", - "bootstrap", + "file", + "filter", + "find", + "finder", + "firefox", + "firmware", "first", - "article", - "gwt_", - "gwt-", - "gwt.", - "classic", - "media", - "websocket", - "touch", - "desktop", - "real", - "read", - "recorder", - "moved", - "storage", - "validator", - "add-on", - "pusher", - "scs_", - "scs-", - "scs.", - "inline", - "asp_", - "asp-", - "asp.", - "timeline", - "base", - "encoding", - "ffmpeg", - "kindle", - "tinymce", - "pretty", - "jpa_", - "jpa-", - "jpa.", - "used", - "user", - "required", - "webhook", - "download", - "resque", - "espresso", - "cloud", - "mongo", - "benchmark", - "pure", - "cakephp", - "modx", - "mode", - "reactive", - "fuel", - "written", + "fish", + "fix-", + "fix_", + "flash", + "flask", + "flat", + "flex", + "flexible", "flickr", - "mail", - "brunch", - "meteor", - "dynamic", - "neo_", - "neo-", - "neo.", - "new_", - "new-", - "new.", - "net_", - "net-", - "net.", - "typo", - "type", - "keyboard", - "erlang", - "adobe", - "logging", - "ckeditor", - "message", - "iso_", - "iso-", - "iso.", - "hook", - "ldap", + "flow", + "fluent", + "fluentd", + "fluid", "folder", - "reference", - "railscast", - "www_", - "www-", - "www.", - "tracker", - "azure", + "font", + "force", + "foreman", "fork", "form", - "digital", - "exporter", - "skin", - "string", - "template", - "designer", - "gollum", - "fluent", - "entity", - "language", - "alfred", - "summary", - "wiki", - "kernel", - "calendar", - "plupload", - "symfony", + "format", + "formatter", + "forum", "foundry", - "remote", - "talk", - "search", - "dev_", - "dev-", - "dev.", - "del_", - "del-", - "del.", - "token", - "idea", - "sencha", - "selector", - "interface", - "create", - "fun_", + "framework", + "free", + "friend", + "friendly", + "front-end", + "frontend", + "ftp-", + "ftp.", + "ftp_", + "fuel", + "full", "fun-", "fun.", - "groovy", - "query", + "fun_", + "func", + "future", + "gaia", + "gallery", + "game", + "gateway", + "gem-", + "gem.", + "gem_", + "gen-", + "gen.", + "gen_", + "general", + "generator", + "generic", + "genetic", + "get-", + "get.", + "get_", + "getenv", + "getting", + "ghost", + "gist", + "git-", + "git.", + "git_", + "github", + "gitignore", + "gitlab", + "glas", + "gmail", + "gnome", + "gnu-", + "gnu.", + "gnu_", + "goal", + "golang", + "gollum", + "good", + "google", + "gpu-", + "gpu.", + "gpu_", + "gradle", "grail", - "red_", - "red-", - "red.", - "laravel", - "monkey", - "slack", - "supported", - "instant", - "value", - "center", - "latest", - "work", - "but_", - "but-", - "but.", - "bug_", - "bug-", - "bug.", - "virtual", - "tweet", - "statsd", - "studio", - "path", - "real-time", - "frontend", - "notifier", - "coding", - "tool", - "firmware", - "flow", - "random", - "mediawiki", - "bosh", - "been", - "beer", - "lightbox", - "theory", - "origin", - "redmine", - "hub_", + "graph", + "graphic", + "great", + "grid", + "groovy", + "group", + "grunt", + "guard", + "gui-", + "gui.", + "gui_", + "guide", + "guideline", + "gulp", + "gwt-", + "gwt.", + "gwt_", + "hack", + "hackathon", + "hacker", + "hacking", + "hadoop", + "haml", + "handler", + "hardware", + "has-", + "has_", + "hash", + "haskell", + "have", + "haxe", + "hello", + "help", + "helper", + "here", + "hero", + "heroku", + "high", + "hipchat", + "history", + "home", + "homebrew", + "homepage", + "hook", + "host", + "hosting", + "hot-", + "hot.", + "hot_", + "house", + "how-", + "how.", + "how_", + "html", + "http", "hub-", "hub.", - "require", - "pro_", - "pro-", - "pro.", - "ant_", - "ant-", - "ant.", - "any_", - "any-", - "any.", - "recipe", - "closure", - "mapper", - "event", - "todo", - "model", - "redi", - "provider", - "rvm_", - "rvm-", - "rvm.", - "program", - "memcached", - "rail", - "silex", - "foreman", - "activity", - "license", - "strategy", - "batch", - "streaming", - "fast", - "use_", - "use-", - "use.", - "usb_", - "usb-", - "usb.", + "hub_", + "hubot", + "human", + "icon", + "ide-", + "ide.", + "ide_", + "idea", + "identity", + "idiomatic", + "image", + "impact", + "import", + "important", + "importer", "impres", - "academy", - "slider", - "please", - "layer", - "cros", - "now_", - "now-", - "now.", - "miner", - "extension", - "own_", - "own-", - "own.", - "app_", - "app-", - "app.", - "debian", - "symphony", - "example", - "feature", - "serie", - "tree", - "project", - "runner", - "entry", - "leetcode", - "layout", - "webrtc", - "logic", - "login", - "worker", - "toolkit", - "mocha", - "support", - "back", - "inside", - "device", - "jenkin", - "contact", - "fake", - "awesome", - "ocaml", - "bit_", - "bit-", - "bit.", - "drive", - "screen", - "prototype", - "gist", - "binary", - "nosql", - "rest", - "overview", - "dart", - "dark", - "emac", - "mongoid", - "solarized", - "homepage", - "emulator", - "commander", - "django", - "yandex", - "gradle", - "xcode", - "writer", - "crm_", - "crm-", - "crm.", - "jade", - "startup", - "error", - "using", - "format", - "name", - "spring", - "parser", - "scratch", - "magic", - "try_", - "try-", - "try.", - "rack", - "directive", - "challenge", - "slim", - "counter", - "element", - "chosen", - "doc_", - "doc-", - "doc.", - "meta", - "should", - "button", - "packet", - "stream", - "hardware", - "android", + "index", "infinite", - "password", - "software", - "ghost", - "xamarin", - "spec", - "chef", - "interview", - "hubot", - "mvc_", - "mvc-", - "mvc.", - "exercise", - "leaflet", - "launcher", - "air_", - "air-", - "air.", - "photo", - "board", - "boxen", - "way_", - "way-", - "way.", - "computing", - "welcome", - "notepad", - "portfolio", - "cat_", - "cat-", - "cat.", - "can_", - "can-", - "can.", - "magento", - "yaml", - "domain", - "card", - "yii_", - "yii-", - "yii.", - "checker", - "browser", - "upgrade", - "only", - "progres", - "aura", - "ruby_", - "ruby-", - "ruby.", - "polymer", - "util", - "lite", - "hackathon", - "rule", - "log_", - "log-", - "log.", - "opengl", - "stanford", - "skeleton", - "history", + "info", + "injection", + "inline", + "input", + "inside", "inspector", - "help", - "soon", - "selenium", - "lab_", - "lab-", - "lab.", - "scheme", - "schema", - "look", - "ready", - "leveldb", - "docker", - "game", - "minimal", - "logstash", - "messaging", - "within", - "heroku", - "mongodb", + "instagram", + "install", + "installer", + "instant", + "intellij", + "interface", + "internet", + "interview", + "into", + "intro", + "ionic", + "iphone", + "ipython", + "irc-", + "irc_", + "iso-", + "iso.", + "iso_", + "issue", + "jade", + "jasmine", + "java", + "jbos", + "jekyll", + "jenkin", + "jetbrains", + "job-", + "job.", + "job_", + "joomla", + "jpa-", + "jpa.", + "jpa_", + "jquery", + "json", + "just", + "kafka", + "karma", "kata", - "suite", - "picker", - "win_", - "win-", - "win.", - "wip_", - "wip-", - "wip.", - "panel", - "started", - "starter", - "front-end", - "detector", - "deploy", - "editing", - "based", - "admin", - "capture", - "spree", - "page", - "bundle", - "goal", - "rpg_", - "rpg-", - "rpg.", - "setup", - "side", - "mean", - "reader", - "cookbook", - "mini", - "modern", - "seed", - "dom_", - "dom-", - "dom.", - "doc_", - "doc-", - "doc.", - "dot_", - "dot-", - "dot.", - "syntax", - "sugar", - "loader", - "website", - "make", - "kit_", + "kernel", + "keyboard", + "kindle", "kit-", "kit.", - "protocol", - "human", - "daemon", - "golang", - "manager", - "countdown", - "connector", - "swagger", - "map_", - "map-", - "map.", - "mac_", + "kit_", + "kitchen", + "knife", + "koan", + "kohana", + "lab-", + "lab.", + "lab_", + "lambda", + "lamp", + "language", + "laravel", + "last", + "latest", + "latex", + "launcher", + "layer", + "layout", + "lazy", + "ldap", + "leaflet", + "league", + "learn", + "learning", + "led-", + "led.", + "led_", + "leetcode", + "les-", + "les.", + "les_", + "level", + "leveldb", + "lib-", + "lib.", + "lib_", + "librarie", + "library", + "license", + "life", + "liferay", + "light", + "lightbox", + "like", + "line", + "link", + "linked", + "linkedin", + "linux", + "lisp", + "list", + "lite", + "little", + "load", + "loader", + "local", + "location", + "lock", + "log-", + "log.", + "log_", + "logger", + "logging", + "logic", + "login", + "logstash", + "longer", + "look", + "love", + "lua-", + "lua.", + "lua_", "mac-", "mac.", - "man_", + "mac_", + "machine", + "made", + "magento", + "magic", + "mail", + "make", + "maker", + "making", "man-", "man.", - "orm_", - "orm-", - "orm.", - "org_", - "org-", - "org.", - "little", - "zsh_", - "zsh-", - "zsh.", - "shop", - "show", - "workshop", + "man_", + "manage", + "manager", + "manifest", + "manual", + "map-", + "map.", + "map_", + "mapper", + "mapping", + "markdown", + "markup", + "master", + "math", + "matrix", + "maven", + "md5", + "mean", + "media", + "mediawiki", + "meetup", + "memcached", + "memory", + "menu", + "merchant", + "message", + "messaging", + "meta", + "metadata", + "meteor", + "method", + "metric", + "micro", + "middleman", + "migration", + "minecraft", + "miner", + "mini", + "minimal", + "mirror", + "mit-", + "mit.", + "mit_", + "mobile", + "mocha", + "mock", + "mod-", + "mod.", + "mod_", + "mode", + "model", + "modern", + "modular", + "module", + "modx", "money", - "grid", - "server", + "mongo", + "mongodb", + "mongoid", + "mongoose", + "monitor", + "monkey", + "more", + "motion", + "moved", + "movie", + "mozilla", + "mqtt", + "mule", + "multi", + "multiple", + "music", + "mustache", + "mvc-", + "mvc.", + "mvc_", + "mysql", + "nagio", + "name", + "native", + "need", + "neo-", + "neo.", + "neo_", + "nest", + "nested", + "net-", + "net.", + "net_", + "nette", + "network", + "new-", + "new.", + "new_", + "next", + "nginx", + "ninja", + "nlp-", + "nlp.", + "nlp_", + "node", + "nodej", + "nosql", + "not-", + "not.", + "not_", + "note", + "notebook", + "notepad", + "notice", + "notifier", + "now-", + "now.", + "now_", + "number", + "oauth", + "object", + "objective", + "obsolete", + "ocaml", "octopres", - "svn_", - "svn-", - "svn.", - "ember", - "embed", - "general", - "file", - "important", - "dropbox", - "portable", - "public", - "docpad", - "fish", - "sbt_", - "sbt-", - "sbt.", - "done", + "official", + "old-", + "old.", + "old_", + "onboard", + "online", + "only", + "open", + "opencv", + "opengl", + "openshift", + "openwrt", + "option", + "oracle", + "org-", + "org.", + "org_", + "origin", + "original", + "orm-", + "orm.", + "orm_", + "osx-", + "osx_", + "our-", + "our.", + "our_", + "out-", + "out.", + "out_", + "output", + "over", + "overview", + "own-", + "own.", + "own_", + "pack", + "package", + "packet", + "page", + "panel", + "paper", + "paperclip", "para", - "network", - "common", - "readme", - "popup", - "simple", - "purpose", - "mirror", - "single", - "cordova", - "exchange", - "object", - "design", - "gateway", - "account", - "lamp", - "intellij", - "math", - "mit_", - "mit-", - "mit.", - "control", - "enhanced", - "emitter", - "multi", - "add_", - "add-", - "add.", - "about", - "socket", - "preview", - "vagrant", - "cli_", - "cli-", - "cli.", - "powerful", - "top_", - "top-", - "top.", - "radio", - "watch", - "fluid", - "amazon", - "report", - "couchbase", - "automatic", - "detection", - "sprite", - "pyramid", - "portal", - "advanced", - "plu_", + "parallax", + "parallel", + "parse", + "parser", + "parsing", + "particle", + "party", + "password", + "patch", + "path", + "pattern", + "payment", + "paypal", + "pdf-", + "pdf.", + "pdf_", + "pebble", + "people", + "perl", + "personal", + "phalcon", + "phoenix", + "phone", + "phonegap", + "photo", + "php-", + "php.", + "php_", + "physic", + "picker", + "pipeline", + "platform", + "play", + "player", + "please", "plu-", "plu.", - "runtime", - "git_", - "git-", - "git.", - "uri_", - "uri-", - "uri.", - "haml", - "node", - "sql_", - "sql-", - "sql.", - "cool", - "core", - "obsolete", - "handler", - "iphone", - "extractor", - "array", - "copy", - "nlp_", - "nlp-", - "nlp.", - "reveal", - "pop_", + "plu_", + "plug-in", + "plugin", + "plupload", + "png-", + "png.", + "png_", + "poker", + "polyfill", + "polymer", + "pool", "pop-", "pop.", - "engine", - "parse", - "check", - "html", - "nest", - "all_", - "all-", - "all.", - "chinese", - "buildpack", - "what", - "tag_", - "tag-", - "tag.", - "proxy", - "style", - "cookie", - "feed", - "restful", - "compiler", - "creating", + "pop_", + "popcorn", + "popup", + "port", + "portable", + "portal", + "portfolio", + "post", + "power", + "powered", + "powerful", "prelude", - "context", - "java", - "rspec", - "mock", - "backbone", - "light", - "spotify", - "flex", + "pretty", + "preview", + "principle", + "print", + "pro-", + "pro.", + "pro_", + "problem", + "proc", + "product", + "profile", + "profiler", + "program", + "progres", + "project", + "protocol", + "prototype", + "provider", + "proxy", + "public", + "pull", + "puppet", + "pure", + "purpose", + "push", + "pusher", + "pyramid", + "python", + "quality", + "query", + "queue", + "quick", + "rabbitmq", + "rack", + "radio", + "rail", + "railscast", + "random", + "range", + "raspberry", + "rdf-", + "rdf.", + "rdf_", + "react", + "reactive", + "read", + "reader", + "readme", + "ready", + "real", + "real-time", + "reality", + "realtime", + "recipe", + "recorder", + "red-", + "red.", + "red_", + "reddit", + "redi", + "redmine", + "reference", + "refinery", + "refresh", + "registry", "related", - "shell", - "which", - "clas", - "webapp", - "swift", - "ansible", - "unity", - "console", - "tumblr", - "export", - "campfire", - "conway'", - "made", - "riak", - "hero", - "here", - "unix", - "unit", - "glas", - "smtp", - "how_", - "how-", - "how.", - "hot_", - "hot-", - "hot.", - "debug", "release", - "diff", - "player", - "easy", + "remote", + "rendering", + "repo", + "report", + "request", + "require", + "required", + "requirej", + "research", + "resource", + "response", + "resque", + "rest", + "restful", + "resume", + "reveal", + "reverse", + "review", + "riak", + "rich", "right", - "old_", - "old-", - "old.", - "animate", - "time", - "push", - "explorer", - "course", - "training", - "nette", + "ring", + "robot", + "role", + "room", "router", - "draft", - "structure", - "note", + "routing", + "rpc-", + "rpc.", + "rpc_", + "rpg-", + "rpg.", + "rpg_", + "rspec", + "ruby-", + "ruby.", + "ruby_", + "rule", + "run-", + "run.", + "run_", + "runner", + "running", + "runtime", + "rust", + "rvm-", + "rvm.", + "rvm_", "salt", - "where", - "spark", - "trello", - "power", - "method", - "social", - "via_", - "via-", - "via.", - "vim_", - "vim-", - "vim.", + "sample", + "sandbox", + "sas-", + "sas.", + "sas_", + "sbt-", + "sbt.", + "sbt_", + "scala", + "scalable", + "scanner", + "schema", + "scheme", + "school", + "science", + "scraper", + "scratch", + "screen", + "script", + "scroll", + "scs-", + "scs.", + "scs_", + "sdk-", + "sdk.", + "sdk_", + "sdl-", + "sdl.", + "sdl_", + "search", + "secure", + "security", + "see-", + "see.", + "see_", + "seed", "select", - "webkit", - "github", - "ftp_", - "ftp-", - "ftp.", - "creator", - "mongoose", - "led_", - "led-", - "led.", - "movie", - "currently", - "pdf_", - "pdf-", - "pdf.", - "load", - "markdown", - "phalcon", - "input", - "custom", - "atom", - "oracle", - "phonegap", - "ubuntu", - "great", - "rdf_", - "rdf-", - "rdf.", - "popcorn", - "firefox", - "zip_", - "zip-", - "zip.", - "cuda", - "dotfile", + "selector", + "selenium", + "semantic", + "sencha", + "send", + "sentiment", + "serie", + "server", + "service", + "session", + "set-", + "set.", + "set_", + "setting", + "setup", + "sha1", + "sha2", + "sha256", + "share", + "shared", + "sharing", + "sheet", + "shell", + "shield", + "shipping", + "shop", + "shopify", + "shortener", + "should", + "show", + "showcase", + "side", + "silex", + "simple", + "simulator", + "single", + "site", + "skeleton", + "sketch", + "skin", + "slack", + "slide", + "slider", + "slim", + "small", + "smart", + "smtp", + "snake", + "snapshot", + "snippet", + "soap", + "social", + "socket", + "software", + "solarized", + "solr", + "solution", + "solver", + "some", + "soon", + "source", + "space", + "spark", + "spatial", + "spec", + "sphinx", + "spine", + "spotify", + "spree", + "spring", + "sprite", + "sql-", + "sql.", + "sql_", + "sqlite", + "ssh-", + "ssh.", + "ssh_", + "stack", + "staging", + "standard", + "stanford", + "start", + "started", + "starter", + "startup", + "stat", + "statamic", + "state", "static", - "openwrt", - "viewer", - "powered", - "graphic", - "les_", - "les-", - "les.", - "doe_", - "doe-", - "doe.", - "maven", - "word", - "eclipse", - "lab_", - "lab-", - "lab.", - "hacking", + "statistic", + "statsd", + "statu", "steam", - "analytic", - "option", - "abstract", - "archive", - "reality", - "switcher", - "club", - "write", - "kafka", - "arduino", - "angular", - "online", - "title", - "don't", - "contao", - "notice", - "analyzer", - "learning", - "zend", - "external", - "staging", - "busines", - "tdd_", - "tdd-", - "tdd.", - "scanner", - "building", - "snippet", - "modular", - "bower", - "stm_", + "step", + "still", "stm-", "stm.", - "lib_", - "lib-", - "lib.", - "alpha", - "mobile", - "clean", - "linux", - "nginx", - "manifest", - "some", - "raspberry", - "gnome", - "ide_", - "ide-", - "ide.", - "block", - "statistic", - "info", - "drag", - "youtube", - "koan", - "facebook", - "paperclip", - "art_", - "art-", - "art.", - "quality", - "tab_", + "stm_", + "storage", + "store", + "storm", + "story", + "strategy", + "stream", + "streaming", + "string", + "stripe", + "structure", + "studio", + "study", + "stuff", + "style", + "sublime", + "sugar", + "suite", + "summary", + "super", + "support", + "supported", + "svg-", + "svg.", + "svg_", + "svn-", + "svn.", + "svn_", + "swagger", + "swift", + "switch", + "switcher", + "symfony", + "symphony", + "sync", + "synopsi", + "syntax", + "system", "tab-", "tab.", - "need", - "dojo", - "shield", - "computer", - "stat", - "state", - "twitter", - "utility", - "converter", - "hosting", - "devise", - "liferay", - "updated", - "force", - "tip_", + "tab_", + "table", + "tag-", + "tag.", + "tag_", + "talk", + "target", + "task", + "tcp-", + "tcp.", + "tcp_", + "tdd-", + "tdd.", + "tdd_", + "team", + "tech", + "template", + "term", + "terminal", + "testing", + "tetri", + "text", + "textmate", + "theme", + "theory", + "three", + "thrift", + "time", + "timeline", + "timer", + "tiny", + "tinymce", "tip-", "tip.", - "behavior", - "active", - "call", - "answer", - "deck", - "better", - "principle", - "ches", - "bar_", - "bar-", - "bar.", - "reddit", - "three", - "haxe", - "just", - "plug-in", - "agile", - "manual", - "tetri", - "super", - "beta", - "parsing", - "doctrine", - "minecraft", + "tip_", + "title", + "todo", + "todomvc", + "token", + "tool", + "toolbox", + "toolkit", + "top-", + "top.", + "top_", + "tornado", + "touch", + "tower", + "tracker", + "tracking", + "traffic", + "training", + "transfer", + "translate", + "transport", + "tree", + "trello", + "try-", + "try.", + "try_", + "tumblr", + "tut-", + "tut.", + "tut_", + "tutorial", + "tweet", + "twig", + "twitter", + "type", + "typo", + "ubuntu", + "uiview", + "ultimate", + "under", + "unit", + "unity", + "universal", + "unix", + "update", + "updated", + "upgrade", + "upload", + "uploader", + "uri-", + "uri.", + "uri_", + "url-", + "url.", + "url_", + "usage", + "usb-", + "usb.", + "usb_", + "use-", + "use.", + "use_", + "used", "useful", - "perl", - "sharing", - "agent", - "switch", + "user", + "using", + "util", + "utilitie", + "utility", + "vagrant", + "validator", + "value", + "variou", + "varnish", + "version", + "via-", + "via.", + "via_", + "video", "view", - "dash", - "channel", - "repo", - "pebble", - "profiler", + "viewer", + "vim-", + "vim.", + "vim_", + "vimrc", + "virtual", + "vision", + "visual", + "vpn", + "want", "warning", - "cluster", - "running", - "markup", - "evented", - "mod_", - "mod-", - "mod.", - "share", - "csv_", - "csv-", - "csv.", - "response", - "good", - "house", - "connect", - "built", - "build", - "find", - "ipython", + "watch", + "watcher", + "wave", + "way-", + "way.", + "way_", + "weather", + "web-", + "web_", + "webapp", "webgl", - "big_", - "big-", - "big.", - "google", - "scala", - "sdl_", - "sdl-", - "sdl.", - "sdk_", - "sdk-", - "sdk.", - "native", - "day_", - "day-", - "day.", - "puppet", - "text", - "routing", - "helper", - "linkedin", - "crawler", - "host", - "guard", - "merchant", - "poker", - "over", + "webhook", + "webkit", + "webrtc", + "website", + "websocket", + "welcome", + "what", + "what'", + "when", + "where", + "which", + "why-", + "why.", + "why_", + "widget", + "wifi", + "wiki", + "win-", + "win.", + "win_", + "window", + "wip-", + "wip.", + "wip_", + "within", + "without", + "wizard", + "word", + "wordpres", + "work", + "worker", + "workflow", + "working", + "workshop", + "world", + "wrapper", + "write", + "writer", "writing", - "free", - "classe", - "component", - "craft", - "nodej", - "phoenix", - "longer", - "quick", - "lazy", - "memory", - "clone", - "hacker", - "middleman", - "factory", - "motion", - "multiple", - "tornado", - "hack", - "ssh_", - "ssh-", - "ssh.", - "review", - "vimrc", - "driver", - "driven", - "blog", - "particle", - "table", - "intro", - "importer", - "thrift", + "written", + "www-", + "www.", + "www_", + "xamarin", + "xcode", + "xml-", + "xml.", + "xml_", "xmpp", - "framework", - "refresh", - "react", - "font", - "librarie", - "variou", - "formatter", - "analysi", - "karma", - "scroll", - "tut_", - "tut-", - "tut.", - "apple", - "tag_", - "tag-", - "tag.", - "tab_", - "tab-", - "tab.", - "category", - "ionic", - "cache", - "homebrew", - "reverse", - "english", - "getting", - "shipping", - "clojure", - "boot", - "book", - "branch", - "combination", - "combo", + "xxxxxx", + "yahoo", + "yaml", + "yandex", + "yeoman", + "yet-", + "yet.", + "yet_", + "yii-", + "yii.", + "yii_", + "youtube", + "yui-", + "yui.", + "yui_", + "zend", + "zero", + "zip-", + "zip.", + "zip_", + "zsh-", + "zsh.", + "zsh_", +] +[[rules.allowlists]] +regexTarget = "line" +regexes = [ + '''--mount=type=secret,''', + '''import[ \t]+{[ \t\w,]+}[ \t]+from[ \t]+['"][^'"]+['"]''', +] +[[rules.allowlists]] +condition = "AND" +paths = [ + '''\.bb$''','''\.bbappend$''','''\.bbclass$''','''\.inc$''', +] +regexTarget = "line" +regexes = [ + '''LICENSE[^=]*=\s*"[^"]+''', + '''LIC_FILES_CHKSUM[^=]*=\s*"[^"]+''', + '''SRC[^=]*=\s*"[a-zA-Z0-9]+''', ] + [[rules]] -description = "GitHub App Token" id = "github-app-token" -regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}''' +description = "Identified a GitHub App Token, which may compromise GitHub application integrations and source code security." +regex = '''(?:ghu|ghs)_[0-9a-zA-Z]{36}''' +entropy = 3 keywords = [ - "ghu_","ghs_", + "ghu_", + "ghs_", +] +[[rules.allowlists]] +paths = [ + '''(?:^|/)@octokit/auth-token/README\.md$''', ] [[rules]] -description = "GitHub Fine-Grained Personal Access Token" id = "github-fine-grained-pat" -regex = '''github_pat_[0-9a-zA-Z_]{82}''' -keywords = [ - "github_pat_", -] +description = "Found a GitHub Fine-Grained Personal Access Token, risking unauthorized repository access and code manipulation." +regex = '''github_pat_\w{82}''' +entropy = 3 +keywords = ["github_pat_"] [[rules]] -description = "GitHub OAuth Access Token" id = "github-oauth" +description = "Discovered a GitHub OAuth Access Token, posing a risk of compromised GitHub account integrations and data leaks." regex = '''gho_[0-9a-zA-Z]{36}''' -keywords = [ - "gho_", +entropy = 3 +keywords = ["gho_"] + +[[rules]] +id = "github-pat" +description = "Uncovered a GitHub Personal Access Token, potentially leading to unauthorized repository access and sensitive content exposure." +regex = '''ghp_[0-9a-zA-Z]{36}''' +entropy = 3 +keywords = ["ghp_"] +[[rules.allowlists]] +paths = [ + '''(?:^|/)@octokit/auth-token/README\.md$''', ] [[rules]] -description = "GitHub Personal Access Token" -id = "github-pat" -regex = '''ghp_[0-9a-zA-Z]{36}''' -keywords = [ - "ghp_", -] +id = "github-refresh-token" +description = "Detected a GitHub Refresh Token, which could allow prolonged unauthorized access to GitHub services." +regex = '''ghr_[0-9a-zA-Z]{36}''' +entropy = 3 +keywords = ["ghr_"] + +[[rules]] +id = "gitlab-cicd-job-token" +description = "Identified a GitLab CI/CD Job Token, potential access to projects and some APIs on behalf of a user while the CI job is running." +regex = '''glcbt-[0-9a-zA-Z]{1,5}_[0-9a-zA-Z_-]{20}''' +entropy = 3 +keywords = ["glcbt-"] + +[[rules]] +id = "gitlab-deploy-token" +description = "Identified a GitLab Deploy Token, risking access to repositories, packages and containers with write access." +regex = '''gldt-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["gldt-"] + +[[rules]] +id = "gitlab-feature-flag-client-token" +description = "Identified a GitLab feature flag client token, risks exposing user lists and features flags used by an application." +regex = '''glffct-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["glffct-"] + +[[rules]] +id = "gitlab-feed-token" +description = "Identified a GitLab feed token, risking exposure of user data." +regex = '''glft-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["glft-"] + +[[rules]] +id = "gitlab-incoming-mail-token" +description = "Identified a GitLab incoming mail token, risking manipulation of data sent by mail." +regex = '''glimt-[0-9a-zA-Z_\-]{25}''' +entropy = 3 +keywords = ["glimt-"] [[rules]] -description = "GitHub Refresh Token" -id = "github-refresh-token" -regex = '''ghr_[0-9a-zA-Z]{36}''' -keywords = [ - "ghr_", -] +id = "gitlab-kubernetes-agent-token" +description = "Identified a GitLab Kubernetes Agent token, risking access to repos and registry of projects connected via agent." +regex = '''glagent-[0-9a-zA-Z_\-]{50}''' +entropy = 3 +keywords = ["glagent-"] + +[[rules]] +id = "gitlab-oauth-app-secret" +description = "Identified a GitLab OIDC Application Secret, risking access to apps using GitLab as authentication provider." +regex = '''gloas-[0-9a-zA-Z_\-]{64}''' +entropy = 3 +keywords = ["gloas-"] [[rules]] -description = "GitLab Personal Access Token" id = "gitlab-pat" -regex = '''glpat-[0-9a-zA-Z\-\_]{20}''' -keywords = [ - "glpat-", -] +description = "Identified a GitLab Personal Access Token, risking unauthorized access to GitLab repositories and codebase exposure." +regex = '''glpat-[\w-]{20}''' +entropy = 3 +keywords = ["glpat-"] + +[[rules]] +id = "gitlab-pat-routable" +description = "Identified a GitLab Personal Access Token (routable), risking unauthorized access to GitLab repositories and codebase exposure." +regex = '''\bglpat-[0-9a-zA-Z_-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b''' +entropy = 4 +keywords = ["glpat-"] [[rules]] -description = "GitLab Pipeline Trigger Token" id = "gitlab-ptt" +description = "Found a GitLab Pipeline Trigger Token, potentially compromising continuous integration workflows and project security." regex = '''glptt-[0-9a-f]{40}''' -keywords = [ - "glptt-", -] +entropy = 3 +keywords = ["glptt-"] [[rules]] -description = "GitLab Runner Registration Token" id = "gitlab-rrt" -regex = '''GR1348941[0-9a-zA-Z\-\_]{20}''' -keywords = [ - "gr1348941", -] +description = "Discovered a GitLab Runner Registration Token, posing a risk to CI/CD pipeline integrity and unauthorized access." +regex = '''GR1348941[\w-]{20}''' +entropy = 3 +keywords = ["gr1348941"] + +[[rules]] +id = "gitlab-runner-authentication-token" +description = "Discovered a GitLab Runner Authentication Token, posing a risk to CI/CD pipeline integrity and unauthorized access." +regex = '''glrt-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["glrt-"] + +[[rules]] +id = "gitlab-runner-authentication-token-routable" +description = "Discovered a GitLab Runner Authentication Token (Routable), posing a risk to CI/CD pipeline integrity and unauthorized access." +regex = '''\bglrt-t\d_[0-9a-zA-Z_\-]{27,300}\.[0-9a-z]{2}[0-9a-z]{7}\b''' +entropy = 4 +keywords = ["glrt-"] + +[[rules]] +id = "gitlab-scim-token" +description = "Discovered a GitLab SCIM Token, posing a risk to unauthorized access for a organization or instance." +regex = '''glsoat-[0-9a-zA-Z_\-]{20}''' +entropy = 3 +keywords = ["glsoat-"] + +[[rules]] +id = "gitlab-session-cookie" +description = "Discovered a GitLab Session Cookie, posing a risk to unauthorized access to a user account." +regex = '''_gitlab_session=[0-9a-z]{32}''' +entropy = 3 +keywords = ["_gitlab_session="] [[rules]] -description = "Gitter Access Token" id = "gitter-access-token" -regex = '''(?i)(?:gitter)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9_-]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "gitter", -] +description = "Uncovered a Gitter Access Token, which may lead to unauthorized access to chat and communication services." +regex = '''(?i)[\w.-]{0,50}?(?:gitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["gitter"] [[rules]] -description = "GoCardless API token" id = "gocardless-api-token" -regex = '''(?i)(?:gocardless)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(live_(?i)[a-z0-9\-_=]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Detected a GoCardless API token, potentially risking unauthorized direct debit payment operations and financial data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:gocardless)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(live_(?i)[a-z0-9\-_=]{40})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "live_","gocardless", + "live_", + "gocardless", ] [[rules]] -description = "Grafana api key (or Grafana cloud api key)" id = "grafana-api-key" -regex = '''(?i)\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,2})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "eyjrijoi", -] +description = "Identified a Grafana API key, which could compromise monitoring dashboards and sensitive data analytics." +regex = '''(?i)\b(eyJrIjoi[A-Za-z0-9]{70,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["eyjrijoi"] [[rules]] -description = "Grafana cloud api token" id = "grafana-cloud-api-token" -regex = '''(?i)\b(glc_[A-Za-z0-9+/]{32,400}={0,2})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "glc_", -] +description = "Found a Grafana cloud API token, risking unauthorized access to cloud-based monitoring services and data exposure." +regex = '''(?i)\b(glc_[A-Za-z0-9+/]{32,400}={0,3})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["glc_"] [[rules]] -description = "Grafana service account token" id = "grafana-service-account-token" -regex = '''(?i)\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Discovered a Grafana service account token, posing a risk of compromised monitoring services and data integrity." +regex = '''(?i)\b(glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["glsa_"] + +[[rules]] +id = "harness-api-key" +description = "Identified a Harness Access Token (PAT or SAT), risking unauthorized access to a Harness account." +regex = '''(?:pat|sat)\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9]{24}\.[a-zA-Z0-9]{20}''' keywords = [ - "glsa_", + "pat.", + "sat.", ] [[rules]] -description = "HashiCorp Terraform user/org API token" id = "hashicorp-tf-api-token" -regex = '''(?i)[a-z0-9]{14}\.atlasv1\.[a-z0-9\-_=]{60,70}''' +description = "Uncovered a HashiCorp Terraform user/org API token, which may lead to unauthorized infrastructure management and security breaches." +regex = '''(?i)[a-z0-9]{14}\.(?-i:atlasv1)\.[a-z0-9\-_=]{60,70}''' +entropy = 3.5 +keywords = ["atlasv1"] + +[[rules]] +id = "hashicorp-tf-password" +description = "Identified a HashiCorp Terraform password field, risking unauthorized infrastructure configuration and security breaches." +regex = '''(?i)[\w.-]{0,50}?(?:administrator_login_password|password)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}("[a-z0-9=_\-]{8,20}")(?:[\x60'"\s;]|\\[nr]|$)''' +path = '''(?i)\.(?:tf|hcl)$''' +entropy = 2 keywords = [ - "atlasv1", + "administrator_login_password", + "password", ] [[rules]] -description = "Heroku API Key" id = "heroku-api-key" -regex = '''(?i)(?:heroku)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "heroku", -] +description = "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security." +regex = '''(?i)[\w.-]{0,50}?(?:heroku)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["heroku"] + +[[rules]] +id = "heroku-api-key-v2" +description = "Detected a Heroku API Key, potentially compromising cloud application deployments and operational security." +regex = '''\b((HRKU-AA[0-9a-zA-Z_-]{58}))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["hrku-aa"] [[rules]] -description = "HubSpot API Token" id = "hubspot-api-key" -regex = '''(?i)(?:hubspot)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Found a HubSpot API Token, posing a risk to CRM data integrity and unauthorized marketing operations." +regex = '''(?i)[\w.-]{0,50}?(?:hubspot)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["hubspot"] + +[[rules]] +id = "huggingface-access-token" +description = "Discovered a Hugging Face Access token, which could lead to unauthorized access to AI models and sensitive data." +regex = '''\b(hf_(?i:[a-z]{34}))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["hf_"] + +[[rules]] +id = "huggingface-organization-api-token" +description = "Uncovered a Hugging Face Organization API token, potentially compromising AI organization accounts and associated data." +regex = '''\b(api_org_(?i:[a-z]{34}))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["api_org_"] + +[[rules]] +id = "infracost-api-token" +description = "Detected an Infracost API Token, risking unauthorized access to cloud cost estimation tools and financial data." +regex = '''\b(ico-[a-zA-Z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["ico-"] + +[[rules]] +id = "intercom-api-key" +description = "Identified an Intercom API Token, which could compromise customer communication channels and data privacy." +regex = '''(?i)[\w.-]{0,50}?(?:intercom)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{60})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["intercom"] + +[[rules]] +id = "intra42-client-secret" +description = "Found a Intra42 client secret, which could lead to unauthorized access to the 42School API and sensitive data." +regex = '''\b(s-s4t2(?:ud|af)-(?i)[abcdef0123456789]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 keywords = [ - "hubspot", + "intra", + "s-s4t2ud-", + "s-s4t2af-", ] [[rules]] -description = "Intercom API Token" -id = "intercom-api-key" -regex = '''(?i)(?:intercom)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{60})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +id = "jfrog-api-key" +description = "Found a JFrog API Key, posing a risk of unauthorized access to software artifact repositories and build pipelines." +regex = '''(?i)[\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{73})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "intercom", + "jfrog", + "artifactory", + "bintray", + "xray", ] [[rules]] -description = "JSON Web Token" -id = "jwt" -regex = '''(?i)\b(ey[0-9a-z]{30,34}\.ey[0-9a-z-\/_]{30,500}\.[0-9a-zA-Z-\/_]{10,200}={0,2})(?:['|\"|\n|\r|\s|\x60|;]|$)''' +id = "jfrog-identity-token" +description = "Discovered a JFrog Identity Token, potentially compromising access to JFrog services and sensitive software artifacts." +regex = '''(?i)[\w.-]{0,50}?(?:jfrog|artifactory|bintray|xray)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "ey", + "jfrog", + "artifactory", + "bintray", + "xray", ] [[rules]] -description = "Kraken Access Token" +id = "jwt" +description = "Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data." +regex = '''\b(ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?)(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["ey"] + +[[rules]] +id = "jwt-base64" +description = "Detected a Base64-encoded JSON Web Token, posing a risk of exposing encoded authentication and data exchange information." +regex = '''\bZXlK(?:(?PaGJHY2lPaU)|(?PaGNIVWlPaU)|(?PaGNIWWlPaU)|(?PaGRXUWlPaU)|(?PaU5qUWlP)|(?PamNtbDBJanBi)|(?PamRIa2lPaU)|(?PbGNHc2lPbn)|(?PbGJtTWlPaU)|(?PcWEzVWlPaU)|(?PcWQyc2lPb)|(?PcGMzTWlPaU)|(?PcGRpSTZJ)|(?PcmFXUWlP)|(?PclpYbGZiM0J6SWpwY)|(?PcmRIa2lPaUp)|(?PdWIyNWpaU0k2)|(?Pd01tTWlP)|(?Pd01uTWlPaU)|(?Pd2NIUWlPaU)|(?PemRXSWlPaU)|(?PemRuUWlP)|(?PMFlXY2lPaU)|(?PMGVYQWlPaUp)|(?PMWNtd2l)|(?PMWMyVWlPaUp)|(?PMlpYSWlPaU)|(?PMlpYSnphVzl1SWpv)|(?PNElqb2)|(?PNE5XTWlP)|(?PNE5YUWlPaU)|(?PNE5YUWpVekkxTmlJNkl)|(?PNE5YVWlPaU)|(?PNmFYQWlPaU))[a-zA-Z0-9\/\\_+\-\r\n]{40,}={0,2}''' +entropy = 2 +keywords = ["zxlk"] + +[[rules]] id = "kraken-access-token" -regex = '''(?i)(?:kraken)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9\/=_\+\-]{80,90})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "kraken", +description = "Identified a Kraken Access Token, potentially compromising cryptocurrency trading accounts and financial security." +regex = '''(?i)[\w.-]{0,50}?(?:kraken)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9\/=_\+\-]{80,90})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["kraken"] + +[[rules]] +id = "kubernetes-secret-yaml" +description = "Possible Kubernetes Secret detected, posing a risk of leaking credentials/tokens from your deployments" +regex = '''(?i)(?:\bkind:[ \t]*["']?\bsecret\b["']?(?s:.){0,200}?\bdata:(?s:.){0,100}?\s+([\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:["']?[a-z0-9+/]{10,}={0,3}["']?|\{\{[ \t\w"|$:=,.-]+}}|""|''))|\bdata:(?s:.){0,100}?\s+([\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:["']?[a-z0-9+/]{10,}={0,3}["']?|\{\{[ \t\w"|$:=,.-]+}}|""|''))(?s:.){0,200}?\bkind:[ \t]*["']?\bsecret\b["']?)''' +path = '''(?i)\.ya?ml$''' +keywords = ["secret"] +[[rules.allowlists]] +regexes = [ + '''[\w.-]+:(?:[ \t]*(?:\||>[-+]?)\s+)?[ \t]*(?:\{\{[ \t\w"|$:=,.-]+}}|""|'')''', +] +[[rules.allowlists]] +regexTarget = "match" +regexes = [ + '''(kind:(?s:.)+\n---\n(?s:.)+\bdata:|data:(?s:.)+\n---\n(?s:.)+\bkind:)''', ] [[rules]] -description = "Kucoin Access Token" id = "kucoin-access-token" -regex = '''(?i)(?:kucoin)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{24})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "kucoin", -] +description = "Found a Kucoin Access Token, risking unauthorized access to cryptocurrency exchange services and transactions." +regex = '''(?i)[\w.-]{0,50}?(?:kucoin)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["kucoin"] [[rules]] -description = "Kucoin Secret Key" id = "kucoin-secret-key" -regex = '''(?i)(?:kucoin)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "kucoin", -] +description = "Discovered a Kucoin Secret Key, which could lead to compromised cryptocurrency operations and financial data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:kucoin)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["kucoin"] [[rules]] -description = "Launchdarkly Access Token" id = "launchdarkly-access-token" -regex = '''(?i)(?:launchdarkly)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "launchdarkly", -] +description = "Uncovered a Launchdarkly Access Token, potentially compromising feature flag management and application functionality." +regex = '''(?i)[\w.-]{0,50}?(?:launchdarkly)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["launchdarkly"] [[rules]] -description = "Linear API Token" id = "linear-api-key" +description = "Detected a Linear API Token, posing a risk to project management tools and sensitive task data." regex = '''lin_api_(?i)[a-z0-9]{40}''' -keywords = [ - "lin_api_", -] +entropy = 2 +keywords = ["lin_api_"] [[rules]] -description = "Linear Client Secret" id = "linear-client-secret" -regex = '''(?i)(?:linear)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "linear", -] +description = "Identified a Linear Client Secret, which may compromise secure integrations and sensitive project management data." +regex = '''(?i)[\w.-]{0,50}?(?:linear)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["linear"] [[rules]] -description = "LinkedIn Client ID" id = "linkedin-client-id" -regex = '''(?i)(?:linkedin|linked-in)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{14})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Found a LinkedIn Client ID, risking unauthorized access to LinkedIn integrations and professional data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:linked[_-]?in)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{14})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 keywords = [ - "linkedin","linked-in", + "linkedin", + "linked_in", + "linked-in", ] [[rules]] -description = "LinkedIn Client secret" id = "linkedin-client-secret" -regex = '''(?i)(?:linkedin|linked-in)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{16})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Discovered a LinkedIn Client secret, potentially compromising LinkedIn application integrations and user data." +regex = '''(?i)[\w.-]{0,50}?(?:linked[_-]?in)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 keywords = [ - "linkedin","linked-in", + "linkedin", + "linked_in", + "linked-in", ] [[rules]] -description = "Lob API Key" id = "lob-api-key" -regex = '''(?i)(?:lob)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}((live|test)_[a-f0-9]{35})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Uncovered a Lob API Key, which could lead to unauthorized access to mailing and address verification services." +regex = '''(?i)[\w.-]{0,50}?(?:lob)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}((live|test)_[a-f0-9]{35})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "test_","live_", + "test_", + "live_", ] [[rules]] -description = "Lob Publishable API Key" id = "lob-pub-api-key" -regex = '''(?i)(?:lob)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}((test|live)_pub_[a-f0-9]{31})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Detected a Lob Publishable API Key, posing a risk of exposing mail and print service integrations." +regex = '''(?i)[\w.-]{0,50}?(?:lob)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}((test|live)_pub_[a-f0-9]{31})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "test_pub","live_pub","_pub", + "test_pub", + "live_pub", + "_pub", ] [[rules]] -description = "Mailchimp API key" id = "mailchimp-api-key" -regex = '''(?i)(?:mailchimp)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{32}-us20)(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "mailchimp", -] +description = "Identified a Mailchimp API key, potentially compromising email marketing campaigns and subscriber data." +regex = '''(?i)[\w.-]{0,50}?(?:MailchimpSDK.initialize|mailchimp)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{32}-us\d\d)(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mailchimp"] [[rules]] -description = "Mailgun private API token" id = "mailgun-private-api-token" -regex = '''(?i)(?:mailgun)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(key-[a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "mailgun", -] +description = "Found a Mailgun private API token, risking unauthorized email service operations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(key-[a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mailgun"] [[rules]] -description = "Mailgun public validation key" id = "mailgun-pub-key" -regex = '''(?i)(?:mailgun)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(pubkey-[a-f0-9]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "mailgun", -] +description = "Discovered a Mailgun public validation key, which could expose email verification processes and associated data." +regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(pubkey-[a-f0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mailgun"] [[rules]] -description = "Mailgun webhook signing key" id = "mailgun-signing-key" -regex = '''(?i)(?:mailgun)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "mailgun", -] +description = "Uncovered a Mailgun webhook signing key, potentially compromising email automation and data integrity." +regex = '''(?i)[\w.-]{0,50}?(?:mailgun)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mailgun"] [[rules]] -description = "MapBox API token" id = "mapbox-api-token" -regex = '''(?i)(?:mapbox)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(pk\.[a-z0-9]{60}\.[a-z0-9]{22})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "mapbox", -] +description = "Detected a MapBox API token, posing a risk to geospatial services and sensitive location data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:mapbox)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(pk\.[a-z0-9]{60}\.[a-z0-9]{22})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mapbox"] [[rules]] -description = "Mattermost Access Token" id = "mattermost-access-token" -regex = '''(?i)(?:mattermost)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{26})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "mattermost", -] +description = "Identified a Mattermost Access Token, which may compromise team communication channels and data privacy." +regex = '''(?i)[\w.-]{0,50}?(?:mattermost)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{26})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["mattermost"] + +[[rules]] +id = "maxmind-license-key" +description = "Discovered a potential MaxMind license key." +regex = '''\b([A-Za-z0-9]{6}_[A-Za-z0-9]{29}_mmk)(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["_mmk"] [[rules]] -description = "MessageBird API token" id = "messagebird-api-token" -regex = '''(?i)(?:messagebird|message-bird|message_bird)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{25})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Found a MessageBird API token, risking unauthorized access to communication platforms and message data." +regex = '''(?i)[\w.-]{0,50}?(?:message[_-]?bird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{25})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "messagebird","message-bird","message_bird", + "messagebird", + "message-bird", + "message_bird", ] [[rules]] -description = "MessageBird client ID" id = "messagebird-client-id" -regex = '''(?i)(?:messagebird|message-bird|message_bird)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Discovered a MessageBird client ID, potentially compromising API integrations and sensitive communication data." +regex = '''(?i)[\w.-]{0,50}?(?:message[_-]?bird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "messagebird","message-bird","message_bird", + "messagebird", + "message-bird", + "message_bird", ] [[rules]] -description = "Microsoft Teams Webhook" id = "microsoft-teams-webhook" -regex = '''https:\/\/[a-z0-9]+\.webhook\.office\.com\/webhookb2\/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}\/IncomingWebhook\/[a-z0-9]{32}\/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}''' +description = "Uncovered a Microsoft Teams Webhook, which could lead to unauthorized access to team collaboration tools and data leaks." +regex = '''https://[a-z0-9]+\.webhook\.office\.com/webhookb2/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}@[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}/IncomingWebhook/[a-z0-9]{32}/[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}''' keywords = [ - "webhook.office.com","webhookb2","incomingwebhook", + "webhook.office.com", + "webhookb2", + "incomingwebhook", ] [[rules]] -description = "Netlify Access Token" id = "netlify-access-token" -regex = '''(?i)(?:netlify)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{40,46})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "netlify", -] +description = "Detected a Netlify Access Token, potentially compromising web hosting services and site management." +regex = '''(?i)[\w.-]{0,50}?(?:netlify)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{40,46})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["netlify"] [[rules]] -description = "New Relic ingest browser API token" id = "new-relic-browser-api-token" -regex = '''(?i)(?:new-relic|newrelic|new_relic)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(NRJS-[a-f0-9]{19})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "nrjs-", -] +description = "Identified a New Relic ingest browser API token, risking unauthorized access to application performance data and analytics." +regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRJS-[a-f0-9]{19})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["nrjs-"] + +[[rules]] +id = "new-relic-insert-key" +description = "Discovered a New Relic insight insert key, compromising data injection into the platform." +regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRII-[a-z0-9-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["nrii-"] [[rules]] -description = "New Relic user API ID" id = "new-relic-user-api-id" -regex = '''(?i)(?:new-relic|newrelic|new_relic)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Found a New Relic user API ID, posing a risk to application monitoring services and data integrity." +regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "new-relic","newrelic","new_relic", + "new-relic", + "newrelic", + "new_relic", ] [[rules]] -description = "New Relic user API Key" id = "new-relic-user-api-key" -regex = '''(?i)(?:new-relic|newrelic|new_relic)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(NRAK-[a-z0-9]{27})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "nrak", -] +description = "Discovered a New Relic user API Key, which could lead to compromised application insights and performance monitoring." +regex = '''(?i)[\w.-]{0,50}?(?:new-relic|newrelic|new_relic)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(NRAK-[a-z0-9]{27})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["nrak"] + +[[rules]] +id = "notion-api-token" +description = "Notion API token" +regex = '''\b(ntn_[0-9]{11}[A-Za-z0-9]{32}[A-Za-z0-9]{3})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["ntn_"] [[rules]] -description = "npm access token" id = "npm-access-token" -regex = '''(?i)\b(npm_[a-z0-9]{36})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "npm_", +description = "Uncovered an npm access token, potentially compromising package management and code repository access." +regex = '''(?i)\b(npm_[a-z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["npm_"] + +[[rules]] +id = "nuget-config-password" +description = "Identified a password within a Nuget config file, potentially compromising package management access." +regex = '''(?i)''' +path = '''(?i)nuget\.config$''' +entropy = 1 +keywords = ["|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{32})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Detected a Nytimes Access Token, risking unauthorized access to New York Times APIs and content services." +regex = '''(?i)[\w.-]{0,50}?(?:nytimes|new-york-times,|newyorktimes)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9=_\-]{32})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "nytimes","new-york-times","newyorktimes", + "nytimes", + "new-york-times", + "newyorktimes", ] [[rules]] -description = "Okta Access Token" +id = "octopus-deploy-api-key" +description = "Discovered a potential Octopus Deploy API key, risking application deployments and operational security." +regex = '''\b(API-[A-Z0-9]{26})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["api-"] + +[[rules]] id = "okta-access-token" -regex = '''(?i)(?:okta)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9=_\-]{42})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "okta", -] +description = "Identified an Okta Access Token, which may compromise identity management services and user authentication data." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Oo]kta|OKTA))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(00[\w=\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["okta"] + +[[rules]] +id = "openai-api-key" +description = "Found an OpenAI API Key, posing a risk of unauthorized access to AI services and data manipulation." +regex = '''\b(sk-(?:proj|svcacct|admin)-(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})T3BlbkFJ(?:[A-Za-z0-9_-]{74}|[A-Za-z0-9_-]{58})\b|sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["t3blbkfj"] + +[[rules]] +id = "openshift-user-token" +description = "Found an OpenShift user token, potentially compromising an OpenShift/Kubernetes cluster." +regex = '''\b(sha256~[\w-]{43})(?:[^\w-]|\z)''' +entropy = 3.5 +keywords = ["sha256~"] + +[[rules]] +id = "perplexity-api-key" +description = "Detected a Perplexity API key, which could lead to unauthorized access to Perplexity AI services and data exposure." +regex = '''\b(pplx-[a-zA-Z0-9]{48})(?:[\x60'"\s;]|\\[nr]|$|\b)''' +entropy = 4 +keywords = ["pplx-"] + +[[rules]] +id = "pkcs12-file" +description = "Found a PKCS #12 file, which commonly contain bundled private keys." +path = '''(?i)(?:^|\/)[^\/]+\.p(?:12|fx)$''' [[rules]] -description = "Plaid API Token" id = "plaid-api-token" -regex = '''(?i)(?:plaid)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(access-(?:sandbox|development|production)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "plaid", -] +description = "Discovered a Plaid API Token, potentially compromising financial data aggregation and banking services." +regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(access-(?:sandbox|development|production)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["plaid"] [[rules]] -description = "Plaid Client ID" id = "plaid-client-id" -regex = '''(?i)(?:plaid)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{24})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "plaid", -] +description = "Uncovered a Plaid Client ID, which could lead to unauthorized financial service integrations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{24})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = ["plaid"] [[rules]] -description = "Plaid Secret key" id = "plaid-secret-key" -regex = '''(?i)(?:plaid)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{30})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "plaid", -] +description = "Detected a Plaid Secret key, risking unauthorized access to financial accounts and sensitive transaction data." +regex = '''(?i)[\w.-]{0,50}?(?:plaid)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = ["plaid"] [[rules]] -description = "PlanetScale API token" id = "planetscale-api-token" -regex = '''(?i)\b(pscale_tkn_(?i)[a-z0-9=\-_\.]{32,64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "pscale_tkn_", -] +description = "Identified a PlanetScale API token, potentially compromising database management and operations." +regex = '''\b(pscale_tkn_(?i)[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["pscale_tkn_"] [[rules]] -description = "PlanetScale OAuth token" id = "planetscale-oauth-token" -regex = '''(?i)\b(pscale_oauth_(?i)[a-z0-9=\-_\.]{32,64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "pscale_oauth_", -] +description = "Found a PlanetScale OAuth token, posing a risk to database access control and sensitive data integrity." +regex = '''\b(pscale_oauth_[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["pscale_oauth_"] [[rules]] -description = "PlanetScale password" id = "planetscale-password" -regex = '''(?i)\b(pscale_pw_(?i)[a-z0-9=\-_\.]{32,64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "pscale_pw_", -] +description = "Discovered a PlanetScale password, which could lead to unauthorized database operations and data breaches." +regex = '''(?i)\b(pscale_pw_(?i)[\w=\.-]{32,64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["pscale_pw_"] [[rules]] -description = "Postman API token" id = "postman-api-token" -regex = '''(?i)\b(PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "pmak-", -] +description = "Uncovered a Postman API token, potentially compromising API testing and development workflows." +regex = '''\b(PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["pmak-"] [[rules]] -description = "Prefect API token" id = "prefect-api-token" -regex = '''(?i)\b(pnu_[a-z0-9]{36})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "pnu_", -] +description = "Detected a Prefect API token, risking unauthorized access to workflow management and automation services." +regex = '''\b(pnu_[a-zA-Z0-9]{36})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["pnu_"] [[rules]] -description = "Private Key" id = "private-key" -regex = '''(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY( BLOCK)?-----[\s\S-]*KEY( BLOCK)?----''' +description = "Identified a Private Key, which may compromise cryptographic security and sensitive data encryption." +regex = '''(?i)-----BEGIN[ A-Z0-9_-]{0,100}PRIVATE KEY(?: BLOCK)?-----[\s\S-]{64,}?KEY(?: BLOCK)?-----''' +keywords = ["-----begin"] + +[[rules]] +id = "privateai-api-token" +description = "Identified a PrivateAI Token, posing a risk of unauthorized access to AI services and data manipulation." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:private[_-]?ai)(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{32})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 keywords = [ - "-----begin", + "privateai", + "private_ai", + "private-ai", ] [[rules]] -description = "Pulumi API token" id = "pulumi-api-token" -regex = '''(?i)\b(pul-[a-f0-9]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "pul-", -] +description = "Found a Pulumi API token, posing a risk to infrastructure as code services and cloud resource management." +regex = '''\b(pul-[a-f0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["pul-"] [[rules]] -description = "PyPI upload token" id = "pypi-upload-token" -regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}''' -keywords = [ - "pypi-ageichlwas5vcmc", -] +description = "Discovered a PyPI upload token, potentially compromising Python package distribution and repository integrity." +regex = '''pypi-AgEIcHlwaS5vcmc[\w-]{50,1000}''' +entropy = 3 +keywords = ["pypi-ageichlwas5vcmc"] [[rules]] -description = "RapidAPI Access Token" id = "rapidapi-access-token" -regex = '''(?i)(?:rapidapi)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9_-]{50})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "rapidapi", -] +description = "Uncovered a RapidAPI Access Token, which could lead to unauthorized access to various APIs and data services." +regex = '''(?i)[\w.-]{0,50}?(?:rapidapi)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9_-]{50})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["rapidapi"] [[rules]] -description = "Readme API token" id = "readme-api-token" -regex = '''(?i)\b(rdme_[a-z0-9]{70})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "rdme_", -] +description = "Detected a Readme API token, risking unauthorized documentation management and content exposure." +regex = '''\b(rdme_[a-z0-9]{70})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["rdme_"] [[rules]] -description = "Rubygem API token" id = "rubygems-api-token" -regex = '''(?i)\b(rubygems_[a-f0-9]{48})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "rubygems_", -] +description = "Identified a Rubygem API token, potentially compromising Ruby library distribution and package management." +regex = '''\b(rubygems_[a-f0-9]{48})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["rubygems_"] + +[[rules]] +id = "scalingo-api-token" +description = "Found a Scalingo API token, posing a risk to cloud platform services and application deployment security." +regex = '''\b(tk-us-[\w-]{48})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["tk-us-"] [[rules]] -description = "Sendbird Access ID" id = "sendbird-access-id" -regex = '''(?i)(?:sendbird)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "sendbird", -] +description = "Discovered a Sendbird Access ID, which could compromise chat and messaging platform integrations." +regex = '''(?i)[\w.-]{0,50}?(?:sendbird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["sendbird"] [[rules]] -description = "Sendbird Access Token" id = "sendbird-access-token" -regex = '''(?i)(?:sendbird)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "sendbird", -] +description = "Uncovered a Sendbird Access Token, potentially risking unauthorized access to communication services and user data." +regex = '''(?i)[\w.-]{0,50}?(?:sendbird)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["sendbird"] [[rules]] -description = "SendGrid API token" id = "sendgrid-api-token" -regex = '''(?i)\b(SG\.(?i)[a-z0-9=_\-\.]{66})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "sg.", -] +description = "Detected a SendGrid API token, posing a risk of unauthorized email service operations and data exposure." +regex = '''\b(SG\.(?i)[a-z0-9=_\-\.]{66})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["sg."] [[rules]] -description = "Sendinblue API token" id = "sendinblue-api-token" -regex = '''(?i)\b(xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "xkeysib-", -] +description = "Identified a Sendinblue API token, which may compromise email marketing services and subscriber data privacy." +regex = '''\b(xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["xkeysib-"] [[rules]] -description = "Sentry Access Token" id = "sentry-access-token" -regex = '''(?i)(?:sentry)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "sentry", -] +description = "Found a Sentry.io Access Token (old format), risking unauthorized access to error tracking services and sensitive application data." +regex = '''(?i)[\w.-]{0,50}?(?:sentry)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sentry"] + +[[rules]] +id = "sentry-org-token" +description = "Found a Sentry.io Organization Token, risking unauthorized access to error tracking services and sensitive application data." +regex = '''\bsntrys_eyJpYXQiO[a-zA-Z0-9+/]{10,200}(?:LCJyZWdpb25fdXJs|InJlZ2lvbl91cmwi|cmVnaW9uX3VybCI6)[a-zA-Z0-9+/]{10,200}={0,2}_[a-zA-Z0-9+/]{43}(?:[^a-zA-Z0-9+/]|\z)''' +entropy = 4.5 +keywords = ["sntrys_eyjpyxqio"] + +[[rules]] +id = "sentry-user-token" +description = "Found a Sentry.io User Token, risking unauthorized access to error tracking services and sensitive application data." +regex = '''\b(sntryu_[a-f0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 +keywords = ["sntryu_"] + +[[rules]] +id = "settlemint-application-access-token" +description = "Found a Settlemint Application Access Token." +regex = '''\b(sm_aat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sm_aat"] + +[[rules]] +id = "settlemint-personal-access-token" +description = "Found a Settlemint Personal Access Token." +regex = '''\b(sm_pat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sm_pat"] + +[[rules]] +id = "settlemint-service-access-token" +description = "Found a Settlemint Service Access Token." +regex = '''\b(sm_sat_[a-zA-Z0-9]{16})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sm_sat"] [[rules]] -description = "Shippo API token" id = "shippo-api-token" -regex = '''(?i)\b(shippo_(live|test)_[a-f0-9]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "shippo_", -] +description = "Discovered a Shippo API token, potentially compromising shipping services and customer order data." +regex = '''\b(shippo_(?:live|test)_[a-fA-F0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 +keywords = ["shippo_"] [[rules]] -description = "Shopify access token" id = "shopify-access-token" +description = "Uncovered a Shopify access token, which could lead to unauthorized e-commerce platform access and data breaches." regex = '''shpat_[a-fA-F0-9]{32}''' -keywords = [ - "shpat_", -] +entropy = 2 +keywords = ["shpat_"] [[rules]] -description = "Shopify custom access token" id = "shopify-custom-access-token" +description = "Detected a Shopify custom access token, potentially compromising custom app integrations and e-commerce data security." regex = '''shpca_[a-fA-F0-9]{32}''' -keywords = [ - "shpca_", -] +entropy = 2 +keywords = ["shpca_"] [[rules]] -description = "Shopify private app access token" id = "shopify-private-app-access-token" +description = "Identified a Shopify private app access token, risking unauthorized access to private app data and store operations." regex = '''shppa_[a-fA-F0-9]{32}''' -keywords = [ - "shppa_", -] +entropy = 2 +keywords = ["shppa_"] [[rules]] -description = "Shopify shared secret" id = "shopify-shared-secret" +description = "Found a Shopify shared secret, posing a risk to application authentication and e-commerce platform security." regex = '''shpss_[a-fA-F0-9]{32}''' -keywords = [ - "shpss_", -] +entropy = 2 +keywords = ["shpss_"] [[rules]] -description = "Sidekiq Secret" id = "sidekiq-secret" -regex = '''(?i)(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +description = "Discovered a Sidekiq Secret, which could lead to compromised background job processing and application data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:BUNDLE_ENTERPRISE__CONTRIBSYS__COM|BUNDLE_GEMS__CONTRIBSYS__COM)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-f0-9]{8}:[a-f0-9]{8})(?:[\x60'"\s;]|\\[nr]|$)''' keywords = [ - "bundle_enterprise__contribsys__com","bundle_gems__contribsys__com", + "bundle_enterprise__contribsys__com", + "bundle_gems__contribsys__com", ] [[rules]] -description = "Sidekiq Sensitive URL" id = "sidekiq-sensitive-url" -regex = '''(?i)\b(http(?:s??):\/\/)([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)''' -secretGroup = 2 +description = "Uncovered a Sidekiq Sensitive URL, potentially exposing internal job queues and sensitive operation details." +regex = '''(?i)\bhttps?://([a-f0-9]{8}:[a-f0-9]{8})@(?:gems.contribsys.com|enterprise.contribsys.com)(?:[\/|\#|\?|:]|$)''' keywords = [ - "gems.contribsys.com","enterprise.contribsys.com", + "gems.contribsys.com", + "enterprise.contribsys.com", ] [[rules]] -description = "Slack token" -id = "slack-access-token" -regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})''' +id = "slack-app-token" +description = "Detected a Slack App-level token, risking unauthorized access to Slack applications and workspace data." +regex = '''(?i)xapp-\d-[A-Z0-9]+-\d+-[a-z0-9]+''' +entropy = 2 +keywords = ["xapp"] + +[[rules]] +id = "slack-bot-token" +description = "Identified a Slack Bot token, which may compromise bot integrations and communication channel security." +regex = '''xoxb-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*''' +entropy = 3 +keywords = ["xoxb"] + +[[rules]] +id = "slack-config-access-token" +description = "Found a Slack Configuration access token, posing a risk to workspace configuration and sensitive data access." +regex = '''(?i)xoxe.xox[bp]-\d-[A-Z0-9]{163,166}''' +entropy = 2 keywords = [ - "xoxb","xoxa","xoxp","xoxr","xoxs", + "xoxe.xoxb-", + "xoxe.xoxp-", ] [[rules]] -description = "Slack Webhook" -id = "slack-web-hook" -regex = '''https:\/\/hooks.slack.com\/(services|workflows)\/[A-Za-z0-9+\/]{44,46}''' +id = "slack-config-refresh-token" +description = "Discovered a Slack Configuration refresh token, potentially allowing prolonged unauthorized access to configuration settings." +regex = '''(?i)xoxe-\d-[A-Z0-9]{146}''' +entropy = 2 +keywords = ["xoxe-"] + +[[rules]] +id = "slack-legacy-bot-token" +description = "Uncovered a Slack Legacy bot token, which could lead to compromised legacy bot operations and data exposure." +regex = '''xoxb-[0-9]{8,14}-[a-zA-Z0-9]{18,26}''' +entropy = 2 +keywords = ["xoxb"] + +[[rules]] +id = "slack-legacy-token" +description = "Detected a Slack Legacy token, risking unauthorized access to older Slack integrations and user data." +regex = '''xox[os]-\d+-\d+-\d+-[a-fA-F\d]+''' +entropy = 2 keywords = [ - "hooks.slack.com", + "xoxo", + "xoxs", ] [[rules]] -description = "Square Access Token" -id = "square-access-token" -regex = '''(?i)\b(sq0atp-[0-9A-Za-z\-_]{22})(?:['|\"|\n|\r|\s|\x60|;]|$)''' +id = "slack-legacy-workspace-token" +description = "Identified a Slack Legacy Workspace token, potentially compromising access to workspace data and legacy features." +regex = '''xox[ar]-(?:\d-)?[0-9a-zA-Z]{8,48}''' +entropy = 2 keywords = [ - "sq0atp-", + "xoxa", + "xoxr", ] [[rules]] -description = "Squarespace Access Token" -id = "squarespace-access-token" -regex = '''(?i)(?:squarespace)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +id = "slack-user-token" +description = "Found a Slack User token, posing a risk of unauthorized user impersonation and data access within Slack workspaces." +regex = '''xox[pe](?:-[0-9]{10,13}){3}-[a-zA-Z0-9-]{28,34}''' +entropy = 2 keywords = [ - "squarespace", + "xoxp-", + "xoxe-", ] [[rules]] -description = "Stripe Access Token" -id = "stripe-access-token" -regex = '''(?i)(sk|pk)_(test|live)_[0-9a-z]{10,32}''' +id = "slack-webhook-url" +description = "Discovered a Slack Webhook, which could lead to unauthorized message posting and data leakage in Slack channels." +regex = '''(?:https?://)?hooks.slack.com/(?:services|workflows|triggers)/[A-Za-z0-9+/]{43,56}''' +keywords = ["hooks.slack.com"] + +[[rules]] +id = "snyk-api-token" +description = "Uncovered a Snyk API token, potentially compromising software vulnerability scanning and code security." +regex = '''(?i)[\w.-]{0,50}?(?:snyk[_.-]?(?:(?:api|oauth)[_.-]?)?(?:key|token))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["snyk"] + +[[rules]] +id = "sonar-api-token" +description = "Uncovered a Sonar API token, potentially compromising software vulnerability scanning and code security." +regex = '''(?i)[\w.-]{0,50}?(?:sonar[_.-]?(login|token))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}((?:squ_|sqp_|sqa_)?[a-z0-9=_\-]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +secretGroup = 2 +keywords = ["sonar"] + +[[rules]] +id = "sourcegraph-access-token" +description = "Sourcegraph is a code search and navigation engine." +regex = '''(?i)\b(\b(sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40}|[a-fA-F0-9]{40})\b)(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 keywords = [ - "sk_test","pk_test","sk_live","pk_live", + "sgp_", + "sourcegraph", ] [[rules]] -description = "SumoLogic Access ID" -id = "sumologic-access-id" -regex = '''(?i)(?:sumo)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{14})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +id = "square-access-token" +description = "Detected a Square Access Token, risking unauthorized payment processing and financial transaction exposure." +regex = '''\b((?:EAAA|sq0atp-)[\w-]{22,60})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 keywords = [ - "sumo", + "sq0atp-", + "eaaa", ] [[rules]] -description = "SumoLogic Access Token" -id = "sumologic-access-token" -regex = '''(?i)(?:sumo)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{64})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 +id = "squarespace-access-token" +description = "Identified a Squarespace Access Token, which may compromise website management and content control on Squarespace." +regex = '''(?i)[\w.-]{0,50}?(?:squarespace)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["squarespace"] + +[[rules]] +id = "stripe-access-token" +description = "Found a Stripe Access Token, posing a risk to payment processing services and sensitive financial data." +regex = '''\b((?:sk|rk)_(?:test|live|prod)_[a-zA-Z0-9]{10,99})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 2 keywords = [ - "sumo", + "sk_test", + "sk_live", + "sk_prod", + "rk_test", + "rk_live", + "rk_prod", ] [[rules]] -description = "Telegram Bot API Token" +id = "sumologic-access-id" +description = "Discovered a SumoLogic Access ID, potentially compromising log management services and data analytics integrity." +regex = '''[\w.-]{0,50}?(?i:[\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \t\w.-]{0,20})[\s'"]{0,3})(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(su[a-zA-Z0-9]{12})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sumo"] + +[[rules]] +id = "sumologic-access-token" +description = "Uncovered a SumoLogic Access Token, which could lead to unauthorized access to log data and analytics insights." +regex = '''(?i)[\w.-]{0,50}?(?:(?-i:[Ss]umo|SUMO))(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{64})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3 +keywords = ["sumo"] + +[[rules]] id = "telegram-bot-api-token" -regex = '''(?i)(?:^|[^0-9])([0-9]{5,16}:A[a-zA-Z0-9_\-]{34})(?:$|[^a-zA-Z0-9_\-])''' -secretGroup = 1 -keywords = [ - "telegram","api","bot","token","url", -] +description = "Detected a Telegram Bot API Token, risking unauthorized bot operations and message interception on Telegram." +regex = '''(?i)[\w.-]{0,50}?(?:telegr)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{5,16}:(?-i:A)[a-z0-9_\-]{34})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["telegr"] [[rules]] -description = "Travis CI Access Token" id = "travisci-access-token" -regex = '''(?i)(?:travis)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{22})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "travis", -] +description = "Identified a Travis CI Access Token, potentially compromising continuous integration services and codebase security." +regex = '''(?i)[\w.-]{0,50}?(?:travis)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{22})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["travis"] [[rules]] -description = "Twilio API Key" id = "twilio-api-key" +description = "Found a Twilio API Key, posing a risk to communication services and sensitive customer interaction data." regex = '''SK[0-9a-fA-F]{32}''' -keywords = [ - "twilio", -] +entropy = 3 +keywords = ["sk"] [[rules]] -description = "Twitch API token" id = "twitch-api-token" -regex = '''(?i)(?:twitch)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{30})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "twitch", -] +description = "Discovered a Twitch API token, which could compromise streaming services and account integrations." +regex = '''(?i)[\w.-]{0,50}?(?:twitch)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{30})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitch"] [[rules]] -description = "Twitter Access Secret" id = "twitter-access-secret" -regex = '''(?i)(?:twitter)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{45})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "twitter", -] +description = "Uncovered a Twitter Access Secret, potentially risking unauthorized Twitter integrations and data breaches." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{45})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] [[rules]] -description = "Twitter Access Token" id = "twitter-access-token" -regex = '''(?i)(?:twitter)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([0-9]{15,25}-[a-zA-Z0-9]{20,40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "twitter", -] +description = "Detected a Twitter Access Token, posing a risk of unauthorized account operations and social media data exposure." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([0-9]{15,25}-[a-zA-Z0-9]{20,40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] [[rules]] -description = "Twitter API Key" id = "twitter-api-key" -regex = '''(?i)(?:twitter)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{25})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "twitter", -] +description = "Identified a Twitter API Key, which may compromise Twitter application integrations and user data security." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{25})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] [[rules]] -description = "Twitter API Secret" id = "twitter-api-secret" -regex = '''(?i)(?:twitter)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{50})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "twitter", -] +description = "Found a Twitter API Secret, risking the security of Twitter app integrations and sensitive data access." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{50})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] [[rules]] -description = "Twitter Bearer Token" id = "twitter-bearer-token" -regex = '''(?i)(?:twitter)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(A{22}[a-zA-Z0-9%]{80,100})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "twitter", -] +description = "Discovered a Twitter Bearer Token, potentially compromising API access and data retrieval from Twitter." +regex = '''(?i)[\w.-]{0,50}?(?:twitter)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(A{22}[a-zA-Z0-9%]{80,100})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["twitter"] [[rules]] -description = "Typeform API token" id = "typeform-api-token" -regex = '''(?i)(?:typeform)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(tfp_[a-z0-9\-_\.=]{59})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "tfp_", -] +description = "Uncovered a Typeform API token, which could lead to unauthorized survey management and data collection." +regex = '''(?i)[\w.-]{0,50}?(?:typeform)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(tfp_[a-z0-9\-_\.=]{59})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["tfp_"] [[rules]] -description = "Vault Batch Token" id = "vault-batch-token" -regex = '''(?i)\b(hvb\.[a-z0-9_-]{138,212})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -keywords = [ - "hvb", -] +description = "Detected a Vault Batch Token, risking unauthorized access to secret management services and sensitive data." +regex = '''\b(hvb\.[\w-]{138,300})(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 4 +keywords = ["hvb."] [[rules]] -description = "Vault Service Token" id = "vault-service-token" -regex = '''(?i)\b(hvs\.[a-z0-9_-]{90,100})(?:['|\"|\n|\r|\s|\x60|;]|$)''' +description = "Identified a Vault Service Token, potentially compromising infrastructure security and access to sensitive credentials." +regex = '''\b((?:hvs\.[\w-]{90,120}|s\.(?i:[a-z0-9]{24})))(?:[\x60'"\s;]|\\[nr]|$)''' +entropy = 3.5 keywords = [ - "hvs", + "hvs.", + "s.", +] +[[rules.allowlists]] +regexes = [ + '''s\.[A-Za-z]{24}''', ] [[rules]] -description = "Yandex Access Token" id = "yandex-access-token" -regex = '''(?i)(?:yandex)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(t1\.[A-Z0-9a-z_-]+[=]{0,2}\.[A-Z0-9a-z_-]{86}[=]{0,2})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "yandex", -] +description = "Found a Yandex Access Token, posing a risk to Yandex service integrations and user data privacy." +regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(t1\.[A-Z0-9a-z_-]+[=]{0,2}\.[A-Z0-9a-z_-]{86}[=]{0,2})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["yandex"] [[rules]] -description = "Yandex API Key" id = "yandex-api-key" -regex = '''(?i)(?:yandex)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(AQVN[A-Za-z0-9_\-]{35,38})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "yandex", -] +description = "Discovered a Yandex API Key, which could lead to unauthorized access to Yandex services and data manipulation." +regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(AQVN[A-Za-z0-9_\-]{35,38})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["yandex"] [[rules]] -description = "Yandex AWS Access Token" id = "yandex-aws-access-token" -regex = '''(?i)(?:yandex)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}(YC[a-zA-Z0-9_\-]{38})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "yandex", -] +description = "Uncovered a Yandex AWS Access Token, potentially compromising cloud resource access and data security on Yandex Cloud." +regex = '''(?i)[\w.-]{0,50}?(?:yandex)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}(YC[a-zA-Z0-9_\-]{38})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["yandex"] [[rules]] -description = "Zendesk Secret Key" id = "zendesk-secret-key" -regex = '''(?i)(?:zendesk)(?:[0-9a-z\-_\t .]{0,20})(?:[\s|']|[\s|"]){0,3}(?:=|>|:=|\|\|:|<=|=>|:)(?:'|\"|\s|=|\x60){0,5}([a-z0-9]{40})(?:['|\"|\n|\r|\s|\x60|;]|$)''' -secretGroup = 1 -keywords = [ - "zendesk", -] +description = "Detected a Zendesk Secret Key, risking unauthorized access to customer support services and sensitive ticketing data." +regex = '''(?i)[\w.-]{0,50}?(?:zendesk)(?:[ \t\w.-]{0,20})[\s'"]{0,3}(?:=|>|:{1,3}=|\|\||:|=>|\?=|,)[\x60'"\s=]{0,5}([a-z0-9]{40})(?:[\x60'"\s;]|\\[nr]|$)''' +keywords = ["zendesk"] + diff --git a/.gitleaksignore b/.gitleaksignore index c8672afc0..68584e487 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -3,3 +3,7 @@ 1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:73 1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:74 8bc1e85075f73906767652ab35d5563efce2a931:packages/mobile-sdk-alpha/src/animations/passport_verify.json:aws-access-token:6 +f506113a22e5b147132834e4659f5af308448389:app/tests/utils/deeplinks.test.ts:generic-api-key:183 +5a67b5cc50f291401d1da4e51706d0cfcf1c2316:app/tests/utils/deeplinks.test.ts:generic-api-key:182 +0e4555eee6589aa9cca68f451227b149277d8c90:app/tests/src/utils/points/api.test.ts:generic-api-key:34 +feb433e3553f8a7fa6c724b2de5a3e32ef079880:app/ios/Podfile.lock:generic-api-key:2594 diff --git a/app/App.tsx b/app/App.tsx index acacb0812..753099f86 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -5,8 +5,19 @@ // CI/CD Pipeline Test - July 31, 2025 - With Permissions Fix import { Buffer } from 'buffer'; import React from 'react'; +import { Platform } from 'react-native'; import { YStack } from 'tamagui'; +import type { + TurnkeyCallbacks, + TurnkeyProviderConfig, +} from '@turnkey/react-native-wallet-kit'; +import { TurnkeyProvider } from '@turnkey/react-native-wallet-kit'; +import { + TURNKEY_AUTH_PROXY_CONFIG_ID, + TURNKEY_GOOGLE_CLIENT_ID, + TURNKEY_ORGANIZATION_ID, +} from './env'; import ErrorBoundary from './src/components/ErrorBoundary'; import AppNavigation from './src/navigation'; import { AuthProvider } from './src/providers/authProvider'; @@ -18,11 +29,63 @@ import { PassportProvider } from './src/providers/passportDataProvider'; import { RemoteConfigProvider } from './src/providers/remoteConfigProvider'; import { SelfClientProvider } from './src/providers/selfClientProvider'; import { initSentry, wrapWithSentry } from './src/Sentry'; +import { + TURNKEY_OAUTH_REDIRECT_URI_ANDROID, + TURNKEY_OAUTH_REDIRECT_URI_IOS, +} from './src/utils/constants'; + +import 'react-native-get-random-values'; +import 'react-native-url-polyfill/auto'; +import '@walletconnect/react-native-compat'; +import '@noble/curves/p256'; +import 'sha256-uint8array'; +import '@turnkey/encoding'; +import '@turnkey/api-key-stamper'; initSentry(); global.Buffer = Buffer; +export const TURNKEY_CALLBACKS: TurnkeyCallbacks = { + beforeSessionExpiry: ({ sessionKey }) => { + console.log('[Turnkey] Session nearing expiry:', sessionKey); + }, + onSessionExpired: ({ sessionKey }) => { + console.log('[Turnkey] Session expired:', sessionKey); + }, + onAuthenticationSuccess: ({ action, method, identifier }) => { + // console.log('[Turnkey] Auth success:', { action, method, identifier }); + }, + onError: error => { + console.error('[Turnkey] Error:', error); + }, +}; + +export const TURNKEY_CONFIG: TurnkeyProviderConfig = { + organizationId: TURNKEY_ORGANIZATION_ID!, + authProxyConfigId: TURNKEY_AUTH_PROXY_CONFIG_ID!, + autoRefreshManagedState: false, + auth: { + passkey: false, + oauth: { + // Should use custom scheme, NOT 'https' for IOS + appScheme: + Platform.OS === 'ios' ? 'com.warroom.proofofpassport' : 'https', + redirectUri: + Platform.OS === 'ios' + ? TURNKEY_OAUTH_REDIRECT_URI_IOS + : TURNKEY_OAUTH_REDIRECT_URI_ANDROID, + google: { + clientId: TURNKEY_GOOGLE_CLIENT_ID!, + redirectUri: + Platform.OS === 'ios' + ? TURNKEY_OAUTH_REDIRECT_URI_IOS + : TURNKEY_OAUTH_REDIRECT_URI_ANDROID, + }, + }, + }, +}; + function App(): React.JSX.Element { return ( @@ -35,7 +98,12 @@ function App(): React.JSX.Element { - + + + diff --git a/app/Gemfile.lock b/app/Gemfile.lock index 2af770c21..11ae3055a 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -34,7 +34,7 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.116.0) + aws-sdk-kms (1.117.0) aws-sdk-core (~> 3, >= 3.234.0) aws-sigv4 (~> 1.5) aws-sdk-s3 (1.203.0) @@ -225,14 +225,14 @@ GEM i18n (1.14.7) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.15.2) + json (2.16.0) jwt (2.10.2) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.26.0) + minitest (5.26.1) molinillo (0.8.0) multi_json (1.17.0) multipart-post (2.4.1) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 8947a5f5f..49e53be83 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -125,12 +125,17 @@ android { preDexLibraries false } + buildFeatures { + buildConfig = true + viewBinding = true + } + defaultConfig { applicationId "com.proofofpassportapp" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 113 - versionName "2.7.3" + versionCode 117 + versionName "2.7.4" manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp'] externalNativeBuild { cmake { diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index b9d458c97..313a1c1b2 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,13 @@ xmlns:tools="http://schemas.android.com/tools" > + + + + + + + diff --git a/app/android/app/src/main/assets/fonts/DINOT-Bold.otf b/app/android/app/src/main/assets/fonts/DINOT-Bold.otf new file mode 100644 index 000000000..62ffe412e Binary files /dev/null and b/app/android/app/src/main/assets/fonts/DINOT-Bold.otf differ diff --git a/app/android/build.gradle b/app/android/build.gradle index 9066104ed..69ce7283b 100644 --- a/app/android/build.gradle +++ b/app/android/build.gradle @@ -4,11 +4,11 @@ buildscript { ext { buildToolsVersion = "35.0.0" minSdkVersion = 24 - compileSdkVersion = 35 + compileSdkVersion = 36 targetSdkVersion = 35 // Updated NDK to support 16k page size devices ndkVersion = "27.0.12077973" - kotlinVersion = "1.9.24" + kotlinVersion = "2.0.21" firebaseMessagingVersion = "23.4.0" firebaseBomVersion = "32.7.3" } diff --git a/app/babel.config.cjs b/app/babel.config.cjs index adc5689a0..44824a90e 100644 --- a/app/babel.config.cjs +++ b/app/babel.config.cjs @@ -13,6 +13,7 @@ module.exports = { }, ], ['@babel/plugin-transform-private-methods', { loose: true }], + '@babel/plugin-transform-export-namespace-from', [ 'module:react-native-dotenv', { diff --git a/app/env.ts b/app/env.ts index f89e3ebc9..de7072800 100644 --- a/app/env.ts +++ b/app/env.ts @@ -28,3 +28,9 @@ export const IS_TEST_BUILD = process.env.IS_TEST_BUILD === 'true'; export const MIXPANEL_NFC_PROJECT_TOKEN = undefined; export const SEGMENT_KEY = process.env.SEGMENT_KEY; export const SENTRY_DSN = process.env.SENTRY_DSN; + +export const TURNKEY_AUTH_PROXY_CONFIG_ID = + process.env.TURNKEY_AUTH_PROXY_CONFIG_ID; + +export const TURNKEY_GOOGLE_CLIENT_ID = process.env.TURNKEY_GOOGLE_CLIENT_ID; +export const TURNKEY_ORGANIZATION_ID = process.env.TURNKEY_ORGANIZATION_ID; diff --git a/app/fastlane/README.md b/app/fastlane/README.md index 6f9ce936e..c643d8023 100644 --- a/app/fastlane/README.md +++ b/app/fastlane/README.md @@ -21,7 +21,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do [bundle exec] fastlane ios sync_version ``` -Sync ios version +Sync ios version (DEPRECATED) ### ios internal_test @@ -50,7 +50,7 @@ Deploy iOS app with automatic version management [bundle exec] fastlane android sync_version ``` -Sync android version +Sync android version (DEPRECATED) ### android internal_test diff --git a/app/ios/OpenPassport/Info.plist b/app/ios/OpenPassport/Info.plist index 665dfb497..906d346ed 100644 --- a/app/ios/OpenPassport/Info.plist +++ b/app/ios/OpenPassport/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.7.3 + 2.7.4 CFBundleSignature ???? CFBundleURLTypes @@ -30,6 +30,7 @@ CFBundleURLSchemes proofofpassport + com.warroom.proofofpassport @@ -39,6 +40,11 @@ LSApplicationCategoryType + LSApplicationQueriesSchemes + + whatsapp + sms + LSRequiresIPhoneOS NFCReaderUsageDescription @@ -63,6 +69,7 @@ UIAppFonts Advercase-Regular.otf + DINOT-Bold.otf DINOT-Medium.otf IBMPlexMono-Regular.otf diff --git a/app/ios/OpenPassport/OpenPassport.entitlements b/app/ios/OpenPassport/OpenPassport.entitlements index 1e81c4e00..083124efd 100644 --- a/app/ios/OpenPassport/OpenPassport.entitlements +++ b/app/ios/OpenPassport/OpenPassport.entitlements @@ -15,6 +15,7 @@ appclips:appclip.openpassport.app applinks:proofofpassport-merkle-tree.xyz applinks:redirect.self.xyz + webcredentials:redirect.self.xyz com.apple.developer.icloud-container-identifiers diff --git a/app/ios/OpenPassport/OpenPassportDebug.entitlements b/app/ios/OpenPassport/OpenPassportDebug.entitlements index be4b119cb..b06b7a62e 100644 --- a/app/ios/OpenPassport/OpenPassportDebug.entitlements +++ b/app/ios/OpenPassport/OpenPassportDebug.entitlements @@ -15,6 +15,7 @@ appclips:appclip.openpassport.app applinks:proofofpassport-merkle-tree.xyz applinks:redirect.self.xyz + webcredentials:redirect.self.xyz com.apple.developer.icloud-container-identifiers diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 84084a941..9c5dd6426 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -1512,12 +1512,54 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-compat (2.22.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-get-random-values (1.11.0): - React-Core - react-native-netinfo (11.4.1): - React-Core - react-native-nfc-manager (3.16.3): - React-Core + - react-native-passkey (3.3.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.10.14.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-safe-area-context (5.6.1): - DoubleConversion - glog @@ -1956,6 +1998,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNInAppBrowser (3.7.0): + - React-Core - RNKeychain (10.0.0): - DoubleConversion - glog @@ -2087,7 +2131,7 @@ PODS: - ReactCommon/turbomodule/core - Sentry/HybridSDK (= 8.53.2) - Yoga - - RNSVG (15.12.1): + - RNSVG (15.14.0): - DoubleConversion - glog - hermes-engine @@ -2107,9 +2151,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.12.1) + - RNSVG/common (= 15.14.0) - Yoga - - RNSVG/common (15.12.1): + - RNSVG/common (15.14.0): - DoubleConversion - glog - hermes-engine @@ -2194,9 +2238,11 @@ DEPENDENCIES: - react-native-biometrics (from `../node_modules/react-native-biometrics`) - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - react-native-cloud-storage (from `../node_modules/react-native-cloud-storage`) + - "react-native-compat (from `../node_modules/@walletconnect/react-native-compat`)" - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-nfc-manager (from `../node_modules/react-native-nfc-manager`) + - react-native-passkey (from `../node_modules/react-native-passkey`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-sqlite-storage (from `../node_modules/react-native-sqlite-storage`) - react-native-webview (from `../node_modules/react-native-webview`) @@ -2234,6 +2280,7 @@ DEPENDENCIES: - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" - "RNFBRemoteConfig (from `../node_modules/@react-native-firebase/remote-config`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNInAppBrowser (from `../node_modules/react-native-inappbrowser-reborn`) - RNKeychain (from `../node_modules/react-native-keychain`) - RNLocalize (from `../node_modules/react-native-localize`) - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) @@ -2360,12 +2407,16 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/blur" react-native-cloud-storage: :path: "../node_modules/react-native-cloud-storage" + react-native-compat: + :path: "../node_modules/@walletconnect/react-native-compat" react-native-get-random-values: :path: "../node_modules/react-native-get-random-values" react-native-netinfo: :path: "../node_modules/@react-native-community/netinfo" react-native-nfc-manager: :path: "../node_modules/react-native-nfc-manager" + react-native-passkey: + :path: "../node_modules/react-native-passkey" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" react-native-sqlite-storage: @@ -2440,6 +2491,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/remote-config" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" + RNInAppBrowser: + :path: "../node_modules/react-native-inappbrowser-reborn" RNKeychain: :path: "../node_modules/react-native-keychain" RNLocalize: @@ -2534,9 +2587,11 @@ SPEC CHECKSUMS: react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d react-native-blur: 6334d934a9b5e67718b8f5725c44cc0a12946009 react-native-cloud-storage: 8d89f2bc574cf11068dfd90933905974087fb9e9 + react-native-compat: d4842cf7cbc566c37a14f368cebffa0c1f4150b7 react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 react-native-nfc-manager: 66a00e5ddab9704efebe19d605b1b8afb0bb1bd7 + react-native-passkey: 8853c3c635164864da68a6dbbcec7148506c3bcf react-native-safe-area-context: 90a89cb349c7f8168a707e6452288c2f665b9fd1 react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed react-native-webview: 3f45e19f0ffc3701168768a6c37695e0f252410e @@ -2574,12 +2629,13 @@ SPEC CHECKSUMS: RNFBMessaging: 92325b0d5619ac90ef023a23cfd16fd3b91d0a88 RNFBRemoteConfig: a569bacaa410acfcaba769370e53a787f80fd13b RNGestureHandler: a63b531307e5b2e6ea21d053a1a7ad4cf9695c57 + RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20 RNKeychain: 471ceef8c13f15a5534c3cd2674dbbd9d0680e52 RNLocalize: 7683e450496a5aea9a2dab3745bfefa7341d3f5e RNReactNativeHapticFeedback: e526ac4a7ca9fb23c7843ea4fd7d823166054c73 RNScreens: 806e1449a8ec63c2a4e4cf8a63cc80203ccda9b8 RNSentry: 6ad982be2c8e32dab912afb4132b6a0d88484ea0 - RNSVG: 0c1fc3e7b147949dc15644845e9124947ac8c9bb + RNSVG: e1cf5a9a5aa12c69f2ec47031defbd87ae7fb697 segment-analytics-react-native: a0c29c75ede1989118b50cac96b9495ea5c91a1d Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index 704a618d5..f2bf9db59 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ E9F9A99C2D57FE2900E1362E /* PassportOCRViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9F9A99A2D57FE2900E1362E /* PassportOCRViewManager.m */; }; E9F9A99D2D57FE2900E1362E /* PassportOCRViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F9A99B2D57FE2900E1362E /* PassportOCRViewManager.swift */; }; EBECCA4983EC6929A7722578 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E56E082698598B41447667BB /* PrivacyInfo.xcprivacy */; }; + F3A8B2C9D4E5F6A7B8C9D0E1 /* DINOT-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F2 /* DINOT-Bold.otf */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -72,6 +73,7 @@ 905B70062A72774000AFA232 /* PassportReader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PassportReader.m; sourceTree = ""; }; 905B70082A729CD400AFA232 /* OpenPassport.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = OpenPassport.entitlements; path = OpenPassport/OpenPassport.entitlements; sourceTree = ""; }; 9BF744D9A73A4BAC96EC569A /* DINOT-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "DINOT-Medium.otf"; path = "../src/assets/fonts/DINOT-Medium.otf"; sourceTree = ""; }; + A1B2C3D4E5F6A7B8C9D0E1F2 /* DINOT-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "DINOT-Bold.otf"; path = "../src/assets/fonts/DINOT-Bold.otf"; sourceTree = ""; }; AE6147EB2DC95A8D00445C0F /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "GoogleService-Info.plist"; sourceTree = ""; }; B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "IBMPlexMono-Regular.otf"; path = "../src/assets/fonts/IBMPlexMono-Regular.otf"; sourceTree = SOURCE_ROOT; }; BF1044802DD53540009B3688 /* LiveMRZScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveMRZScannerView.swift; sourceTree = ""; }; @@ -155,8 +157,9 @@ isa = PBXGroup; children = ( 7E5C3CEF7EDA4871B3D0EBE1 /* Advercase-Regular.otf */, - B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */, + A1B2C3D4E5F6A7B8C9D0E1F2 /* DINOT-Bold.otf */, 9BF744D9A73A4BAC96EC569A /* DINOT-Medium.otf */, + B49D2B102E28AA7900946F64 /* IBMPlexMono-Regular.otf */, ); name = Resources; sourceTree = ""; @@ -270,8 +273,9 @@ AE6147EC2DC95A8D00445C0F /* GoogleService-Info.plist in Resources */, EBECCA4983EC6929A7722578 /* PrivacyInfo.xcprivacy in Resources */, DAC618BCA5874DD8AD74FFFC /* Advercase-Regular.otf in Resources */, - B49D2B112E28AA7900946F64 /* IBMPlexMono-Regular.otf in Resources */, + F3A8B2C9D4E5F6A7B8C9D0E1 /* DINOT-Bold.otf in Resources */, D427791AA5714251A5EAF8AD /* DINOT-Medium.otf in Resources */, + B49D2B112E28AA7900946F64 /* IBMPlexMono-Regular.otf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -542,7 +546,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/MoproKit/Libs", ); - MARKETING_VERSION = 2.7.3; + MARKETING_VERSION = 2.7.4; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -682,7 +686,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/MoproKit/Libs", ); - MARKETING_VERSION = 2.7.3; + MARKETING_VERSION = 2.7.4; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/app/jest.setup.js b/app/jest.setup.js index 9687871b3..492a2fa78 100644 --- a/app/jest.setup.js +++ b/app/jest.setup.js @@ -84,6 +84,16 @@ jest.mock('react-native/Libraries/TurboModule/TurboModuleRegistry', () => ({ }), }; } + if (name === 'RNDeviceInfo') { + return { + getConstants: () => ({ + Dimensions: { + window: { width: 375, height: 667, scale: 2 }, + screen: { width: 375, height: 667, scale: 2 }, + }, + }), + }; + } return { getConstants: () => ({}), }; @@ -134,6 +144,27 @@ jest.mock( { virtual: true }, ); +// Mock @turnkey/react-native-wallet-kit to prevent loading of problematic dependencies +jest.mock( + '@turnkey/react-native-wallet-kit', + () => ({ + AuthState: { + Authenticated: 'Authenticated', + Unauthenticated: 'Unauthenticated', + }, + useTurnkey: jest.fn(() => ({ + handleGoogleOauth: jest.fn(), + fetchWallets: jest.fn().mockResolvedValue([]), + exportWallet: jest.fn(), + importWallet: jest.fn(), + authState: 'Unauthenticated', + logout: jest.fn(), + })), + TurnkeyProvider: ({ children }) => children, + }), + { virtual: true }, +); + // Mock the mobile-sdk-alpha's TurboModuleRegistry to prevent native module errors jest.mock( '../packages/mobile-sdk-alpha/node_modules/react-native/Libraries/TurboModule/TurboModuleRegistry', @@ -239,6 +270,16 @@ jest.mock('react-native/src/private/specs/modules/NativeDeviceInfo', () => ({ })), })); +// Mock NativeStatusBarManagerIOS for react-native-edge-to-edge SystemBars +jest.mock( + 'react-native/src/private/specs/modules/NativeStatusBarManagerIOS', + () => ({ + setStyle: jest.fn(), + setHidden: jest.fn(), + setNetworkActivityIndicatorVisible: jest.fn(), + }), +); + // Mock react-native-gesture-handler to prevent getConstants errors jest.mock('react-native-gesture-handler', () => { const RN = jest.requireActual('react-native'); @@ -267,19 +308,76 @@ jest.mock('react-native-safe-area-context', () => { // Mock NativeEventEmitter to prevent null argument errors jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => { - return class MockNativeEventEmitter { - constructor(nativeModule) { - // Accept any nativeModule argument (including null/undefined) - this.nativeModule = nativeModule; - } + function MockNativeEventEmitter(nativeModule) { + // Accept any nativeModule argument (including null/undefined) + this.nativeModule = nativeModule; + this.addListener = jest.fn(); + this.removeListener = jest.fn(); + this.removeAllListeners = jest.fn(); + this.emit = jest.fn(); + } - addListener = jest.fn(); - removeListener = jest.fn(); - removeAllListeners = jest.fn(); - emit = jest.fn(); - }; + // The mock needs to be the constructor itself, not wrapped + MockNativeEventEmitter.default = MockNativeEventEmitter; + return MockNativeEventEmitter; }); +// Mock react-native-device-info to prevent NativeEventEmitter errors +jest.mock('react-native-device-info', () => ({ + getUniqueId: jest.fn().mockResolvedValue('mock-device-id'), + getReadableVersion: jest.fn().mockReturnValue('1.0.0'), + getVersion: jest.fn().mockReturnValue('1.0.0'), + getBuildNumber: jest.fn().mockReturnValue('1'), + getModel: jest.fn().mockReturnValue('mock-model'), + getBrand: jest.fn().mockReturnValue('mock-brand'), + isTablet: jest.fn().mockReturnValue(false), + isLandscape: jest.fn().mockResolvedValue(false), + getSystemVersion: jest.fn().mockReturnValue('14.0'), + getSystemName: jest.fn().mockReturnValue('iOS'), + default: { + getUniqueId: jest.fn().mockResolvedValue('mock-device-id'), + getReadableVersion: jest.fn().mockReturnValue('1.0.0'), + getVersion: jest.fn().mockReturnValue('1.0.0'), + getBuildNumber: jest.fn().mockReturnValue('1'), + getModel: jest.fn().mockReturnValue('mock-model'), + getBrand: jest.fn().mockReturnValue('mock-brand'), + isTablet: jest.fn().mockReturnValue(false), + isLandscape: jest.fn().mockResolvedValue(false), + getSystemVersion: jest.fn().mockReturnValue('14.0'), + getSystemName: jest.fn().mockReturnValue('iOS'), + }, +})); + +// Mock react-native-device-info nested in @turnkey/react-native-wallet-kit +jest.mock( + 'node_modules/@turnkey/react-native-wallet-kit/node_modules/react-native-device-info', + () => ({ + getUniqueId: jest.fn().mockResolvedValue('mock-device-id'), + getReadableVersion: jest.fn().mockReturnValue('1.0.0'), + getVersion: jest.fn().mockReturnValue('1.0.0'), + getBuildNumber: jest.fn().mockReturnValue('1'), + getModel: jest.fn().mockReturnValue('mock-model'), + getBrand: jest.fn().mockReturnValue('mock-brand'), + isTablet: jest.fn().mockReturnValue(false), + isLandscape: jest.fn().mockResolvedValue(false), + getSystemVersion: jest.fn().mockReturnValue('14.0'), + getSystemName: jest.fn().mockReturnValue('iOS'), + default: { + getUniqueId: jest.fn().mockResolvedValue('mock-device-id'), + getReadableVersion: jest.fn().mockReturnValue('1.0.0'), + getVersion: jest.fn().mockReturnValue('1.0.0'), + getBuildNumber: jest.fn().mockReturnValue('1'), + getModel: jest.fn().mockReturnValue('mock-model'), + getBrand: jest.fn().mockReturnValue('mock-brand'), + isTablet: jest.fn().mockReturnValue(false), + isLandscape: jest.fn().mockResolvedValue(false), + getSystemVersion: jest.fn().mockReturnValue('14.0'), + getSystemName: jest.fn().mockReturnValue('iOS'), + }, + }), + { virtual: true }, +); + // Mock problematic mobile-sdk-alpha components that use React Native StyleSheet jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ NFCScannerScreen: jest.fn(() => null), diff --git a/app/metro.config.cjs b/app/metro.config.cjs index 74fcb51fd..9c5198d8e 100644 --- a/app/metro.config.cjs +++ b/app/metro.config.cjs @@ -192,6 +192,73 @@ const config = { // Handle problematic package exports and Node.js modules + // Fix @turnkey/encoding to use CommonJS instead of ESM + if (moduleName === '@turnkey/encoding') { + const filePath = path.resolve( + projectRoot, + 'node_modules/@turnkey/encoding/dist/index.js', + ); + return { + type: 'sourceFile', + filePath, + }; + } + + // Fix @turnkey/encoding submodules to use CommonJS + if (moduleName.startsWith('@turnkey/encoding/')) { + const subpath = moduleName.replace('@turnkey/encoding/', ''); + const filePath = path.resolve( + projectRoot, + `node_modules/@turnkey/encoding/dist/${subpath}.js`, + ); + return { + type: 'sourceFile', + filePath, + }; + } + + // Fix @turnkey/api-key-stamper to use CommonJS instead of ESM + if (moduleName === '@turnkey/api-key-stamper') { + const filePath = path.resolve( + projectRoot, + 'node_modules/@turnkey/api-key-stamper/dist/index.js', + ); + return { + type: 'sourceFile', + filePath, + }; + } + + // Fix @turnkey/api-key-stamper dynamic imports by resolving submodules statically + if (moduleName.startsWith('@turnkey/api-key-stamper/')) { + const subpath = moduleName.replace('@turnkey/api-key-stamper/', ''); + const filePath = path.resolve( + projectRoot, + `node_modules/@turnkey/api-key-stamper/dist/${subpath}`, + ); + return { + type: 'sourceFile', + filePath, + }; + } + + // Fix viem dynamic import resolution + if (moduleName === 'viem') { + try { + // Viem uses package exports, so we need to resolve to the actual file path + const viemPath = path.resolve( + projectRoot, + 'node_modules/viem/_cjs/index.js', + ); + return { + type: 'sourceFile', + filePath: viemPath, + }; + } catch (error) { + console.warn('Failed to resolve viem:', error); + } + } + // Fix @tamagui/config v2-native export resolution if (moduleName === '@tamagui/config/v2-native') { try { diff --git a/app/package.json b/app/package.json index 79b29b10a..578e11543 100644 --- a/app/package.json +++ b/app/package.json @@ -43,6 +43,7 @@ "mobile-local-deploy:android": "FORCE_UPLOAD_LOCAL_DEV=true node scripts/mobile-deploy-confirm.cjs android", "mobile-local-deploy:ios": "FORCE_UPLOAD_LOCAL_DEV=true node scripts/mobile-deploy-confirm.cjs ios", "nice": "sh -c 'if [ -z \"$SKIP_BUILD_DEPS\" ]; then yarn build:deps; fi; yarn imports:fix && yarn lint:fix && yarn fmt:fix'", + "postinstall": "npx patch-package --patch-dir ../patches || true", "reinstall": "yarn --top-level run reinstall-app", "release": "./scripts/release.sh", "release:major": "./scripts/release.sh major", @@ -107,11 +108,18 @@ "@tamagui/config": "1.126.14", "@tamagui/lucide-icons": "1.126.14", "@tamagui/toast": "1.126.14", + "@turnkey/api-key-stamper": "^0.5.0", + "@turnkey/encoding": "^0.6.0", + "@turnkey/react-native-wallet-kit": "1.0.0", + "@walletconnect/react-native-compat": "^2.22.4", "@xstate/react": "^5.0.3", "asn1js": "^3.0.6", + "axios": "^1.13.2", + "buffer": "^6.0.3", "country-emoji": "^1.5.6", "elliptic": "^6.6.1", "ethers": "^6.11.0", + "expo-application": "^7.0.7", "expo-modules-core": "^2.2.1", "hash.js": "^1.1.7", "js-sha1": "^0.7.0", @@ -135,16 +143,19 @@ "react-native-gesture-handler": "2.19.0", "react-native-get-random-values": "^1.11.0", "react-native-haptic-feedback": "^2.3.3", + "react-native-inappbrowser-reborn": "^3.7.0", "react-native-keychain": "^10.0.0", "react-native-localize": "^3.5.2", "react-native-logs": "^5.3.0", "react-native-nfc-manager": "3.16.3", + "react-native-passkey": "^3.3.1", "react-native-passport-reader": "1.0.3", - "react-native-safe-area-context": "5.6.1", + "react-native-safe-area-context": "^5.6.1", "react-native-screens": "4.15.3", "react-native-sqlite-storage": "^6.0.1", - "react-native-svg": "15.12.1", + "react-native-svg": "^15.14.0", "react-native-svg-web": "^1.0.9", + "react-native-url-polyfill": "^3.0.0", "react-native-web": "^0.19.0", "react-native-webview": "^13.16.0", "react-qr-barcode-scanner": "^2.1.8", @@ -156,6 +167,7 @@ }, "devDependencies": { "@babel/core": "^7.28.3", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/preset-env": "^7.28.3", "@react-native-community/cli": "^16.0.3", @@ -185,7 +197,6 @@ "@typescript-eslint/parser": "^8.39.0", "@vitejs/plugin-react-swc": "^3.10.2", "babel-plugin-module-resolver": "^5.0.2", - "buffer": "^6.0.3", "constants-browserify": "^1.0.0", "dompurify": "^3.2.6", "eslint": "^8.57.0", diff --git a/app/scripts/bundle-analyze-ci.cjs b/app/scripts/bundle-analyze-ci.cjs index 309c85ee5..914cf6afd 100755 --- a/app/scripts/bundle-analyze-ci.cjs +++ b/app/scripts/bundle-analyze-ci.cjs @@ -17,8 +17,8 @@ if (!platform || !['android', 'ios'].includes(platform)) { // Bundle size thresholds in MB - easy to update! const BUNDLE_THRESHOLDS_MB = { // TODO: fix temporary bundle bump - ios: 42, - android: 42, + ios: 44, + android: 44, }; function formatBytes(bytes) { diff --git a/app/src/assets/fonts/DINOT-Bold.otf b/app/src/assets/fonts/DINOT-Bold.otf new file mode 100644 index 000000000..62ffe412e Binary files /dev/null and b/app/src/assets/fonts/DINOT-Bold.otf differ diff --git a/app/src/components/NavBar/HomeNavBar.tsx b/app/src/components/NavBar/HomeNavBar.tsx index 6831891e2..85b1bba92 100644 --- a/app/src/components/NavBar/HomeNavBar.tsx +++ b/app/src/components/NavBar/HomeNavBar.tsx @@ -13,9 +13,9 @@ import type { SelfApp } from '@selfxyz/common/utils/appType'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { NavBar } from '@/components/NavBar/BaseNavBar'; -import ActivityIcon from '@/images/icons/activity.svg'; +import CogHollowIcon from '@/images/icons/cog_hollow.svg'; +import PlusCircleIcon from '@/images/icons/plus_circle.svg'; import ScanIcon from '@/images/icons/qr_scan.svg'; -import SettingsIcon from '@/images/icons/settings.svg'; import { black, charcoal, slate50 } from '@/utils/colors'; import { extraYPadding } from '@/utils/constants'; import { buttonTap } from '@/utils/haptic'; @@ -38,9 +38,7 @@ export const HomeNavBar = (props: NativeStackHeaderProps) => { const response = await fetch( `https://api.self.xyz/consume-deferred-linking-token?token=${content}`, ); - console.log('Consume token response:', response); const result = await response.json(); - console.log('Consume token result:', result); if (result.status !== 'success') { throw new Error( `Failed to consume token: ${result.message || 'Unknown error'}`, @@ -110,18 +108,18 @@ export const HomeNavBar = (props: NativeStackHeaderProps) => { size={'$3'} unstyled icon={ - + } onPress={() => { buttonTap(); - props.navigation.navigate('ProofHistory'); + props.navigation.navigate('CountryPicker'); }} /> + + + + ); +}; + +const styles = StyleSheet.create({ + pointsCard: { + backgroundColor: white, + borderRadius: 10, + borderWidth: 1, + borderColor: slate200, + overflow: 'hidden', + }, + pointsCardContent: { + paddingVertical: 30, + paddingHorizontal: 40, + alignItems: 'center', + gap: 20, + }, + logoContainer: { + width: 68, + height: 68, + borderRadius: 12, + borderWidth: 1, + borderColor: slate200, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: white, + }, + pointsTitle: { + color: black, + textAlign: 'center', + fontFamily: 'DIN OT', + fontWeight: '500', + fontSize: 32, + lineHeight: 32, + letterSpacing: -1, + }, + pointsDescription: { + color: black, + fontFamily: 'DIN OT', + fontSize: 18, + fontWeight: '500', + textAlign: 'center', + paddingHorizontal: 20, + }, + incomingPointsBar: { + backgroundColor: slate50, + borderTopWidth: 1, + borderTopColor: slate200, + paddingVertical: 10, + paddingHorizontal: 10, + alignItems: 'center', + gap: 4, + }, + incomingPointsAmount: { + flex: 1, + fontFamily: 'DIN OT', + fontWeight: '500', + fontSize: 14, + color: black, + }, + incomingPointsTime: { + fontFamily: 'DIN OT', + fontWeight: '500', + fontSize: 14, + color: blue600, + }, + actionCard: { + gap: 22, + backgroundColor: white, + padding: 16, + borderRadius: 17, + borderWidth: 1, + borderColor: slate200, + }, + actionIconContainer: { + width: 60, + height: 60, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: black, + }, + actionTitle: { + color: black, + fontFamily: 'DIN OT', + fontWeight: '500', + fontSize: 16, + }, + actionSubtitle: { + color: slate500, + fontFamily: 'DIN OT', + fontSize: 14, + }, + referralCard: { + height: 270, + backgroundColor: white, + borderRadius: 16, + borderWidth: 1, + borderColor: slate200, + }, + referralImageContainer: { + borderBottomWidth: 1, + borderBottomColor: slate200, + height: 170, + }, + referralImage: { + width: '80%', + height: '100%', + position: 'absolute', + right: 0, + top: 0, + }, + referralStarIcon: { + marginLeft: 16, + marginTop: 16, + }, + referralTitle: { + fontFamily: 'DIN OT', + fontSize: 16, + color: black, + }, + referralLink: { + fontFamily: 'DIN OT', + fontSize: 16, + color: blue600, + }, + blurView: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + height: 100, + }, + exploreButtonContainer: { + position: 'absolute', + left: 20, + right: 20, + display: 'none', + }, + exploreButton: { + backgroundColor: black, + paddingHorizontal: 20, + paddingVertical: 14, + borderRadius: 5, + height: 52, + }, + exploreButtonText: { + fontFamily: 'DIN OT', + fontSize: 16, + color: white, + textAlign: 'center', + }, +}); + +export default Points; diff --git a/app/src/components/NavBar/PointsNavBar.tsx b/app/src/components/NavBar/PointsNavBar.tsx new file mode 100644 index 000000000..5d008850a --- /dev/null +++ b/app/src/components/NavBar/PointsNavBar.tsx @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import type { NativeStackHeaderProps } from '@react-navigation/native-stack'; + +import { Text, View } from '@selfxyz/mobile-sdk-alpha/components'; + +import { NavBar } from '@/components/NavBar/BaseNavBar'; +import { black, slate50 } from '@/utils/colors'; +import { extraYPadding } from '@/utils/constants'; +import { buttonTap } from '@/utils/haptic'; + +export const PointsNavBar = (props: NativeStackHeaderProps) => { + const insets = useSafeAreaInsets(); + const closeButtonWidth = 50; + + return ( + + { + buttonTap(); + props.navigation.goBack(); + }} + /> + + + Self Points + + + + } + /> + + ); +}; diff --git a/app/src/components/PointHistoryList.tsx b/app/src/components/PointHistoryList.tsx new file mode 100644 index 000000000..1378d62bd --- /dev/null +++ b/app/src/components/PointHistoryList.tsx @@ -0,0 +1,340 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useCallback, useMemo, useState } from 'react'; +import { + ActivityIndicator, + RefreshControl, + SectionList, + StyleSheet, +} from 'react-native'; +import { Card, Text, View, XStack, YStack } from 'tamagui'; + +import HeartIcon from '@/images/icons/heart.svg'; +import StarBlackIcon from '@/images/icons/star_black.svg'; +import { usePointEventStore } from '@/stores/pointEventStore'; +import { + black, + blue600, + slate50, + slate200, + slate300, + slate400, + slate500, + white, +} from '@/utils/colors'; +import { dinot, plexMono } from '@/utils/fonts'; +import type { PointEvent } from '@/utils/points'; + +type Section = { + title: string; + data: PointEvent[]; +}; + +export type PointHistoryListProps = { + ListHeaderComponent?: + | React.ComponentType> + | React.ReactElement + | null; + onLayout?: () => void; +}; + +const TIME_PERIODS = { + TODAY: 'TODAY', + THIS_WEEK: 'THIS WEEK', + THIS_MONTH: 'THIS MONTH', + MONTH_NAME: (date: Date): string => { + return date.toLocaleString('default', { month: 'long' }).toUpperCase(); + }, + OLDER: 'OLDER', +}; + +const getIconForEventType = (type: PointEvent['type']) => { + switch (type) { + case 'disclosure': + return ; + default: + return ; + } +}; + +export const PointHistoryList: React.FC = ({ + ListHeaderComponent, + onLayout, +}) => { + const [refreshing, setRefreshing] = useState(false); + // Subscribe to events directly from store - component will auto-update when store changes + const pointEvents = usePointEventStore(state => state.getAllPointEvents()); + const isLoading = usePointEventStore(state => state.isLoading); + const refreshPoints = usePointEventStore(state => state.refreshPoints); + const refreshIncomingPoints = usePointEventStore( + state => state.refreshIncomingPoints, + ); + // loadEvents only needs to be called once on mount. + // and it is called in Points.ts + + const formatDate = (timestamp: number) => { + return new Date(timestamp).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + }; + + const formatDateFull = (timestamp: number) => { + return new Date(timestamp).toLocaleDateString([], { + month: 'short', + day: 'numeric', + }); + }; + + const getTimePeriod = useCallback((timestamp: number): string => { + const now = new Date(); + const eventDate = new Date(timestamp); + const startOfToday = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + ); + const startOfThisWeek = new Date(startOfToday); + startOfThisWeek.setDate(startOfToday.getDate() - startOfToday.getDay()); + const startOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); + + if (eventDate >= startOfToday) { + return TIME_PERIODS.TODAY; + } else if (eventDate >= startOfThisWeek) { + return TIME_PERIODS.THIS_WEEK; + } else if (eventDate >= startOfThisMonth) { + return TIME_PERIODS.THIS_MONTH; + } else if (eventDate >= startOfLastMonth) { + return TIME_PERIODS.MONTH_NAME(eventDate); + } else { + return TIME_PERIODS.OLDER; + } + }, []); + + const groupedEvents = useMemo(() => { + const groups: Record = {}; + + [ + TIME_PERIODS.TODAY, + TIME_PERIODS.THIS_WEEK, + TIME_PERIODS.THIS_MONTH, + TIME_PERIODS.OLDER, + ].forEach(period => { + groups[period] = []; + }); + + const monthGroups = new Set(); + + pointEvents.forEach(event => { + const period = getTimePeriod(event.timestamp); + if ( + period !== TIME_PERIODS.TODAY && + period !== TIME_PERIODS.THIS_WEEK && + period !== TIME_PERIODS.THIS_MONTH && + period !== TIME_PERIODS.OLDER + ) { + monthGroups.add(period); + if (!groups[period]) { + groups[period] = []; + } + } + groups[period].push(event); + }); + + const sections: Section[] = []; + [ + TIME_PERIODS.TODAY, + TIME_PERIODS.THIS_WEEK, + TIME_PERIODS.THIS_MONTH, + ].forEach(period => { + if (groups[period] && groups[period].length > 0) { + sections.push({ title: period, data: groups[period] }); + } + }); + + Array.from(monthGroups) + .sort( + (a, b) => + new Date(groups[b][0].timestamp).getMonth() - + new Date(groups[a][0].timestamp).getMonth(), + ) + .forEach(month => { + sections.push({ title: month, data: groups[month] }); + }); + + if (groups[TIME_PERIODS.OLDER] && groups[TIME_PERIODS.OLDER].length > 0) { + sections.push({ + title: TIME_PERIODS.OLDER, + data: groups[TIME_PERIODS.OLDER], + }); + } + + return sections; + }, [pointEvents, getTimePeriod]); + + const renderItem = useCallback( + ({ + item, + index, + section, + }: { + item: PointEvent; + index: number; + section: Section; + }) => { + const borderRadiusSize = 16; + const isFirstItem = index === 0; + const isLastItem = index === section.data.length - 1; + + return ( + + + + + + {getIconForEventType(item.type)} + + + + {item.title} + + + {formatDateFull(item.timestamp)} •{' '} + {formatDate(item.timestamp)} + + + + +{item.points} + + + + + + ); + }, + [], + ); + + const renderSectionHeader = useCallback( + ({ section }: { section: Section }) => { + return ( + + + {section.title.toUpperCase()} + + + ); + }, + [], + ); + + // Pull-to-refresh handler + const onRefresh = useCallback(() => { + setRefreshing(true); + Promise.all([refreshPoints(), refreshIncomingPoints()]).finally(() => + setRefreshing(false), + ); + }, [refreshPoints, refreshIncomingPoints]); + + const keyExtractor = useCallback((item: PointEvent) => item.id, []); + + const renderEmptyComponent = useCallback(() => { + if (isLoading) { + return ( + + + + Loading point history... + + + ); + } + return ( + + No point history available yet. + + Start earning points by completing actions! + + + ); + }, [isLoading]); + + return ( + + } + contentContainerStyle={[ + styles.listContent, + groupedEvents.length === 0 && styles.emptyList, + ]} + showsVerticalScrollIndicator={false} + stickySectionHeadersEnabled={false} + ListEmptyComponent={renderEmptyComponent} + ListHeaderComponent={ListHeaderComponent} + style={{ marginHorizontal: 15, marginBottom: 25 }} + onLayout={onLayout} + /> + ); +}; + +const styles = StyleSheet.create({ + listContent: { + paddingBottom: 100, + }, + emptyList: { + flexGrow: 1, + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 20, + paddingTop: 5, + }, +}); + +export default PointHistoryList; diff --git a/app/src/components/referral/CopyReferralButton.tsx b/app/src/components/referral/CopyReferralButton.tsx new file mode 100644 index 000000000..866cc8618 --- /dev/null +++ b/app/src/components/referral/CopyReferralButton.tsx @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useState } from 'react'; +import { Button, Text, XStack } from 'tamagui'; +import Clipboard from '@react-native-clipboard/clipboard'; + +import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { PointEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; + +import CopyToClipboard from '@/images/icons/copy_to_clipboard.svg'; +import { black, green500, white } from '@/utils/colors'; +import { dinot } from '@/utils/fonts'; + +export interface CopyReferralButtonProps { + referralLink: string; + onCopy?: () => void; +} + +export const CopyReferralButton: React.FC = ({ + referralLink, + onCopy, +}) => { + const [isCopied, setIsCopied] = useState(false); + const selfClient = useSelfClient(); + + const handleCopyLink = async () => { + try { + selfClient.trackEvent(PointEvents.EARN_REFERRAL_COPY_LINK); + await Clipboard.setString(referralLink); + setIsCopied(true); + + // Reset after 1.65 seconds + setTimeout(() => { + setIsCopied(false); + }, 1650); + + onCopy?.(); + } catch (error) { + console.error('Failed to copy to clipboard:', error); + } + }; + + return ( + + ); +}; diff --git a/app/src/components/referral/ReferralHeader.tsx b/app/src/components/referral/ReferralHeader.tsx new file mode 100644 index 000000000..81cd046fa --- /dev/null +++ b/app/src/components/referral/ReferralHeader.tsx @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import type { ImageSourcePropType } from 'react-native'; +import { Image, Pressable } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Text, View } from 'tamagui'; + +import ArrowLeft from '@/images/icons/arrow_left.svg'; +import { black, white } from '@/utils/colors'; + +export interface ReferralHeaderProps { + imageSource: ImageSourcePropType; + onBackPress: () => void; +} + +export const ReferralHeader: React.FC = ({ + imageSource, + onBackPress, +}) => { + const { top } = useSafeAreaInsets(); + + return ( + + + + {/* Back button */} + + + + + + + + + + + ); +}; diff --git a/app/src/components/referral/ReferralInfo.tsx b/app/src/components/referral/ReferralInfo.tsx new file mode 100644 index 000000000..a10e98b41 --- /dev/null +++ b/app/src/components/referral/ReferralInfo.tsx @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Pressable } from 'react-native'; +import { Text, YStack } from 'tamagui'; + +import { black, blue600, slate500 } from '@/utils/colors'; +import { dinot } from '@/utils/fonts'; + +export interface ReferralInfoProps { + title: string; + description: string; + learnMoreText?: string; + onLearnMorePress?: () => void; +} + +export const ReferralInfo: React.FC = ({ + title, + description, + learnMoreText, + onLearnMorePress, +}) => { + return ( + + + {title} + + + + {description} + + {learnMoreText && ( + + + {learnMoreText} + + + )} + + + ); +}; diff --git a/app/src/components/referral/ShareButton.tsx b/app/src/components/referral/ShareButton.tsx new file mode 100644 index 000000000..e123f7f90 --- /dev/null +++ b/app/src/components/referral/ShareButton.tsx @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Pressable } from 'react-native'; +import { Text, View, YStack } from 'tamagui'; + +import { slate800 } from '@/utils/colors'; +import { dinot } from '@/utils/fonts'; + +export interface ShareButtonProps { + icon: React.ReactNode; + label: string; + backgroundColor: string; + onPress: () => void; +} + +export const ShareButton: React.FC = ({ + icon, + label, + backgroundColor, + onPress, +}) => { + return ( + + + + {icon} + + + {label} + + + + ); +}; diff --git a/app/src/hooks/useEarnPointsFlow.ts b/app/src/hooks/useEarnPointsFlow.ts new file mode 100644 index 000000000..125f3723d --- /dev/null +++ b/app/src/hooks/useEarnPointsFlow.ts @@ -0,0 +1,255 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; + +import { useRegisterReferral } from '@/hooks/useRegisterReferral'; +import type { RootStackParamList } from '@/navigation'; +import useUserStore from '@/stores/userStore'; +import { IS_DEV_MODE } from '@/utils/devUtils'; +import { registerModalCallbacks } from '@/utils/modalCallbackRegistry'; +import { + hasUserAnIdentityDocumentRegistered, + hasUserDoneThePointsDisclosure, + POINT_VALUES, + pointsSelfApp, +} from '@/utils/points'; + +type UseEarnPointsFlowParams = { + hasReferrer: boolean; + isReferralConfirmed: boolean | undefined; +}; + +export const useEarnPointsFlow = ({ + hasReferrer, + isReferralConfirmed, +}: UseEarnPointsFlowParams) => { + const selfClient = useSelfClient(); + const navigation = + useNavigation>(); + const { registerReferral } = useRegisterReferral(); + const referrer = useUserStore(state => state.deepLinkReferrer); + + const navigateToPointsProof = useCallback(async () => { + const selfApp = await pointsSelfApp(); + selfClient.getSelfAppState().setSelfApp(selfApp); + + // Use setTimeout to ensure modal dismisses before navigating + setTimeout(() => { + navigation.navigate('Prove'); + }, 100); + }, [selfClient, navigation]); + + const showIdentityVerificationModal = useCallback(() => { + const callbackId = registerModalCallbacks({ + onButtonPress: () => { + // Use setTimeout to ensure modal dismisses before navigating + setTimeout(() => { + navigation.navigate('DocumentOnboarding'); + }, 100); + }, + onModalDismiss: () => { + if (hasReferrer) { + useUserStore.getState().clearDeepLinkReferrer(); + } + }, + }); + + navigation.navigate('Modal', { + titleText: 'Identity Verification Required', + bodyText: + 'To access Self Points, you need to register an identity document with Self first. This helps us verify your identity and keep your points secure.', + buttonText: 'Verify Identity', + secondaryButtonText: 'Not Now', + callbackId, + }); + }, [hasReferrer, navigation]); + + const showPointsDisclosureModal = useCallback(() => { + const callbackId = registerModalCallbacks({ + onButtonPress: () => { + navigateToPointsProof(); + }, + onModalDismiss: () => { + if (hasReferrer) { + useUserStore.getState().clearDeepLinkReferrer(); + } + }, + }); + + navigation.navigate('Modal', { + titleText: 'Points Disclosure Required', + bodyText: + 'To access Self Points, you need to complete the points disclosure first. This helps us verify your identity and keep your points secure.', + buttonText: 'Complete Points Disclosure', + secondaryButtonText: 'Not Now', + callbackId, + }); + }, [hasReferrer, navigation, navigateToPointsProof]); + + const handleReferralFlow = useCallback(async () => { + if (!referrer) { + return; + } + + if (IS_DEV_MODE) { + console.log('[DEV MODE] Starting referral flow for referrer:', referrer); + } + + const showReferralErrorModal = (errorMessage: string) => { + const callbackId = registerModalCallbacks({ + onButtonPress: async () => { + // Retry the referral flow + await handleReferralFlow(); + }, + onModalDismiss: () => { + // Preserve referrer for future retry attempts + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] Referral error modal dismissed, preserving referrer for retry', + ); + } + }, + }); + + navigation.navigate('Modal', { + titleText: 'Referral Registration Failed', + bodyText: `We couldn't register your referral at this time. ${errorMessage}. You can try again or dismiss this message.`, + buttonText: 'Try Again', + secondaryButtonText: 'Dismiss', + callbackId, + }); + }; + + const store = useUserStore.getState(); + // Check if already registered to avoid duplicate calls + if (!store.isReferrerRegistered(referrer)) { + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] 3. Registering referral (mocked in __DEV__)...', + ); + } + const result = await registerReferral(referrer); + if (result.success) { + store.markReferrerAsRegistered(referrer); + if (IS_DEV_MODE) { + console.log('[DEV MODE] ✓ Referral registration successful (mocked)'); + } + + // Only navigate to GratificationScreen on success + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] 4. Navigating to Gratification screen with points:', + POINT_VALUES.referee, + ); + } + store.clearDeepLinkReferrer(); + navigation.navigate('Gratification', { + points: POINT_VALUES.referee, + }); + } else { + // Registration failed - show error and preserve referrer + const errorMessage = result.error || 'Unknown error occurred'; + if (IS_DEV_MODE) { + console.error('[DEV MODE] Error registering referral:', errorMessage); + } + console.error('Referral registration failed:', errorMessage); + + // Show error modal with retry option, don't clear referrer + showReferralErrorModal(errorMessage); + } + } else { + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] Referrer already registered, skipping registration', + ); + } + + // Already registered, navigate to gratification + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] 4. Navigating to Gratification screen with points:', + POINT_VALUES.referee, + ); + } + store.clearDeepLinkReferrer(); + navigation.navigate('Gratification', { + points: POINT_VALUES.referee, + }); + } + }, [referrer, registerReferral, navigation]); + + const onEarnPointsPress = useCallback( + async (skipReferralFlow = true) => { + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] 1. Checking if identity document is registered...', + ); + } + const hasUserAnIdentityDocumentRegistered_result = + await hasUserAnIdentityDocumentRegistered(); + if (!hasUserAnIdentityDocumentRegistered_result) { + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] Identity document not registered, showing modal', + ); + } + showIdentityVerificationModal(); + return; + } + if (IS_DEV_MODE) { + console.log('[DEV MODE] ✓ Identity document is registered'); + } + + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] 2. Checking if points disclosure is completed...', + ); + } + const hasUserDoneThePointsDisclosure_result = + await hasUserDoneThePointsDisclosure(); + if (!hasUserDoneThePointsDisclosure_result) { + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] Points disclosure not completed, showing modal', + ); + } + showPointsDisclosureModal(); + return; + } + if (IS_DEV_MODE) { + console.log('[DEV MODE] ✓ Points disclosure is completed'); + } + + // User has completed both checks + if (!skipReferralFlow && hasReferrer && isReferralConfirmed === true) { + if (IS_DEV_MODE) { + console.log( + '[DEV MODE] 3. Both checks passed, proceeding with referral flow...', + ); + } + await handleReferralFlow(); + } else { + // Just go to points upon pressing "Earn Points" button + if (!hasReferrer) { + navigation.navigate('Points'); + } + } + }, + [ + hasReferrer, + isReferralConfirmed, + navigation, + showIdentityVerificationModal, + showPointsDisclosureModal, + handleReferralFlow, + ], + ); + + return { onEarnPointsPress }; +}; diff --git a/app/src/hooks/usePoints.ts b/app/src/hooks/usePoints.ts new file mode 100644 index 000000000..c9d779bb1 --- /dev/null +++ b/app/src/hooks/usePoints.ts @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useEffect } from 'react'; + +import { usePointEventStore } from '@/stores/pointEventStore'; +import { getNextSundayNoonUTC, type IncomingPoints } from '@/utils/points'; + +/* + * Hook to get incoming points for the user. It shows the optimistic incoming points. + * Refreshes incoming points once on mount. + */ +export const useIncomingPoints = (): IncomingPoints => { + const incomingPoints = usePointEventStore(state => state.incomingPoints); + const totalOptimisticIncomingPoints = usePointEventStore(state => + state.totalOptimisticIncomingPoints(), + ); + const refreshIncomingPoints = usePointEventStore( + state => state.refreshIncomingPoints, + ); + + useEffect(() => { + // Only refresh once on mount - the store handles promise caching for concurrent calls + refreshIncomingPoints(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Empty deps: only run once on mount + + return { + amount: totalOptimisticIncomingPoints, + expectedDate: incomingPoints.expectedDate, + }; +}; + +/* + * Hook to fetch total points for the user. It refetches the total points when the next points update time is reached (each Sunday noon UTC). + */ +export const usePoints = () => { + const points = usePointEventStore(state => state.points); + const nextPointsUpdate = getNextSundayNoonUTC().getTime(); + const refreshPoints = usePointEventStore(state => state.refreshPoints); + + useEffect(() => { + refreshPoints(); + // refresh when points update time changes as its the only time points can change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nextPointsUpdate]); + + return { + amount: points, + refetch: refreshPoints, + }; +}; diff --git a/app/src/hooks/usePointsGuardrail.ts b/app/src/hooks/usePointsGuardrail.ts new file mode 100644 index 000000000..3291ab0b7 --- /dev/null +++ b/app/src/hooks/usePointsGuardrail.ts @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback } from 'react'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import type { RootStackParamList } from '@/navigation'; +import { + hasUserAnIdentityDocumentRegistered, + hasUserDoneThePointsDisclosure, +} from '@/utils/points'; + +/** + * Guard hook that validates points screen access requirements. + * Redirects to Home if user hasn't: + * 1. Registered an identity document + * 2. Completed the points disclosure + * + * This prevents users from accessing the Points screen through: + * - GratificationScreen's "Explore rewards" button + * - CloudBackupSettings return paths + * - Any other navigation bypass + */ +export const usePointsGuardrail = () => { + const navigation = + useNavigation>(); + + useFocusEffect( + useCallback(() => { + let isActive = true; + + const checkRequirements = async () => { + const hasDocument = await hasUserAnIdentityDocumentRegistered(); + const hasDisclosed = await hasUserDoneThePointsDisclosure(); + + // Only navigate if the screen is still focused + if (isActive && (!hasDocument || !hasDisclosed)) { + // User hasn't met requirements, redirect to Home + navigation.navigate('Home', {}); + } + }; + checkRequirements(); + + return () => { + isActive = false; + }; + }, [navigation]), + ); +}; diff --git a/app/src/hooks/useReferralConfirmation.ts b/app/src/hooks/useReferralConfirmation.ts new file mode 100644 index 000000000..2a5bdfa36 --- /dev/null +++ b/app/src/hooks/useReferralConfirmation.ts @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback, useEffect, useState } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import type { RootStackParamList } from '@/navigation'; +import useUserStore from '@/stores/userStore'; +import { registerModalCallbacks } from '@/utils/modalCallbackRegistry'; + +type UseReferralConfirmationParams = { + hasReferrer: boolean; + onConfirmed: () => void; +}; + +export const useReferralConfirmation = ({ + hasReferrer, + onConfirmed, +}: UseReferralConfirmationParams) => { + const navigation = + useNavigation>(); + const referrer = useUserStore(state => state.deepLinkReferrer); + const isReferrerRegistered = useUserStore( + state => state.isReferrerRegistered, + ); + const [isReferralConfirmed, setIsReferralConfirmed] = useState< + boolean | undefined + >(undefined); + + const showReferralConfirmationModal = useCallback(() => { + const callbackId = registerModalCallbacks({ + onButtonPress: async () => { + setIsReferralConfirmed(true); + // Use setTimeout to ensure modal dismisses before any navigation triggered by state change + setTimeout(() => { + navigation.goBack(); + }, 100); + }, + onModalDismiss: () => { + setIsReferralConfirmed(false); + useUserStore.getState().clearDeepLinkReferrer(); + }, + }); + + navigation.navigate('Modal', { + titleText: 'Referral Confirmation', + bodyText: + 'Seems like you opened the app from a referral link. Please confirm to continue.', + buttonText: 'Confirm', + secondaryButtonText: 'Dismiss', + callbackId, + }); + }, [navigation]); + + // Handle referral confirmation flow + useEffect(() => { + // This should trigger the flow when user comes back from any of the onboarding screens + if (isReferralConfirmed === true && hasReferrer) { + onConfirmed(); + return; + } + + // Only show modal if referrer exists, not yet confirmed, and hasn't been registered + if ( + hasReferrer && + referrer && + isReferralConfirmed === undefined && + !isReferrerRegistered(referrer) + ) { + showReferralConfirmationModal(); + } + }, [ + hasReferrer, + referrer, + isReferralConfirmed, + isReferrerRegistered, + showReferralConfirmationModal, + onConfirmed, + ]); + + return { isReferralConfirmed }; +}; diff --git a/app/src/hooks/useReferralMessage.ts b/app/src/hooks/useReferralMessage.ts new file mode 100644 index 000000000..228a88eb6 --- /dev/null +++ b/app/src/hooks/useReferralMessage.ts @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useEffect, useMemo, useState } from 'react'; + +import { getOrGeneratePointsAddress } from '@/providers/authProvider'; +import { useSettingStore } from '@/stores/settingStore'; + +interface ReferralMessageResult { + message: string; + referralLink: string; +} + +const buildReferralMessageFromAddress = ( + userPointsAddress: string, +): ReferralMessageResult => { + const baseDomain = 'https://referral.self.xyz'; + const referralLink = `${baseDomain}/referral/${userPointsAddress}`; + return { + message: `Join Self and use my referral link:\n\n${referralLink}`, + referralLink, + }; +}; + +export const useReferralMessage = () => { + const pointsAddress = useSettingStore(state => state.pointsAddress); + const [fetchedAddress, setFetchedAddress] = useState(null); + + // Use store address if available, otherwise use fetched address + const address = pointsAddress ?? fetchedAddress; + + // Compute message synchronously when address is available + const result = useMemo( + () => (address ? buildReferralMessageFromAddress(address) : null), + [address], + ); + + useEffect(() => { + if (!pointsAddress) { + // Only fetch if not already in store + const loadReferralData = async () => { + const fetchedAddr = await getOrGeneratePointsAddress(); + setFetchedAddress(fetchedAddr); + }; + + loadReferralData(); + } + }, [pointsAddress]); + + return useMemo( + () => ({ + message: result?.message ?? '', + referralLink: result?.referralLink ?? '', + isLoading: !result, + }), + [result], + ); +}; diff --git a/app/src/hooks/useReferralRegistration.ts b/app/src/hooks/useReferralRegistration.ts new file mode 100644 index 000000000..039bb46db --- /dev/null +++ b/app/src/hooks/useReferralRegistration.ts @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useEffect } from 'react'; +import { useRoute } from '@react-navigation/native'; + +import { useRegisterReferral } from '@/hooks/useRegisterReferral'; +import useUserStore from '@/stores/userStore'; +import { IS_DEV_MODE } from '@/utils/devUtils'; + +/** + * Hook to handle referral registration when a referrer is present in route params. + * Automatically registers the referral if: + * - A referrer is present in route params + * - The referrer hasn't been registered yet + * - Registration is not already in progress + */ +export const useReferralRegistration = () => { + const route = useRoute(); + const params = route.params as { referrer?: string } | undefined; + const referrer = params?.referrer; + const { registerReferral, isLoading: isRegisteringReferral } = + useRegisterReferral(); + + useEffect(() => { + if (IS_DEV_MODE) { + if (referrer) { + console.log( + '[useReferralRegistration] Referrer found in params:', + referrer, + ); + } else { + console.log('[useReferralRegistration] No referrer in route params'); + } + } + + if (!referrer || isRegisteringReferral) { + return; + } + + const store = useUserStore.getState(); + + // Check if this referrer has already been registered + if (store.isReferrerRegistered(referrer)) { + if (IS_DEV_MODE) { + console.log( + '[useReferralRegistration] Referrer already registered:', + referrer, + ); + } + return; + } + + if (IS_DEV_MODE) { + console.log('[useReferralRegistration] Registering referrer:', referrer); + } + + // Register the referral + const register = async () => { + const result = await registerReferral(referrer); + if (result.success) { + store.markReferrerAsRegistered(referrer); + if (IS_DEV_MODE) { + console.log( + '[useReferralRegistration] Successfully registered referrer:', + referrer, + ); + } + } else { + if (IS_DEV_MODE) { + console.error( + '[useReferralRegistration] Failed to register referrer:', + result.error, + ); + } + } + }; + + register(); + }, [referrer, isRegisteringReferral, registerReferral]); +}; diff --git a/app/src/hooks/useRegisterReferral.ts b/app/src/hooks/useRegisterReferral.ts new file mode 100644 index 000000000..ff6c93768 --- /dev/null +++ b/app/src/hooks/useRegisterReferral.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { ethers } from 'ethers'; +import { useCallback, useState } from 'react'; + +import { recordReferralPointEvent } from '@/utils/points'; + +export const useRegisterReferral = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const registerReferral = useCallback(async (referrer: string) => { + setIsLoading(true); + setError(null); + + try { + // Validate referrer address format + if (!ethers.isAddress(referrer)) { + const errorMessage = + 'Invalid referrer address. Must be a valid hex address.'; + setError(errorMessage); + return { success: false, error: errorMessage }; + } + + // recordReferralPointEvent handles both API registration and local event recording + const result = await recordReferralPointEvent(referrer); + if (result.success) { + return { success: true }; + } + const errorMessage = result.error || 'Failed to register referral'; + setError(errorMessage); + return { success: false, error: errorMessage }; + } catch (err) { + const errorMessage = + err instanceof Error ? err.message : 'An unexpected error occurred'; + setError(errorMessage); + return { success: false, error: errorMessage }; + } finally { + setIsLoading(false); + } + }, []); + + return { + registerReferral, + isLoading, + error, + }; +}; diff --git a/app/src/hooks/useTestReferralFlow.ts b/app/src/hooks/useTestReferralFlow.ts new file mode 100644 index 000000000..e376178e7 --- /dev/null +++ b/app/src/hooks/useTestReferralFlow.ts @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback, useEffect, useRef } from 'react'; + +import useUserStore from '@/stores/userStore'; +import { IS_DEV_MODE } from '@/utils/devUtils'; + +const TEST_REFERRER = '0x1234567890123456789012345678901234567890'; + +/** + * Hook for testing referral flow in DEV mode. + * Provides automatic timeout trigger (3 seconds) and manual trigger function. + * + * Flow: Sets referrer → shows confirmation modal → on confirm, checks prerequisites + * → if identity doc & points disclosure done → registers referral → navigates to Gratification + * + * @param shouldAutoTrigger - Whether to automatically trigger the flow after 3 seconds (default: false) + */ +export const useTestReferralFlow = (shouldAutoTrigger = false) => { + const referralTimerRef = useRef(null); + + const triggerReferralFlow = useCallback(() => { + if (IS_DEV_MODE) { + const testReferrer = TEST_REFERRER; + const store = useUserStore.getState(); + + // Always reset state for full flow testing + console.log('[DEV MODE] Resetting test state for full flow:'); + console.log(' - Clearing all registered referrers'); + // Clear the "already registered" flag for all referrers + useUserStore.setState({ registeredReferrers: new Set() }); + console.log(' - Referrer will be treated as first-time registration'); + + console.log( + '[DEV MODE] Simulating referral flow with referrer:', + testReferrer, + ); + store.setDeepLinkReferrer(testReferrer); + // Trigger the referral confirmation modal + // The useReferralConfirmation hook will handle showing the modal + } + }, []); + + // Automatic trigger after 3 seconds (only if shouldAutoTrigger is true) + useEffect(() => { + if (IS_DEV_MODE && shouldAutoTrigger) { + console.log('[DEV MODE] Auto-triggering referral flow in 3 seconds...'); + referralTimerRef.current = setTimeout(() => { + triggerReferralFlow(); + }, 3000); + } + + return () => { + if (referralTimerRef.current) { + clearTimeout(referralTimerRef.current); + } + }; + }, [triggerReferralFlow, shouldAutoTrigger]); + + const handleTestReferralFlow = useCallback(() => { + if (IS_DEV_MODE) { + triggerReferralFlow(); + } + }, [triggerReferralFlow]); + + return { + handleTestReferralFlow, + isDevMode: IS_DEV_MODE, + }; +}; diff --git a/app/src/images/gratification_bg.svg b/app/src/images/gratification_bg.svg new file mode 100644 index 000000000..b23d678d6 --- /dev/null +++ b/app/src/images/gratification_bg.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/images/icons/arrow_left.svg b/app/src/images/icons/arrow_left.svg new file mode 100644 index 000000000..c09fe0ce6 --- /dev/null +++ b/app/src/images/icons/arrow_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/bell_white.svg b/app/src/images/icons/bell_white.svg new file mode 100644 index 000000000..ff38e5f84 --- /dev/null +++ b/app/src/images/icons/bell_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/clock.svg b/app/src/images/icons/clock.svg new file mode 100644 index 000000000..8f37f8e0c --- /dev/null +++ b/app/src/images/icons/clock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/images/icons/cog_hollow.svg b/app/src/images/icons/cog_hollow.svg new file mode 100644 index 000000000..3514a1a3c --- /dev/null +++ b/app/src/images/icons/cog_hollow.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/copy_to_clipboard.svg b/app/src/images/icons/copy_to_clipboard.svg new file mode 100644 index 000000000..e0548bbf7 --- /dev/null +++ b/app/src/images/icons/copy_to_clipboard.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/hear_red.png b/app/src/images/icons/hear_red.png new file mode 100644 index 000000000..31e39e2e9 Binary files /dev/null and b/app/src/images/icons/hear_red.png differ diff --git a/app/src/images/icons/heart.svg b/app/src/images/icons/heart.svg new file mode 100644 index 000000000..0ca574c75 --- /dev/null +++ b/app/src/images/icons/heart.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/lock_white.svg b/app/src/images/icons/lock_white.svg new file mode 100644 index 000000000..30b549a39 --- /dev/null +++ b/app/src/images/icons/lock_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/logo_white.svg b/app/src/images/icons/logo_white.svg new file mode 100644 index 000000000..6748e43b3 --- /dev/null +++ b/app/src/images/icons/logo_white.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/images/icons/message.svg b/app/src/images/icons/message.svg new file mode 100644 index 000000000..3794bdf42 --- /dev/null +++ b/app/src/images/icons/message.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/plus_circle.svg b/app/src/images/icons/plus_circle.svg new file mode 100644 index 000000000..1bc13701a --- /dev/null +++ b/app/src/images/icons/plus_circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/share_blue.svg b/app/src/images/icons/share_blue.svg new file mode 100644 index 000000000..65ff28abb --- /dev/null +++ b/app/src/images/icons/share_blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/star_black.svg b/app/src/images/icons/star_black.svg new file mode 100644 index 000000000..808fd807d --- /dev/null +++ b/app/src/images/icons/star_black.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/icons/whatsapp.svg b/app/src/images/icons/whatsapp.svg new file mode 100644 index 000000000..bb7218362 --- /dev/null +++ b/app/src/images/icons/whatsapp.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/src/images/majong.png b/app/src/images/majong.png new file mode 100644 index 000000000..4220700ae Binary files /dev/null and b/app/src/images/majong.png differ diff --git a/app/src/images/referral.png b/app/src/images/referral.png new file mode 100644 index 000000000..66ba6a9a2 Binary files /dev/null and b/app/src/images/referral.png differ diff --git a/app/src/images/unverified_human.png b/app/src/images/unverified_human.png new file mode 100644 index 000000000..51e19d172 Binary files /dev/null and b/app/src/images/unverified_human.png differ diff --git a/app/src/navigation/account.ts b/app/src/navigation/account.ts index 5ee1467ab..206593a80 100644 --- a/app/src/navigation/account.ts +++ b/app/src/navigation/account.ts @@ -49,12 +49,12 @@ const accountScreens = { CloudBackupSettings: { screen: CloudBackupScreen, options: { - title: 'Cloud backup', + title: 'Account Backup', headerStyle: { - backgroundColor: black, + backgroundColor: white, }, headerTitleStyle: { - color: slate300, + color: black, }, } as NativeStackNavigationOptions, }, diff --git a/app/src/navigation/app.tsx b/app/src/navigation/app.tsx index 5a2622a0f..f7396ff38 100644 --- a/app/src/navigation/app.tsx +++ b/app/src/navigation/app.tsx @@ -9,6 +9,7 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stac import type { DocumentCategory } from '@selfxyz/common/utils/types'; import DeferredLinkingInfoScreen from '@/screens/app/DeferredLinkingInfoScreen'; +import GratificationScreen from '@/screens/app/GratificationScreen'; import LaunchScreen from '@/screens/app/LaunchScreen'; import LoadingScreen from '@/screens/app/LoadingScreen'; import type { ModalNavigationParams } from '@/screens/app/ModalScreen'; @@ -56,6 +57,16 @@ const appScreens = { header: () => , }, }, + Gratification: { + screen: GratificationScreen, + options: { + headerShown: false, + contentStyle: { backgroundColor: '#000000' }, + } as NativeStackNavigationOptions, + params: {} as { + points?: number; + }, + }, }; export default appScreens; diff --git a/app/src/navigation/home.ts b/app/src/navigation/home.ts index e8471cc6a..c510f9b4b 100644 --- a/app/src/navigation/home.ts +++ b/app/src/navigation/home.ts @@ -5,6 +5,9 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; import { HomeNavBar } from '@/components/NavBar'; +import PointsScreen from '@/components/NavBar/Points'; +import { PointsNavBar } from '@/components/NavBar/PointsNavBar'; +import ReferralScreen from '@/screens/app/ReferralScreen'; import HomeScreen from '@/screens/home/HomeScreen'; import ProofHistoryDetailScreen from '@/screens/home/ProofHistoryDetailScreen'; import ProofHistoryScreen from '@/screens/home/ProofHistoryScreen'; @@ -18,6 +21,20 @@ const homeScreens = { presentation: 'card', } as NativeStackNavigationOptions, }, + Points: { + screen: PointsScreen, + options: { + title: 'Self Points', + header: PointsNavBar, + presentation: 'card', + } as NativeStackNavigationOptions, + }, + Referral: { + screen: ReferralScreen, + options: { + headerShown: false, + } as NativeStackNavigationOptions, + }, ProofHistory: { screen: ProofHistoryScreen, options: { diff --git a/app/src/navigation/index.tsx b/app/src/navigation/index.tsx index d5ad3ce24..419a0c750 100644 --- a/app/src/navigation/index.tsx +++ b/app/src/navigation/index.tsx @@ -58,22 +58,30 @@ type BaseRootStackParamList = StaticParamList; // Explicitly declare route params that are not inferred from initialParams export type RootStackParamList = Omit< BaseRootStackParamList, - | 'ComingSoon' - | 'IDPicker' | 'AadhaarUpload' | 'AadhaarUploadError' - | 'WebView' + | 'AadhaarUploadSuccess' | 'AccountRecovery' - | 'SaveRecoveryPhrase' + | 'AccountVerifiedSuccess' | 'CloudBackupSettings' + | 'ComingSoon' | 'ConfirmBelonging' - | 'ProofHistoryDetail' + | 'CreateMock' + | 'Disclaimer' + | 'DocumentNFCScan' + | 'DocumentOnboarding' + | 'Gratification' + | 'Home' + | 'IDPicker' + | 'IdDetails' | 'Loading' | 'Modal' - | 'CreateMock' | 'MockDataDeepLink' - | 'DocumentNFCScan' - | 'AadhaarUploadSuccess' + | 'Points' + | 'ProofHistoryDetail' + | 'Prove' + | 'SaveRecoveryPhrase' + | 'WebView' > & { // Shared screens ComingSoon: { @@ -102,6 +110,7 @@ export type RootStackParamList = Omit< } | undefined; DocumentCameraTrouble: undefined; + DocumentOnboarding: undefined; // Aadhaar screens AadhaarUpload: { @@ -125,14 +134,17 @@ export type RootStackParamList = Omit< | undefined; CloudBackupSettings: | { - nextScreen?: string; + nextScreen?: 'SaveRecoveryPhrase'; + returnToScreen?: 'Points'; } | undefined; + AccountVerifiedSuccess: undefined; // Proof/Verification screens ProofHistoryDetail: { data: ProofHistory; }; + Prove: undefined; // App screens Loading: { @@ -141,6 +153,19 @@ export type RootStackParamList = Omit< curveOrExponent?: string; }; Modal: ModalNavigationParams; + Gratification: { + points?: number; + }; + + // Home screens + Home: { + testReferralFlow?: boolean; + }; + Points: undefined; + IdDetails: undefined; + + // Onboarding screens + Disclaimer: undefined; // Dev screens CreateMock: undefined; diff --git a/app/src/providers/authProvider.tsx b/app/src/providers/authProvider.tsx index 92fb1cf8f..461eeddc6 100644 --- a/app/src/providers/authProvider.tsx +++ b/app/src/providers/authProvider.tsx @@ -137,6 +137,7 @@ async function restoreFromMnemonic( ...options.setOptions, service: SERVICE_NAME, }); + generateAndStorePointsAddress(mnemonic); trackEvent(AuthEvents.MNEMONIC_RESTORE_SUCCESS); return data; } catch (error: unknown) { @@ -278,7 +279,7 @@ export const AuthProvider = ({ keychainOptions => loadOrCreateMnemonic(keychainOptions), str => JSON.parse(str), { - requireAuth: false, + requireAuth: true, }, ), [], @@ -328,15 +329,25 @@ function _generateAddressFromMnemonic(mnemonic: string, index: number): string { return wallet.address; } +export async function generateAndStorePointsAddress( + mnemonic: string, +): Promise { + const pointsAddr = _generateAddressFromMnemonic(mnemonic, 1); + useSettingStore.getState().setPointsAddress(pointsAddr); + return pointsAddr; +} + /** * Gets the second address if it exists, or generates and stores it if not. * By Generate, it means we need the user's biometric auth. * * Flow is, when the user visits the points screen for the first time, we need to generate the points address. */ -export async function getOrGeneratePointsAddress(): Promise { +export async function getOrGeneratePointsAddress( + forceGenerateFromMnemonic: boolean = false, +): Promise { const pointsAddress = useSettingStore.getState().pointsAddress; - if (pointsAddress) { + if (pointsAddress && !forceGenerateFromMnemonic) { return pointsAddress; } diff --git a/app/src/providers/selfClientProvider.tsx b/app/src/providers/selfClientProvider.tsx index 58af65687..0f4641fff 100644 --- a/app/src/providers/selfClientProvider.tsx +++ b/app/src/providers/selfClientProvider.tsx @@ -146,7 +146,10 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { addListener(SdkEvents.PROVING_ACCOUNT_VERIFIED_SUCCESS, () => { setTimeout(() => { if (navigationRef.isReady()) { - navigationRef.navigate('AccountVerifiedSuccess'); + navigationRef.navigate({ + name: 'AccountVerifiedSuccess', + params: undefined, + }); } }, 1000); }); @@ -157,9 +160,9 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { setTimeout(() => { if (navigationRef.isReady()) { if (hasValidDocument) { - navigationRef.navigate('Home'); + navigationRef.navigate({ name: 'Home', params: {} }); } else { - navigationRef.navigate('Launch'); + navigationRef.navigate({ name: 'Launch', params: undefined }); } } }, 3000); diff --git a/app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx b/app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx index 663d701f5..4bdb2b3f2 100644 --- a/app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx +++ b/app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx @@ -2,10 +2,11 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Separator, View, XStack, YStack } from 'tamagui'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { AuthState, useTurnkey } from '@turnkey/react-native-wallet-kit'; import { isUserRegisteredWithAlternativeCSCA } from '@selfxyz/common/utils/passports/validate'; import { @@ -32,97 +33,145 @@ import { reStorePassportDataWithRightCSCA, } from '@/providers/passportDataProvider'; import { useSettingStore } from '@/stores/settingStore'; +import type { Mnemonic } from '@/types/mnemonic'; import { STORAGE_NAME, useBackupMnemonic } from '@/utils/cloudBackup'; import { black, slate500, slate600, white } from '@/utils/colors'; +import { useTurnkeyUtils } from '@/utils/turnkey'; const AccountRecoveryChoiceScreen: React.FC = () => { const selfClient = useSelfClient(); const { useProtocolStore } = selfClient; const { trackEvent } = useSelfClient(); const { restoreAccountFromMnemonic } = useAuth(); + const { turnkeyWallets, refreshWallets } = useTurnkeyUtils(); + const { getMnemonic } = useTurnkeyUtils(); + const { authState } = useTurnkey(); const [restoring, setRestoring] = useState(false); const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } = useSettingStore(); const { download } = useBackupMnemonic(); const navigation = useNavigation>(); + const setBackedUpWithTurnKey = useSettingStore( + state => state.setBackedUpWithTurnKey, + ); const onRestoreFromCloudNext = useHapticNavigation('AccountVerifiedSuccess'); const onEnterRecoveryPress = useHapticNavigation('RecoverWithPhrase'); - const onRestoreFromCloudPress = useCallback(async () => { - setRestoring(true); - try { - const mnemonic = await download(); - const result = await restoreAccountFromMnemonic(mnemonic.phrase); + useEffect(() => { + refreshWallets(); + }, [refreshWallets]); + + const restoreAccountFlow = useCallback( + async ( + mnemonic: Mnemonic, + isCloudRestore: boolean = false, + ): Promise => { + try { + const result = await restoreAccountFromMnemonic(mnemonic.phrase); + + if (!result) { + console.warn('Failed to restore account'); + trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_UNKNOWN); + navigation.navigate('Launch'); + setRestoring(false); + return false; + } - if (!result) { - console.warn('Failed to restore account'); + const passportDataAndSecret = + (await loadPassportDataAndSecret()) as string; + const { passportData, secret } = JSON.parse(passportDataAndSecret); + const { isRegistered, csca } = + await isUserRegisteredWithAlternativeCSCA(passportData, secret, { + getCommitmentTree(docCategory) { + return useProtocolStore.getState()[docCategory].commitment_tree; + }, + getAltCSCA(docCategory) { + if (docCategory === 'aadhaar') { + const publicKeys = + useProtocolStore.getState().aadhaar.public_keys; + // Convert string[] to Record format expected by AlternativeCSCA + return publicKeys + ? Object.fromEntries(publicKeys.map(key => [key, key])) + : {}; + } + + return useProtocolStore.getState()[docCategory].alternative_csca; + }, + }); + if (!isRegistered) { + console.warn( + 'Secret provided did not match a registered ID. Please try again.', + ); + trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED); + navigation.navigate('Launch'); + setRestoring(false); + return false; + } + if (isCloudRestore && !cloudBackupEnabled) { + toggleCloudBackupEnabled(); + } + reStorePassportDataWithRightCSCA(passportData, csca as string); + await markCurrentDocumentAsRegistered(selfClient); + trackEvent(BackupEvents.CLOUD_RESTORE_SUCCESS); + trackEvent(BackupEvents.ACCOUNT_RECOVERY_COMPLETED); + onRestoreFromCloudNext(); + setRestoring(false); + return true; + } catch (e: unknown) { + console.error(e); trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_UNKNOWN); - navigation.navigate('Launch'); setRestoring(false); - return; + return false; } + }, + [ + trackEvent, + restoreAccountFromMnemonic, + cloudBackupEnabled, + onRestoreFromCloudNext, + navigation, + toggleCloudBackupEnabled, + useProtocolStore, + ], + ); - const passportDataAndSecret = - (await loadPassportDataAndSecret()) as string; - const { passportData, secret } = JSON.parse(passportDataAndSecret); - const { isRegistered, csca } = await isUserRegisteredWithAlternativeCSCA( - passportData, - secret, - { - getCommitmentTree(docCategory) { - return useProtocolStore.getState()[docCategory].commitment_tree; - }, - getAltCSCA(docCategory) { - if (docCategory === 'aadhaar') { - const publicKeys = - useProtocolStore.getState().aadhaar.public_keys; - // Convert string[] to Record format expected by AlternativeCSCA - return publicKeys - ? Object.fromEntries(publicKeys.map(key => [key, key])) - : {}; - } - - return useProtocolStore.getState()[docCategory].alternative_csca; - }, + const onRestoreFromTurnkeyPress = useCallback(async () => { + setRestoring(true); + try { + const mnemonicPhrase = await getMnemonic(); + const mnemonic: Mnemonic = { + phrase: mnemonicPhrase, + password: '', + wordlist: { + locale: 'en', }, - ); - if (!isRegistered) { - console.warn( - 'Secret provided did not match a registered ID. Please try again.', - ); - trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_PASSPORT_NOT_REGISTERED); - navigation.navigate('Launch'); - setRestoring(false); - return; + entropy: '', + }; + const success = await restoreAccountFlow(mnemonic); + if (success) { + setBackedUpWithTurnKey(true); } - if (!cloudBackupEnabled) { - toggleCloudBackupEnabled(); - } - reStorePassportDataWithRightCSCA(passportData, csca as string); - await markCurrentDocumentAsRegistered(selfClient); - trackEvent(BackupEvents.CLOUD_RESTORE_SUCCESS); - trackEvent(BackupEvents.ACCOUNT_RECOVERY_COMPLETED); - onRestoreFromCloudNext(); + } catch (error) { + console.error('Turnkey restore error:', error); + trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_UNKNOWN); + } finally { setRestoring(false); - } catch (e: unknown) { - console.error(e); + } + }, [getMnemonic, restoreAccountFlow, setBackedUpWithTurnKey, trackEvent]); + + const onRestoreFromCloudPress = useCallback(async () => { + setRestoring(true); + try { + const mnemonic = await download(); + await restoreAccountFlow(mnemonic, true); + } catch (error) { + console.error('Cloud restore error:', error); trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_UNKNOWN); setRestoring(false); - throw new Error('Something wrong happened during cloud recovery'); } - }, [ - trackEvent, - download, - restoreAccountFromMnemonic, - cloudBackupEnabled, - onRestoreFromCloudNext, - navigation, - toggleCloudBackupEnabled, - useProtocolStore, - selfClient, - ]); + }, [download, restoreAccountFlow, trackEvent]); const handleManualRecoveryPress = useCallback(() => { onEnterRecoveryPress(); @@ -155,9 +204,24 @@ const AccountRecoveryChoiceScreen: React.FC = () => { + + {restoring ? 'Restoring' : 'Restore'} from Turnkey + {restoring ? '…' : ''} + {restoring ? 'Restoring' : 'Restore'} from {STORAGE_NAME} diff --git a/app/src/screens/account/settings/CloudBackupScreen.tsx b/app/src/screens/account/settings/CloudBackupScreen.tsx index 469477ce7..69499ec15 100644 --- a/app/src/screens/account/settings/CloudBackupScreen.tsx +++ b/app/src/screens/account/settings/CloudBackupScreen.tsx @@ -3,52 +3,63 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import React, { useCallback, useMemo, useState } from 'react'; +import { Pressable, StyleSheet, Text, View } from 'react-native'; import { YStack } from 'tamagui'; import type { StaticScreenProps } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { Wallet } from '@tamagui/lucide-icons'; import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; import { - Caption, - Description, PrimaryButton, SecondaryButton, - Title, } from '@selfxyz/mobile-sdk-alpha/components'; import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; -import BackupDocumentationLink from '@/components/BackupDocumentationLink'; import { useModal } from '@/hooks/useModal'; -import Cloud from '@/images/icons/logo_cloud_backup.svg'; -import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; +import CloudIcon from '@/images/icons/settings_cloud_backup.svg'; import type { RootStackParamList } from '@/navigation'; import { useAuth } from '@/providers/authProvider'; import { useSettingStore } from '@/stores/settingStore'; import { STORAGE_NAME, useBackupMnemonic } from '@/utils/cloudBackup'; import { black, white } from '@/utils/colors'; import { buttonTap, confirmTap } from '@/utils/haptic'; +import { useTurnkeyUtils } from '@/utils/turnkey'; type NextScreen = keyof Pick; type CloudBackupScreenProps = StaticScreenProps< | { nextScreen?: NextScreen; + returnToScreen?: 'Points'; } | undefined >; +type BackupMethod = 'icloud' | 'turnkey' | null; + const CloudBackupScreen: React.FC = ({ route: { params }, }) => { const { trackEvent } = useSelfClient(); + const { backupAccount } = useTurnkeyUtils(); const { getOrCreateMnemonic, loginWithBiometrics } = useAuth(); - const { cloudBackupEnabled, toggleCloudBackupEnabled, biometricsAvailable } = - useSettingStore(); + const { + cloudBackupEnabled, + toggleCloudBackupEnabled, + biometricsAvailable, + backedUpWithTurnKey, + } = useSettingStore(); const { upload, disableBackup } = useBackupMnemonic(); - const [pending, setPending] = useState(false); + const navigation = + useNavigation>(); - const { showModal } = useModal( + const [_selectedMethod, setSelectedMethod] = useState(null); + const [iCloudPending, setICloudPending] = useState(false); + const [turnkeyPending, setTurnkeyPending] = useState(false); + + const { showModal: showDisableModal } = useModal( useMemo( () => ({ titleText: 'Disable cloud backups', @@ -63,11 +74,11 @@ const CloudBackupScreen: React.FC = ({ toggleCloudBackupEnabled(); trackEvent(BackupEvents.CLOUD_BACKUP_DISABLED_DONE); } finally { - setPending(false); + setICloudPending(false); } }, onModalDismiss: () => { - setPending(false); + setICloudPending(false); }, }), [ @@ -79,100 +90,213 @@ const CloudBackupScreen: React.FC = ({ ), ); - const enableCloudBackups = useCallback(async () => { + const { showModal: showAlreadySignedInModal } = useModal({ + titleText: 'Cannot use this email', + bodyText: + 'You cannot use this email. Please try again with a different email address.', + buttonText: 'OK', + onButtonPress: () => {}, + onModalDismiss: () => {}, + }); + + const { showModal: showAlreadyBackedUpModal } = useModal({ + titleText: 'Already backed up with Turnkey', + bodyText: 'You have already backed up your account with Turnkey.', + buttonText: 'OK', + onButtonPress: () => {}, + onModalDismiss: () => {}, + }); + const handleICloudBackup = useCallback(async () => { buttonTap(); - if (cloudBackupEnabled) { + setSelectedMethod('icloud'); + + if (cloudBackupEnabled || !biometricsAvailable) { return; } trackEvent(BackupEvents.CLOUD_BACKUP_ENABLE_STARTED); + setICloudPending(true); - setPending(true); + try { + const storedMnemonic = await getOrCreateMnemonic(); + if (!storedMnemonic) { + setICloudPending(false); + return; + } + await upload(storedMnemonic.data); + toggleCloudBackupEnabled(); + trackEvent(BackupEvents.CLOUD_BACKUP_ENABLED_DONE); - const storedMnemonic = await getOrCreateMnemonic(); - if (!storedMnemonic) { - setPending(false); - return; + if (params?.returnToScreen) { + navigation.navigate(params.returnToScreen); + } + } catch (error) { + console.error('iCloud backup error', error); + } finally { + setICloudPending(false); } - await upload(storedMnemonic.data); - toggleCloudBackupEnabled(); - trackEvent(BackupEvents.CLOUD_BACKUP_ENABLED_DONE); - setPending(false); }, [ cloudBackupEnabled, + biometricsAvailable, getOrCreateMnemonic, upload, toggleCloudBackupEnabled, trackEvent, + navigation, + params, ]); const disableCloudBackups = useCallback(() => { confirmTap(); - setPending(true); - showModal(); - }, [showModal]); + setICloudPending(true); + showDisableModal(); + }, [showDisableModal]); + + const handleTurnkeyBackup = useCallback(async () => { + buttonTap(); + setSelectedMethod('turnkey'); + + if (backedUpWithTurnKey) { + return; + } + + setTurnkeyPending(true); + + try { + const mnemonics = await getOrCreateMnemonic(); + + if (!mnemonics?.data.phrase) { + console.error('No mnemonic found'); + setTurnkeyPending(false); + return; + } + + await backupAccount(mnemonics.data.phrase); + setTurnkeyPending(false); + + if (params?.returnToScreen) { + navigation.navigate(params.returnToScreen); + } + } catch (error) { + if (error instanceof Error && error.message === 'already_exists') { + console.log('Already signed in with Turnkey'); + showAlreadySignedInModal(); + } else if ( + error instanceof Error && + error.message === 'already_backed_up' + ) { + console.log('Already backed up with Turnkey'); + if (params?.returnToScreen) { + navigation.navigate(params.returnToScreen); + } else if (params?.nextScreen) { + navigation.navigate(params.nextScreen); + } else { + showAlreadyBackedUpModal(); + } + } else { + console.error('Turnkey backup error', error); + } + setTurnkeyPending(false); + } + }, [ + backedUpWithTurnKey, + backupAccount, + getOrCreateMnemonic, + showAlreadySignedInModal, + showAlreadyBackedUpModal, + navigation, + params, + ]); return ( - - - - - + - - - {cloudBackupEnabled - ? `${STORAGE_NAME} is enabled` - : `Enable ${STORAGE_NAME}`} - - - {cloudBackupEnabled - ? `Your account is being end-to-end encrypted backed up to ${STORAGE_NAME} so you can easily restore it if you ever get a new phone.` - : `Your account will be end-to-end encrypted backed up to ${STORAGE_NAME} so you can easily restore it if you ever get a new phone.`} - - - {biometricsAvailable ? ( - <> - Learn more about - - ) : ( - <> - Your device doesn't support biometrics or is disabled for apps - and is required for cloud storage. - - )} - + + + + + + + Protect your account + + Back up your account so you can restore your data if you lose your + device or get a new one. + + - + {cloudBackupEnabled ? ( + {iCloudPending ? 'Disabling' : 'Disable'} {STORAGE_NAME} backups + {iCloudPending ? '…' : ''} + + ) : ( + + + + {iCloudPending ? 'Enabling' : 'Backup with'} {STORAGE_NAME} + {iCloudPending ? '…' : ''} + + + )} + + {backedUpWithTurnKey ? ( + - {pending ? 'Disabling' : 'Disable'} {STORAGE_NAME} backups - {pending ? '…' : ''} + Backed up with Turnkey ) : ( - - {pending ? 'Enabling' : 'Enable'} {STORAGE_NAME} backups - {pending ? '…' : ''} - + + + {turnkeyPending ? 'Importing' : 'Backup with'} Turnkey + {turnkeyPending ? '…' : ''} + + )} + - - - - + + + {!biometricsAvailable && ( + + Your device doesn't support biometrics or is disabled for apps and + is required for cloud storage. + + )} + + + ); }; @@ -238,4 +362,74 @@ function BottomButton({ } } +const styles = StyleSheet.create({ + content: { + width: '100%', + alignItems: 'center', + gap: 30, + }, + iconContainer: { + width: 120, + height: 120, + borderRadius: 32, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#3B82F6', + }, + descriptionContainer: { + width: '100%', + gap: 12, + alignItems: 'center', + }, + title: { + width: '100%', + fontSize: 28, + letterSpacing: 1, + fontFamily: 'Advercase', + color: '#000', + textAlign: 'center', + }, + description: { + width: '100%', + fontSize: 18, + fontWeight: '500', + fontFamily: 'DIN OT', + color: '#000', + textAlign: 'center', + }, + optionsContainer: { + width: '100%', + gap: 10, + }, + optionButton: { + backgroundColor: white, + borderWidth: 1, + borderColor: '#E5E7EB', + borderRadius: 5, + paddingVertical: 20, + paddingHorizontal: 20, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, + optionButtonDisabled: { + opacity: 0.5, + }, + optionText: { + fontFamily: 'DIN OT', + fontWeight: '500', + fontSize: 18, + color: black, + }, + warningText: { + fontFamily: 'DIN OT', + fontWeight: '500', + fontSize: 14, + color: '#6B7280', + textAlign: 'center', + marginTop: 10, + }, +}); + export default CloudBackupScreen; diff --git a/app/src/screens/app/GratificationScreen.tsx b/app/src/screens/app/GratificationScreen.tsx new file mode 100644 index 000000000..8175beed1 --- /dev/null +++ b/app/src/screens/app/GratificationScreen.tsx @@ -0,0 +1,265 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React, { useCallback, useState } from 'react'; +import { + Dimensions, + Pressable, + StyleSheet, + Text as RNText, +} from 'react-native'; +import { SystemBars } from 'react-native-edge-to-edge'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Text, View, YStack } from 'tamagui'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { DelayedLottieView } from '@selfxyz/mobile-sdk-alpha'; +import youWinAnimation from '@selfxyz/mobile-sdk-alpha/animations/loading/youWin.json'; +import { PrimaryButton } from '@selfxyz/mobile-sdk-alpha/components'; + +import GratificationBg from '@/images/gratification_bg.svg'; +import ArrowLeft from '@/images/icons/arrow_left.svg'; +import LogoWhite from '@/images/icons/logo_white.svg'; +import type { RootStackParamList } from '@/navigation'; +import { black, slate700, white } from '@/utils/colors'; +import { dinot, dinotBold } from '@/utils/fonts'; + +const GratificationScreen: React.FC = () => { + const { top, bottom } = useSafeAreaInsets(); + const navigation = + useNavigation>(); + const route = useRoute(); + const params = route.params as { points?: number } | undefined; + const pointsEarned = params?.points ?? 0; + const [isAnimationFinished, setIsAnimationFinished] = useState(false); + const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + + const handleExploreRewards = () => { + // Navigate to Points screen + navigation.navigate('Points' as never); + }; + + const handleInviteFriend = () => { + navigation.navigate('Referral' as never); + }; + + const handleBackPress = () => { + navigation.goBack(); + }; + + const handleAnimationFinish = useCallback(() => { + setIsAnimationFinished(true); + }, []); + + // Show animation first, then content after it finishes + if (!isAnimationFinished) { + return ( + + + + ); + } + + return ( + + + {/* Full screen background */} + + + + + {/* Black overlay for top safe area (status bar) */} + + + {/* Black overlay for bottom safe area */} + + + {/* Back button */} + + + + + + + + + {/* Main content container */} + + {/* Dialogue container */} + + {/* Logo icon */} + + + + + {/* Points display */} + + + {pointsEarned} + + + points earned + + + + {/* Description text */} + + Earn more points by proving your identity and referring friends + + + + {/* Bottom button container */} + + + Explore rewards + + [ + styles.secondaryButton, + pressed && styles.secondaryButtonPressed, + ]} + > + Invite friends + + + + + ); +}; + +export default GratificationScreen; + +const styles = StyleSheet.create({ + primaryButton: { + borderRadius: 60, + borderWidth: 1, + borderColor: slate700, + padding: 14, + }, + secondaryButton: { + width: '100%', + backgroundColor: white, + borderWidth: 1, + borderColor: white, + padding: 14, + borderRadius: 60, + alignItems: 'center', + justifyContent: 'center', + }, + secondaryButtonPressed: { + opacity: 0.8, + }, + secondaryButtonText: { + fontFamily: dinot, + fontSize: 18, + color: black, + textAlign: 'center', + }, + logoContainer: { + paddingBottom: 24, + }, + animation: { + width: '100%', + height: '100%', + }, +}); diff --git a/app/src/screens/app/LaunchScreen.tsx b/app/src/screens/app/LaunchScreen.tsx index f2e42ef7f..686028c1e 100644 --- a/app/src/screens/app/LaunchScreen.tsx +++ b/app/src/screens/app/LaunchScreen.tsx @@ -2,11 +2,11 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import React from 'react'; -import { StyleSheet, View } from 'react-native'; +import { Pressable, StyleSheet, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Anchor, Text, YStack } from 'tamagui'; +import { useTurnkey } from '@turnkey/react-native-wallet-kit'; import { AbstractButton, @@ -18,6 +18,7 @@ import { AppEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; import { privacyUrl, termsUrl } from '@/consts/links'; import useConnectionModal from '@/hooks/useConnectionModal'; import useHapticNavigation from '@/hooks/useHapticNavigation'; +import { useModal } from '@/hooks/useModal'; import IDCardPlaceholder from '@/images/icons/id_card_placeholder.svg'; import { black, @@ -31,10 +32,34 @@ import { advercase, dinot } from '@/utils/fonts'; const LaunchScreen: React.FC = () => { useConnectionModal(); + const { handleGoogleOauth, fetchWallets } = useTurnkey(); const onPress = useHapticNavigation('CountryPicker'); const createMock = useHapticNavigation('CreateMock'); const { bottom } = useSafeAreaInsets(); + const { showModal: showNoWalletsModal } = useModal({ + titleText: 'No wallets found', + bodyText: 'No wallets found. Please sign in with Turnkey to continue.', + buttonText: 'OK', + onButtonPress: () => {}, + onModalDismiss: () => {}, + }); + const onImportWalletPress = async () => { + try { + await handleGoogleOauth(); + const fetchedWallets = await fetchWallets(); + + if (fetchedWallets.length === 0) { + showNoWalletsModal(); + return; + } + + onPress(); + } catch (error) { + console.error('handleGoogleOauth error', error); + } + }; + const devModeTap = Gesture.Tap() .numberOfTaps(5) .onStart(() => { @@ -98,6 +123,13 @@ const LaunchScreen: React.FC = () => { Get Started + + + {`Have an account? `} + restore + + + By continuing, you agree to the  @@ -149,7 +181,21 @@ const styles = StyleSheet.create({ width: 40, height: 40, }, - + disclaimer: { + width: '100%', + fontSize: 11, + letterSpacing: 0.4, + textTransform: 'uppercase', + fontWeight: '500', + fontFamily: 'DIN OT', + textAlign: 'center', + }, + haveAnAccount: { + color: '#6b7280', + }, + restore: { + color: '#fff', + }, notice: { fontFamily: dinot, marginVertical: 10, diff --git a/app/src/screens/app/ReferralScreen.tsx b/app/src/screens/app/ReferralScreen.tsx new file mode 100644 index 000000000..042325486 --- /dev/null +++ b/app/src/screens/app/ReferralScreen.tsx @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { Platform } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { XStack, YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; + +import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; +import { PointEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; + +import { CopyReferralButton } from '@/components/referral/CopyReferralButton'; +import { ReferralHeader } from '@/components/referral/ReferralHeader'; +import { ReferralInfo } from '@/components/referral/ReferralInfo'; +import { ShareButton } from '@/components/referral/ShareButton'; +import { useReferralMessage } from '@/hooks/useReferralMessage'; +import Message from '@/images/icons/message.svg'; +import ShareBlue from '@/images/icons/share_blue.svg'; +import WhatsApp from '@/images/icons/whatsapp.svg'; +import Referral from '@/images/referral.png'; +import type { RootStackParamList } from '@/navigation'; +import { blue600, green500, slate50, slate200 } from '@/utils/colors'; +import { + shareViaNative, + shareViaSMS, + shareViaWhatsApp, +} from '@/utils/referralShare'; + +const ReferralScreen: React.FC = () => { + const selfClient = useSelfClient(); + const { bottom } = useSafeAreaInsets(); + const navigation = + useNavigation>(); + + const { message, referralLink } = useReferralMessage(); + + // Android Messages uses blue, iOS Messages uses green + const messagesButtonColor = Platform.OS === 'android' ? blue600 : green500; + + const handleShareMessages = async () => { + selfClient.trackEvent(PointEvents.EARN_REFERAL_MESSAGES); + await shareViaSMS(message); + }; + + const handleShare = async () => { + selfClient.trackEvent(PointEvents.EARN_REFERAL_SHARE); + await shareViaNative(message, referralLink, 'Join Self'); + }; + + const handleShareWhatsApp = async () => { + selfClient.trackEvent(PointEvents.EARN_REFERAL_WHATSAPP); + await shareViaWhatsApp(message); + }; + + return ( + + navigation.goBack()} + /> + + + + + + } + label="Messages" + backgroundColor={messagesButtonColor} + onPress={handleShareMessages} + /> + } + label="Share" + backgroundColor={slate200} + onPress={handleShare} + /> + } + label="WhatsApp" + backgroundColor={green500} + onPress={handleShareWhatsApp} + /> + + + + + + ); +}; + +export default ReferralScreen; diff --git a/app/src/screens/app/SplashScreen.tsx b/app/src/screens/app/SplashScreen.tsx index dc6d4d060..952cec614 100644 --- a/app/src/screens/app/SplashScreen.tsx +++ b/app/src/screens/app/SplashScreen.tsx @@ -29,6 +29,7 @@ import { handleUrl, setDeeplinkParentScreen, } from '@/utils/deeplinks'; +import { IS_DEV_MODE } from '@/utils/devUtils'; import { impactLight } from '@/utils/haptic'; const SplashScreen: React.FC = ({}) => { @@ -72,7 +73,7 @@ const SplashScreen: React.FC = ({}) => { } const hasValid = await hasAnyValidRegisteredDocument(selfClient); - const parentScreen = hasValid ? 'Home' : 'Launch'; + const parentScreen = hasValid ? 'Home' : 'Home'; // Migrate keychain to secure storage with biometric protection try { @@ -85,7 +86,7 @@ const SplashScreen: React.FC = ({}) => { const queuedUrl = getAndClearQueuedUrl(); if (queuedUrl) { - if (typeof __DEV__ !== 'undefined' && __DEV__) { + if (IS_DEV_MODE) { console.log('Processing queued deeplink:', queuedUrl); } setQueuedDeepLink(queuedUrl); diff --git a/app/src/screens/dev/DevSettingsScreen.tsx b/app/src/screens/dev/DevSettingsScreen.tsx index afae0cd38..e15cd2f0f 100644 --- a/app/src/screens/dev/DevSettingsScreen.tsx +++ b/app/src/screens/dev/DevSettingsScreen.tsx @@ -25,6 +25,7 @@ import WarningIcon from '@/images/icons/warning.svg'; import type { RootStackParamList } from '@/navigation'; import { unsafe_clearSecrets } from '@/providers/authProvider'; import { usePassport } from '@/providers/passportDataProvider'; +import { usePointEventStore } from '@/stores/pointEventStore'; import { useSettingStore } from '@/stores/settingStore'; import { red500, @@ -38,6 +39,7 @@ import { white, yellow500, } from '@/utils/colors'; +import { IS_DEV_MODE } from '@/utils/devUtils'; import { dinot } from '@/utils/fonts'; import { isNotificationSystemReady, @@ -204,6 +206,7 @@ const items = [ 'QRCodeViewFinder', 'Prove', 'ProofRequestStatus', + 'Referral', 'Settings', 'AccountRecovery', 'SaveRecoveryPhrase', @@ -292,6 +295,8 @@ const ScreenSelector = ({}) => { const DevSettingsScreen: React.FC = ({}) => { const { clearDocumentCatalogForMigrationTesting } = usePassport(); + const clearPointEvents = usePointEventStore(state => state.clearEvents); + const { resetBackupForPoints } = useSettingStore(); const navigation = useNavigation() as NativeStackScreenProps['navigation']; const subscribedTopics = useSettingStore(state => state.subscribedTopics); @@ -460,6 +465,81 @@ const DevSettingsScreen: React.FC = ({}) => { ); }; + const handleClearPointEventsPress = () => { + Alert.alert( + 'Clear Point Events', + 'Are you sure you want to clear all point events from local storage?\n\nThis will reset your point history but not affect your actual points on the blockchain.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Clear', + style: 'destructive', + onPress: async () => { + await clearPointEvents(); + Alert.alert('Success', 'Point events cleared successfully.', [ + { text: 'OK' }, + ]); + }, + }, + ], + ); + }; + + const handleResetBackupStatePress = () => { + Alert.alert( + 'Reset Backup State', + 'Are you sure you want to reset the backup state?\n\nThis will allow you to see and trigger the backup points flow again.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Reset', + style: 'destructive', + onPress: () => { + resetBackupForPoints(); + Alert.alert('Success', 'Backup state reset successfully.', [ + { text: 'OK' }, + ]); + }, + }, + ], + ); + }; + + const handleClearBackupEventsPress = () => { + Alert.alert( + 'Clear Backup Events', + 'Are you sure you want to clear all backup point events from local storage?\n\nThis will remove backup events from your point history.', + [ + { + text: 'Cancel', + style: 'cancel', + }, + { + text: 'Clear', + style: 'destructive', + onPress: async () => { + const events = usePointEventStore.getState().events; + const backupEvents = events.filter( + event => event.type === 'backup', + ); + for (const event of backupEvents) { + await usePointEventStore.getState().removeEvent(event.id); + } + Alert.alert('Success', 'Backup events cleared successfully.', [ + { text: 'OK' }, + ]); + }, + }, + ], + ); + }; + return ( = ({}) => { + {IS_DEV_MODE && ( + + )} @@ -607,6 +712,21 @@ const DevSettingsScreen: React.FC = ({}) => { onPress: handleClearDocumentCatalogPress, dangerTheme: true, }, + { + label: 'Clear point events', + onPress: handleClearPointEventsPress, + dangerTheme: true, + }, + { + label: 'Reset backup state', + onPress: handleResetBackupStatePress, + dangerTheme: true, + }, + { + label: 'Clear backup events', + onPress: handleClearBackupEventsPress, + dangerTheme: true, + }, ].map(({ label, onPress, dangerTheme }) => ( + ); }; diff --git a/app/src/screens/onboarding/AccountVerifiedSuccessScreen.tsx b/app/src/screens/onboarding/AccountVerifiedSuccessScreen.tsx index ec1627902..8c81920a2 100644 --- a/app/src/screens/onboarding/AccountVerifiedSuccessScreen.tsx +++ b/app/src/screens/onboarding/AccountVerifiedSuccessScreen.tsx @@ -58,7 +58,7 @@ const AccountVerifiedSuccessScreen: React.FC = ({}) => { trackEvent={BackupEvents.ACCOUNT_VERIFICATION_COMPLETED} onPress={() => { buttonTap(); - navigation.navigate('Home'); + navigation.navigate({ name: 'Home', params: {} }); }} > Continue diff --git a/app/src/screens/onboarding/DisclaimerScreen.tsx b/app/src/screens/onboarding/DisclaimerScreen.tsx index 5f8ceb562..cbbc86a7c 100644 --- a/app/src/screens/onboarding/DisclaimerScreen.tsx +++ b/app/src/screens/onboarding/DisclaimerScreen.tsx @@ -63,7 +63,7 @@ const DisclaimerScreen: React.FC = () => { onPress={() => { confirmTap(); dismissPrivacyNote(); - navigation.navigate('Home'); + navigation.navigate({ name: 'Home', params: {} }); }} > Dismiss diff --git a/app/src/screens/verification/ProveScreen.tsx b/app/src/screens/verification/ProveScreen.tsx index 8f3b64280..e0cd3c471 100644 --- a/app/src/screens/verification/ProveScreen.tsx +++ b/app/src/screens/verification/ProveScreen.tsx @@ -235,8 +235,8 @@ const ProveScreen: React.FC = () => { )} @@ -249,7 +249,7 @@ const ProveScreen: React.FC = () => { style={{ fontSize: 24, color: slate300, textAlign: 'center' }} > {selectedApp.appName} is requesting - that you prove the following information: + you to prove the following information: )} diff --git a/app/src/stores/pointEventStore.ts b/app/src/stores/pointEventStore.ts new file mode 100644 index 000000000..2e8945c37 --- /dev/null +++ b/app/src/stores/pointEventStore.ts @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { create } from 'zustand'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +import type { + IncomingPoints, + PointEvent, + PointEventType, +} from '@/utils/points'; +import { + getIncomingPoints, + getNextSundayNoonUTC, + getPointsAddress, + getTotalPoints, +} from '@/utils/points'; +import { pollEventProcessingStatus } from '@/utils/points/eventPolling'; + +interface PointEventState { + events: PointEvent[]; + isLoading: boolean; + loadEvents: () => Promise; + addEvent: ( + title: string, + type: PointEventType, + points: number, + id: string, + ) => Promise; + markEventAsProcessed: (id: string) => Promise; + markEventAsFailed: (id: string) => Promise; + removeEvent: (id: string) => Promise; + clearEvents: () => Promise; + getUnprocessedEvents: () => PointEvent[]; + totalOptimisticIncomingPoints: () => number; + incomingPoints: IncomingPoints & { + lastUpdated: number | null; + promise: Promise | null; + }; + // these are the real points that are on chain. each sunday noon UTC they get updated based on incoming points + points: number; + refreshPoints: () => Promise; + fetchIncomingPoints: () => Promise; + refreshIncomingPoints: () => Promise; + getAllPointEvents: () => PointEvent[]; +} + +const STORAGE_KEY = '@point_events'; + +const DESIRED_EVENT_TYPES = ['refer', 'notification', 'backup', 'disclosure']; + +export const usePointEventStore = create()((set, get) => ({ + incomingPoints: { + amount: 0, + lastUpdated: null, + promise: null, + expectedDate: getNextSundayNoonUTC(), + }, + points: 0, + events: [], + isLoading: false, + refreshPoints: async () => { + try { + const address = await getPointsAddress(); + const points = await getTotalPoints(address); + set({ points }); + } catch (error) { + console.error('Error refreshing points:', error); + } + }, + // should only be called once on app startup + getAllPointEvents: () => { + return get() + .events.filter(event => DESIRED_EVENT_TYPES.includes(event.type)) + .sort((a, b) => b.timestamp - a.timestamp); + }, + loadEvents: async () => { + try { + set({ isLoading: true }); + const stored = await AsyncStorage.getItem(STORAGE_KEY); + if (stored) { + try { + const parsed = JSON.parse(stored); + // Validate that parsed data is an array + if (!Array.isArray(parsed)) { + console.error('Invalid stored events format, expected array'); + set({ events: [], isLoading: false }); + return; + } + // Validate each event has required fields + const events: PointEvent[] = parsed.filter((event: unknown) => { + if ( + typeof event === 'object' && + event !== null && + 'id' in event && + 'status' in event && + 'points' in event + ) { + return true; + } + console.warn('Skipping invalid event:', event); + return false; + }) as PointEvent[]; + set({ events, isLoading: false }); + // Resume polling for any pending events that were interrupted by app restart + // (New events are polled immediately in recordEvents.ts when created) + get() + .getUnprocessedEvents() + .forEach(event => { + // Use event.id as job_id (id is the job_id) + pollEventProcessingStatus(event.id).then(result => { + if (result === 'completed') { + get().markEventAsProcessed(event.id); + } else if (result === 'failed') { + get().markEventAsFailed(event.id); + } + }); + }); + } catch (parseError) { + console.error('Error parsing stored events:', parseError); + // Clear corrupted data + await AsyncStorage.removeItem(STORAGE_KEY); + set({ events: [], isLoading: false }); + } + } else { + set({ isLoading: false }); + } + } catch (error) { + console.error('Error loading point events:', error); + set({ isLoading: false }); + } + }, + + fetchIncomingPoints: async () => { + if (get().incomingPoints.promise) { + return await get().incomingPoints.promise; + } + const promise = getIncomingPoints(); + set({ + incomingPoints: { + ...get().incomingPoints, + promise: promise, + }, + }); + try { + const points = await promise; + return points; + } finally { + // Clear promise after completion (success or failure) + // Only clear if it's still the same promise (no concurrent update) + if (get().incomingPoints.promise === promise) { + set({ + incomingPoints: { + ...get().incomingPoints, + promise: null, + }, + }); + } + } + }, + /* + * Fetches incoming points from the backend and updates the store. + * @param otherState Optional additional state to merge into incomingPoints. so they can be updated atomically. + */ + refreshIncomingPoints: async () => { + // Avoid concurrent updates + if (get().incomingPoints.promise) { + return; + } + + // Fetch incoming points + try { + const points = await get().fetchIncomingPoints(); + if (points === null) { + // Fetch failed, promise already cleared by fetchIncomingPoints + return; + } + // points are not saved to local storage as that would lead to stale data + // Refresh expectedDate to ensure it's current + set({ + incomingPoints: { + ...get().incomingPoints, + lastUpdated: Date.now(), + amount: points.amount, + promise: null, // Already cleared by fetchIncomingPoints, but ensure it's null + expectedDate: points.expectedDate, + }, + }); + } catch (error) { + console.error('Error refreshing incoming points:', error); + // Promise already cleared by fetchIncomingPoints in finally block + } + }, + getUnprocessedEvents: () => { + return get().events.filter(event => event.status === 'pending'); + }, + /* + * Calculates the total optimistic incoming points based on the current events. + */ + totalOptimisticIncomingPoints: () => { + const optimisticIncomingPoints = get() + .getUnprocessedEvents() + .reduce((sum, event) => sum + event.points, 0); + return optimisticIncomingPoints + get().incomingPoints.amount; + }, + + addEvent: async (title, type, points, id) => { + try { + const newEvent: PointEvent = { + id, + title, + type, + timestamp: Date.now(), + points, + status: 'pending', + }; + + const currentEvents = get().events; + const updatedEvents = [newEvent, ...currentEvents]; + + // Save to storage first, then update state to maintain consistency + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updatedEvents)); + set({ events: updatedEvents }); + } catch (error) { + console.error('Error adding point event:', error); + // Don't update state if storage fails - maintain consistency + throw error; // Re-throw so caller knows it failed + } + }, + + markEventAsProcessed: async (id: string) => { + try { + // Re-read events to avoid race conditions with concurrent updates + const currentEvents = get().events; + // Check if event still exists and is still pending + const event = currentEvents.find(e => e.id === id); + if (!event) { + console.warn(`Event ${id} not found when marking as processed`); + return; + } + if (event.status !== 'pending') { + // Already processed, skip + return; + } + + const updatedEvents = currentEvents.map(e => + e.id === id ? { ...e, status: 'completed' as const } : e, + ); + // Fetch fresh incoming points from server while saving events to storage + // points are not saved to local storage as that would lead to stale data + // Use fetchIncomingPoints to reuse promise caching and avoid race conditions + const [points] = await Promise.all([ + get().fetchIncomingPoints(), + AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updatedEvents)), + ]); + + // Re-check events haven't changed during async operations + const latestEvents = get().events; + const latestEvent = latestEvents.find(e => e.id === id); + if (latestEvent && latestEvent.status !== 'pending') { + // Event was already updated by another call, merge updates carefully + const finalEvents = latestEvents.map(e => + e.id === id ? { ...e, status: 'completed' as const } : e, + ); + set({ events: finalEvents }); + } else { + // Atomically update both events and incoming points in single state update + if (points !== null) { + set({ + events: updatedEvents, + incomingPoints: { + ...get().incomingPoints, + promise: null, + lastUpdated: Date.now(), + amount: points.amount, + expectedDate: points.expectedDate, + }, + }); + } else { + // If fetch failed, just update events + set({ events: updatedEvents }); + } + } + } catch (error) { + console.error('Error marking point event as processed:', error); + // Don't update state if storage fails + } + }, + + markEventAsFailed: async (id: string) => { + try { + const currentEvents = get().events; + const event = currentEvents.find(e => e.id === id); + if (!event) { + console.warn(`Event ${id} not found when marking as failed`); + return; + } + if (event.status !== 'pending') { + // Already processed, skip + return; + } + + const updatedEvents = currentEvents.map(e => + e.id === id ? { ...e, status: 'failed' as const } : e, + ); + + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updatedEvents)); + set({ events: updatedEvents }); + } catch (error) { + console.error('Error marking point event as failed:', error); + // Don't update state if storage fails + } + }, + + removeEvent: async id => { + try { + const currentEvents = get().events; + const updatedEvents = currentEvents.filter(event => event.id !== id); + + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updatedEvents)); + set({ events: updatedEvents }); + } catch (error) { + console.error('Error removing point event:', error); + } + }, + + clearEvents: async () => { + try { + await AsyncStorage.removeItem(STORAGE_KEY); + set({ events: [] }); + } catch (error) { + console.error('Error clearing point events:', error); + } + }, +})); diff --git a/app/src/stores/settingStore.ts b/app/src/stores/settingStore.ts index c1c2f0545..268d167b2 100644 --- a/app/src/stores/settingStore.ts +++ b/app/src/stores/settingStore.ts @@ -24,10 +24,15 @@ interface PersistedSettingsState { setKeychainMigrationCompleted: () => void; fcmToken: string | null; setFcmToken: (token: string | null) => void; + backedUpWithTurnKey: boolean; + setBackedUpWithTurnKey: (backedUpWithTurnKey: boolean) => void; subscribedTopics: string[]; setSubscribedTopics: (topics: string[]) => void; addSubscribedTopic: (topic: string) => void; removeSubscribedTopic: (topic: string) => void; + hasCompletedBackupForPoints: boolean; + setBackupForPointsCompleted: () => void; + resetBackupForPoints: () => void; pointsAddress: string | null; setPointsAddress: (address: string | null) => void; } @@ -98,6 +103,13 @@ export const useSettingStore = create()( subscribedTopics: state.subscribedTopics.filter(t => t !== topic), })), + backedUpWithTurnKey: false, + setBackedUpWithTurnKey: (backedUpWithTurnKey: boolean) => + set({ backedUpWithTurnKey }), + hasCompletedBackupForPoints: false, + setBackupForPointsCompleted: () => + set({ hasCompletedBackupForPoints: true }), + resetBackupForPoints: () => set({ hasCompletedBackupForPoints: false }), pointsAddress: null, setPointsAddress: (address: string | null) => set({ pointsAddress: address }), diff --git a/app/src/stores/userStore.ts b/app/src/stores/userStore.ts index d4a68de80..7493b227b 100644 --- a/app/src/stores/userStore.ts +++ b/app/src/stores/userStore.ts @@ -12,9 +12,12 @@ interface UserState { deepLinkNationality?: IdDocInput['nationality']; deepLinkBirthDate?: string; deepLinkGender?: string; + deepLinkReferrer?: string; idDetailsDocumentId?: string; + registeredReferrers: Set; update: (patch: Partial) => void; setIdDetailsDocumentId: (documentId: string) => void; + setDeepLinkReferrer: (referrer: string) => void; setDeepLinkUserDetails: (details: { name?: string; surname?: string; @@ -23,15 +26,20 @@ interface UserState { gender?: string; }) => void; clearDeepLinkUserDetails: () => void; + clearDeepLinkReferrer: () => void; + isReferrerRegistered: (referrer: string) => boolean; + markReferrerAsRegistered: (referrer: string) => void; } -const useUserStore = create((set, _get) => ({ +const useUserStore = create((set, get) => ({ deepLinkName: undefined, deepLinkSurname: undefined, deepLinkNationality: undefined, deepLinkBirthDate: undefined, deepLinkGender: undefined, idDetailsDocumentId: undefined, + deepLinkReferrer: undefined, + registeredReferrers: new Set(), update: patch => { set(state => ({ ...state, ...patch })); @@ -57,6 +65,23 @@ const useUserStore = create((set, _get) => ({ deepLinkBirthDate: undefined, deepLinkGender: undefined, }), + + setDeepLinkReferrer: (referrer: string) => + set({ deepLinkReferrer: referrer }), + + clearDeepLinkReferrer: () => set({ deepLinkReferrer: undefined }), + + isReferrerRegistered: (referrer: string) => { + const state = get(); + return state.registeredReferrers.has(referrer.toLowerCase()); + }, + + markReferrerAsRegistered: (referrer: string) => + set(state => { + const newSet = new Set(state.registeredReferrers); + newSet.add(referrer.toLowerCase()); + return { registeredReferrers: newSet }; + }), })); export default useUserStore; diff --git a/app/src/utils/constants.ts b/app/src/utils/constants.ts index 9c15d3da1..fcaf06fe1 100644 --- a/app/src/utils/constants.ts +++ b/app/src/utils/constants.ts @@ -2,4 +2,7 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. +export const TURNKEY_OAUTH_REDIRECT_URI_ANDROID = 'https://redirect.self.xyz'; +export const TURNKEY_OAUTH_REDIRECT_URI_IOS = + 'https://oauth-redirect.turnkey.com'; export const extraYPadding = 15; diff --git a/app/src/utils/deeplinks.ts b/app/src/utils/deeplinks.ts index 082af95c8..4f4eebcb5 100644 --- a/app/src/utils/deeplinks.ts +++ b/app/src/utils/deeplinks.ts @@ -11,18 +11,31 @@ import type { SelfClient } from '@selfxyz/mobile-sdk-alpha'; import { navigationRef } from '@/navigation'; import useUserStore from '@/stores/userStore'; +import { IS_DEV_MODE } from '@/utils/devUtils'; // Validation patterns for each expected parameter const VALIDATION_PATTERNS = { sessionId: /^[a-zA-Z0-9_-]+$/, selfApp: /^[\s\S]*$/, // JSON strings can contain any characters, we'll validate JSON parsing separately mock_passport: /^[\s\S]*$/, // JSON strings can contain any characters, we'll validate JSON parsing separately + code: /^[a-zA-Z0-9._/-]+$/, // OAuth authorization code (may include forward slashes) + state: /^[a-zA-Z0-9._-]+$/, // OAuth state parameter for CSRF protection + id_token: /^[\w\-.]+$/, // JWT token format (base64url encoded segments) + scope: /^[\w\s%:/.=&+*-]+$/, // OAuth scopes (can include spaces, encoded chars, and URL-encoded content) + scheme: /^https?$/, // Redirect scheme (http or https) + referrer: /^0x[a-fA-F0-9]+$/, } as const; type ValidatedParams = { sessionId?: string; selfApp?: string; mock_passport?: string; + code?: string; + state?: string; + id_token?: string; + scope?: string; + scheme?: string; + referrer?: string; }; // Define proper types for the mock data structure @@ -51,7 +64,7 @@ const validateAndSanitizeParam = ( try { decodedValue = decodeURIComponent(value); } catch (error) { - if (typeof __DEV__ !== 'undefined' && __DEV__) { + if (IS_DEV_MODE) { console.error(`Error decoding parameter ${key}:`, error); } return undefined; @@ -62,7 +75,7 @@ const validateAndSanitizeParam = ( const pattern = VALIDATION_PATTERNS[key as keyof typeof VALIDATION_PATTERNS]; if (!pattern.test(decodedValue)) { - if (typeof __DEV__ !== 'undefined' && __DEV__) { + if (IS_DEV_MODE) { console.error(`Parameter ${key} failed validation:`, decodedValue); } return undefined; @@ -97,7 +110,14 @@ export const getAndClearQueuedUrl = (): string | null => { export const handleUrl = (selfClient: SelfClient, uri: string) => { const validatedParams = parseAndValidateUrlParams(uri); - const { sessionId, selfApp: selfAppStr, mock_passport } = validatedParams; + const { + sessionId, + selfApp: selfAppStr, + mock_passport, + code, + id_token, + referrer, + } = validatedParams; if (selfAppStr) { try { @@ -109,7 +129,7 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => { return; } catch (error) { - if (typeof __DEV__ !== 'undefined' && __DEV__) { + if (IS_DEV_MODE) { console.error('Error parsing selfApp:', error); } navigationRef.reset( @@ -152,19 +172,44 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => { createDeeplinkNavigationState('MockDataDeepLink', correctParentScreen), ); } catch (error) { - if (typeof __DEV__ !== 'undefined' && __DEV__) { + if (IS_DEV_MODE) { console.error('Error parsing mock_passport data or navigating:', error); } navigationRef.reset( createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen), ); } + } else if (referrer && typeof referrer === 'string') { + useUserStore.getState().setDeepLinkReferrer(referrer); + + if (IS_DEV_MODE) { + console.log( + '[deeplinks] Setting referrer and navigating to HomeScreen for confirmation:', + referrer, + ); + } + + // Navigate to HomeScreen - it will show confirmation modal and then navigate to GratificationScreen + navigationRef.reset({ + index: 0, + routes: [{ name: 'Home' }], + }); } else if (Platform.OS === 'web') { // TODO: web handle links if we need to idk if we do // For web, we can handle the URL some other way if we dont do this loading app in web always navigates to QRCodeTrouble - } else { + } else if (code || id_token) { + // Handle Turnkey OAuth redirect if (typeof __DEV__ !== 'undefined' && __DEV__) { - console.error('No sessionId or selfApp found in the data'); + console.log( + '[Deeplinks] Turnkey OAuth redirect received with valid parameters', + ); + } + return; + } else { + if (IS_DEV_MODE) { + console.error( + 'No sessionId, selfApp or valid OAuth parameters found in the data', + ); } navigationRef.reset( createDeeplinkNavigationState('QRCodeTrouble', correctParentScreen), @@ -182,6 +227,22 @@ export const parseAndValidateUrlParams = (uri: string): ValidatedParams => { const parsed = parseUrl(uri); const query = parsed.query || {}; + if (uri.includes('#')) { + const fragmentString = uri.split('#')[1]; + if (fragmentString) { + try { + const fragmentParams = new URLSearchParams(fragmentString); + for (const [key, value] of fragmentParams.entries()) { + query[key] = value; + } + } catch (error) { + if (typeof __DEV__ !== 'undefined' && __DEV__) { + console.error('Error parsing fragment parameters:', error); + } + } + } + } + const validatedParams: ValidatedParams = {}; // Only process expected parameters and validate them @@ -191,7 +252,7 @@ export const parseAndValidateUrlParams = (uri: string): ValidatedParams => { if (sanitizedValue !== undefined) { validatedParams[key as keyof ValidatedParams] = sanitizedValue; } - } else if (typeof __DEV__ !== 'undefined' && __DEV__) { + } else if (IS_DEV_MODE) { // Log unexpected parameters in development console.warn(`Unexpected or invalid parameter ignored: ${key}`); } @@ -217,16 +278,34 @@ export const setupUniversalLinkListenerInNavigation = ( // Get the initial URL and store it for splash screen handling Linking.getInitialURL().then(url => { if (url) { - // Store the initial URL instead of handling it immediately + // Check if it's an OAuth callback - if so, don't queue it, let Turnkey handle it + // const validatedParams = parseAndValidateUrlParams(url); + // if (!validatedParams.code && !validatedParams.id_token) { + // console.log( + // 'not an OAuth callback, storing for splash screen handling', + // ); + // Not an OAuth callback, store for splash screen handling queuedInitialUrl = url; + // } } }); - // Handle subsequent URL events normally (when app is already running) const linkingEventListener = Linking.addEventListener('url', ({ url }) => { + // Check if this is an OAuth callback + // const validatedParams = parseAndValidateUrlParams(url); + // // console.log('validatedParams', validatedParams); + // if (validatedParams.code || validatedParams.id_token) { + // // This is an OAuth callback - don't handle it, let Turnkey SDK handle it + // if (typeof __DEV__ !== 'undefined' && __DEV__) { + // console.log( + // '[Deeplinks] OAuth callback detected - letting Turnkey SDK handle it', + // ); + // } + // return; // Don't call handleUrl for OAuth callbacks + // } + // For non-OAuth URLs, handle normally handleUrl(selfClient, url); }); - return () => { linkingEventListener.remove(); }; diff --git a/app/src/utils/devUtils.ts b/app/src/utils/devUtils.ts new file mode 100644 index 000000000..d0c958b50 --- /dev/null +++ b/app/src/utils/devUtils.ts @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +/** + * Constant indicating if the app is running in development mode. + * Safely handles cases where __DEV__ might not be defined. + * Use this constant instead of checking __DEV__ directly throughout the codebase. + */ +export const IS_DEV_MODE = typeof __DEV__ !== 'undefined' && __DEV__; diff --git a/app/src/utils/fonts.ts b/app/src/utils/fonts.ts index a7e941a2d..8740a2cb0 100644 --- a/app/src/utils/fonts.ts +++ b/app/src/utils/fonts.ts @@ -2,4 +2,9 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -export { advercase, dinot, plexMono } from '@selfxyz/mobile-sdk-alpha'; +export { + advercase, + dinot, + dinotBold, + plexMono, +} from '@selfxyz/mobile-sdk-alpha'; diff --git a/app/src/utils/notifications/notificationService.ts b/app/src/utils/notifications/notificationService.ts index e56033fc4..6384a6297 100644 --- a/app/src/utils/notifications/notificationService.ts +++ b/app/src/utils/notifications/notificationService.ts @@ -38,10 +38,6 @@ const error = (...args: unknown[]) => { export { getStateMessage }; -/** - * Check if notifications are ready on iOS (APNs token registered with FCM) - * @returns true if ready, false otherwise - */ export async function isNotificationSystemReady(): Promise<{ ready: boolean; message: string; @@ -93,6 +89,19 @@ export async function isNotificationSystemReady(): Promise<{ } } +export async function isTopicSubscribed(topic: string): Promise { + try { + const readiness = await isNotificationSystemReady(); + if (!readiness.ready) { + return false; + } + const subscribedTopics = useSettingStore.getState().subscribedTopics; + return subscribedTopics.includes(topic); + } catch { + return false; + } +} + export async function registerDeviceToken( sessionId: string, deviceToken?: string, diff --git a/app/src/utils/points/api.ts b/app/src/utils/points/api.ts new file mode 100644 index 000000000..617202ba1 --- /dev/null +++ b/app/src/utils/points/api.ts @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type { AxiosError } from 'axios'; +import axios from 'axios'; +import { Buffer } from 'buffer'; +import { ethers } from 'ethers'; + +import { unsafe_getPrivateKey } from '@/providers/authProvider'; +import { showErrorModal } from '@/utils/points/showErrorModal'; + +export type ApiResponse = { + success: boolean; + status: number; + error?: string; + data?: T; +}; + +export interface SignatureData { + signature: string; // base64-encoded signature + parity: number; // yParity value (0 or 1) +} + +/** + * Interface for signature data to be included in API requests + */ +export const POINTS_API_BASE_URL = + 'https://points-backend-1025466915061.us-central1.run.app'; + +/** + * Successful HTTP status codes accepted by the points API + */ +const SUCCESSFUL_STATUS_CODES = [200, 202] as const; + +/** + * Checks if a status code is considered successful + */ +export const isSuccessfulStatus = (status: number): boolean => + SUCCESSFUL_STATUS_CODES.includes( + status as (typeof SUCCESSFUL_STATUS_CODES)[number], + ); + +/** + * Generates a signature for API authentication. + * Signs the lowercase wallet address using the user's private key. + * + * @param address - The wallet address to sign (will be lowercased) + * @returns Signature data including base64 signature and parity + * @throws Error if private key cannot be retrieved or signing fails + */ +const generateSignature = async (address: string): Promise => { + try { + // Get the private key from keychain (requires biometric auth) + const privateKey = await unsafe_getPrivateKey(); + if (!privateKey) { + throw new Error('Failed to retrieve private key for signing'); + } + + // Create wallet from private key + const wallet = new ethers.Wallet(privateKey); + + // Sign the lowercase address + const message = address.toLowerCase(); + const signature = await wallet.signMessage(message); + + // Parse signature to extract parity + const sig = ethers.Signature.from(signature); + + // Convert signature to base64 + const sigBytes = ethers.getBytes(signature); + const signatureBase64 = Buffer.from(sigBytes).toString('base64'); + + return { + signature: signatureBase64, + parity: sig.yParity, + }; + } catch (error) { + console.error('Error generating signature:', error); + throw new Error( + `Failed to generate signature: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + } +}; + +/** + * Makes a POST request to the points API with consistent error handling. + * Automatically includes signature and parity for authentication by detecting + * the signing address from the request body (uses 'referee' or 'address' field). + * + * @param endpoint - The API endpoint path + * @param body - The request body data + * @param errorMessages - Optional custom error messages for specific error codes + */ +export const makeApiRequest = async ( + endpoint: string, + body: Record, + errorMessages?: Record, +): Promise> => { + try { + // Auto-detect signing address from body (referee for referrals, address for other endpoints) + const signingAddress = (body.referee as string) || (body.address as string); + + // Lowercase address fields and prepare request body + let requestBody = { ...body }; + if (body.referee) { + requestBody.referee = (body.referee as string).toLowerCase(); + } + if (body.referrer) { + requestBody.referrer = (body.referrer as string).toLowerCase(); + } + if (body.address) { + requestBody.address = (body.address as string).toLowerCase(); + } + + // Generate signature if a signing address is detected + if (signingAddress) { + const signatureData = await generateSignature(signingAddress); + requestBody = { + ...requestBody, + signature: signatureData.signature, + parity: signatureData.parity, + }; + } + + const response = await axios.post( + `${POINTS_API_BASE_URL}${endpoint}`, + requestBody, + { + headers: { + 'Content-Type': 'application/json', + }, + validateStatus: () => true, // Don't throw on any status + }, + ); + + if (isSuccessfulStatus(response.status)) { + return { success: true, status: response.status, data: response.data }; + } + + let errorMessage = 'An unexpected error occurred. Please try again.'; + if (errorMessages && response.data?.status) { + errorMessage = + errorMessages[response.data.status] || + response.data.message || + errorMessage; + } else if (response.data?.message) { + errorMessage = response.data.message; + } + + return { success: false, status: response.status, error: errorMessage }; + } catch (error) { + console.error(`Error making API request to ${endpoint}:`, error); + const axiosError = error as AxiosError; + showErrorModal(); + return { + success: false, + status: axiosError.response?.status || 500, + error: + axiosError.message || + 'Network error. Please check your connection and try again.', + }; + } +}; diff --git a/app/src/utils/points/eventPolling.ts b/app/src/utils/points/eventPolling.ts new file mode 100644 index 000000000..31b0f013d --- /dev/null +++ b/app/src/utils/points/eventPolling.ts @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { checkEventProcessingStatus } from './jobStatus'; + +/** + * Polls the server to check if an event has been processed. + * Checks at: 2s, 4s, 8s, 16s, 32s, 32s, 32s, 32s + * Returns 'completed' if completed, 'failed' if failed, or null if max attempts reached + */ +export async function pollEventProcessingStatus( + id: string, +): Promise<'completed' | 'failed' | null> { + let delay = 2000; // Start at 2 seconds + const maxAttempts = 10; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + await sleep(delay); + + try { + const status = await checkEventProcessingStatus(id); + if (status === 'completed') { + return 'completed'; + } + if (status === 'failed') { + return 'failed'; + } + // If status is 'pending' or null, continue polling + } catch (error) { + console.error(`Error checking event ${id} status:`, error); + // Continue polling even on error + } + + // Exponential backoff, max 32 seconds + delay = Math.min(delay * 2, 32000); + } + + return null; // Gave up after max attempts +} + +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/app/src/utils/points/getEvents.ts b/app/src/utils/points/getEvents.ts new file mode 100644 index 000000000..bcc0af372 --- /dev/null +++ b/app/src/utils/points/getEvents.ts @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type { PointEvent, PointEventType } from '@/utils/points/types'; + +/** + * Shared helper to get events from store filtered by type. + */ +const getEventsByType = async (type: PointEventType): Promise => { + try { + const { usePointEventStore } = await import('@/stores/pointEventStore'); + const events = usePointEventStore.getState().events; + return events.filter(event => event.type === type); + } catch (error) { + console.error(`Error loading ${type} point events:`, error); + return []; + } +}; + +export const getAllPointEvents = async (): Promise => { + const [disclosures, notifications, backups, referrals] = await Promise.all([ + getDisclosurePointEvents(), + getPushNotificationPointEvents(), + getBackupPointEvents(), + getReferralPointEvents(), + ]); + return [...disclosures, ...notifications, ...backups, ...referrals].sort( + (a, b) => b.timestamp - a.timestamp, + ); +}; + +export const getBackupPointEvents = async (): Promise => { + return getEventsByType('backup'); +}; + +export const getDisclosurePointEvents = async (): Promise => { + return getEventsByType('disclosure'); +}; + +export const getPushNotificationPointEvents = async (): Promise< + PointEvent[] +> => { + return getEventsByType('notification'); +}; + +export const getReferralPointEvents = async (): Promise => { + return getEventsByType('refer'); +}; diff --git a/app/src/utils/points/index.ts b/app/src/utils/points/index.ts new file mode 100644 index 000000000..d87a733ed --- /dev/null +++ b/app/src/utils/points/index.ts @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +// Re-export all types and constants +export type { + IncomingPoints, + PointEvent, + PointEventType, +} from '@/utils/points/types'; +export { POINT_VALUES } from '@/utils/points/types'; + +// Re-export all utility functions +export { + formatTimeUntilDate, + getIncomingPoints, + getNextSundayNoonUTC, + getPointsAddress, + getTotalPoints, + getWhiteListedDisclosureAddresses, + hasUserAnIdentityDocumentRegistered, + hasUserDoneThePointsDisclosure, + pointsSelfApp, +} from '@/utils/points/utils'; + +// Re-export event getter functions +export { + getAllPointEvents, + getBackupPointEvents, + getDisclosurePointEvents, + getPushNotificationPointEvents, + getReferralPointEvents, +} from '@/utils/points/getEvents'; + +// Re-export event recording functions +export { + recordBackupPointEvent, + recordNotificationPointEvent, + recordReferralPointEvent, +} from '@/utils/points/recordEvents'; + +// Re-export event registration functions +export { + registerBackupPoints, + registerNotificationPoints, + registerReferralPoints, +} from '@/utils/points/registerEvents'; diff --git a/app/src/utils/points/jobStatus.ts b/app/src/utils/points/jobStatus.ts new file mode 100644 index 000000000..111516529 --- /dev/null +++ b/app/src/utils/points/jobStatus.ts @@ -0,0 +1,45 @@ +import { POINTS_API_BASE_URL } from './api'; + +export type JobStatusResponse = { + job_id: string; + status: 'complete' | 'failed'; +}; + +export async function checkEventProcessingStatus( + jobId: string, +): Promise<'pending' | 'completed' | 'failed' | null> { + try { + const response = await fetch(`${POINTS_API_BASE_URL}/job/${jobId}/status`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + // 102 means pending + if (response.status === 102) { + return 'pending'; + } + + // 404 means job not found - stop polling as it will never be found + if (response.status === 404) { + return 'failed'; + } + + // 200 means completed or failed - check the response body + if (response.status === 200) { + const data: JobStatusResponse = await response.json(); + if (data.status === 'complete') { + return 'completed'; + } + if (data.status === 'failed') { + return 'failed'; + } + } + + return null; + } catch (error) { + console.error(`Error checking job ${jobId} status:`, error); + return null; + } +} diff --git a/app/src/utils/points/recordEvents.ts b/app/src/utils/points/recordEvents.ts new file mode 100644 index 000000000..75f14353a --- /dev/null +++ b/app/src/utils/points/recordEvents.ts @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { isSuccessfulStatus } from '@/utils/points/api'; +import { pollEventProcessingStatus } from '@/utils/points/eventPolling'; +import { + registerBackupPoints, + registerNotificationPoints, + registerReferralPoints, +} from '@/utils/points/registerEvents'; +import type { PointEventType } from '@/utils/points/types'; +import { POINT_VALUES } from '@/utils/points/types'; +import { getPointsAddress } from '@/utils/points/utils'; + +/** + * Shared helper to add an event to the store and start polling for processing. + */ +const addEventToStoreAndPoll = async ( + title: string, + type: PointEventType, + points: number, + jobId: string, +): Promise => { + const { usePointEventStore } = await import('@/stores/pointEventStore'); + // Use job_id as the event id + await usePointEventStore.getState().addEvent(title, type, points, jobId); + + // Start polling in background - don't await + pollEventProcessingStatus(jobId).then(result => { + if (result === 'completed') { + usePointEventStore.getState().markEventAsProcessed(jobId); + } else if (result === 'failed') { + usePointEventStore.getState().markEventAsFailed(jobId); + } + }); +}; + +/** + * Records a backup event by registering with API and storing locally. + * + * @returns Promise resolving to success status and error message if any + */ +export const recordBackupPointEvent = async (): Promise<{ + success: boolean; + error?: string; +}> => { + try { + const userAddress = await getPointsAddress(); + const response = await registerBackupPoints(userAddress); + + if ( + response.success && + isSuccessfulStatus(response.status) && + response.jobId + ) { + await addEventToStoreAndPoll( + 'Secret backed up', + 'backup', + POINT_VALUES.backup, + response.jobId, + ); + return { success: true }; + } + return { success: false, error: response.error }; + } catch (error) { + console.error('Error recording backup point event:', error); + return { + success: false, + error: 'An unexpected error occurred. Please try again.', + }; + } +}; + +/** + * Records a notification event by registering with API and storing locally. + * + * @returns Promise resolving to success status and error message if any + */ +export const recordNotificationPointEvent = async (): Promise<{ + success: boolean; + error?: string; +}> => { + try { + const userAddress = await getPointsAddress(); + const response = await registerNotificationPoints(userAddress); + + if ( + response.success && + isSuccessfulStatus(response.status) && + response.jobId + ) { + await addEventToStoreAndPoll( + 'Push notifications enabled', + 'notification', + POINT_VALUES.notification, + response.jobId, + ); + return { success: true }; + } + return { success: false, error: response.error }; + } catch (error) { + console.error('Error recording notification point event:', error); + return { + success: false, + error: 'An unexpected error occurred. Please try again.', + }; + } +}; + +/** + * Records a referral event by registering with API and storing locally. + * + * @param referrer - The address of the user referring + * @returns Promise resolving to success status and error message if any + */ +export const recordReferralPointEvent = async ( + referrer: string, +): Promise<{ + success: boolean; + error?: string; +}> => { + try { + const referee = await getPointsAddress(); + const response = await registerReferralPoints({ referee, referrer }); + + if ( + response.success && + isSuccessfulStatus(response.status) && + response.jobId + ) { + await addEventToStoreAndPoll( + 'Friend referred', + 'refer', + POINT_VALUES.referee, + response.jobId, + ); + return { success: true }; + } + return { success: false, error: response.error }; + } catch (error) { + console.error('Error recording referral point event:', error); + return { + success: false, + error: 'An unexpected error occurred. Please try again.', + }; + } +}; diff --git a/app/src/utils/points/registerEvents.ts b/app/src/utils/points/registerEvents.ts new file mode 100644 index 000000000..26e31dfb0 --- /dev/null +++ b/app/src/utils/points/registerEvents.ts @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { IS_DEV_MODE } from '@/utils/devUtils'; +import { makeApiRequest, POINTS_API_BASE_URL } from '@/utils/points/api'; + +type VerifyActionResponse = { + job_id: string; +}; + +/** + * Registers backup action with the points API. + * + * @param userAddress - The user's wallet address + * @returns Promise resolving to job_id, operation status and error message if any + */ +export const registerBackupPoints = async ( + userAddress: string, +): Promise<{ + success: boolean; + status: number; + error?: string; + jobId?: string; +}> => { + const errorMessages: Record = { + already_verified: + 'You have already backed up your secret for this account.', + unknown_action: 'Invalid action type. Please try again.', + verification_failed: 'Verification failed. Please try again.', + invalid_address: 'Invalid wallet address. Please check your account.', + }; + + const response = await makeApiRequest( + '/verify-action', + { + action: 'secret_backup', + address: userAddress, + }, + errorMessages, + ); + + if (response.success && response.data?.job_id) { + return { + success: true, + status: response.status, + jobId: response.data.job_id, + }; + } + + return { + success: false, + status: response.status, + error: response.error, + }; +}; + +/** + * Registers push notification action with the points API. + * + * @param userAddress - The user's wallet address + * @returns Promise resolving to job_id, operation status and error message if any + */ +export const registerNotificationPoints = async ( + userAddress: string, +): Promise<{ + success: boolean; + status: number; + error?: string; + jobId?: string; +}> => { + const errorMessages: Record = { + already_verified: + 'You have already verified push notifications for this account.', + unknown_action: 'Invalid action type. Please try again.', + verification_failed: + 'Verification failed. Please ensure you have enabled push notifications.', + invalid_address: 'Invalid wallet address. Please check your account.', + }; + + const response = await makeApiRequest( + '/verify-action', + { + action: 'push_notification', + address: userAddress, + }, + errorMessages, + ); + + if (response.success && response.data?.job_id) { + return { + success: true, + status: response.status, + jobId: response.data.job_id, + }; + } + + return { + success: false, + status: response.status, + error: response.error, + }; +}; + +/** + * Registers a referral relationship between referee and referrer. + * + * Calls POST /referrals/refer endpoint. + * + * @param referee - The address of the user being referred + * @param referrer - The address of the user referring + * @returns Promise resolving to job_id, operation status and error message if any + */ +export const registerReferralPoints = async ({ + referee, + referrer, +}: { + referee: string; + referrer: string; +}): Promise<{ + success: boolean; + status: number; + error?: string; + jobId?: string; +}> => { + // In __DEV__ mode, log the request instead of sending it + if (IS_DEV_MODE) { + // Redact addresses for security - show first 6 and last 4 characters only + const redactAddress = (addr: string) => + `${addr.slice(0, 6)}...${addr.slice(-4)}`; + console.log('[DEV MODE] Would have sent referral registration request:', { + url: `${POINTS_API_BASE_URL}/referrals/refer`, + method: 'POST', + body: { + referee: redactAddress(referee), + referrer: redactAddress(referrer), + }, + }); + // Simulate a successful response with mock job_id for testing + return { success: true, status: 200, jobId: 'dev-refer-' + Date.now() }; + } + + try { + const response = await makeApiRequest( + '/referrals/refer', + { + referee: referee.toLowerCase(), + referrer: referrer.toLowerCase(), + }, + ); + + if (response.success && response.data?.job_id) { + return { + success: true, + status: response.status, + jobId: response.data.job_id, + }; + } + + // For referral endpoint, try to extract message from response + let errorMessage = + 'Failed to register referral relationship. Please try again.'; + if (response.error) { + errorMessage = response.error; + } + + return { success: false, status: response.status, error: errorMessage }; + } catch (error) { + console.error('Error registering referral points:', error); + return { + success: false, + status: 500, + error: 'Network error. Please check your connection and try again.', + }; + } +}; diff --git a/app/src/utils/points/showErrorModal.ts b/app/src/utils/points/showErrorModal.ts new file mode 100644 index 000000000..a97ae1a3f --- /dev/null +++ b/app/src/utils/points/showErrorModal.ts @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { navigationRef } from '@/navigation'; +import { registerModalCallbacks } from '@/utils/modalCallbackRegistry'; + +export const showErrorModal = (title?: string, message?: string): void => { + if (!navigationRef.isReady()) { + console.warn('Navigation not ready, cannot show error modal'); + return; + } + + // Check if a modal is already open + const currentRoute = navigationRef.getCurrentRoute(); + if (currentRoute?.name === 'Modal') { + // Modal already open, skip showing another one + return; + } + + const callbackId = registerModalCallbacks({ + onButtonPress: () => {}, + onModalDismiss: () => {}, + }); + + navigationRef.navigate('Modal', { + titleText: title ?? 'Something went wrong', + bodyText: message ?? 'An error occurred. Please try again.', + buttonText: 'OK', + callbackId, + }); +}; diff --git a/app/src/utils/points/types.ts b/app/src/utils/points/types.ts new file mode 100644 index 000000000..e271c3a2b --- /dev/null +++ b/app/src/utils/points/types.ts @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +export type IncomingPoints = { + amount: number; + expectedDate: Date; +}; + +export type PointEvent = { + id: string; + title: string; + type: PointEventType; + timestamp: number; + points: number; + status: PointEventStatus; +}; + +export type PointEventStatus = 'pending' | 'completed' | 'failed'; + +export type PointEventType = 'refer' | 'notification' | 'backup' | 'disclosure'; + +export const POINT_VALUES = { + disclosure: 10, + notification: 20, + backup: 100, + referrer: 80, + referee: 24, +} as const; diff --git a/app/src/utils/points/utils.ts b/app/src/utils/points/utils.ts new file mode 100644 index 000000000..878ff2851 --- /dev/null +++ b/app/src/utils/points/utils.ts @@ -0,0 +1,173 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { v4 } from 'uuid'; + +import { SelfAppBuilder } from '@selfxyz/common/utils/appType'; + +import { getOrGeneratePointsAddress } from '@/providers/authProvider'; +import { POINTS_API_BASE_URL } from '@/utils/points/api'; +import { showErrorModal } from '@/utils/points/showErrorModal'; +import type { IncomingPoints } from '@/utils/points/types'; + +export const formatTimeUntilDate = (targetDate: Date): string => { + const now = new Date(); + const diffMs = targetDate.getTime() - now.getTime(); + const diffHours = diffMs / (1000 * 60 * 60); + const diffDays = diffHours / 24; + + if (diffDays >= 1) { + const days = Math.ceil(diffDays); + return `${days} ${days === 1 ? 'day' : 'days'}`; + } else { + const hours = Math.ceil(diffHours); + return `${hours} ${hours === 1 ? 'hour' : 'hours'}`; + } +}; + +export const getIncomingPoints = async (): Promise => { + try { + const userAddress = await getPointsAddress(); + const nextSundayDate = getNextSundayNoonUTC(); + + const response = await fetch( + `${POINTS_API_BASE_URL}/points/${userAddress.toLowerCase()}`, + ); + + if (!response.ok) { + showErrorModal(); + return null; + } + + const data = await response.json(); + console.log('data', data); + if (!data.points || data.points <= 0) { + return null; + } + + return { + amount: data.points, + expectedDate: nextSundayDate, + }; + } catch (error) { + console.error('Error fetching incoming points:', error); + showErrorModal(); + return null; + } +}; + +export const getNextSundayNoonUTC = (): Date => { + const now = new Date(); + const nextSunday = new Date(now); + + // Get current day (0 = Sunday, 1 = Monday, ..., 6 = Saturday) + const currentDay = now.getUTCDay(); + + // Calculate days until next Sunday (0 = this Sunday if before noon, otherwise next Sunday) + let daysUntilSunday = 7 - currentDay; + + // If it's already Sunday, check if it's before or after noon UTC + if (currentDay === 0) { + const currentHourUTC = now.getUTCHours(); + // If it's already past noon UTC on Sunday, go to next Sunday + if (currentHourUTC >= 12) { + daysUntilSunday = 7; + } else { + // It's before noon on Sunday, so target is today at noon + daysUntilSunday = 0; + } + } + + nextSunday.setUTCDate(now.getUTCDate() + daysUntilSunday); + nextSunday.setUTCHours(12, 0, 0, 0); + return nextSunday; +}; + +export const getPointsAddress = async (): Promise => { + return getOrGeneratePointsAddress(); +}; + +export const getTotalPoints = async (address: string): Promise => { + try { + const url = `${POINTS_API_BASE_URL}/distribution/${address.toLowerCase()}`; + const response = await fetch(url); + + if (!response.ok) { + showErrorModal('Error fetching total points', 'Please try again later'); + return 0; + } + + const data = await response.json(); + return data.total_points || 0; + } catch (error) { + console.error('Error fetching total points:', error); + showErrorModal(); + return 0; + } +}; + +export const getWhiteListedDisclosureAddresses = async (): Promise< + string[] +> => { + return []; +}; + +export const hasUserAnIdentityDocumentRegistered = + async (): Promise => { + try { + const { loadDocumentCatalogDirectlyFromKeychain } = await import( + '@/providers/passportDataProvider' + ); + const catalog = await loadDocumentCatalogDirectlyFromKeychain(); + + return catalog.documents.some(doc => doc.isRegistered === true); + } catch (error) { + console.warn( + 'Error checking if user has identity document registered:', + error, + ); + return false; + } + }; + +export const hasUserDoneThePointsDisclosure = async (): Promise => { + try { + const userAddress = await getPointsAddress(); + const response = await fetch( + `${POINTS_API_BASE_URL}/has-disclosed/${userAddress.toLowerCase()}`, + ); + + if (!response.ok) { + showErrorModal(); + return false; + } + + const data = await response.json(); + console.log('data', data); + return data.has_disclosed || false; + } catch (error) { + console.error('Error checking disclosure status:', error); + return false; + } +}; + +export const pointsSelfApp = async () => { + const userAddress = (await getPointsAddress())?.toLowerCase(); + const endpoint = '0x829d183faaa675f8f80e8bb25fb1476cd4f7c1f0'; + const builder = new SelfAppBuilder({ + appName: '✨ Self Points', + endpoint: endpoint.toLowerCase(), + endpointType: 'celo', + scope: 'minimal-disclosure-quest', + userId: v4(), + userIdType: 'uuid', + disclosures: {}, + logoBase64: + 'https://storage.googleapis.com/self-logo-reverse/Self%20Logomark%20Reverse.png', + selfDefinedData: userAddress, + header: '', + }); + + return builder.build(); +}; diff --git a/app/src/utils/referralShare.ts b/app/src/utils/referralShare.ts new file mode 100644 index 000000000..c212bab30 --- /dev/null +++ b/app/src/utils/referralShare.ts @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { Alert, Linking, Platform, Share } from 'react-native'; + +/** + * Shares a message via the native Share API. + * + * @param message - The message to share + * @param url - The URL to share + * @param title - The title of the share + */ +export const shareViaNative = async ( + message: string, + url: string, + title: string, +): Promise => { + try { + await Share.share({ + message, + title, + url, + }); + } catch (error) { + console.error('Error sharing:', error); + } +}; + +/** + * Shares a message via SMS/iMessage. + * + * @param message - The message to share + * @throws Will show an alert if the Messages app cannot be opened + */ +export const shareViaSMS = async (message: string): Promise => { + try { + // iOS uses sms:&body=, Android uses sms:?body= + const separator = Platform.OS === 'ios' ? '&' : '?'; + const url = `sms:${separator}body=${encodeURIComponent(message)}`; + + const canOpen = await Linking.canOpenURL(url); + if (canOpen) { + await Linking.openURL(url); + } else { + if (Platform.OS === 'android') { + try { + await Linking.openURL(url); + + return; + } catch { + // same as for WhatsApp, we try anyway and show alert if it fails + } + } + + Alert.alert('Error', 'Unable to open Messages app'); + } + } catch (error) { + console.error('Error opening Messages:', error); + Alert.alert('Error', 'Failed to open Messages app'); + } +}; + +/** + * Shares a message via WhatsApp. + * + * @param message - The message to share + * @throws Will show an alert if WhatsApp is not installed or cannot be opened + */ +export const shareViaWhatsApp = async (message: string): Promise => { + try { + const url = `whatsapp://send?text=${encodeURIComponent(message)}`; + + const schemeToCheck = Platform.OS === 'ios' ? 'whatsapp://' : url; + + const canOpen = await Linking.canOpenURL(schemeToCheck); + if (canOpen) { + await Linking.openURL(url); + } else { + // openURL() works even if canOpenURL() returns false in android + if (Platform.OS === 'android') { + try { + await Linking.openURL(url); + return; + } catch { + //atleast we tried + //fallthrough to show alert + } + } + Alert.alert( + 'WhatsApp Not Installed', + 'Please install WhatsApp to share via this method, or use the Share button instead.', + [{ text: 'OK' }], + ); + } + } catch (error) { + console.error('Error opening WhatsApp:', error); + Alert.alert('Error', 'Failed to open WhatsApp'); + } +}; diff --git a/app/src/utils/turnkey.ts b/app/src/utils/turnkey.ts new file mode 100644 index 000000000..18de6175b --- /dev/null +++ b/app/src/utils/turnkey.ts @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback, useMemo, useState } from 'react'; +import type { Wallet as TurnkeyWallet } from '@turnkey/core'; +import { AuthState, useTurnkey } from '@turnkey/react-native-wallet-kit'; + +import { useSettingStore } from '@/stores/settingStore'; + +export function useTurnkeyUtils() { + const turnkey = useTurnkey(); + const { + handleGoogleOauth, + fetchWallets, + exportWallet, + importWallet, + authState, + logout, + } = turnkey; + + const setBackedUpWithTurnKey = useSettingStore( + state => state.setBackedUpWithTurnKey, + ); + const backedUpWithTurnKey = useSettingStore( + state => state.backedUpWithTurnKey, + ); + const [turnkeyWallets, setTurnkeyWallets] = useState>( + [], + ); + + const authenticateIfNeeded = useCallback( + async (authenticate: boolean = true): Promise => { + if (!authenticate || authState !== AuthState.Unauthenticated) { + return; + } + await handleGoogleOauth(); + }, + [authState, handleGoogleOauth], + ); + + const refreshWallets = useCallback(async () => { + const fetchedWallets = await fetchWallets(); + setTurnkeyWallets(fetchedWallets); + }, [fetchWallets]); + + return useMemo( + () => ({ + isAuthenticated: (): boolean => { + return authState === AuthState.Authenticated; + }, + + restoreAccount: async ( + authenticate: boolean = true, + ): Promise<{ + message: string; + error?: string; + }> => { + try { + await authenticateIfNeeded(authenticate); + const fetchedWallets = await fetchWallets(); + if (fetchedWallets.length > 0) { + if (!backedUpWithTurnKey) { + setBackedUpWithTurnKey(true); + } + return { message: 'Wallet restored successfully' }; + } + return { message: 'No wallets found' }; + } catch (error) { + console.error('restoreAccount error:', error); + return { + message: 'Failed to restore wallet', + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }, + + backupAccount: async ( + mnemonic: string, + authenticate: boolean = true, + ): Promise => { + await authenticateIfNeeded(authenticate); + const fetchedWallets = await fetchWallets(); + + if (fetchedWallets.length > 0) { + // Get the existing mnemonic to compare + const existingMnemonic = await exportWallet({ + walletId: fetchedWallets[0].walletId, + decrypt: true, + }); + + // Compare mnemonics (normalize whitespace) + const normalizedExisting = existingMnemonic.trim().toLowerCase(); + const normalizedProvided = mnemonic.trim().toLowerCase(); + + if (normalizedExisting === normalizedProvided) { + // Same wallet, already backed up + if (!backedUpWithTurnKey) { + setBackedUpWithTurnKey(true); + } + throw new Error('already_backed_up'); + } else { + // Different wallet exists + throw new Error('already_exists'); + } + } + + await importWallet({ + mnemonic, + walletName: `Self-${new Date().toISOString()}`, + }); + setBackedUpWithTurnKey(true); + + await refreshWallets(); + }, + + getMnemonic: async (authenticate: boolean = true): Promise => { + await authenticateIfNeeded(authenticate); + const fetchedWallets = await fetchWallets(); + if (fetchedWallets.length === 0) { + throw new Error('No wallets found'); + } + const exportedWallet = await exportWallet({ + walletId: fetchedWallets[0].walletId, + decrypt: true, + }); + return exportedWallet; + }, + logout, + turnkeyWallets, + refreshWallets, + }), + [ + logout, + turnkeyWallets, + refreshWallets, + authState, + authenticateIfNeeded, + fetchWallets, + backedUpWithTurnKey, + setBackedUpWithTurnKey, + importWallet, + exportWallet, + ], + ); +} diff --git a/app/tests/__mocks__/mobile-sdk-components.js b/app/tests/__mocks__/mobile-sdk-components.js new file mode 100644 index 000000000..082e8245e --- /dev/null +++ b/app/tests/__mocks__/mobile-sdk-components.js @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +// Minimal JS mock for @selfxyz/mobile-sdk-alpha/components used in tests +const React = require('react'); + +const getTextFromChildren = ch => { + if (typeof ch === 'string') return ch; + if (Array.isArray(ch)) return ch.map(getTextFromChildren).join(''); + if (ch && ch.props && ch.props.children) + return getTextFromChildren(ch.props.children); + return ''; +}; + +const Caption = ({ children }) => + React.createElement(React.Fragment, null, children); +const Description = ({ children }) => + React.createElement(React.Fragment, null, children); +const Title = ({ children }) => + React.createElement(React.Fragment, null, children); + +const { View } = require('react-native'); + +const PrimaryButton = ({ children, onPress, disabled, testID }) => { + const buttonText = getTextFromChildren(children); + const id = + testID || `button-${buttonText.toLowerCase().replace(/\\s+/g, '-')}`; + return React.createElement( + View, + { onPress, disabled, testID: id, accessibilityRole: 'button' }, + children, + ); +}; + +const SecondaryButton = ({ children, onPress, disabled, testID }) => { + const buttonText = getTextFromChildren(children); + const id = + testID || `button-${buttonText.toLowerCase().replace(/\\s+/g, '-')}`; + return React.createElement( + View, + { onPress, disabled, testID: id, accessibilityRole: 'button' }, + children, + ); +}; + +module.exports = { + __esModule: true, + Caption, + Description, + Title, + PrimaryButton, + SecondaryButton, +}; diff --git a/app/tests/src/hooks/useEarnPointsFlow.test.ts b/app/tests/src/hooks/useEarnPointsFlow.test.ts new file mode 100644 index 000000000..148fa3d95 --- /dev/null +++ b/app/tests/src/hooks/useEarnPointsFlow.test.ts @@ -0,0 +1,721 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useNavigation } from '@react-navigation/native'; +import { act, renderHook } from '@testing-library/react-native'; + +import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; + +import { useEarnPointsFlow } from '@/hooks/useEarnPointsFlow'; +import { useRegisterReferral } from '@/hooks/useRegisterReferral'; +import useUserStore from '@/stores/userStore'; +import { getModalCallbacks } from '@/utils/modalCallbackRegistry'; +import { + hasUserAnIdentityDocumentRegistered, + hasUserDoneThePointsDisclosure, + POINT_VALUES, + pointsSelfApp, +} from '@/utils/points'; + +jest.mock('@react-navigation/native', () => ({ + useNavigation: jest.fn(), +})); + +jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ + useSelfClient: jest.fn(), +})); + +jest.mock('@/hooks/useRegisterReferral', () => ({ + useRegisterReferral: jest.fn(), +})); + +jest.mock('@/utils/points', () => ({ + hasUserAnIdentityDocumentRegistered: jest.fn(), + hasUserDoneThePointsDisclosure: jest.fn(), + pointsSelfApp: jest.fn(), + POINT_VALUES: { + referee: 24, + }, +})); + +jest.mock('@/stores/userStore', () => { + const actual = jest.requireActual('@/stores/userStore'); + return { + __esModule: true, + default: actual.default, + }; +}); + +const mockNavigate = jest.fn(); +const mockUseNavigation = useNavigation as jest.MockedFunction< + typeof useNavigation +>; +const mockUseSelfClient = useSelfClient as jest.MockedFunction< + typeof useSelfClient +>; +const mockUseRegisterReferral = useRegisterReferral as jest.MockedFunction< + typeof useRegisterReferral +>; +const mockHasUserAnIdentityDocumentRegistered = + hasUserAnIdentityDocumentRegistered as jest.MockedFunction< + typeof hasUserAnIdentityDocumentRegistered + >; +const mockHasUserDoneThePointsDisclosure = + hasUserDoneThePointsDisclosure as jest.MockedFunction< + typeof hasUserDoneThePointsDisclosure + >; +const mockPointsSelfApp = pointsSelfApp as jest.MockedFunction< + typeof pointsSelfApp +>; + +describe('useEarnPointsFlow', () => { + const mockSetSelfApp = jest.fn(); + const mockSelfClient = { + getSelfAppState: jest.fn(() => ({ + setSelfApp: mockSetSelfApp, + })), + }; + const mockRegisterReferral = jest.fn(); + const mockSelfApp = { + appName: '✨ Self Points', + endpoint: '0x829d183faaa675f8f80e8bb25fb1476cd4f7c1f0', + sessionId: 'test-session-id', + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockSetSelfApp.mockClear(); + jest.useFakeTimers(); + + mockUseNavigation.mockReturnValue({ + navigate: mockNavigate, + } as any); + + mockUseSelfClient.mockReturnValue(mockSelfClient as any); + + mockUseRegisterReferral.mockReturnValue({ + registerReferral: mockRegisterReferral, + isLoading: false, + error: null, + }); + + // Reset user store state + useUserStore.getState().clearDeepLinkReferrer(); + useUserStore.getState().registeredReferrers.clear(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('Identity verification flow', () => { + it('should show identity verification modal when user has no identity document', async () => { + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(false); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + expect(mockHasUserAnIdentityDocumentRegistered).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith('Modal', { + titleText: 'Identity Verification Required', + bodyText: + 'To access Self Points, you need to register an identity document with Self first. This helps us verify your identity and keep your points secure.', + buttonText: 'Verify Identity', + secondaryButtonText: 'Not Now', + callbackId: expect.any(Number), + }); + }); + + it('should navigate to DocumentOnboarding when identity verification modal button is pressed', async () => { + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(false); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + expect(callbacks).toBeDefined(); + + act(() => { + callbacks!.onButtonPress(); + }); + + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(mockNavigate).toHaveBeenCalledWith('DocumentOnboarding'); + }); + + it('should clear referrer when identity verification modal is dismissed with referrer', async () => { + const referrer = '0x1234567890123456789012345678901234567890'; + useUserStore.getState().setDeepLinkReferrer(referrer); + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(false); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks!.onModalDismiss(); + }); + + expect(useUserStore.getState().deepLinkReferrer).toBeUndefined(); + }); + }); + + describe('Points disclosure flow', () => { + it('should show points disclosure modal when user has not done disclosure', async () => { + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + mockHasUserDoneThePointsDisclosure.mockResolvedValue(false); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + expect(mockHasUserAnIdentityDocumentRegistered).toHaveBeenCalled(); + expect(mockHasUserDoneThePointsDisclosure).toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith('Modal', { + titleText: 'Points Disclosure Required', + bodyText: + 'To access Self Points, you need to complete the points disclosure first. This helps us verify your identity and keep your points secure.', + buttonText: 'Complete Points Disclosure', + secondaryButtonText: 'Not Now', + callbackId: expect.any(Number), + }); + }); + + it('should navigate to Prove screen when points disclosure modal button is pressed', async () => { + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + mockHasUserDoneThePointsDisclosure.mockResolvedValue(false); + mockPointsSelfApp.mockResolvedValue(mockSelfApp as any); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + expect(callbacks).toBeDefined(); + + await act(async () => { + await callbacks!.onButtonPress(); + }); + + expect(mockPointsSelfApp).toHaveBeenCalled(); + + // setSelfApp is called synchronously after pointsSelfApp resolves + expect(mockSetSelfApp).toHaveBeenCalledWith(mockSelfApp); + + act(() => { + jest.advanceTimersByTime(100); + }); + + expect(mockNavigate).toHaveBeenCalledWith('Prove'); + }); + + it('should clear referrer when points disclosure modal is dismissed with referrer', async () => { + const referrer = '0x1234567890123456789012345678901234567890'; + useUserStore.getState().setDeepLinkReferrer(referrer); + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + mockHasUserDoneThePointsDisclosure.mockResolvedValue(false); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks!.onModalDismiss(); + }); + + expect(useUserStore.getState().deepLinkReferrer).toBeUndefined(); + }); + }); + + describe('Direct navigation flow', () => { + it('should navigate to Points screen when user has completed all checks and no referrer', async () => { + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + mockHasUserDoneThePointsDisclosure.mockResolvedValue(true); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + expect(mockNavigate).toHaveBeenCalledWith('Points'); + }); + + it('should not navigate when user has completed all checks, has referrer, but skipReferralFlow is true', async () => { + const referrer = '0x1234567890123456789012345678901234567890'; + useUserStore.getState().setDeepLinkReferrer(referrer); + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + mockHasUserDoneThePointsDisclosure.mockResolvedValue(true); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: true, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(true); + }); + + // Should not navigate to Points or Gratification + expect(mockNavigate).not.toHaveBeenCalledWith('Points'); + expect(mockNavigate).not.toHaveBeenCalledWith('Gratification'); + }); + }); + + describe('Referral flow', () => { + const referrer = '0x1234567890123456789012345678901234567890'; + + beforeEach(() => { + useUserStore.getState().setDeepLinkReferrer(referrer); + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + mockHasUserDoneThePointsDisclosure.mockResolvedValue(true); + }); + + it('should handle referral flow when referrer is confirmed and not skipped', async () => { + mockRegisterReferral.mockResolvedValue({ success: true }); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: true, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + expect(mockRegisterReferral).toHaveBeenCalledWith(referrer); + expect(useUserStore.getState().isReferrerRegistered(referrer)).toBe(true); + expect(useUserStore.getState().deepLinkReferrer).toBeUndefined(); + expect(mockNavigate).toHaveBeenCalledWith('Gratification', { + points: POINT_VALUES.referee, + }); + }); + + it('should not register referral if already registered', async () => { + useUserStore.getState().markReferrerAsRegistered(referrer); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: true, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + expect(mockRegisterReferral).not.toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith('Gratification', { + points: POINT_VALUES.referee, + }); + }); + + it('should show error modal and preserve referrer if referral registration fails', async () => { + mockRegisterReferral.mockResolvedValue({ + success: false, + error: 'Network error occurred', + }); + + const originalConsoleError = console.error; + console.error = jest.fn(); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: true, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + expect(mockRegisterReferral).toHaveBeenCalledWith(referrer); + + // Should NOT navigate to Gratification on failure + expect(mockNavigate).not.toHaveBeenCalledWith('Gratification', { + points: POINT_VALUES.referee, + }); + + // Should show error modal instead + expect(mockNavigate).toHaveBeenCalledWith('Modal', { + titleText: 'Referral Registration Failed', + bodyText: expect.stringContaining('Network error occurred'), + buttonText: 'Try Again', + secondaryButtonText: 'Dismiss', + callbackId: expect.any(Number), + }); + + // Should preserve the referrer for retry + expect(useUserStore.getState().deepLinkReferrer).toBe(referrer); + + // Should log the error + expect(console.error).toHaveBeenCalledWith( + 'Referral registration failed:', + 'Network error occurred', + ); + + console.error = originalConsoleError; + }); + + it('should retry referral registration when error modal retry button is pressed', async () => { + // First call fails, second call succeeds + mockRegisterReferral + .mockResolvedValueOnce({ + success: false, + error: 'Network error', + }) + .mockResolvedValueOnce({ + success: true, + }); + + const originalConsoleError = console.error; + console.error = jest.fn(); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: true, + }), + ); + + // First attempt - should fail + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + expect(mockRegisterReferral).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenCalledWith('Modal', { + titleText: 'Referral Registration Failed', + bodyText: expect.stringContaining('Network error'), + buttonText: 'Try Again', + secondaryButtonText: 'Dismiss', + callbackId: expect.any(Number), + }); + + // Referrer should still be in store + expect(useUserStore.getState().deepLinkReferrer).toBe(referrer); + + // Get the callback from the error modal and trigger retry + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + mockNavigate.mockClear(); + + // Retry - should succeed + await act(async () => { + await callbacks!.onButtonPress(); + }); + + expect(mockRegisterReferral).toHaveBeenCalledTimes(2); + expect(mockRegisterReferral).toHaveBeenCalledWith(referrer); + + // Should now navigate to Gratification + expect(mockNavigate).toHaveBeenCalledWith('Gratification', { + points: POINT_VALUES.referee, + }); + + // Should mark referrer as registered and clear it + expect(useUserStore.getState().isReferrerRegistered(referrer)).toBe(true); + expect(useUserStore.getState().deepLinkReferrer).toBeUndefined(); + + console.error = originalConsoleError; + }); + + it('should preserve referrer when error modal is dismissed', async () => { + mockRegisterReferral.mockResolvedValue({ + success: false, + error: 'API error', + }); + + const originalConsoleError = console.error; + console.error = jest.fn(); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: true, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + // Dismiss the error modal + act(() => { + callbacks!.onModalDismiss(); + }); + + // Referrer should still be preserved + expect(useUserStore.getState().deepLinkReferrer).toBe(referrer); + + console.error = originalConsoleError; + }); + + it('should not handle referral flow when isReferralConfirmed is false', async () => { + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: false, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + expect(mockRegisterReferral).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalledWith('Gratification'); + }); + + it('should not handle referral flow when isReferralConfirmed is undefined', async () => { + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + expect(mockRegisterReferral).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalledWith('Gratification'); + }); + + it('should not handle referral flow when hasReferrer is false', async () => { + useUserStore.getState().clearDeepLinkReferrer(); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: true, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + expect(mockRegisterReferral).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalledWith('Gratification'); + }); + + it('should handle referral flow when referrer is not in store but hasReferrer is true', async () => { + useUserStore.getState().clearDeepLinkReferrer(); + mockRegisterReferral.mockResolvedValue({ success: true }); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: true, + isReferralConfirmed: true, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + // Should not call registerReferral if referrer is not in store + expect(mockRegisterReferral).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalledWith('Gratification'); + }); + }); + + describe('Edge cases', () => { + it('should handle errors in hasUserAnIdentityDocumentRegistered gracefully', async () => { + // Mock to return false on error (as the actual function catches errors and returns false) + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(false); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + // The function catches errors and returns false, so it should show identity verification modal + expect(mockNavigate).toHaveBeenCalledWith( + 'Modal', + expect.objectContaining({ + titleText: 'Identity Verification Required', + callbackId: expect.any(Number), + }), + ); + }); + + it('should handle errors in hasUserDoneThePointsDisclosure gracefully', async () => { + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + // Mock to return false on error (as the actual function catches errors and returns false) + mockHasUserDoneThePointsDisclosure.mockResolvedValue(false); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + // The function catches errors and returns false, so it should show points disclosure modal + expect(mockNavigate).toHaveBeenCalledWith( + 'Modal', + expect.objectContaining({ + titleText: 'Points Disclosure Required', + callbackId: expect.any(Number), + }), + ); + }); + + it('should call pointsSelfApp when navigating to points proof', async () => { + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + mockHasUserDoneThePointsDisclosure.mockResolvedValue(false); + mockPointsSelfApp.mockResolvedValue(mockSelfApp as any); + + const { result } = renderHook(() => + useEarnPointsFlow({ + hasReferrer: false, + isReferralConfirmed: undefined, + }), + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + await act(async () => { + await callbacks!.onButtonPress(); + }); + + // Verify pointsSelfApp was called + expect(mockPointsSelfApp).toHaveBeenCalled(); + + // setSelfApp should be called when pointsSelfApp succeeds + expect(mockSetSelfApp).toHaveBeenCalledWith(mockSelfApp); + }); + }); + + describe('Callback dependencies', () => { + it('should update callbacks when dependencies change', async () => { + mockHasUserAnIdentityDocumentRegistered.mockResolvedValue(true); + mockHasUserDoneThePointsDisclosure.mockResolvedValue(true); + + const referrer = '0x1234567890123456789012345678901234567890'; + useUserStore.getState().setDeepLinkReferrer(referrer); + mockRegisterReferral.mockResolvedValue({ success: true }); + + const { result, rerender } = renderHook( + ({ hasReferrer, isReferralConfirmed }) => + useEarnPointsFlow({ hasReferrer, isReferralConfirmed }), + { + initialProps: { + hasReferrer: false, + isReferralConfirmed: undefined, + }, + }, + ); + + await act(async () => { + await result.current.onEarnPointsPress(); + }); + + expect(mockNavigate).toHaveBeenCalledWith('Points'); + + mockNavigate.mockClear(); + + rerender({ + hasReferrer: true, + isReferralConfirmed: true, + }); + + await act(async () => { + await result.current.onEarnPointsPress(false); + }); + + expect(mockRegisterReferral).toHaveBeenCalledWith(referrer); + }); + }); +}); diff --git a/app/tests/src/hooks/useReferralConfirmation.test.ts b/app/tests/src/hooks/useReferralConfirmation.test.ts new file mode 100644 index 000000000..9ab47a1b7 --- /dev/null +++ b/app/tests/src/hooks/useReferralConfirmation.test.ts @@ -0,0 +1,558 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useNavigation } from '@react-navigation/native'; +import { act, renderHook, waitFor } from '@testing-library/react-native'; + +import { useReferralConfirmation } from '@/hooks/useReferralConfirmation'; +import useUserStore from '@/stores/userStore'; +import { getModalCallbacks } from '@/utils/modalCallbackRegistry'; + +jest.mock('@react-navigation/native', () => ({ + useNavigation: jest.fn(), +})); + +jest.mock('@/stores/userStore', () => { + const actual = jest.requireActual('@/stores/userStore'); + return { + __esModule: true, + default: actual.default, + }; +}); + +const mockNavigate = jest.fn(); +const mockUseNavigation = useNavigation as jest.MockedFunction< + typeof useNavigation +>; + +describe('useReferralConfirmation', () => { + const mockOnConfirmed = jest.fn(); + const testReferrer = '0x1234567890123456789012345678901234567890'; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + + mockUseNavigation.mockReturnValue({ + navigate: mockNavigate, + goBack: jest.fn(), + } as any); + + // Reset user store state + useUserStore.getState().clearDeepLinkReferrer(); + // Set a test referrer for tests that need it + useUserStore.getState().setDeepLinkReferrer(testReferrer); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + // Clean up store state to prevent leaks to other tests + useUserStore.getState().clearDeepLinkReferrer(); + }); + + describe('Modal display', () => { + it('should show referral confirmation modal when hasReferrer is true and isReferralConfirmed is undefined', () => { + renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + expect(mockNavigate).toHaveBeenCalledWith('Modal', { + titleText: 'Referral Confirmation', + bodyText: + 'Seems like you opened the app from a referral link. Please confirm to continue.', + buttonText: 'Confirm', + secondaryButtonText: 'Dismiss', + callbackId: expect.any(Number), + }); + }); + + it('should not show modal when hasReferrer is false', () => { + renderHook(() => + useReferralConfirmation({ + hasReferrer: false, + onConfirmed: mockOnConfirmed, + }), + ); + + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should not show modal when isReferralConfirmed is already true', () => { + const { rerender } = renderHook( + ({ hasReferrer, onConfirmed }) => + useReferralConfirmation({ hasReferrer, onConfirmed }), + { + initialProps: { + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }, + }, + ); + + // First render shows modal + expect(mockNavigate).toHaveBeenCalled(); + + // Save callbackId before clearing + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + mockNavigate.mockClear(); + + // Manually set confirmed state (simulating user interaction) + act(() => { + callbacks?.onButtonPress(); + }); + + // Re-render with same props should not show modal again + rerender({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }); + + // Modal should not be shown again since isReferralConfirmed is now true + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should not show modal when isReferralConfirmed is already false', () => { + const { rerender } = renderHook( + ({ hasReferrer, onConfirmed }) => + useReferralConfirmation({ hasReferrer, onConfirmed }), + { + initialProps: { + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }, + }, + ); + + // First render shows modal + expect(mockNavigate).toHaveBeenCalled(); + + // Save callbackId before clearing + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + mockNavigate.mockClear(); + + // Manually set dismissed state + act(() => { + callbacks?.onModalDismiss(); + }); + + // Re-render should not show modal again + rerender({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }); + + expect(mockNavigate).not.toHaveBeenCalled(); + }); + }); + + describe('Confirmation flow', () => { + it('should set isReferralConfirmed to true when confirm button is pressed', () => { + const { result } = renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + expect(result.current.isReferralConfirmed).toBeUndefined(); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onButtonPress(); + }); + + expect(result.current.isReferralConfirmed).toBe(true); + }); + + it('should call onConfirmed when isReferralConfirmed becomes true and hasReferrer is true', async () => { + renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onButtonPress(); + }); + + await waitFor(() => { + expect(mockOnConfirmed).toHaveBeenCalledTimes(1); + }); + }); + + it('should not call onConfirmed when isReferralConfirmed is true but hasReferrer is false', () => { + renderHook(() => + useReferralConfirmation({ + hasReferrer: false, + onConfirmed: mockOnConfirmed, + }), + ); + + // Manually set confirmed (simulating state change) + act(() => { + // This simulates the state being set externally + // In real usage, this would happen through the modal callback + }); + + expect(mockOnConfirmed).not.toHaveBeenCalled(); + }); + }); + + describe('Dismissal flow', () => { + it('should set isReferralConfirmed to false when modal is dismissed', () => { + const { result } = renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onModalDismiss(); + }); + + expect(result.current.isReferralConfirmed).toBe(false); + }); + + it('should clear deep link referrer when modal is dismissed', () => { + // testReferrer is already set in beforeEach + renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onModalDismiss(); + }); + + expect(useUserStore.getState().deepLinkReferrer).toBeUndefined(); + }); + + it('should not call onConfirmed when modal is dismissed', () => { + renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onModalDismiss(); + }); + + expect(mockOnConfirmed).not.toHaveBeenCalled(); + }); + }); + + describe('State transitions', () => { + it('should handle transition from undefined to true', async () => { + const { result } = renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + expect(result.current.isReferralConfirmed).toBeUndefined(); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onButtonPress(); + }); + + expect(result.current.isReferralConfirmed).toBe(true); + + await waitFor(() => { + expect(mockOnConfirmed).toHaveBeenCalled(); + }); + }); + + it('should handle transition from undefined to false', () => { + const { result } = renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + expect(result.current.isReferralConfirmed).toBeUndefined(); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onModalDismiss(); + }); + + expect(result.current.isReferralConfirmed).toBe(false); + expect(mockOnConfirmed).not.toHaveBeenCalled(); + }); + + it('should not show modal again after confirmation', () => { + const { rerender } = renderHook( + ({ hasReferrer, onConfirmed }) => + useReferralConfirmation({ hasReferrer, onConfirmed }), + { + initialProps: { + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }, + }, + ); + + // First render shows modal + expect(mockNavigate).toHaveBeenCalledTimes(1); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onButtonPress(); + }); + + mockNavigate.mockClear(); + + // Re-render with same props + rerender({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }); + + // Modal should not be shown again + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should not show modal again after dismissal', () => { + const { rerender } = renderHook( + ({ hasReferrer, onConfirmed }) => + useReferralConfirmation({ hasReferrer, onConfirmed }), + { + initialProps: { + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }, + }, + ); + + // First render shows modal + expect(mockNavigate).toHaveBeenCalledTimes(1); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onModalDismiss(); + }); + + mockNavigate.mockClear(); + + // Re-render with same props + rerender({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }); + + // Modal should not be shown again + expect(mockNavigate).not.toHaveBeenCalled(); + }); + }); + + describe('Props changes', () => { + it('should show modal when hasReferrer changes from false to true', () => { + const { rerender } = renderHook( + ({ hasReferrer, onConfirmed }) => + useReferralConfirmation({ hasReferrer, onConfirmed }), + { + initialProps: { + hasReferrer: false, + onConfirmed: mockOnConfirmed, + }, + }, + ); + + expect(mockNavigate).not.toHaveBeenCalled(); + + rerender({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }); + + expect(mockNavigate).toHaveBeenCalledWith('Modal', { + titleText: 'Referral Confirmation', + bodyText: + 'Seems like you opened the app from a referral link. Please confirm to continue.', + buttonText: 'Confirm', + secondaryButtonText: 'Dismiss', + callbackId: expect.any(Number), + }); + }); + + it('should not show modal when hasReferrer changes from true to false', () => { + const { rerender } = renderHook( + ({ hasReferrer, onConfirmed }) => + useReferralConfirmation({ hasReferrer, onConfirmed }), + { + initialProps: { + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }, + }, + ); + + expect(mockNavigate).toHaveBeenCalledTimes(1); + + mockNavigate.mockClear(); + + rerender({ + hasReferrer: false, + onConfirmed: mockOnConfirmed, + }); + + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should call onConfirmed with updated callback when onConfirmed prop changes', async () => { + const firstCallback = jest.fn(); + const secondCallback = jest.fn(); + + const { rerender } = renderHook( + ({ hasReferrer, onConfirmed }) => + useReferralConfirmation({ hasReferrer, onConfirmed }), + { + initialProps: { + hasReferrer: true, + onConfirmed: firstCallback, + }, + }, + ); + + const firstCallbackId = mockNavigate.mock.calls[0][1].callbackId; + const firstCallbacks = getModalCallbacks(firstCallbackId); + + act(() => { + firstCallbacks?.onButtonPress(); + }); + + await waitFor(() => { + expect(firstCallback).toHaveBeenCalledTimes(1); + }); + + mockNavigate.mockClear(); + + rerender({ + hasReferrer: true, + onConfirmed: secondCallback, + }); + + // Since isReferralConfirmed is already true, onConfirmed should be called immediately + await waitFor(() => { + expect(secondCallback).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('Edge cases', () => { + it('should handle multiple rapid confirmations gracefully', () => { + const { result } = renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onButtonPress(); + callbacks?.onButtonPress(); // Call again + }); + + expect(result.current.isReferralConfirmed).toBe(true); + }); + + it('should handle multiple rapid dismissals gracefully', () => { + const { result } = renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onModalDismiss(); + callbacks?.onModalDismiss(); // Call again + }); + + expect(result.current.isReferralConfirmed).toBe(false); + }); + + it('should return isReferralConfirmed state correctly', () => { + const { result } = renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + expect(result.current.isReferralConfirmed).toBeUndefined(); + + const callbackId = mockNavigate.mock.calls[0][1].callbackId; + const callbacks = getModalCallbacks(callbackId); + + act(() => { + callbacks?.onButtonPress(); + }); + + expect(result.current.isReferralConfirmed).toBe(true); + + // Reset and test false + const { result: result2 } = renderHook(() => + useReferralConfirmation({ + hasReferrer: true, + onConfirmed: mockOnConfirmed, + }), + ); + + const callbackId2 = mockNavigate.mock.calls[1][1].callbackId; + const callbacks2 = getModalCallbacks(callbackId2); + + act(() => { + callbacks2?.onModalDismiss(); + }); + + expect(result2.current.isReferralConfirmed).toBe(false); + }); + }); +}); diff --git a/app/tests/src/hooks/useReferralMessage.test.ts b/app/tests/src/hooks/useReferralMessage.test.ts new file mode 100644 index 000000000..9ffc27859 --- /dev/null +++ b/app/tests/src/hooks/useReferralMessage.test.ts @@ -0,0 +1,368 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { act, renderHook, waitFor } from '@testing-library/react-native'; + +import { useReferralMessage } from '@/hooks/useReferralMessage'; +import { getOrGeneratePointsAddress } from '@/providers/authProvider'; +import { useSettingStore } from '@/stores/settingStore'; + +jest.mock('@/providers/authProvider', () => ({ + getOrGeneratePointsAddress: jest.fn(), +})); + +const mockGetOrGeneratePointsAddress = + getOrGeneratePointsAddress as jest.MockedFunction< + typeof getOrGeneratePointsAddress + >; + +describe('useReferralMessage', () => { + beforeEach(() => { + jest.clearAllMocks(); + act(() => { + useSettingStore.setState({ pointsAddress: null }); + }); + }); + + describe('initial state', () => { + it('should have loading state when no address is available', () => { + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.isLoading).toBe(true); + expect(result.current.message).toBe(''); + expect(result.current.referralLink).toBe(''); + }); + }); + + describe('when address is in store', () => { + it('should use address from store and generate message immediately', () => { + const mockAddress = '0x1234567890123456789012345678901234567890'; + act(() => { + useSettingStore.setState({ pointsAddress: mockAddress }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.isLoading).toBe(false); + expect(result.current.referralLink).toBe( + `https://referral.self.xyz/referral/${mockAddress}`, + ); + expect(result.current.message).toBe( + `Join Self and use my referral link:\n\nhttps://referral.self.xyz/referral/${mockAddress}`, + ); + expect(mockGetOrGeneratePointsAddress).not.toHaveBeenCalled(); + }); + + it('should update when store address changes', () => { + const firstAddress = '0x1111111111111111111111111111111111111111'; + const secondAddress = '0x2222222222222222222222222222222222222222'; + + act(() => { + useSettingStore.setState({ pointsAddress: firstAddress }); + }); + + const { result, rerender } = renderHook(() => useReferralMessage()); + + expect(result.current.referralLink).toContain(firstAddress); + + act(() => { + useSettingStore.setState({ pointsAddress: secondAddress }); + }); + + rerender(); + + expect(result.current.referralLink).toContain(secondAddress); + expect(result.current.referralLink).not.toContain(firstAddress); + }); + }); + + describe('when address needs to be fetched', () => { + it('should fetch address when not in store', async () => { + const mockAddress = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12'; + mockGetOrGeneratePointsAddress.mockResolvedValue(mockAddress); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.isLoading).toBe(true); + expect(mockGetOrGeneratePointsAddress).toHaveBeenCalledTimes(1); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.referralLink).toBe( + `https://referral.self.xyz/referral/${mockAddress}`, + ); + expect(result.current.message).toBe( + `Join Self and use my referral link:\n\nhttps://referral.self.xyz/referral/${mockAddress}`, + ); + }); + + it('should not fetch if address is already in store', async () => { + const mockAddress = '0x1234567890123456789012345678901234567890'; + mockGetOrGeneratePointsAddress.mockResolvedValue('0xOTHER'); + + act(() => { + useSettingStore.setState({ pointsAddress: mockAddress }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.isLoading).toBe(false); + expect(mockGetOrGeneratePointsAddress).not.toHaveBeenCalled(); + expect(result.current.referralLink).toContain(mockAddress); + }); + + it('should remain in loading state when fetch is delayed', async () => { + const mockAddress = '0x1234567890123456789012345678901234567890'; + mockGetOrGeneratePointsAddress.mockImplementation( + () => + new Promise(resolve => setTimeout(() => resolve(mockAddress), 200)), + ); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.isLoading).toBe(true); + expect(mockGetOrGeneratePointsAddress).toHaveBeenCalledTimes(1); + + // Check that it's still loading before the promise resolves + await new Promise(resolve => setTimeout(resolve, 50)); + expect(result.current.isLoading).toBe(true); + + // Wait for the promise to resolve + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.referralLink).toContain(mockAddress); + }); + }); + + describe('referral link generation', () => { + it('should generate correct referral link format', () => { + const mockAddress = '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4'; + act(() => { + useSettingStore.setState({ pointsAddress: mockAddress }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.referralLink).toBe( + `https://referral.self.xyz/referral/${mockAddress}`, + ); + }); + + it('should generate correct message format', () => { + const mockAddress = '0x1234567890123456789012345678901234567890'; + act(() => { + useSettingStore.setState({ pointsAddress: mockAddress }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + const expectedLink = `https://referral.self.xyz/referral/${mockAddress}`; + expect(result.current.message).toBe( + `Join Self and use my referral link:\n\n${expectedLink}`, + ); + expect(result.current.message).toContain( + 'Join Self and use my referral link:', + ); + expect(result.current.message).toContain(expectedLink); + }); + + it('should handle different address formats', () => { + const addresses = [ + '0x0000000000000000000000000000000000000000', + '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', + ]; + + addresses.forEach(address => { + act(() => { + useSettingStore.setState({ pointsAddress: address }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.referralLink).toContain(address); + expect(result.current.message).toContain(address); + }); + }); + }); + + describe('loading state transitions', () => { + it('should transition from loading to loaded when address is fetched', async () => { + const mockAddress = '0x1234567890123456789012345678901234567890'; + mockGetOrGeneratePointsAddress.mockImplementation( + () => + new Promise(resolve => setTimeout(() => resolve(mockAddress), 100)), + ); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.referralLink).toBeTruthy(); + expect(result.current.message).toBeTruthy(); + }); + + it('should not be loading when address is immediately available from store', () => { + const mockAddress = '0x1234567890123456789012345678901234567890'; + act(() => { + useSettingStore.setState({ pointsAddress: mockAddress }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.isLoading).toBe(false); + expect(result.current.referralLink).toBeTruthy(); + }); + }); + + describe('store address priority', () => { + it('should prefer store address over fetched address', async () => { + const storeAddress = '0xSTORE_ADDRESS'; + const fetchedAddress = '0xFETCHED_ADDRESS'; + + mockGetOrGeneratePointsAddress.mockResolvedValue(fetchedAddress); + + act(() => { + useSettingStore.setState({ pointsAddress: storeAddress }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.referralLink).toContain(storeAddress); + expect(result.current.referralLink).not.toContain(fetchedAddress); + expect(mockGetOrGeneratePointsAddress).not.toHaveBeenCalled(); + }); + + it('should use fetched address when store address is null', async () => { + const fetchedAddress = '0xFETCHED_ADDRESS'; + mockGetOrGeneratePointsAddress.mockResolvedValue(fetchedAddress); + + act(() => { + useSettingStore.setState({ pointsAddress: null }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.referralLink).toContain(fetchedAddress); + expect(mockGetOrGeneratePointsAddress).toHaveBeenCalled(); + }); + + it('should update when store address changes from null to value', async () => { + const fetchedAddress = '0xFETCHED_ADDRESS'; + const storeAddress = '0xSTORE_ADDRESS'; + + mockGetOrGeneratePointsAddress.mockResolvedValue(fetchedAddress); + + act(() => { + useSettingStore.setState({ pointsAddress: null }); + }); + + const { result, rerender } = renderHook(() => useReferralMessage()); + + await waitFor(() => { + expect(result.current.referralLink).toContain(fetchedAddress); + }); + + act(() => { + useSettingStore.setState({ pointsAddress: storeAddress }); + }); + + rerender(); + + expect(result.current.referralLink).toContain(storeAddress); + expect(result.current.referralLink).not.toContain(fetchedAddress); + }); + }); + + describe('memoization', () => { + it('should memoize result based on address', () => { + const mockAddress = '0x1234567890123456789012345678901234567890'; + act(() => { + useSettingStore.setState({ pointsAddress: mockAddress }); + }); + + const { result, rerender } = renderHook(() => useReferralMessage()); + + const firstMessage = result.current.message; + const firstLink = result.current.referralLink; + + rerender(); + + expect(result.current.message).toBe(firstMessage); + expect(result.current.referralLink).toBe(firstLink); + }); + + it('should update memoized values when address changes', () => { + const firstAddress = '0x1111111111111111111111111111111111111111'; + const secondAddress = '0x2222222222222222222222222222222222222222'; + + act(() => { + useSettingStore.setState({ pointsAddress: firstAddress }); + }); + + const { result, rerender } = renderHook(() => useReferralMessage()); + + const firstMessage = result.current.message; + + act(() => { + useSettingStore.setState({ pointsAddress: secondAddress }); + }); + + rerender(); + + expect(result.current.message).not.toBe(firstMessage); + expect(result.current.message).toContain(secondAddress); + }); + }); + + describe('edge cases', () => { + it('should handle empty string address in store', () => { + act(() => { + useSettingStore.setState({ pointsAddress: '' }); + }); + + renderHook(() => useReferralMessage()); + + // Empty string is falsy, so it should trigger fetch + expect(mockGetOrGeneratePointsAddress).toHaveBeenCalled(); + }); + + it('should handle very long addresses', () => { + const longAddress = + '0x1234567890123456789012345678901234567890123456789012345678901234'; + act(() => { + useSettingStore.setState({ pointsAddress: longAddress }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.referralLink).toContain(longAddress); + expect(result.current.message).toContain(longAddress); + }); + + it('should handle address with checksum casing', () => { + const checksumAddress = '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4'; + act(() => { + useSettingStore.setState({ pointsAddress: checksumAddress }); + }); + + const { result } = renderHook(() => useReferralMessage()); + + expect(result.current.referralLink).toContain(checksumAddress); + expect(result.current.message).toContain(checksumAddress); + }); + }); +}); diff --git a/app/tests/src/hooks/useReferralRegistration.test.ts b/app/tests/src/hooks/useReferralRegistration.test.ts new file mode 100644 index 000000000..8b2d517b9 --- /dev/null +++ b/app/tests/src/hooks/useReferralRegistration.test.ts @@ -0,0 +1,195 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useRoute } from '@react-navigation/native'; +import { renderHook, waitFor } from '@testing-library/react-native'; + +import { useReferralRegistration } from '@/hooks/useReferralRegistration'; +import { useRegisterReferral } from '@/hooks/useRegisterReferral'; +import useUserStore from '@/stores/userStore'; + +jest.mock('@react-navigation/native', () => ({ + useRoute: jest.fn(), + useNavigation: jest.fn(() => ({ + navigate: jest.fn(), + goBack: jest.fn(), + })), +})); + +jest.mock('@/hooks/useRegisterReferral'); +jest.mock('@/stores/userStore'); + +const mockUseRoute = useRoute as jest.MockedFunction; +const mockUseRegisterReferral = useRegisterReferral as jest.MockedFunction< + typeof useRegisterReferral +>; +const mockUseUserStore = useUserStore as jest.MockedFunction< + typeof useUserStore +>; + +describe('useReferralRegistration', () => { + const validReferrer = '0x1234567890123456789012345678901234567890'; + const mockRegisterReferral = jest.fn(); + const mockIsReferrerRegistered = jest.fn(); + const mockMarkReferrerAsRegistered = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseRoute.mockReturnValue({ + params: {}, + } as any); + + mockUseRegisterReferral.mockReturnValue({ + registerReferral: mockRegisterReferral, + isLoading: false, + error: null, + }); + + const mockStore = { + isReferrerRegistered: mockIsReferrerRegistered, + markReferrerAsRegistered: mockMarkReferrerAsRegistered, + }; + + mockUseUserStore.getState = jest.fn(() => mockStore as any); + + mockRegisterReferral.mockResolvedValue({ success: true }); + mockIsReferrerRegistered.mockReturnValue(false); + }); + + it('should not register if no referrer in params', () => { + mockUseRoute.mockReturnValue({ + params: {}, + } as any); + + renderHook(() => useReferralRegistration()); + + expect(mockRegisterReferral).not.toHaveBeenCalled(); + }); + + it('should correctly extract referrer from route params', () => { + const referrer = '0x1234567890123456789012345678901234567890'; + mockUseRoute.mockReturnValue({ + params: { referrer }, + } as any); + + renderHook(() => useReferralRegistration()); + + expect(mockIsReferrerRegistered).toHaveBeenCalledWith(referrer); + }); + + it('should register referral when referrer is present and not registered', async () => { + mockUseRoute.mockReturnValue({ + params: { referrer: validReferrer }, + } as any); + + renderHook(() => useReferralRegistration()); + + await waitFor(() => { + expect(mockIsReferrerRegistered).toHaveBeenCalledWith(validReferrer); + expect(mockRegisterReferral).toHaveBeenCalledWith(validReferrer); + }); + + await waitFor(() => { + expect(mockMarkReferrerAsRegistered).toHaveBeenCalledWith(validReferrer); + }); + }); + + it('should not register if referrer is already registered', async () => { + mockUseRoute.mockReturnValue({ + params: { referrer: validReferrer }, + } as any); + + mockIsReferrerRegistered.mockReturnValue(true); + + renderHook(() => useReferralRegistration()); + + await waitFor(() => { + expect(mockIsReferrerRegistered).toHaveBeenCalledWith(validReferrer); + }); + + expect(mockRegisterReferral).not.toHaveBeenCalled(); + expect(mockMarkReferrerAsRegistered).not.toHaveBeenCalled(); + }); + + it('should not register if registration is in progress', async () => { + mockUseRoute.mockReturnValue({ + params: { referrer: validReferrer }, + } as any); + + mockUseRegisterReferral.mockReturnValue({ + registerReferral: mockRegisterReferral, + isLoading: true, + error: null, + }); + + renderHook(() => useReferralRegistration()); + + // Wait a bit to ensure useEffect has run + await new Promise(resolve => setTimeout(resolve, 100)); + + expect(mockRegisterReferral).not.toHaveBeenCalled(); + }); + + it('should not mark as registered if registration fails', async () => { + mockUseRoute.mockReturnValue({ + params: { referrer: validReferrer }, + } as any); + + mockRegisterReferral.mockResolvedValue({ + success: false, + error: 'Registration failed', + }); + + renderHook(() => useReferralRegistration()); + + await waitFor(() => { + expect(mockRegisterReferral).toHaveBeenCalledWith(validReferrer); + }); + + expect(mockMarkReferrerAsRegistered).not.toHaveBeenCalled(); + }); + + it('should handle case-insensitive referrer addresses', async () => { + const upperCaseReferrer = validReferrer.toUpperCase(); + mockUseRoute.mockReturnValue({ + params: { referrer: upperCaseReferrer }, + } as any); + + renderHook(() => useReferralRegistration()); + + await waitFor(() => { + expect(mockIsReferrerRegistered).toHaveBeenCalledWith(upperCaseReferrer); + expect(mockRegisterReferral).toHaveBeenCalledWith(upperCaseReferrer); + }); + }); + + it('should handle other params alongside referrer', async () => { + mockUseRoute.mockReturnValue({ + params: { referrer: validReferrer, points: 24 }, + } as any); + + renderHook(() => useReferralRegistration()); + + await waitFor(() => { + expect(mockRegisterReferral).toHaveBeenCalledWith(validReferrer); + }); + + // Verify points param doesn't interfere with referrer extraction + expect(mockIsReferrerRegistered).toHaveBeenCalledWith(validReferrer); + }); + + it('should extract referrer correctly when points is also present', async () => { + mockUseRoute.mockReturnValue({ + params: { points: 24, referrer: validReferrer }, + } as any); + + renderHook(() => useReferralRegistration()); + + await waitFor(() => { + expect(mockIsReferrerRegistered).toHaveBeenCalledWith(validReferrer); + expect(mockRegisterReferral).toHaveBeenCalledWith(validReferrer); + }); + }); +}); diff --git a/app/tests/src/hooks/useRegisterReferral.test.ts b/app/tests/src/hooks/useRegisterReferral.test.ts new file mode 100644 index 000000000..fe38b7509 --- /dev/null +++ b/app/tests/src/hooks/useRegisterReferral.test.ts @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { act, renderHook, waitFor } from '@testing-library/react-native'; + +import { useRegisterReferral } from '@/hooks/useRegisterReferral'; +import { recordReferralPointEvent } from '@/utils/points'; + +jest.mock('@/utils/points', () => ({ + recordReferralPointEvent: jest.fn(), +})); + +const mockRecordReferralPointEvent = + recordReferralPointEvent as jest.MockedFunction< + typeof recordReferralPointEvent + >; + +describe('useRegisterReferral', () => { + const validReferrer = '0x1234567890123456789012345678901234567890'; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should initialize with loading false and no error', () => { + const { result } = renderHook(() => useRegisterReferral()); + + expect(result.current.isLoading).toBe(false); + expect(result.current.error).toBe(null); + }); + + it('should validate referrer address format', async () => { + const { result } = renderHook(() => useRegisterReferral()); + + await act(async () => { + const response = await result.current.registerReferral('invalid-address'); + expect(response.success).toBe(false); + expect(response.error).toContain('Invalid referrer address'); + }); + + expect(result.current.error).toContain('Invalid referrer address'); + expect(mockRecordReferralPointEvent).not.toHaveBeenCalled(); + }); + + it('should register referral successfully', async () => { + mockRecordReferralPointEvent.mockResolvedValue({ success: true }); + + const { result } = renderHook(() => useRegisterReferral()); + + await act(async () => { + const response = await result.current.registerReferral(validReferrer); + expect(response.success).toBe(true); + }); + + expect(mockRecordReferralPointEvent).toHaveBeenCalledWith(validReferrer); + expect(result.current.error).toBe(null); + }); + + it('should handle registration failure', async () => { + const errorMessage = 'Registration failed'; + mockRecordReferralPointEvent.mockResolvedValue({ + success: false, + error: errorMessage, + }); + + const { result } = renderHook(() => useRegisterReferral()); + + await act(async () => { + const response = await result.current.registerReferral(validReferrer); + expect(response.success).toBe(false); + expect(response.error).toBe(errorMessage); + }); + + expect(result.current.error).toBe(errorMessage); + }); + + it('should handle registration failure without error message', async () => { + mockRecordReferralPointEvent.mockResolvedValue({ + success: false, + error: undefined, + }); + + const { result } = renderHook(() => useRegisterReferral()); + + await act(async () => { + const response = await result.current.registerReferral(validReferrer); + expect(response.success).toBe(false); + expect(response.error).toBe('Failed to register referral'); + }); + + expect(result.current.error).toBe('Failed to register referral'); + }); + + it('should handle exceptions during registration', async () => { + const errorMessage = 'Network error'; + mockRecordReferralPointEvent.mockRejectedValue(new Error(errorMessage)); + + const { result } = renderHook(() => useRegisterReferral()); + + await act(async () => { + const response = await result.current.registerReferral(validReferrer); + expect(response.success).toBe(false); + expect(response.error).toBe(errorMessage); + }); + + expect(result.current.error).toBe(errorMessage); + }); + + it('should handle non-Error exceptions', async () => { + mockRecordReferralPointEvent.mockRejectedValue('String error'); + + const { result } = renderHook(() => useRegisterReferral()); + + await act(async () => { + const response = await result.current.registerReferral(validReferrer); + expect(response.success).toBe(false); + expect(response.error).toBe('An unexpected error occurred'); + }); + + expect(result.current.error).toBe('An unexpected error occurred'); + }); + + it('should set loading state during registration', async () => { + let resolvePromise: (value: any) => void; + const promise = new Promise(resolve => { + resolvePromise = resolve; + }); + mockRecordReferralPointEvent.mockReturnValue(promise as any); + + const { result } = renderHook(() => useRegisterReferral()); + + act(() => { + result.current.registerReferral(validReferrer); + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(true); + }); + + await act(async () => { + resolvePromise!({ success: true }); + await promise; + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + }); + + it('should clear error on new registration attempt', async () => { + mockRecordReferralPointEvent + .mockResolvedValueOnce({ success: false, error: 'First error' }) + .mockResolvedValueOnce({ success: true }); + + const { result } = renderHook(() => useRegisterReferral()); + + // First attempt fails + await act(async () => { + await result.current.registerReferral(validReferrer); + }); + + expect(result.current.error).toBe('First error'); + + // Second attempt succeeds + await act(async () => { + await result.current.registerReferral(validReferrer); + }); + + expect(result.current.error).toBe(null); + }); +}); diff --git a/app/tests/src/iosInfoPlist.test.ts b/app/tests/src/iosInfoPlist.test.ts index 31fe9315a..f991215ac 100644 --- a/app/tests/src/iosInfoPlist.test.ts +++ b/app/tests/src/iosInfoPlist.test.ts @@ -30,6 +30,7 @@ describe('iOS Info.plist Configuration', () => { it('lists required fonts', () => { expect(plistContent).toContain('Advercase-Regular.otf'); + expect(plistContent).toContain('DINOT-Bold.otf'); expect(plistContent).toContain('DINOT-Medium.otf'); }); }); diff --git a/app/tests/src/navigation.test.ts b/app/tests/src/navigation.test.ts index fdd058260..ec10f1918 100644 --- a/app/tests/src/navigation.test.ts +++ b/app/tests/src/navigation.test.ts @@ -2,13 +2,73 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -// Mock ConfirmIdentificationScreen to avoid PixelRatio issues -jest.mock( - '@selfxyz/mobile-sdk-alpha/onboarding/confirm-identification', - () => ({ - ConfirmIdentificationScreen: ({ children }: any) => children, - }), -); +// Mock the navigation module to avoid deep import chains that overwhelm the parser +jest.mock('@/navigation', () => { + const mockScreens = { + // App screens + Home: {}, + Launch: {}, + Loading: {}, + Modal: {}, + Gratification: {}, + WebView: {}, + Points: {}, + // Onboarding screens + Disclaimer: {}, + Splash: {}, + // Documents screens + IDPicker: {}, + IdDetails: {}, + CountryPicker: {}, + DocumentCamera: {}, + DocumentCameraTrouble: {}, + DocumentDataInfo: {}, + DocumentDataNotFound: {}, + DocumentNFCMethodSelection: {}, + DocumentNFCScan: {}, + DocumentNFCTrouble: {}, + DocumentOnboarding: {}, + ManageDocuments: {}, + // Verification screens + ConfirmBelonging: {}, + Prove: {}, + ProofHistory: {}, + ProofHistoryDetail: {}, + ProofRequestStatus: {}, + QRCodeViewFinder: {}, + QRCodeTrouble: {}, + // Account screens + AccountRecovery: {}, + AccountRecoveryChoice: {}, + AccountVerifiedSuccess: {}, + CloudBackupSettings: {}, + SaveRecoveryPhrase: {}, + ShowRecoveryPhrase: {}, + RecoverWithPhrase: {}, + Settings: {}, + Referral: {}, + DeferredLinkingInfo: {}, + // Shared screens + ComingSoon: {}, + // Dev screens + DevSettings: {}, + DevFeatureFlags: {}, + DevHapticFeedback: {}, + DevLoadingScreen: {}, + DevPrivateKey: {}, + CreateMock: {}, + MockDataDeepLink: {}, + // Aadhaar screens + AadhaarUpload: {}, + AadhaarUploadSuccess: {}, + AadhaarUploadError: {}, + }; + + return { + navigationScreens: mockScreens, + navigationRef: { current: null }, + }; +}); describe('navigation', () => { it('should have the correct navigation screens', () => { @@ -41,6 +101,7 @@ describe('navigation', () => { 'DocumentNFCScan', 'DocumentNFCTrouble', 'DocumentOnboarding', + 'Gratification', 'Home', 'IDPicker', 'IdDetails', @@ -49,6 +110,7 @@ describe('navigation', () => { 'ManageDocuments', 'MockDataDeepLink', 'Modal', + 'Points', 'ProofHistory', 'ProofHistoryDetail', 'ProofRequestStatus', @@ -56,6 +118,7 @@ describe('navigation', () => { 'QRCodeTrouble', 'QRCodeViewFinder', 'RecoverWithPhrase', + 'Referral', 'SaveRecoveryPhrase', 'Settings', 'ShowRecoveryPhrase', diff --git a/app/tests/src/screens/GratificationScreen.test.tsx b/app/tests/src/screens/GratificationScreen.test.tsx new file mode 100644 index 000000000..dbe82e28f --- /dev/null +++ b/app/tests/src/screens/GratificationScreen.test.tsx @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import React from 'react'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import { render, waitFor } from '@testing-library/react-native'; + +import GratificationScreen from '@/screens/app/GratificationScreen'; + +jest.mock('@react-navigation/native', () => ({ + useNavigation: jest.fn(), + useRoute: jest.fn(), +})); + +// Mock Tamagui components to avoid theme provider requirement +jest.mock('tamagui', () => { + const ReactMock = require('react'); + const { View: RNView, Text: RNText } = require('react-native'); + const YStack = ReactMock.forwardRef(({ children, ...props }: any, ref: any) => + ReactMock.createElement(RNView, { ref, ...props }, children), + ); + YStack.displayName = 'YStack'; + const Text = ReactMock.forwardRef(({ children, ...props }: any, ref: any) => + ReactMock.createElement(RNText, { ref, ...props }, children), + ); + Text.displayName = 'Text'; + const View = ReactMock.forwardRef(({ children, ...props }: any, ref: any) => + ReactMock.createElement(RNView, { ref, ...props }, children), + ); + View.displayName = 'View'; + return { + YStack, + Text, + View, + }; +}); + +jest.mock('@selfxyz/mobile-sdk-alpha', () => ({ + DelayedLottieView: ({ onAnimationFinish }: any) => { + // Simulate animation finishing immediately + setTimeout(() => { + onAnimationFinish?.(); + }, 0); + return null; + }, +})); + +jest.mock('@selfxyz/mobile-sdk-alpha/components', () => ({ + PrimaryButton: ({ children, onPress }: any) => ( + + ), +})); + +jest.mock('@/images/icons/arrow_left.svg', () => 'ArrowLeft'); +jest.mock('@/images/icons/logo_white.svg', () => 'LogoWhite'); + +const mockUseNavigation = useNavigation as jest.MockedFunction< + typeof useNavigation +>; +const mockUseRoute = useRoute as jest.MockedFunction; + +describe('GratificationScreen', () => { + const mockNavigate = jest.fn(); + const mockGoBack = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + mockUseNavigation.mockReturnValue({ + navigate: mockNavigate, + goBack: mockGoBack, + } as any); + + mockUseRoute.mockReturnValue({ + params: {}, + } as any); + }); + + it('should use default points value when not provided', async () => { + mockUseRoute.mockReturnValue({ + params: {}, + } as any); + + const { getByText } = render(); + + await waitFor(() => { + expect(getByText('0')).toBeTruthy(); + }); + }); + + it('should use custom points value when provided', async () => { + mockUseRoute.mockReturnValue({ + params: { points: 50 }, + } as any); + + const { getByText } = render(); + + await waitFor(() => { + expect(getByText('50')).toBeTruthy(); + }); + }); + + it('should display referral points value (24) when passed', async () => { + mockUseRoute.mockReturnValue({ + params: { points: 24 }, + } as any); + + const { getByText } = render(); + + await waitFor(() => { + expect(getByText('24')).toBeTruthy(); + }); + }); +}); diff --git a/app/tests/src/screens/WebViewScreen.test.tsx b/app/tests/src/screens/WebViewScreen.test.tsx index 604cb6ee7..0492b7fde 100644 --- a/app/tests/src/screens/WebViewScreen.test.tsx +++ b/app/tests/src/screens/WebViewScreen.test.tsx @@ -9,10 +9,10 @@ import { render, screen, waitFor } from '@testing-library/react-native'; import { WebViewScreen } from '@/screens/shared/WebViewScreen'; jest.mock('react-native-webview', () => { - const ReactModule = require('react'); + const ReactMock = require('react'); const { View } = require('react-native'); - const MockWebView = ReactModule.forwardRef((props: any, _ref) => { - return ReactModule.createElement(View, { testID: 'webview', ...props }); + const MockWebView = ReactMock.forwardRef((props: any, _ref) => { + return ReactMock.createElement(View, { testID: 'webview', ...props }); }); MockWebView.displayName = 'MockWebView'; return { diff --git a/app/tests/src/utils/points/api.test.ts b/app/tests/src/utils/points/api.test.ts new file mode 100644 index 000000000..8270368aa --- /dev/null +++ b/app/tests/src/utils/points/api.test.ts @@ -0,0 +1,627 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type { AxiosResponse } from 'axios'; +import axios from 'axios'; +import { ethers } from 'ethers'; + +import { unsafe_getPrivateKey } from '@/providers/authProvider'; +import { + isSuccessfulStatus, + makeApiRequest, + POINTS_API_BASE_URL, +} from '@/utils/points/api'; + +// Mock dependencies +jest.mock('axios'); +jest.mock('@/providers/authProvider', () => ({ + unsafe_getPrivateKey: jest.fn(), +})); +jest.mock('ethers', () => { + const actualEthers = jest.requireActual('ethers'); + return { + ...actualEthers, + ethers: { + ...actualEthers.ethers, + Wallet: jest.fn(), + Signature: { + from: jest.fn(), + }, + getBytes: jest.fn(), + }, + }; +}); + +const mockAxios = axios as jest.Mocked; +const mockUnsafeGetPrivateKey = unsafe_getPrivateKey as jest.MockedFunction< + typeof unsafe_getPrivateKey +>; + +describe('isSuccessfulStatus', () => { + it('should return true for 200 status code', () => { + expect(isSuccessfulStatus(200)).toBe(true); + }); + + it('should return true for 202 status code', () => { + expect(isSuccessfulStatus(202)).toBe(true); + }); + + it('should return false for other 2xx codes', () => { + expect(isSuccessfulStatus(201)).toBe(false); + expect(isSuccessfulStatus(204)).toBe(false); + }); + + it('should return false for error status codes', () => { + expect(isSuccessfulStatus(400)).toBe(false); + expect(isSuccessfulStatus(404)).toBe(false); + expect(isSuccessfulStatus(500)).toBe(false); + }); +}); + +describe('Points API - Signature Logic', () => { + const mockPrivateKey = + '0x1234567890123456789012345678901234567890123456789012345678901234'; + const mockAddress = '0xAbCdEf1234567890aBcDeF1234567890AbCdEf12'; + const mockSignatureBytes = new Uint8Array([1, 2, 3, 4, 5]); + const mockSignatureBase64 = 'AQIDBAU='; // base64 of [1,2,3,4,5] + + let mockWallet: any; + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + + // Suppress console.error in tests + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + + // Setup wallet mock + mockWallet = { + signMessage: jest.fn(), + address: mockAddress, + }; + + // Mock ethers.Wallet constructor + (ethers.Wallet as any).mockImplementation(() => mockWallet); + + // Mock ethers.Signature.from + (ethers.Signature.from as jest.Mock).mockReturnValue({ + yParity: 1, + }); + + // Mock ethers.getBytes + (ethers.getBytes as jest.Mock).mockReturnValue(mockSignatureBytes); + + // Mock Buffer.from for base64 conversion + global.Buffer.from = jest.fn().mockReturnValue({ + toString: jest.fn().mockReturnValue(mockSignatureBase64), + }) as any; + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + describe('Signature Generation', () => { + it('should generate signature with correct parameters for referee address', async () => { + const refereeAddress = '0x1234567890123456789012345678901234567890'; + const mockSignatureHex = + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab'; + + mockUnsafeGetPrivateKey.mockResolvedValue(mockPrivateKey); + mockWallet.signMessage.mockResolvedValue(mockSignatureHex); + + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + await makeApiRequest('/referrals/refer', { + referee: refereeAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + // Verify wallet was created with correct private key + expect(ethers.Wallet).toHaveBeenCalledWith(mockPrivateKey); + + // Verify message signed was lowercase address + expect(mockWallet.signMessage).toHaveBeenCalledWith( + refereeAddress.toLowerCase(), + ); + + // Verify signature was parsed + expect(ethers.Signature.from).toHaveBeenCalledWith(mockSignatureHex); + + // Verify signature bytes were extracted + expect(ethers.getBytes).toHaveBeenCalledWith(mockSignatureHex); + }); + + it('should generate signature with correct parameters for address field', async () => { + const userAddress = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12'; + const mockSignatureHex = + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab'; + + mockUnsafeGetPrivateKey.mockResolvedValue(mockPrivateKey); + mockWallet.signMessage.mockResolvedValue(mockSignatureHex); + + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + await makeApiRequest( + '/verify-action', + { + action: 'secret_backup', + address: userAddress, + }, + undefined, + ); + + // Verify message signed was lowercase address + expect(mockWallet.signMessage).toHaveBeenCalledWith( + userAddress.toLowerCase(), + ); + }); + + it('should handle signature generation errors', async () => { + mockUnsafeGetPrivateKey.mockRejectedValue( + new Error('Biometric auth failed'), + ); + + const result = await makeApiRequest('/referrals/refer', { + referee: mockAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + expect(result.success).toBe(false); + expect(result.status).toBe(500); + expect(result.error).toContain('Failed to generate signature'); + }); + + it('should handle missing private key', async () => { + mockUnsafeGetPrivateKey.mockResolvedValue(null); + + const result = await makeApiRequest('/referrals/refer', { + referee: mockAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + expect(result.success).toBe(false); + expect(result.status).toBe(500); + expect(result.error).toContain('Failed to generate signature'); + }); + }); + + describe('makeApiRequest - Auto-detection of signing address', () => { + beforeEach(() => { + mockUnsafeGetPrivateKey.mockResolvedValue(mockPrivateKey); + mockWallet.signMessage.mockResolvedValue( + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab', + ); + }); + + it('should auto-detect referee field and include signature', async () => { + const refereeAddress = '0x1234567890123456789012345678901234567890'; + const referrerAddress = '0x9876543210987654321098765432109876543210'; + + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + await makeApiRequest('/referrals/refer', { + referee: refereeAddress, + referrer: referrerAddress, + }); + + expect(mockAxios.post).toHaveBeenCalledWith( + `${POINTS_API_BASE_URL}/referrals/refer`, + expect.objectContaining({ + referee: refereeAddress.toLowerCase(), + referrer: referrerAddress.toLowerCase(), + signature: mockSignatureBase64, + parity: 1, + }), + expect.any(Object), + ); + }); + + it('should auto-detect address field and include signature', async () => { + const userAddress = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12'; + + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + await makeApiRequest( + '/verify-action', + { + action: 'secret_backup', + address: userAddress, + }, + undefined, + ); + + expect(mockAxios.post).toHaveBeenCalledWith( + `${POINTS_API_BASE_URL}/verify-action`, + expect.objectContaining({ + action: 'secret_backup', + address: userAddress.toLowerCase(), + signature: mockSignatureBase64, + parity: 1, + }), + expect.any(Object), + ); + }); + + it('should prioritize referee over address field', async () => { + // Edge case: if both exist, referee should be used + const refereeAddress = '0x1111111111111111111111111111111111111111'; + const addressField = '0x2222222222222222222222222222222222222222'; + + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + await makeApiRequest('/some-endpoint', { + referee: refereeAddress, + address: addressField, + }); + + // Should sign with referee (first in || chain) + expect(mockWallet.signMessage).toHaveBeenCalledWith( + refereeAddress.toLowerCase(), + ); + }); + + it('should not include signature if no address fields present', async () => { + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + await makeApiRequest('/some-endpoint', { + someOtherField: 'value', + }); + + // Should not attempt to get private key or sign + expect(mockUnsafeGetPrivateKey).not.toHaveBeenCalled(); + expect(mockWallet.signMessage).not.toHaveBeenCalled(); + + // Should send request without signature + expect(mockAxios.post).toHaveBeenCalledWith( + `${POINTS_API_BASE_URL}/some-endpoint`, + { + someOtherField: 'value', + }, + expect.any(Object), + ); + }); + }); + + describe('makeApiRequest - Response handling', () => { + beforeEach(() => { + mockUnsafeGetPrivateKey.mockResolvedValue(mockPrivateKey); + mockWallet.signMessage.mockResolvedValue( + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab', + ); + }); + + it('should handle successful 200 response', async () => { + mockAxios.post.mockResolvedValue({ + status: 200, + data: { result: 'success' }, + } as AxiosResponse); + + const result = await makeApiRequest('/referrals/refer', { + referee: mockAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + expect(result).toEqual({ + success: true, + status: 200, + data: { result: 'success' }, + }); + }); + + it('should handle successful 202 response', async () => { + mockAxios.post.mockResolvedValue({ + status: 202, + data: { result: 'accepted' }, + } as AxiosResponse); + + const result = await makeApiRequest('/referrals/refer', { + referee: mockAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + expect(result).toEqual({ + success: true, + status: 202, + data: { result: 'accepted' }, + }); + }); + + it('should handle error responses with custom error messages', async () => { + mockAxios.post.mockResolvedValue({ + status: 400, + data: { status: 'already_verified', message: 'Already verified' }, + } as AxiosResponse); + + const errorMessages = { + already_verified: 'You have already verified this action.', + }; + + const result = await makeApiRequest( + '/verify-action', + { + action: 'secret_backup', + address: mockAddress, + }, + errorMessages, + ); + + expect(result).toEqual({ + success: false, + status: 400, + error: 'You have already verified this action.', + }); + }); + + it('should handle error responses with generic message from response', async () => { + mockAxios.post.mockResolvedValue({ + status: 400, + data: { message: 'Invalid request data' }, + } as AxiosResponse); + + const result = await makeApiRequest('/referrals/refer', { + referee: mockAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + expect(result).toEqual({ + success: false, + status: 400, + error: 'Invalid request data', + }); + }); + + it('should handle error responses with fallback message', async () => { + mockAxios.post.mockResolvedValue({ + status: 500, + data: {}, + } as AxiosResponse); + + const result = await makeApiRequest('/referrals/refer', { + referee: mockAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + expect(result).toEqual({ + success: false, + status: 500, + error: 'An unexpected error occurred. Please try again.', + }); + }); + + it('should handle network errors', async () => { + mockAxios.post.mockRejectedValue(new Error('Network error')); + + const result = await makeApiRequest('/referrals/refer', { + referee: mockAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + expect(result.success).toBe(false); + expect(result.status).toBe(500); + expect(result.error).toBe('Network error'); + }); + + it('should handle axios errors with response status', async () => { + mockAxios.post.mockRejectedValue({ + message: 'Request failed', + response: { status: 503 }, + }); + + const result = await makeApiRequest('/referrals/refer', { + referee: mockAddress, + referrer: '0x9876543210987654321098765432109876543210', + }); + + expect(result.success).toBe(false); + expect(result.status).toBe(503); + expect(result.error).toBe('Request failed'); + }); + }); + + describe('Integration tests - Full flow', () => { + beforeEach(() => { + mockUnsafeGetPrivateKey.mockResolvedValue(mockPrivateKey); + }); + + it('should complete full signature flow for referral endpoint', async () => { + const refereeAddress = '0x1234567890123456789012345678901234567890'; + const referrerAddress = '0x9876543210987654321098765432109876543210'; + const mockSignatureHex = + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab'; + + mockWallet.signMessage.mockResolvedValue(mockSignatureHex); + mockAxios.post.mockResolvedValue({ + status: 200, + data: { success: true }, + } as AxiosResponse); + + const result = await makeApiRequest('/referrals/refer', { + referee: refereeAddress, + referrer: referrerAddress, + }); + + // Verify complete flow + expect(mockUnsafeGetPrivateKey).toHaveBeenCalled(); + expect(ethers.Wallet).toHaveBeenCalledWith(mockPrivateKey); + expect(mockWallet.signMessage).toHaveBeenCalledWith( + refereeAddress.toLowerCase(), + ); + expect(ethers.Signature.from).toHaveBeenCalledWith(mockSignatureHex); + expect(ethers.getBytes).toHaveBeenCalledWith(mockSignatureHex); + expect(mockAxios.post).toHaveBeenCalledWith( + `${POINTS_API_BASE_URL}/referrals/refer`, + expect.objectContaining({ + referee: refereeAddress.toLowerCase(), + referrer: referrerAddress.toLowerCase(), + signature: mockSignatureBase64, + parity: 1, + }), + expect.any(Object), + ); + expect(result.success).toBe(true); + }); + + it('should complete full signature flow for backup endpoint', async () => { + const userAddress = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12'; + const mockSignatureHex = + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab'; + + mockWallet.signMessage.mockResolvedValue(mockSignatureHex); + mockAxios.post.mockResolvedValue({ + status: 200, + data: { success: true }, + } as AxiosResponse); + + const result = await makeApiRequest('/verify-action', { + action: 'secret_backup', + address: userAddress, + }); + + // Verify complete flow + expect(mockUnsafeGetPrivateKey).toHaveBeenCalled(); + expect(mockWallet.signMessage).toHaveBeenCalledWith( + userAddress.toLowerCase(), + ); + expect(mockAxios.post).toHaveBeenCalledWith( + `${POINTS_API_BASE_URL}/verify-action`, + expect.objectContaining({ + action: 'secret_backup', + address: userAddress.toLowerCase(), + signature: mockSignatureBase64, + parity: 1, + }), + expect.any(Object), + ); + expect(result.success).toBe(true); + }); + + it('should complete full signature flow for notification endpoint', async () => { + const userAddress = '0xABCDEF1234567890ABCDEF1234567890ABCDEF12'; + const mockSignatureHex = + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab'; + + mockWallet.signMessage.mockResolvedValue(mockSignatureHex); + mockAxios.post.mockResolvedValue({ + status: 200, + data: { success: true }, + } as AxiosResponse); + + const result = await makeApiRequest('/verify-action', { + action: 'push_notification', + address: userAddress, + }); + + // Verify signature included + expect(mockAxios.post).toHaveBeenCalledWith( + `${POINTS_API_BASE_URL}/verify-action`, + expect.objectContaining({ + action: 'push_notification', + address: userAddress.toLowerCase(), + signature: mockSignatureBase64, + parity: 1, + }), + expect.any(Object), + ); + expect(result.success).toBe(true); + }); + }); + + describe('Edge cases', () => { + beforeEach(() => { + mockUnsafeGetPrivateKey.mockResolvedValue(mockPrivateKey); + mockWallet.signMessage.mockResolvedValue( + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab', + ); + }); + + it('should handle empty body object', async () => { + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + const result = await makeApiRequest('/some-endpoint', {}); + + expect(mockUnsafeGetPrivateKey).not.toHaveBeenCalled(); + expect(result.success).toBe(true); + }); + + it('should handle null address values', async () => { + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + const result = await makeApiRequest('/some-endpoint', { + address: null, + }); + + expect(mockUnsafeGetPrivateKey).not.toHaveBeenCalled(); + expect(result.success).toBe(true); + }); + + it('should handle undefined address values', async () => { + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + const result = await makeApiRequest('/some-endpoint', { + address: undefined, + }); + + expect(mockUnsafeGetPrivateKey).not.toHaveBeenCalled(); + expect(result.success).toBe(true); + }); + + it('should lowercase addresses in request body', async () => { + const mixedCaseReferee = '0xAbCdEf1234567890aBcDeF1234567890AbCdEf12'; + const mixedCaseReferrer = '0xAbCdEf0987654321aBcDeF0987654321AbCdEf09'; + + mockAxios.post.mockResolvedValue({ + status: 200, + data: {}, + } as AxiosResponse); + + await makeApiRequest('/referrals/refer', { + referee: mixedCaseReferee, + referrer: mixedCaseReferrer, + }); + + // Should sign with lowercase + expect(mockWallet.signMessage).toHaveBeenCalledWith( + mixedCaseReferee.toLowerCase(), + ); + + // Should send lowercase in body + expect(mockAxios.post).toHaveBeenCalledWith( + `${POINTS_API_BASE_URL}/referrals/refer`, + expect.objectContaining({ + referee: mixedCaseReferee.toLowerCase(), + referrer: mixedCaseReferrer.toLowerCase(), + }), + expect.any(Object), + ); + }); + }); +}); diff --git a/app/tests/utils/deeplinks.test.ts b/app/tests/utils/deeplinks.test.ts index 0a72e13b1..003986757 100644 --- a/app/tests/utils/deeplinks.test.ts +++ b/app/tests/utils/deeplinks.test.ts @@ -112,6 +112,27 @@ describe('deeplinks', () => { }); }); + it('handles referrer parameter and navigates to HomeScreen for confirmation', () => { + const referrer = '0x1234567890123456789012345678901234567890'; + const url = `scheme://open?referrer=${referrer}`; + + const mockSetDeepLinkReferrer = jest.fn(); + mockUserStore.default.getState.mockReturnValue({ + setDeepLinkReferrer: mockSetDeepLinkReferrer, + }); + + handleUrl({} as SelfClient, url); + + expect(mockSetDeepLinkReferrer).toHaveBeenCalledWith(referrer); + + const { navigationRef } = require('@/navigation'); + // Should navigate to HomeScreen, which will show confirmation modal + expect(navigationRef.reset).toHaveBeenCalledWith({ + index: 0, + routes: [{ name: 'Home' }], + }); + }); + it('navigates to QRCodeTrouble for invalid data', () => { const consoleErrorSpy = jest .spyOn(console, 'error') @@ -150,7 +171,11 @@ describe('deeplinks', () => { routes: [{ name: 'Home' }, { name: 'QRCodeTrouble' }], }); expect(consoleErrorSpy).toHaveBeenCalledWith( - 'No sessionId or selfApp found in the data', + 'Parameter sessionId failed validation:', + 'abc', + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'No sessionId, selfApp or valid OAuth parameters found in the data', ); consoleWarnSpy.mockRestore(); @@ -173,6 +198,100 @@ describe('deeplinks', () => { consoleErrorSpy.mockRestore(); }); + + it('handles valid Turnkey OAuth redirect with code and state', () => { + const consoleLogSpy = jest + .spyOn(console, 'log') + .mockImplementation(() => {}); + + const url = + 'https://redirect.self.xyz?scheme=https#code=4/0Ab32j93MfuUU-vJKJth_t0fnnPkg1O7&id_token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMDQwMTAwODA2NDc2NTA5MzU5MzgiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature'; + handleUrl({} as SelfClient, url); + + const { navigationRef } = require('@/navigation'); + // Turnkey OAuth should return silently without navigation + expect(navigationRef.navigate).not.toHaveBeenCalled(); + expect(navigationRef.reset).not.toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalledWith( + '[Deeplinks] Turnkey OAuth redirect received with valid parameters', + ); + + consoleLogSpy.mockRestore(); + }); + + it('navigates to QRCodeTrouble when only code is present (missing id_token)', () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const url = + 'https://redirect.self.xyz?scheme=https#code=4/0Ab32j93MfuUU-vJKJth_t0fnnPkg1O7'; + handleUrl({} as SelfClient, url); + + const { navigationRef } = require('@/navigation'); + // With just code and id_token validation removed, this should be accepted as valid OAuth + expect(navigationRef.navigate).not.toHaveBeenCalled(); + expect(navigationRef.reset).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + + it('handles valid Turnkey OAuth with only id_token (implicit flow)', () => { + const consoleLogSpy = jest + .spyOn(console, 'log') + .mockImplementation(() => {}); + + const url = + 'https://redirect.self.xyz?scheme=https#id_token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMDQwMTAwODA2NDc2NTA5MzU5MzgiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature&scope=email%20profile'; + handleUrl({} as SelfClient, url); + + const { navigationRef } = require('@/navigation'); + expect(navigationRef.navigate).not.toHaveBeenCalled(); + expect(navigationRef.reset).not.toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalledWith( + '[Deeplinks] Turnkey OAuth redirect received with valid parameters', + ); + + consoleLogSpy.mockRestore(); + }); + + it('navigates to QRCodeTrouble when neither code nor id_token is present', () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + const url = + 'https://redirect.self.xyz?scheme=https#scope=email%20profile'; + handleUrl({} as SelfClient, url); + + const { navigationRef } = require('@/navigation'); + expect(navigationRef.reset).toHaveBeenCalledWith({ + index: 1, + routes: [{ name: 'Home' }, { name: 'QRCodeTrouble' }], + }); + + consoleErrorSpy.mockRestore(); + }); + + it('rejects Turnkey OAuth with invalid id_token format', () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + // id_token with invalid characters (XSS attempt) - should be rejected + // code is valid, but since id_token is invalid and rejected, code alone shouldn't trigger OAuth + const url = + 'https://redirect.self.xyz?scheme=https#code=4/0Ab32j93&id_token='; + handleUrl({} as SelfClient, url); + + const { navigationRef } = require('@/navigation'); + // Code without valid id_token should still be accepted as valid OAuth (authorization code flow) + // So this should NOT navigate to QRCodeTrouble + expect(navigationRef.navigate).not.toHaveBeenCalled(); + expect(navigationRef.reset).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); }); describe('parseAndValidateUrlParams', () => { @@ -206,6 +325,8 @@ describe('deeplinks', () => { const result = parseAndValidateUrlParams(url); expect(result).toEqual({ sessionId: 'abc123' }); + // Check both warnings were called, regardless of order + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); expect(consoleWarnSpy).toHaveBeenCalledWith( 'Unexpected or invalid parameter ignored: maliciousParam', ); @@ -375,6 +496,74 @@ describe('deeplinks', () => { }); }); + describe('Turnkey OAuth parameter validation', () => { + it('returns valid code and state parameters', () => { + const url = + 'https://redirect.self.xyz?scheme=https#code=4/0Ab32j93MfuUU-vJKJth_t0fnnPkg1O7&state=state_abc'; + const result = parseAndValidateUrlParams(url); + expect(result.code).toBe('4/0Ab32j93MfuUU-vJKJth_t0fnnPkg1O7'); + expect(result.state).toBe('state_abc'); + }); + + it('returns id_token and scope parameters', () => { + const url = + 'https://redirect.self.xyz?scheme=https#id_token=eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMDQwMTAwODA2NDc2NTA5MzU5MzgiLCJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature&scope=email%20profile'; + const result = parseAndValidateUrlParams(url); + expect(result.id_token).toBeTruthy(); + expect(result.scope).toBe('email profile'); + }); + + it('handles code with forward slashes (Google OAuth format)', () => { + const url = + 'https://redirect.self.xyz?scheme=https#code=4/0Ab32j93MfuUU-vJKJth_t0fnnPkg1O7CMFt3YS0RKh9yreKIqdMg4qZh6MaIkfonjNlJFw'; + const result = parseAndValidateUrlParams(url); + expect(result.code).toBe( + '4/0Ab32j93MfuUU-vJKJth_t0fnnPkg1O7CMFt3YS0RKh9yreKIqdMg4qZh6MaIkfonjNlJFw', + ); + }); + + it('rejects id_token with invalid characters (XSS attempt)', () => { + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + // URL with only an invalid id_token - this should reject the id_token + const url = + 'https://redirect.self.xyz#id_token='; + const result = parseAndValidateUrlParams(url); + + // The invalid id_token should be rejected + expect(result.id_token).toBeUndefined(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Parameter id_token failed validation:', + expect.any(String), + ); + + consoleErrorSpy.mockRestore(); + }); + + it('filters out unexpected OAuth-related parameters', () => { + const consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + const url = + 'https://redirect.self.xyz?scheme=https#code=4/0Ab32j93&state=state_abc&error=access_denied&error_description=user_denied'; + const result = parseAndValidateUrlParams(url); + + expect(result.code).toBe('4/0Ab32j93'); + expect(result.state).toBe('state_abc'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Unexpected or invalid parameter ignored: error', + ); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Unexpected or invalid parameter ignored: error_description', + ); + + consoleWarnSpy.mockRestore(); + }); + }); + it('setup listener registers and cleans up', () => { const remove = jest.fn(); (Linking.getInitialURL as jest.Mock).mockResolvedValue(undefined); diff --git a/app/tests/utils/proving/provingUtils.test.ts b/app/tests/utils/proving/provingUtils.test.ts index 90bfa7cc8..dae08da03 100644 --- a/app/tests/utils/proving/provingUtils.test.ts +++ b/app/tests/utils/proving/provingUtils.test.ts @@ -58,6 +58,7 @@ describe('provingUtils', () => { circuit: { name: 'vc_and_disclose', inputs: JSON.stringify(inputs) }, version: 2, userDefinedData: '0xabc', + selfDefinedData: '', }); }); diff --git a/app/version.json b/app/version.json index e1af2bd4b..fde89e2b3 100644 --- a/app/version.json +++ b/app/version.json @@ -4,7 +4,7 @@ "lastDeployed": "2025-11-08T01:25:41.109Z" }, "android": { - "build": 115, - "lastDeployed": "2025-11-04T18:00:33.470Z" + "build": 117, + "lastDeployed": "2025-11-08T17:54:14Z" } } diff --git a/app/web/fonts.css b/app/web/fonts.css index fe681b64a..7f6adfc35 100644 --- a/app/web/fonts.css +++ b/app/web/fonts.css @@ -6,6 +6,14 @@ font-display: swap; } +@font-face { + font-family: 'DINOT-Bold'; + src: url('./fonts/DINOT-Bold.otf') format('opentype'); + font-weight: bold; + font-style: normal; + font-display: swap; +} + @font-face { font-family: 'DINOT-Medium'; src: url('./fonts/DINOT-Medium.otf') format('opentype'); diff --git a/app/web/fonts/DINOT-Bold.otf b/app/web/fonts/DINOT-Bold.otf new file mode 100644 index 000000000..62ffe412e Binary files /dev/null and b/app/web/fonts/DINOT-Bold.otf differ diff --git a/common/src/utils/appType.ts b/common/src/utils/appType.ts index 0d1e27d88..a478e54b3 100644 --- a/common/src/utils/appType.ts +++ b/common/src/utils/appType.ts @@ -31,6 +31,7 @@ export interface SelfApp { version: number; chainID: 42220 | 11142220; userDefinedData: string; + selfDefinedData: string; } export interface SelfAppDisclosureConfig { @@ -115,6 +116,7 @@ export class SelfAppBuilder { chainID: config.endpointType === 'staging_celo' ? 11142220 : 42220, version: config.version ?? 2, userDefinedData: '', + selfDefinedData: '', ...config, } as SelfApp; } diff --git a/common/src/utils/circuits/registerInputs.ts b/common/src/utils/circuits/registerInputs.ts index 3c0cae1ed..858aa6057 100644 --- a/common/src/utils/circuits/registerInputs.ts +++ b/common/src/utils/circuits/registerInputs.ts @@ -250,10 +250,6 @@ export async function generateTEEInputsRegister( dscTree as string[], env ); - console.log('inputs-aadhaar', inputs); - console.log('circuitName-aadhaar', circuitName); - console.log('endpointType-aadhaar', endpointType); - console.log('endpoint-aadhaar', endpoint); return { inputs, circuitName, endpointType, endpoint }; } diff --git a/common/src/utils/proving.ts b/common/src/utils/proving.ts index 91d53db29..fcbc234db 100644 --- a/common/src/utils/proving.ts +++ b/common/src/utils/proving.ts @@ -24,6 +24,7 @@ export type TEEPayloadDisclose = TEEPayloadBase & { onchain: boolean; endpoint: string; userDefinedData: string; + selfDefinedData: string; version: number; }; @@ -64,7 +65,8 @@ export function getPayload( endpointType: EndpointType, endpoint: string, version: number = 1, - userDefinedData: string = '' + userDefinedData: string = '', + selfDefinedData: string = '' ) { if (circuitType === 'disclose') { const type = @@ -84,6 +86,7 @@ export function getPayload( }, version, userDefinedData, + selfDefinedData, }; return payload; } else { diff --git a/package.json b/package.json index f8b6d553e..f1abd8856 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "resolutions": { "@babel/core": "^7.28.4", "@babel/runtime": "^7.28.4", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", "@swc/core": "1.7.36", "@tamagui/animations-react-native": "1.126.14", "@tamagui/toast": "1.126.14", @@ -44,7 +46,8 @@ "punycode": "npm:punycode.js@^2.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-native": "0.76.9" + "react-native": "0.76.9", + "react-native-passkey": "3.3.1" }, "dependencies": { "@babel/runtime": "^7.28.3", diff --git a/packages/mobile-sdk-alpha/assets/fonts/DINOT-Bold.otf b/packages/mobile-sdk-alpha/assets/fonts/DINOT-Bold.otf new file mode 100644 index 000000000..62ffe412e Binary files /dev/null and b/packages/mobile-sdk-alpha/assets/fonts/DINOT-Bold.otf differ diff --git a/packages/mobile-sdk-alpha/package.json b/packages/mobile-sdk-alpha/package.json index e0e6cc53f..efb93f8fc 100644 --- a/packages/mobile-sdk-alpha/package.json +++ b/packages/mobile-sdk-alpha/package.json @@ -69,6 +69,12 @@ "import": "./dist/esm/hooks/index.js", "require": "./dist/cjs/hooks/index.cjs" }, + "./hooks/useSafeBottomPadding": { + "types": "./dist/esm/hooks/useSafeBottomPadding.d.ts", + "react-native": "./dist/esm/hooks/useSafeBottomPadding.js", + "import": "./dist/esm/hooks/useSafeBottomPadding.js", + "require": "./dist/cjs/hooks/useSafeBottomPadding.cjs" + }, "./svgs/*.svg": { "react-native": "./dist/svgs/*.svg", "import": "./dist/svgs/*.svg", diff --git a/packages/mobile-sdk-alpha/src/constants/analytics.ts b/packages/mobile-sdk-alpha/src/constants/analytics.ts index 9c1e1d16b..679831617 100644 --- a/packages/mobile-sdk-alpha/src/constants/analytics.ts +++ b/packages/mobile-sdk-alpha/src/constants/analytics.ts @@ -135,6 +135,22 @@ export const PassportEvents = { START_PASSPORT_NFC: 'Passport: Start Passport NFC', }; +export const PointEvents = { + HOME_POINT_EARN_POINTS_OPENED: 'Points: Home Earn Points Opened', + EXPLORE_APPS: 'Points: Explore Apps Opened', + EARN_REFERRAL: 'Points: Earn Referral Opened', + EARN_REFERAL_MESSAGES: 'Points: Earn Referral via Messages', + EARN_REFERAL_WHATSAPP: 'Points: Earn Referral via WhatsApp', + EARN_REFERAL_SHARE: 'Points: Earn Referral via Share', + EARN_REFERRAL_COPY_LINK: 'Points: Earn Referral Copy Link', + EARN_BACKUP: 'Points: Earn with Backup', + EARN_BACKUP_SUCCESS: 'Points: Earn with Backup Success', + EARN_BACKUP_FAILED: 'Points: Earn with Backup Failed', + EARN_NOTIFICATION: 'Points: Earn with Notification', + EARN_NOTIFICATION_FAILED: 'Points: Earn with Notification Failed', + EARN_NOTIFICATION_SUCCESS: 'Points: Earn with Notification Success', +}; + export const ProofEvents = { ALREADY_REGISTERED: 'Proof: Already Registered', ATTESTATION_RECEIVED: 'Proof: Attestation Received', diff --git a/packages/mobile-sdk-alpha/src/constants/fonts.ts b/packages/mobile-sdk-alpha/src/constants/fonts.ts index c47b2f486..fc7a2be5a 100644 --- a/packages/mobile-sdk-alpha/src/constants/fonts.ts +++ b/packages/mobile-sdk-alpha/src/constants/fonts.ts @@ -6,4 +6,5 @@ import { Platform } from 'react-native'; export const advercase = 'Advercase-Regular'; export const dinot = 'DINOT-Medium'; +export const dinotBold = 'DINOT-Bold'; export const plexMono = Platform.OS === 'ios' ? 'IBM Plex Mono' : 'IBMPlexMono-Regular'; diff --git a/packages/mobile-sdk-alpha/src/flows/onboarding/confirm-identification.tsx b/packages/mobile-sdk-alpha/src/flows/onboarding/confirm-identification.tsx index 4562dd017..ca181efad 100644 --- a/packages/mobile-sdk-alpha/src/flows/onboarding/confirm-identification.tsx +++ b/packages/mobile-sdk-alpha/src/flows/onboarding/confirm-identification.tsx @@ -39,6 +39,8 @@ export const ConfirmIdentificationScreen = ({ onBeforeConfirm }: { onBeforeConfi await onConfirm(selfClient); }, [onBeforeConfirm, selfClient]); + // Calculate bottom padding to prevent button bleeding into system navigation + // ExpandableBottomLayout.BottomSection handles safe areas internally const paddingBottom = useSafeBottomPadding(20); return ( diff --git a/packages/mobile-sdk-alpha/src/hooks/useSafeBottomPadding.ts b/packages/mobile-sdk-alpha/src/hooks/useSafeBottomPadding.ts index a537152e3..b37b88491 100644 --- a/packages/mobile-sdk-alpha/src/hooks/useSafeBottomPadding.ts +++ b/packages/mobile-sdk-alpha/src/hooks/useSafeBottomPadding.ts @@ -21,6 +21,7 @@ import { Dimensions } from 'react-native'; * const bottomPadding = useSafeBottomPadding(20); * * ``` + * */ export const useSafeBottomPadding = (basePadding: number = 20): number => { const { height: windowHeight } = Dimensions.get('window'); diff --git a/packages/mobile-sdk-alpha/src/index.ts b/packages/mobile-sdk-alpha/src/index.ts index 9179a8aca..51bf05283 100644 --- a/packages/mobile-sdk-alpha/src/index.ts +++ b/packages/mobile-sdk-alpha/src/index.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -// Types export type { Adapters, AnalyticsAdapter, @@ -28,10 +27,8 @@ export type { WsConn, } from './types/public'; -// LogEvent Types export type { BaseContext, NFCScanContext, ProofContext } from './proving/internal/logging'; -// MRZ module export type { DG1, DG2, ParsedNFCResponse } from './nfc'; export type { DocumentData, DocumentMetadata, PassportCameraProps, ScreenProps } from './types/ui'; @@ -40,14 +37,12 @@ export type { HapticOptions, HapticType } from './haptic/shared'; export type { MRZScanOptions } from './mrz'; -// QR module export type { PassportValidationCallbacks } from './validation/document'; export type { SDKEvent, SDKEventMap } from './types/events'; -// Error handling export type { SdkErrorCategory } from './errors'; -// Screen Components (React Native-based) + export type { provingMachineCircuitType } from './proving/provingMachine'; export { @@ -74,15 +69,14 @@ export { export { NFCScannerScreen } from './components/screens/NFCScannerScreen'; export { type ProvingStateType } from './proving/provingMachine'; -// Components + export { QRCodeScreen } from './components/screens/QRCodeScreen'; -// Documents utils + export { SdkEvents } from './types/events'; export { SelfClientContext, SelfClientProvider, useSelfClient } from './context'; -// Haptic feedback utilities -export { advercase, dinot, plexMono } from './constants/fonts'; +export { advercase, dinot, dinotBold, plexMono } from './constants/fonts'; export { buttonTap, @@ -114,17 +108,14 @@ export { export { createListenersMap, createSelfClient } from './client'; -// Document utils export { defaultConfig } from './config/defaults'; export { defaultOptions } from './haptic/shared'; export { extractMRZInfo, extractNameFromMRZ, formatDateToYYMMDD } from './mrz'; -// Core functions export { extractNameFromDocument } from './documents/utils'; -// Document validation export { generateMockDocument, signatureAlgorithmToStrictSignatureAlgorithm } from './mock/generator'; export { isPassportDataValid } from './validation/document'; diff --git a/packages/mobile-sdk-alpha/src/proving/provingMachine.ts b/packages/mobile-sdk-alpha/src/proving/provingMachine.ts index ed7b1465e..50ffd4e6b 100644 --- a/packages/mobile-sdk-alpha/src/proving/provingMachine.ts +++ b/packages/mobile-sdk-alpha/src/proving/provingMachine.ts @@ -898,20 +898,17 @@ export const useProvingStore = create((set, get) => { set({ passportData, secret, env }); set({ circuitType }); - if (passportData.documentCategory === 'aadhaar') { + // Skip parsing for disclosure if passport is already parsed + // Re-parsing would overwrite the alternative CSCA used during registration and is unnecessary + // skip also the register circuit as the passport already got parsed in during the dsc step + console.log('circuitType', circuitType); + if (circuitType !== 'dsc') { + console.log('skipping id document parsing'); actor.send({ type: 'FETCH_DATA' }); selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); } else { - // Skip parsing for disclosure if passport is already parsed - // Re-parsing would overwrite the alternative CSCA used during registration and is unnecessary - const isParsed = passportData.passportMetadata !== undefined; - if (circuitType === 'disclose' && isParsed) { - actor.send({ type: 'FETCH_DATA' }); - selfClient.trackEvent(ProofEvents.FETCH_DATA_STARTED); - } else { - actor.send({ type: 'PARSE_ID_DOCUMENT' }); - selfClient.trackEvent(ProofEvents.PARSE_ID_DOCUMENT_STARTED); - } + actor.send({ type: 'PARSE_ID_DOCUMENT' }); + selfClient.trackEvent(ProofEvents.PARSE_ID_DOCUMENT_STARTED); } }, @@ -1470,6 +1467,7 @@ export const useProvingStore = create((set, get) => { endpoint as string, selfApp?.version, userDefinedData, + selfApp?.selfDefinedData ?? '', ); const payloadSize = JSON.stringify(payload).length; diff --git a/packages/mobile-sdk-alpha/tests/proving/provingMachine.generatePayload.test.ts b/packages/mobile-sdk-alpha/tests/proving/provingMachine.generatePayload.test.ts index 2a099b075..08e38533a 100644 --- a/packages/mobile-sdk-alpha/tests/proving/provingMachine.generatePayload.test.ts +++ b/packages/mobile-sdk-alpha/tests/proving/provingMachine.generatePayload.test.ts @@ -173,6 +173,7 @@ describe('_generatePayload', () => { chainID: 42220, userId: '12345678-1234-1234-1234-123456789abc', // Valid UUID format userDefinedData: '0x0', + selfDefinedData: '', endpointType: 'https', endpoint: 'https://e', scope: 's', diff --git a/patches/react-native-svg+15.14.0.patch b/patches/react-native-svg+15.14.0.patch new file mode 100644 index 000000000..18f7e025e --- /dev/null +++ b/patches/react-native-svg+15.14.0.patch @@ -0,0 +1,98 @@ +diff --git a/node_modules/react-native-svg/android/.project b/node_modules/react-native-svg/android/.project +new file mode 100644 +index 0000000..dd0da62 +--- /dev/null ++++ b/node_modules/react-native-svg/android/.project +@@ -0,0 +1,28 @@ ++ ++ ++ react-native-svg ++ Project OpenPassport-android-react-native-svg created by Buildship. ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectbuilder ++ ++ ++ ++ ++ ++ org.eclipse.buildship.core.gradleprojectnature ++ ++ ++ ++ 1759738232589 ++ ++ 30 ++ ++ org.eclipse.core.resources.regexFilterMatcher ++ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ ++ ++ ++ ++ +diff --git a/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp b/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp +index 11718dd..fe993b3 100644 +--- a/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp ++++ b/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp +@@ -28,8 +28,8 @@ void RNSVGLayoutableShadowNode::setZeroDimensions() { + // views in the layout inspector when Yoga attempts to interpret SVG + // properties like width when viewBox scale is set. + auto style = yogaNode_.style(); +- style.setDimension(yoga::Dimension::Width, yoga::StyleSizeLength::points(0)); +- style.setDimension(yoga::Dimension::Height, yoga::StyleSizeLength::points(0)); ++ style.setDimension(yoga::Dimension::Width, yoga::StyleLength::points(0)); ++ style.setDimension(yoga::Dimension::Height, yoga::StyleLength::points(0)); + yogaNode_.setStyle(style); + } + +diff --git a/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp.bak b/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp.bak +new file mode 100644 +index 0000000..11718dd +--- /dev/null ++++ b/node_modules/react-native-svg/common/cpp/react/renderer/components/rnsvg/RNSVGLayoutableShadowNode.cpp.bak +@@ -0,0 +1,43 @@ ++#include "RNSVGLayoutableShadowNode.h" ++#include ++ ++namespace facebook::react { ++ ++RNSVGLayoutableShadowNode::RNSVGLayoutableShadowNode( ++ const ShadowNodeFragment &fragment, ++ const ShadowNodeFamily::Shared &family, ++ ShadowNodeTraits traits) ++ : YogaLayoutableShadowNode(fragment, family, traits) { ++ if (std::strcmp(this->getComponentName(), "RNSVGGroup") != 0) { ++ setZeroDimensions(); ++ } ++} ++ ++RNSVGLayoutableShadowNode::RNSVGLayoutableShadowNode( ++ const ShadowNode &sourceShadowNode, ++ const ShadowNodeFragment &fragment) ++ : YogaLayoutableShadowNode(sourceShadowNode, fragment) { ++ if (std::strcmp(this->getComponentName(), "RNSVGGroup") != 0) { ++ setZeroDimensions(); ++ } ++} ++ ++void RNSVGLayoutableShadowNode::setZeroDimensions() { ++ // SVG handles its layout manually on the native side and does not depend on ++ // the Yoga layout. Setting the dimensions to 0 eliminates randomly positioned ++ // views in the layout inspector when Yoga attempts to interpret SVG ++ // properties like width when viewBox scale is set. ++ auto style = yogaNode_.style(); ++ style.setDimension(yoga::Dimension::Width, yoga::StyleSizeLength::points(0)); ++ style.setDimension(yoga::Dimension::Height, yoga::StyleSizeLength::points(0)); ++ yogaNode_.setStyle(style); ++} ++ ++void RNSVGLayoutableShadowNode::layout(LayoutContext layoutContext) { ++ auto affectedNodes = layoutContext.affectedNodes; ++ layoutContext.affectedNodes = nullptr; ++ YogaLayoutableShadowNode::layout(layoutContext); ++ layoutContext.affectedNodes = affectedNodes; ++} ++ ++} // namespace facebook::react diff --git a/yarn.lock b/yarn.lock index b254538c7..ff7c418f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:^1.11.0": + version: 1.11.1 + resolution: "@adraffy/ens-normalize@npm:1.11.1" + checksum: 10c0/b364e2a57131db278ebf2f22d1a1ac6d8aea95c49dd2bbbc1825870b38aa91fd8816aba580a1f84edc50a45eb6389213dacfd1889f32893afc8549a82d304767 + languageName: node + linkType: hard + "@algolia/abtesting@npm:1.1.0": version: 1.1.0 resolution: "@algolia/abtesting@npm:1.1.0" @@ -4582,6 +4589,13 @@ __metadata: languageName: node linkType: hard +"@msgpack/msgpack@npm:3.1.2": + version: 3.1.2 + resolution: "@msgpack/msgpack@npm:3.1.2" + checksum: 10c0/4fee6dbea70a485d3a787ac76dd43687f489d662f22919237db1f2abbc3c88070c1d3ad78417ce6e764bcd041051680284654021f52068e0aff82d570cb942d5 + languageName: node + linkType: hard + "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.3": version: 3.0.3 resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.3" @@ -4845,25 +4859,14 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:1.2.0": - version: 1.2.0 - resolution: "@noble/curves@npm:1.2.0" - dependencies: - "@noble/hashes": "npm:1.3.2" - checksum: 10c0/0bac7d1bbfb3c2286910b02598addd33243cb97c3f36f987ecc927a4be8d7d88e0fcb12b0f0ef8a044e7307d1844dd5c49bb724bfa0a79c8ec50ba60768c97f6 - languageName: node - linkType: hard - -"@noble/curves@npm:1.4.2, @noble/curves@npm:~1.4.0": - version: 1.4.2 - resolution: "@noble/curves@npm:1.4.2" - dependencies: - "@noble/hashes": "npm:1.4.0" - checksum: 10c0/65620c895b15d46e8087939db6657b46a1a15cd4e0e4de5cd84b97a0dfe0af85f33a431bb21ac88267e3dc508618245d4cb564213959d66a84d690fe18a63419 +"@noble/ciphers@npm:1.3.0, @noble/ciphers@npm:^1.3.0": + version: 1.3.0 + resolution: "@noble/ciphers@npm:1.3.0" + checksum: 10c0/3ba6da645ce45e2f35e3b2e5c87ceba86b21dfa62b9466ede9edfb397f8116dae284f06652c0cd81d99445a2262b606632e868103d54ecc99fd946ae1af8cd37 languageName: node linkType: hard -"@noble/curves@npm:^1.4.2": +"@noble/curves@npm:1.9.7": version: 1.9.7 resolution: "@noble/curves@npm:1.9.7" dependencies: @@ -4872,44 +4875,7 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:~1.8.1": - version: 1.8.2 - resolution: "@noble/curves@npm:1.8.2" - dependencies: - "@noble/hashes": "npm:1.7.2" - checksum: 10c0/e7ef119b114681d6b7530b29a21f9bbea6fa6973bc369167da2158d05054cc6e6dbfb636ba89fad7707abacc150de30188b33192f94513911b24bdb87af50bbd - languageName: node - linkType: hard - -"@noble/hashes@npm:1.2.0, @noble/hashes@npm:~1.2.0": - version: 1.2.0 - resolution: "@noble/hashes@npm:1.2.0" - checksum: 10c0/8bd3edb7bb6a9068f806a9a5a208cc2144e42940a21c049d8e9a0c23db08bef5cf1cfd844a7e35489b5ab52c6fa6299352075319e7f531e0996d459c38cfe26a - languageName: node - linkType: hard - -"@noble/hashes@npm:1.3.2": - version: 1.3.2 - resolution: "@noble/hashes@npm:1.3.2" - checksum: 10c0/2482cce3bce6a596626f94ca296e21378e7a5d4c09597cbc46e65ffacc3d64c8df73111f2265444e36a3168208628258bbbaccba2ef24f65f58b2417638a20e7 - languageName: node - linkType: hard - -"@noble/hashes@npm:1.4.0, @noble/hashes@npm:~1.4.0": - version: 1.4.0 - resolution: "@noble/hashes@npm:1.4.0" - checksum: 10c0/8c3f005ee72e7b8f9cff756dfae1241485187254e3f743873e22073d63906863df5d4f13d441b7530ea614b7a093f0d889309f28b59850f33b66cb26a779a4a5 - languageName: node - linkType: hard - -"@noble/hashes@npm:1.7.2, @noble/hashes@npm:~1.7.1": - version: 1.7.2 - resolution: "@noble/hashes@npm:1.7.2" - checksum: 10c0/b1411eab3c0b6691d847e9394fe7f1fcd45eeb037547c8f97e7d03c5068a499b4aef188e8e717eee67389dca4fee17d69d7e0f58af6c092567b0b76359b114b2 - languageName: node - linkType: hard - -"@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.4.0, @noble/hashes@npm:^1.5.0": +"@noble/hashes@npm:1.8.0": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" checksum: 10c0/06a0b52c81a6fa7f04d67762e08b2c476a00285858150caeaaff4037356dd5e119f45b2a530f638b77a5eeca013168ec1b655db41bae3236cb2e9d511484fc77 @@ -5795,7 +5761,7 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-cms@npm:^2.5.0": +"@peculiar/asn1-cms@npm:^2.3.13, @peculiar/asn1-cms@npm:^2.5.0": version: 2.5.0 resolution: "@peculiar/asn1-cms@npm:2.5.0" dependencies: @@ -5808,7 +5774,7 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-csr@npm:^2.5.0": +"@peculiar/asn1-csr@npm:^2.3.13, @peculiar/asn1-csr@npm:^2.5.0": version: 2.5.0 resolution: "@peculiar/asn1-csr@npm:2.5.0" dependencies: @@ -5820,7 +5786,7 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-ecc@npm:^2.5.0": +"@peculiar/asn1-ecc@npm:^2.3.14, @peculiar/asn1-ecc@npm:^2.5.0": version: 2.5.0 resolution: "@peculiar/asn1-ecc@npm:2.5.0" dependencies: @@ -5858,7 +5824,7 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-pkcs9@npm:^2.5.0": +"@peculiar/asn1-pkcs9@npm:^2.3.13, @peculiar/asn1-pkcs9@npm:^2.5.0": version: 2.5.0 resolution: "@peculiar/asn1-pkcs9@npm:2.5.0" dependencies: @@ -5874,7 +5840,7 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-rsa@npm:^2.5.0": +"@peculiar/asn1-rsa@npm:^2.3.13, @peculiar/asn1-rsa@npm:^2.5.0": version: 2.5.0 resolution: "@peculiar/asn1-rsa@npm:2.5.0" dependencies: @@ -5886,7 +5852,7 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-schema@npm:^2.5.0": +"@peculiar/asn1-schema@npm:^2.3.13, @peculiar/asn1-schema@npm:^2.3.8, @peculiar/asn1-schema@npm:^2.5.0": version: 2.5.0 resolution: "@peculiar/asn1-schema@npm:2.5.0" dependencies: @@ -5909,7 +5875,7 @@ __metadata: languageName: node linkType: hard -"@peculiar/asn1-x509@npm:^2.5.0": +"@peculiar/asn1-x509@npm:^2.3.13, @peculiar/asn1-x509@npm:^2.5.0": version: 2.5.0 resolution: "@peculiar/asn1-x509@npm:2.5.0" dependencies: @@ -5921,6 +5887,47 @@ __metadata: languageName: node linkType: hard +"@peculiar/json-schema@npm:^1.1.12": + version: 1.1.12 + resolution: "@peculiar/json-schema@npm:1.1.12" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10c0/202132c66dcc6b6aca5d0af971c015be2e163da2f7f992910783c5d39c8a7db59b6ec4f4ce419459a1f954b7e1d17b6b253f0e60072c1b3d254079f4eaebc311 + languageName: node + linkType: hard + +"@peculiar/webcrypto@npm:1.5.0": + version: 1.5.0 + resolution: "@peculiar/webcrypto@npm:1.5.0" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.8" + "@peculiar/json-schema": "npm:^1.1.12" + pvtsutils: "npm:^1.3.5" + tslib: "npm:^2.6.2" + webcrypto-core: "npm:^1.8.0" + checksum: 10c0/4f6f24b2c52c2155b9c569b6eb1d57954cb5f7bd2764a50cdaed7aea17a6dcf304b75b87b57ba318756ffec8179a07d9a76534aaf77855912b838543e5ff8983 + languageName: node + linkType: hard + +"@peculiar/x509@npm:1.12.3": + version: 1.12.3 + resolution: "@peculiar/x509@npm:1.12.3" + dependencies: + "@peculiar/asn1-cms": "npm:^2.3.13" + "@peculiar/asn1-csr": "npm:^2.3.13" + "@peculiar/asn1-ecc": "npm:^2.3.14" + "@peculiar/asn1-pkcs9": "npm:^2.3.13" + "@peculiar/asn1-rsa": "npm:^2.3.13" + "@peculiar/asn1-schema": "npm:^2.3.13" + "@peculiar/asn1-x509": "npm:^2.3.13" + pvtsutils: "npm:^1.3.5" + reflect-metadata: "npm:^0.2.2" + tslib: "npm:^2.7.0" + tsyringe: "npm:^4.8.0" + checksum: 10c0/5395af84f04b221989f7e2868c1ee641e5e91ee21aff201dc31bfa85578009fbce00b19db1f7fdebc11b5357f3285731cfe9784e4ae21556da7b62effd976b5c + languageName: node + linkType: hard + "@peculiar/x509@npm:^1.12.3, @peculiar/x509@npm:^1.13.0": version: 1.14.0 resolution: "@peculiar/x509@npm:1.14.0" @@ -6993,6 +7000,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:1.2.6, @scure/base@npm:~1.2.5": + version: 1.2.6 + resolution: "@scure/base@npm:1.2.6" + checksum: 10c0/49bd5293371c4e062cb6ba689c8fe3ea3981b7bb9c000400dc4eafa29f56814cdcdd27c04311c2fec34de26bc373c593a1d6ca6d754398a488d587943b7c128a + languageName: node + linkType: hard + "@scure/base@npm:~1.1.0, @scure/base@npm:~1.1.6": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" @@ -7000,13 +7014,6 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:~1.2.5": - version: 1.2.6 - resolution: "@scure/base@npm:1.2.6" - checksum: 10c0/49bd5293371c4e062cb6ba689c8fe3ea3981b7bb9c000400dc4eafa29f56814cdcdd27c04311c2fec34de26bc373c593a1d6ca6d754398a488d587943b7c128a - languageName: node - linkType: hard - "@scure/bip32@npm:1.1.5": version: 1.1.5 resolution: "@scure/bip32@npm:1.1.5" @@ -7029,6 +7036,17 @@ __metadata: languageName: node linkType: hard +"@scure/bip32@npm:1.7.0, @scure/bip32@npm:^1.7.0": + version: 1.7.0 + resolution: "@scure/bip32@npm:1.7.0" + dependencies: + "@noble/curves": "npm:~1.9.0" + "@noble/hashes": "npm:~1.8.0" + "@scure/base": "npm:~1.2.5" + checksum: 10c0/e3d4c1f207df16abcd79babcdb74d36f89bdafc90bf02218a5140cc5cba25821d80d42957c6705f35210cc5769714ea9501d4ae34732cdd1c26c9ff182a219f7 + languageName: node + linkType: hard + "@scure/bip39@npm:1.1.1": version: 1.1.1 resolution: "@scure/bip39@npm:1.1.1" @@ -7049,6 +7067,16 @@ __metadata: languageName: node linkType: hard +"@scure/bip39@npm:1.6.0, @scure/bip39@npm:^1.6.0": + version: 1.6.0 + resolution: "@scure/bip39@npm:1.6.0" + dependencies: + "@noble/hashes": "npm:~1.8.0" + "@scure/base": "npm:~1.2.5" + checksum: 10c0/73a54b5566a50a3f8348a5cfd74d2092efeefc485efbed83d7a7374ffd9a75defddf446e8e5ea0385e4adb49a94b8ae83c5bad3e16333af400e932f7da3aaff8 + languageName: node + linkType: hard + "@segment/analytics-react-native@npm:^2.21.2": version: 2.21.3 resolution: "@segment/analytics-react-native@npm:2.21.3" @@ -7304,6 +7332,7 @@ __metadata: resolution: "@selfxyz/mobile-app@workspace:app" dependencies: "@babel/core": "npm:^7.28.3" + "@babel/plugin-transform-export-namespace-from": "npm:^7.27.1" "@babel/plugin-transform-private-methods": "npm:^7.27.1" "@babel/preset-env": "npm:^7.28.3" "@babel/runtime": "npm:^7.28.3" @@ -7345,6 +7374,9 @@ __metadata: "@tamagui/vite-plugin": "npm:1.126.14" "@testing-library/react-native": "npm:^13.3.3" "@tsconfig/react-native": "npm:^3.0.6" + "@turnkey/api-key-stamper": "npm:^0.5.0" + "@turnkey/encoding": "npm:^0.6.0" + "@turnkey/react-native-wallet-kit": "npm:1.0.0" "@types/bn.js": "npm:^5.2.0" "@types/dompurify": "npm:^3.2.0" "@types/elliptic": "npm:^6.4.18" @@ -7361,8 +7393,10 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:^8.39.0" "@typescript-eslint/parser": "npm:^8.39.0" "@vitejs/plugin-react-swc": "npm:^3.10.2" + "@walletconnect/react-native-compat": "npm:^2.22.4" "@xstate/react": "npm:^5.0.3" asn1js: "npm:^3.0.6" + axios: "npm:^1.13.2" babel-plugin-module-resolver: "npm:^5.0.2" buffer: "npm:^6.0.3" constants-browserify: "npm:^1.0.0" @@ -7380,6 +7414,7 @@ __metadata: eslint-plugin-simple-import-sort: "npm:^12.1.1" eslint-plugin-sort-exports: "npm:^0.9.1" ethers: "npm:^6.11.0" + expo-application: "npm:^7.0.7" expo-modules-core: "npm:^2.2.1" hash.js: "npm:^1.1.7" hermes-eslint: "npm:^0.19.1" @@ -7407,17 +7442,20 @@ __metadata: react-native-gesture-handler: "npm:2.19.0" react-native-get-random-values: "npm:^1.11.0" react-native-haptic-feedback: "npm:^2.3.3" + react-native-inappbrowser-reborn: "npm:^3.7.0" react-native-keychain: "npm:^10.0.0" react-native-localize: "npm:^3.5.2" react-native-logs: "npm:^5.3.0" react-native-nfc-manager: "npm:3.16.3" + react-native-passkey: "npm:^3.3.1" react-native-passport-reader: "npm:1.0.3" - react-native-safe-area-context: "npm:5.6.1" + react-native-safe-area-context: "npm:^5.6.1" react-native-screens: "npm:4.15.3" react-native-sqlite-storage: "npm:^6.0.1" - react-native-svg: "npm:15.12.1" + react-native-svg: "npm:^15.14.0" react-native-svg-transformer: "npm:^1.5.1" react-native-svg-web: "npm:^1.0.9" + react-native-url-polyfill: "npm:^3.0.0" react-native-web: "npm:^0.19.0" react-native-webview: "npm:^13.16.0" react-qr-barcode-scanner: "npm:^2.1.8" @@ -11400,6 +11438,146 @@ __metadata: languageName: node linkType: hard +"@turnkey/api-key-stamper@npm:0.5.0, @turnkey/api-key-stamper@npm:^0.5.0": + version: 0.5.0 + resolution: "@turnkey/api-key-stamper@npm:0.5.0" + dependencies: + "@noble/curves": "npm:^1.3.0" + "@turnkey/encoding": "npm:0.6.0" + sha256-uint8array: "npm:^0.10.7" + checksum: 10c0/7747153baf92e893160971cf319728104ffd5d0363d7ba8eeb460a83c788f5db14d2b5e3900225f84efc3e17b936128c7d516f65bbc459aff1aa8b54280307b4 + languageName: node + linkType: hard + +"@turnkey/core@npm:1.4.1": + version: 1.4.1 + resolution: "@turnkey/core@npm:1.4.1" + dependencies: + "@turnkey/api-key-stamper": "npm:0.5.0" + "@turnkey/crypto": "npm:2.8.1" + "@turnkey/encoding": "npm:0.6.0" + "@turnkey/http": "npm:3.13.0" + "@turnkey/sdk-types": "npm:0.6.1" + "@turnkey/webauthn-stamper": "npm:0.6.0" + "@wallet-standard/app": "npm:^1.1.0" + "@wallet-standard/base": "npm:^1.1.0" + "@walletconnect/sign-client": "npm:^2.21.8" + "@walletconnect/types": "npm:^2.21.8" + cross-fetch: "npm:^3.1.5" + ethers: "npm:^6.10.0" + jwt-decode: "npm:4.0.0" + uuid: "npm:^11.1.0" + viem: "npm:^2.33.1" + peerDependencies: + "@react-native-async-storage/async-storage": ^2.2.0 + "@turnkey/react-native-passkey-stamper": 1.2.2 + react-native-keychain: ^8.1.0 || ^9.2.2 || ^10.0.0 + peerDependenciesMeta: + "@react-native-async-storage/async-storage": + optional: true + "@turnkey/react-native-passkey-stamper": + optional: true + react-native-keychain: + optional: true + checksum: 10c0/4a9e4154ca9998a64d9a6d4a0e93de044deba271ec85420b8116bbb681f34b45fbd6cb3d4d05488199a0ae9d6a8995c81bd39ffa8f4c676b9ea0c9814d51c8ff + languageName: node + linkType: hard + +"@turnkey/crypto@npm:2.8.1": + version: 2.8.1 + resolution: "@turnkey/crypto@npm:2.8.1" + dependencies: + "@noble/ciphers": "npm:1.3.0" + "@noble/curves": "npm:1.9.0" + "@noble/hashes": "npm:1.8.0" + "@peculiar/webcrypto": "npm:1.5.0" + "@peculiar/x509": "npm:1.12.3" + "@turnkey/encoding": "npm:0.6.0" + "@turnkey/sdk-types": "npm:0.6.1" + borsh: "npm:2.0.0" + cbor-js: "npm:0.1.0" + checksum: 10c0/ea3f39d61244237de004ecd02d681db89572014ff28150a77c31c29e36ef89bdbb44b4f77d44b7f8520006ae31ca4b3a04be15614bef1c7cd59a396b799ed982 + languageName: node + linkType: hard + +"@turnkey/encoding@npm:0.6.0, @turnkey/encoding@npm:^0.6.0": + version: 0.6.0 + resolution: "@turnkey/encoding@npm:0.6.0" + dependencies: + bs58: "npm:6.0.0" + bs58check: "npm:4.0.0" + checksum: 10c0/1411dc416eb1be3f4edb82c4d38a05faa58964ece6458d44330de9b7578d918c98c3a36e053d4676d2c6fe24912c686bb356164ebc0a7cd41be7797c90771ffb + languageName: node + linkType: hard + +"@turnkey/http@npm:3.13.0": + version: 3.13.0 + resolution: "@turnkey/http@npm:3.13.0" + dependencies: + "@turnkey/api-key-stamper": "npm:0.5.0" + "@turnkey/encoding": "npm:0.6.0" + "@turnkey/webauthn-stamper": "npm:0.6.0" + cross-fetch: "npm:^3.1.5" + checksum: 10c0/5fbde22bd48cd92a1831fdf258c09ebc88777628ff17965a5e3a11e93a5ef06a138919de2ce41d310466106eb2df027ebbb4655c6f04ee22cbad99dd69e2753c + languageName: node + linkType: hard + +"@turnkey/react-native-passkey-stamper@npm:1.2.2": + version: 1.2.2 + resolution: "@turnkey/react-native-passkey-stamper@npm:1.2.2" + dependencies: + "@turnkey/encoding": "npm:0.6.0" + "@turnkey/http": "npm:3.13.0" + buffer: "npm:6.0.3" + react-native-passkey: "npm:3.0.0" + sha256-uint8array: "npm:0.10.7" + checksum: 10c0/98efb5ce8f6c00fb589da7019985fccde606ab38e1f727efba389c153cf77021f12bbf1c84b0cc84f0b351e9a19f4b532ac4886cf99c3fdcf02a45029269c03b + languageName: node + linkType: hard + +"@turnkey/react-native-wallet-kit@npm:1.0.0": + version: 1.0.0 + resolution: "@turnkey/react-native-wallet-kit@npm:1.0.0" + dependencies: + "@noble/hashes": "npm:^1.8.0" + "@react-native-async-storage/async-storage": "npm:^2.2.0" + "@turnkey/core": "npm:1.4.1" + "@turnkey/crypto": "npm:2.8.1" + "@turnkey/encoding": "npm:0.6.0" + "@turnkey/react-native-passkey-stamper": "npm:1.2.2" + "@turnkey/sdk-types": "npm:0.6.1" + react-native-device-info: "npm:^11.1.0" + peerDependencies: + "@types/react": ">=16.8.0 <20" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-native: ">=0.70.0" + react-native-gesture-handler: ">=2.0.0" + react-native-inappbrowser-reborn: ">=3.7.0" + react-native-safe-area-context: ">=4.0.0" + react-native-svg: ">=13.0.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/5183a3f60c470dbd4d95a7afd5f73d5b8e122d7f58186006bdaf9687341fdd8f6384de97518a7d52f4a2f61c6af74cf4cdef30e20122794f8c139c9e4526cfea + languageName: node + linkType: hard + +"@turnkey/sdk-types@npm:0.6.1": + version: 0.6.1 + resolution: "@turnkey/sdk-types@npm:0.6.1" + checksum: 10c0/5a966d1cbd9d040cf1748e23520d8575d0f0c1ac8bc97b9ea63d4bc2d41af54b56dc79724fa42284d0a28912a3fd2339af19ce6875182a16d78588bceb1d7e53 + languageName: node + linkType: hard + +"@turnkey/webauthn-stamper@npm:0.6.0": + version: 0.6.0 + resolution: "@turnkey/webauthn-stamper@npm:0.6.0" + dependencies: + sha256-uint8array: "npm:^0.10.7" + checksum: 10c0/59130342cf9cb4cf7937547d3fd35d176544127c9cb34b7531da93406ce4fd699ccba7fd34e70ede76a13eb728e6cda90fcef7ad0e664476dae5913132eb89c9 + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.10.0, @tybys/wasm-util@npm:^0.10.1": version: 0.10.1 resolution: "@tybys/wasm-util@npm:0.10.1" @@ -12774,6 +12952,285 @@ __metadata: languageName: node linkType: hard +"@wallet-standard/app@npm:^1.1.0": + version: 1.1.0 + resolution: "@wallet-standard/app@npm:1.1.0" + dependencies: + "@wallet-standard/base": "npm:^1.1.0" + checksum: 10c0/04650f92d512493f4556cbf48e49626745a0fe55633b03a96d99698e415d5e66114733ba3cff25867b9f89ef607f5755b0ad964a914e8b43f94df508be6998d0 + languageName: node + linkType: hard + +"@wallet-standard/base@npm:^1.1.0": + version: 1.1.0 + resolution: "@wallet-standard/base@npm:1.1.0" + checksum: 10c0/4cae344d5a74ba4b7d063b649b191f2267bd11ea9573ebb9e78874163c03b58e3ec531bb296d0a8d7941bc09231761d97afb4c6ca8c0dc399c81d39884b4e408 + languageName: node + linkType: hard + +"@walletconnect/core@npm:2.22.4": + version: 2.22.4 + resolution: "@walletconnect/core@npm:2.22.4" + dependencies: + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/jsonrpc-ws-connection": "npm:1.0.16" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:3.0.0" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.1.0" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.22.4" + "@walletconnect/utils": "npm:2.22.4" + "@walletconnect/window-getters": "npm:1.0.1" + es-toolkit: "npm:1.39.3" + events: "npm:3.3.0" + uint8arrays: "npm:3.1.1" + checksum: 10c0/e4d98b0845988e4618ea29711e1cf4cdbee13b4cfc81658d31dc93ff922a4ae2249e20f76eb4c8dac65806be08288ffe2679699a623f5aba753053b62b11976d + languageName: node + linkType: hard + +"@walletconnect/environment@npm:^1.0.1": + version: 1.0.1 + resolution: "@walletconnect/environment@npm:1.0.1" + dependencies: + tslib: "npm:1.14.1" + checksum: 10c0/08eacce6452950a17f4209c443bd4db6bf7bddfc860593bdbd49edda9d08821696dee79e5617a954fbe90ff32c1d1f1691ef0c77455ed3e4201b328856a5e2f7 + languageName: node + linkType: hard + +"@walletconnect/events@npm:1.0.1, @walletconnect/events@npm:^1.0.1": + version: 1.0.1 + resolution: "@walletconnect/events@npm:1.0.1" + dependencies: + keyvaluestorage-interface: "npm:^1.0.0" + tslib: "npm:1.14.1" + checksum: 10c0/919a97e1dacf7096aefe07af810362cfc190533a576dcfa21387295d825a3c3d5f90bedee73235b1b343f5c696f242d7bffc5ea3359d3833541349ca23f50df8 + languageName: node + linkType: hard + +"@walletconnect/heartbeat@npm:1.2.2": + version: 1.2.2 + resolution: "@walletconnect/heartbeat@npm:1.2.2" + dependencies: + "@walletconnect/events": "npm:^1.0.1" + "@walletconnect/time": "npm:^1.0.2" + events: "npm:^3.3.0" + checksum: 10c0/a97b07764c397fe3cd26e8ea4233ecc8a26049624df7edc05290d286266bc5ba1de740d12c50dc1b7e8605198c5974e34e2d5318087bd4e9db246e7b273f4592 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-provider@npm:1.0.14": + version: 1.0.14 + resolution: "@walletconnect/jsonrpc-provider@npm:1.0.14" + dependencies: + "@walletconnect/jsonrpc-utils": "npm:^1.0.8" + "@walletconnect/safe-json": "npm:^1.0.2" + events: "npm:^3.3.0" + checksum: 10c0/9801bd516d81e92977b6add213da91e0e4a7a5915ad22685a4d2a733bab6199e9053485b76340cd724c7faa17a1b0eb842696247944fd57fb581488a2e1bed75 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-types@npm:1.0.4, @walletconnect/jsonrpc-types@npm:^1.0.2, @walletconnect/jsonrpc-types@npm:^1.0.3": + version: 1.0.4 + resolution: "@walletconnect/jsonrpc-types@npm:1.0.4" + dependencies: + events: "npm:^3.3.0" + keyvaluestorage-interface: "npm:^1.0.0" + checksum: 10c0/752978685b0596a4ba02e1b689d23873e464460e4f376c97ef63e6b3ab273658ca062de2bfcaa8a498d31db0c98be98c8bbfbe5142b256a4b3ef425e1707f353 + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-utils@npm:1.0.8, @walletconnect/jsonrpc-utils@npm:^1.0.6, @walletconnect/jsonrpc-utils@npm:^1.0.8": + version: 1.0.8 + resolution: "@walletconnect/jsonrpc-utils@npm:1.0.8" + dependencies: + "@walletconnect/environment": "npm:^1.0.1" + "@walletconnect/jsonrpc-types": "npm:^1.0.3" + tslib: "npm:1.14.1" + checksum: 10c0/e4a6bd801cf555bca775e03d961d1fe5ad0a22838e3496adda43ab4020a73d1b38de7096c06940e51f00fccccc734cd422fe4f1f7a8682302467b9c4d2a93d5d + languageName: node + linkType: hard + +"@walletconnect/jsonrpc-ws-connection@npm:1.0.16": + version: 1.0.16 + resolution: "@walletconnect/jsonrpc-ws-connection@npm:1.0.16" + dependencies: + "@walletconnect/jsonrpc-utils": "npm:^1.0.6" + "@walletconnect/safe-json": "npm:^1.0.2" + events: "npm:^3.3.0" + ws: "npm:^7.5.1" + checksum: 10c0/30a09d24ffb6b4b291e2d1263504c4ea6c6797c992f5e6eb8033e58bd24749c80fd4e5ba6ffaadb28f8ced0c6b131213195b616f8983bb9f56aa7c91e83e6218 + languageName: node + linkType: hard + +"@walletconnect/keyvaluestorage@npm:1.1.1": + version: 1.1.1 + resolution: "@walletconnect/keyvaluestorage@npm:1.1.1" + dependencies: + "@walletconnect/safe-json": "npm:^1.0.1" + idb-keyval: "npm:^6.2.1" + unstorage: "npm:^1.9.0" + peerDependencies: + "@react-native-async-storage/async-storage": 1.x + peerDependenciesMeta: + "@react-native-async-storage/async-storage": + optional: true + checksum: 10c0/de2ec39d09ce99370865f7d7235b93c42b3e4fd3406bdbc644329eff7faea2722618aa88ffc4ee7d20b1d6806a8331261b65568187494cbbcceeedbe79dc30e8 + languageName: node + linkType: hard + +"@walletconnect/logger@npm:3.0.0": + version: 3.0.0 + resolution: "@walletconnect/logger@npm:3.0.0" + dependencies: + "@walletconnect/safe-json": "npm:^1.0.2" + pino: "npm:10.0.0" + checksum: 10c0/d8666a3074ed1d2b3afd04b76990e6552ed230381949c19dd19115a5e306314e4aede3492b1f715a3f9e49f45269d2ab58cc6f3de101ad4367ea8b617a23233b + languageName: node + linkType: hard + +"@walletconnect/react-native-compat@npm:^2.22.4": + version: 2.22.4 + resolution: "@walletconnect/react-native-compat@npm:2.22.4" + dependencies: + events: "npm:3.3.0" + fast-text-encoding: "npm:1.0.6" + react-native-url-polyfill: "npm:2.0.0" + peerDependencies: + "@react-native-async-storage/async-storage": "*" + "@react-native-community/netinfo": "*" + expo-application: "*" + react-native: "*" + react-native-get-random-values: "*" + peerDependenciesMeta: + expo-application: + optional: true + checksum: 10c0/dd536da58a6a2e2a9a52fc67c4220322417cb5170657bf614f5f03c43d2391d0ed063c5184e0e633545b692c1c749720e16bbb60648e431f2fec7997145983fb + languageName: node + linkType: hard + +"@walletconnect/relay-api@npm:1.0.11": + version: 1.0.11 + resolution: "@walletconnect/relay-api@npm:1.0.11" + dependencies: + "@walletconnect/jsonrpc-types": "npm:^1.0.2" + checksum: 10c0/2595d7e68d3a93e7735e0b6204811762898b0ce1466e811d78be5bcec7ac1cde5381637615a99104099165bf63695da5ef9381d6ded29924a57a71b10712a91d + languageName: node + linkType: hard + +"@walletconnect/relay-auth@npm:1.1.0": + version: 1.1.0 + resolution: "@walletconnect/relay-auth@npm:1.1.0" + dependencies: + "@noble/curves": "npm:1.8.0" + "@noble/hashes": "npm:1.7.0" + "@walletconnect/safe-json": "npm:^1.0.1" + "@walletconnect/time": "npm:^1.0.2" + uint8arrays: "npm:^3.0.0" + checksum: 10c0/29eb41ce8e70d581a3a8c8f771a70d2775d6feca548ac7ea85a792471d865a6d63be02f7deb1591056299abc2f77e1a7b5e7a0c7f95f0e48cd62e783047cee46 + languageName: node + linkType: hard + +"@walletconnect/safe-json@npm:1.0.2, @walletconnect/safe-json@npm:^1.0.1, @walletconnect/safe-json@npm:^1.0.2": + version: 1.0.2 + resolution: "@walletconnect/safe-json@npm:1.0.2" + dependencies: + tslib: "npm:1.14.1" + checksum: 10c0/8689072018c1ff7ab58eca67bd6f06b53702738d8183d67bfe6ed220aeac804e41901b8ee0fb14299e83c70093fafb90a90992202d128d53b2832bb01b591752 + languageName: node + linkType: hard + +"@walletconnect/sign-client@npm:^2.21.8": + version: 2.22.4 + resolution: "@walletconnect/sign-client@npm:2.22.4" + dependencies: + "@walletconnect/core": "npm:2.22.4" + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/logger": "npm:3.0.0" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.22.4" + "@walletconnect/utils": "npm:2.22.4" + events: "npm:3.3.0" + checksum: 10c0/4fe41594f06ad8226d87e6b643c1a356f3c4f173988b317b74e5d504c375937e0a6029f30e13152e728cb43c431f4e5d1c4043f080e579f4e3072791ceccbc83 + languageName: node + linkType: hard + +"@walletconnect/time@npm:1.0.2, @walletconnect/time@npm:^1.0.2": + version: 1.0.2 + resolution: "@walletconnect/time@npm:1.0.2" + dependencies: + tslib: "npm:1.14.1" + checksum: 10c0/6317f93086e36daa3383cab4a8579c7d0bed665fb0f8e9016575200314e9ba5e61468f66142a7bb5b8489bb4c9250196576d90a60b6b00e0e856b5d0ab6ba474 + languageName: node + linkType: hard + +"@walletconnect/types@npm:2.22.4, @walletconnect/types@npm:^2.21.8": + version: 2.22.4 + resolution: "@walletconnect/types@npm:2.22.4" + dependencies: + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:3.0.0" + events: "npm:3.3.0" + checksum: 10c0/546b25aa116ea073c3e4b82fa3a3edd92e1c431ceff008d183054a1d63f912d2c5ddb2d2af497f7df2ab5801dc20b1ebae4f8ab65e39f7e89d41e65c9dda7c2d + languageName: node + linkType: hard + +"@walletconnect/utils@npm:2.22.4": + version: 2.22.4 + resolution: "@walletconnect/utils@npm:2.22.4" + dependencies: + "@msgpack/msgpack": "npm:3.1.2" + "@noble/ciphers": "npm:1.3.0" + "@noble/curves": "npm:1.9.7" + "@noble/hashes": "npm:1.8.0" + "@scure/base": "npm:1.2.6" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:3.0.0" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.1.0" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.22.4" + "@walletconnect/window-getters": "npm:1.0.1" + "@walletconnect/window-metadata": "npm:1.0.1" + blakejs: "npm:1.2.1" + bs58: "npm:6.0.0" + detect-browser: "npm:5.3.0" + ox: "npm:0.9.3" + uint8arrays: "npm:3.1.1" + checksum: 10c0/1b29d591bf4473cbb4561bb99e55836d818d71e827c4022075f6c624e9e45118894fa898ead7fa131677f346448f474e10512e40ccbb15c2acaaed90f0fa4e6b + languageName: node + linkType: hard + +"@walletconnect/window-getters@npm:1.0.1, @walletconnect/window-getters@npm:^1.0.1": + version: 1.0.1 + resolution: "@walletconnect/window-getters@npm:1.0.1" + dependencies: + tslib: "npm:1.14.1" + checksum: 10c0/c3aedba77aa9274b8277c4189ec992a0a6000377e95656443b3872ca5b5fe77dd91170b1695027fc524dc20362ce89605d277569a0d9a5bedc841cdaf14c95df + languageName: node + linkType: hard + +"@walletconnect/window-metadata@npm:1.0.1": + version: 1.0.1 + resolution: "@walletconnect/window-metadata@npm:1.0.1" + dependencies: + "@walletconnect/window-getters": "npm:^1.0.1" + tslib: "npm:1.14.1" + checksum: 10c0/f190e9bed77282d8ba868a4895f4d813e135f9bbecb8dd4aed988ab1b06992f78128ac19d7d073cf41d8a6a74d0c055cd725908ce0a894649fd25443ad934cf4 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1": version: 1.14.1 resolution: "@webassemblyjs/ast@npm:1.14.1" @@ -13216,16 +13673,46 @@ __metadata: languageName: node linkType: hard -"abort-controller@npm:^3.0.0": - version: 3.0.0 - resolution: "abort-controller@npm:3.0.0" - dependencies: - event-target-shim: "npm:^5.0.0" - checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5 - languageName: node - linkType: hard - -"accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.7, accepts@npm:~1.3.8": +"abitype@npm:1.1.0": + version: 1.1.0 + resolution: "abitype@npm:1.1.0" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: 10c0/99218d442951c60324fcd96a372c30d71ca8d5434cab62b95d5d80bae89e3024a445a90db323ef1fe4da0d749d86e815ca555a37719b06e6ca03ccad2116c45b + languageName: node + linkType: hard + +"abitype@npm:^1.0.9": + version: 1.1.1 + resolution: "abitype@npm:1.1.1" + peerDependencies: + typescript: ">=5.0.4" + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + checksum: 10c0/d52fd8195cb37cdb462ba4d1817dafdba8da403eeab50f144f251748d7458a43308ee29ea46889db2969c91c074780e6d1f00f86acd22dc5772570432ee56b9c + languageName: node + linkType: hard + +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5 + languageName: node + linkType: hard + +"accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.7, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -13596,7 +14083,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": +"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -13991,6 +14478,13 @@ __metadata: languageName: node linkType: hard +"atomic-sleep@npm:^1.0.0": + version: 1.0.0 + resolution: "atomic-sleep@npm:1.0.0" + checksum: 10c0/e329a6665512736a9bbb073e1761b4ec102f7926cce35037753146a9db9c8104f5044c1662e4a863576ce544fb8be27cd2be6bc8c1a40147d03f31eb1cfb6e8a + languageName: node + linkType: hard + "autoprefixer@npm:10.4.21": version: 10.4.21 resolution: "autoprefixer@npm:10.4.21" @@ -14018,6 +14512,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.13.2": + version: 1.13.2 + resolution: "axios@npm:1.13.2" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10c0/e8a42e37e5568ae9c7a28c348db0e8cf3e43d06fcbef73f0048669edfe4f71219664da7b6cc991b0c0f01c28a48f037c515263cb79be1f1ae8ff034cd813867b + languageName: node + linkType: hard + "axios@npm:^1.5.1, axios@npm:^1.6.2, axios@npm:^1.7.2": version: 1.12.2 resolution: "axios@npm:1.12.2" @@ -14320,6 +14825,13 @@ __metadata: languageName: node linkType: hard +"base-x@npm:^5.0.0": + version: 5.0.1 + resolution: "base-x@npm:5.0.1" + checksum: 10c0/4ab6b02262b4fd499b147656f63ce7328bd5f895450401ce58a2f9e87828aea507cf0c320a6d8725389f86e8a48397562661c0bca28ef3276a22821b30f7a713 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -14457,7 +14969,7 @@ __metadata: languageName: node linkType: hard -"blakejs@npm:^1.1.0": +"blakejs@npm:1.2.1, blakejs@npm:^1.1.0": version: 1.2.1 resolution: "blakejs@npm:1.2.1" checksum: 10c0/c284557ce55b9c70203f59d381f1b85372ef08ee616a90162174d1291a45d3e5e809fdf9edab6e998740012538515152471dc4f1f9dbfa974ba2b9c1f7b9aad7 @@ -14546,6 +15058,13 @@ __metadata: languageName: node linkType: hard +"borsh@npm:2.0.0": + version: 2.0.0 + resolution: "borsh@npm:2.0.0" + checksum: 10c0/59a96dd9c707450862198510fc518dff92ac7f0ed0d228c9b38affd968bb4debec805c6e2afed8ec13efd9fad63fd47f8e6ed420253542a8d10fb59f28fc7d01 + languageName: node + linkType: hard + "boxen@npm:^5.1.2": version: 5.1.2 resolution: "boxen@npm:5.1.2" @@ -14633,6 +15152,15 @@ __metadata: languageName: node linkType: hard +"bs58@npm:6.0.0, bs58@npm:^6.0.0": + version: 6.0.0 + resolution: "bs58@npm:6.0.0" + dependencies: + base-x: "npm:^5.0.0" + checksum: 10c0/61910839746625ee4f69369f80e2634e2123726caaa1da6b3bcefcf7efcd9bdca86603360fed9664ffdabe0038c51e542c02581c72ca8d44f60329fe1a6bc8f4 + languageName: node + linkType: hard + "bs58@npm:^4.0.0": version: 4.0.1 resolution: "bs58@npm:4.0.1" @@ -14642,6 +15170,16 @@ __metadata: languageName: node linkType: hard +"bs58check@npm:4.0.0": + version: 4.0.0 + resolution: "bs58check@npm:4.0.0" + dependencies: + "@noble/hashes": "npm:^1.2.0" + bs58: "npm:^6.0.0" + checksum: 10c0/a4e695202711daffa157ada2044bb55ff21adcfe22c92ede12111d55570e170dd4cb8cd058db12980dca6bd51733f17f7534cddc19ea1f7dfa9852583f888eea + languageName: node + linkType: hard + "bs58check@npm:^2.1.2": version: 2.1.2 resolution: "bs58check@npm:2.1.2" @@ -14683,23 +15221,23 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.5.0": - version: 5.7.1 - resolution: "buffer@npm:5.7.1" +"buffer@npm:6.0.3, buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" dependencies: base64-js: "npm:^1.3.1" - ieee754: "npm:^1.1.13" - checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e + ieee754: "npm:^1.2.1" + checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 languageName: node linkType: hard -"buffer@npm:^6.0.3": - version: 6.0.3 - resolution: "buffer@npm:6.0.3" +"buffer@npm:^5.4.3, buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" dependencies: base64-js: "npm:^1.3.1" - ieee754: "npm:^1.2.1" - checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 + ieee754: "npm:^1.1.13" + checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e languageName: node linkType: hard @@ -14892,6 +15430,13 @@ __metadata: languageName: node linkType: hard +"cbor-js@npm:0.1.0": + version: 0.1.0 + resolution: "cbor-js@npm:0.1.0" + checksum: 10c0/1204d0eba63ef41546f622175663fad91c681cc9e7cb4e3f09e8be4081b7ecc0ce07b3f2a83004124bdb74c1824693eebf50991e092f6ca7a4bfcf1bd93a785d + languageName: node + linkType: hard + "cbor@npm:^8.1.0": version: 8.1.0 resolution: "cbor@npm:8.1.0" @@ -15804,6 +16349,13 @@ __metadata: languageName: node linkType: hard +"cookie-es@npm:^1.2.2": + version: 1.2.2 + resolution: "cookie-es@npm:1.2.2" + checksum: 10c0/210eb67cd40a53986fda99d6f47118cfc45a69c4abc03490d15ab1b83ac978d5518356aecdd7a7a4969292445e3063c2302deda4c73706a67edc008127608638 + languageName: node + linkType: hard + "cookie-signature@npm:1.0.6": version: 1.0.6 resolution: "cookie-signature@npm:1.0.6" @@ -16013,6 +16565,15 @@ __metadata: languageName: node linkType: hard +"crossws@npm:^0.3.5": + version: 0.3.5 + resolution: "crossws@npm:0.3.5" + dependencies: + uncrypto: "npm:^0.1.3" + checksum: 10c0/9e873546f0806606c4f775219f6811768fc3b3b0765ca8230722e849058ad098318af006e1faa39a8008c03009c37c519f6bccad41b0d78586237585c75fb38b + languageName: node + linkType: hard + "crypt@npm:>= 0.0.1": version: 0.0.2 resolution: "crypt@npm:0.0.2" @@ -16484,6 +17045,13 @@ __metadata: languageName: node linkType: hard +"defu@npm:^6.1.4": + version: 6.1.4 + resolution: "defu@npm:6.1.4" + checksum: 10c0/2d6cc366262dc0cb8096e429368e44052fdf43ed48e53ad84cc7c9407f890301aa5fcb80d0995abaaf842b3949f154d060be4160f7a46cb2bc2f7726c81526f5 + languageName: node + linkType: hard + "degenerator@npm:^5.0.0": version: 5.0.1 resolution: "degenerator@npm:5.0.1" @@ -16530,6 +17098,13 @@ __metadata: languageName: node linkType: hard +"destr@npm:^2.0.3, destr@npm:^2.0.5": + version: 2.0.5 + resolution: "destr@npm:2.0.5" + checksum: 10c0/efabffe7312a45ad90d79975376be958c50069f1156b94c181199763a7f971e113bd92227c26b94a169c71ca7dbc13583b7e96e5164743969fc79e1ff153e646 + languageName: node + linkType: hard + "destroy@npm:1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -16537,6 +17112,13 @@ __metadata: languageName: node linkType: hard +"detect-browser@npm:5.3.0": + version: 5.3.0 + resolution: "detect-browser@npm:5.3.0" + checksum: 10c0/88d49b70ce3836e7971345b2ebdd486ad0d457d1e4f066540d0c12f9210c8f731ccbed955fcc9af2f048f5d4629702a8e46bedf5bcad42ad49a3a0927bfd5a76 + languageName: node + linkType: hard + "detect-libc@npm:^1.0.3": version: 1.0.3 resolution: "detect-libc@npm:1.0.3" @@ -17224,6 +17806,18 @@ __metadata: languageName: node linkType: hard +"es-toolkit@npm:1.39.3": + version: 1.39.3 + resolution: "es-toolkit@npm:1.39.3" + dependenciesMeta: + "@trivago/prettier-plugin-sort-imports@4.3.0": + unplugged: true + prettier-plugin-sort-re-exports@0.0.1: + unplugged: true + checksum: 10c0/1c85e518b1d129d38fdc5796af353f45e8dcb8a20968ff25da1ae1749fc4a36f914570fcd992df33b47c7bca9f3866d53e4e6fa6411c21eb424e99a3e479c96e + languageName: node + linkType: hard + "es-toolkit@npm:^1.39.7": version: 1.40.0 resolution: "es-toolkit@npm:1.40.0" @@ -18340,7 +18934,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^6.11.0, ethers@npm:^6.12.1, ethers@npm:^6.13.5, ethers@npm:^6.14.0, ethers@npm:^6.14.4": +"ethers@npm:^6.10.0, ethers@npm:^6.11.0, ethers@npm:^6.12.1, ethers@npm:^6.13.5, ethers@npm:^6.14.0, ethers@npm:^6.14.4": version: 6.15.0 resolution: "ethers@npm:6.15.0" dependencies: @@ -18372,6 +18966,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:5.0.1, eventemitter3@npm:^5.0.1": + version: 5.0.1 + resolution: "eventemitter3@npm:5.0.1" + checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.0": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -18379,13 +18980,6 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^5.0.1": - version: 5.0.1 - resolution: "eventemitter3@npm:5.0.1" - checksum: 10c0/4ba5c00c506e6c786b4d6262cfbce90ddc14c10d4667e5c83ae993c9de88aa856033994dd2b35b83e8dc1170e224e66a319fa80adc4c32adcd2379bbc75da814 - languageName: node - linkType: hard - "events-universal@npm:^1.0.0": version: 1.0.1 resolution: "events-universal@npm:1.0.1" @@ -18395,7 +18989,7 @@ __metadata: languageName: node linkType: hard -"events@npm:^3.2.0": +"events@npm:3.3.0, events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 @@ -18487,6 +19081,15 @@ __metadata: languageName: node linkType: hard +"expo-application@npm:^7.0.7": + version: 7.0.7 + resolution: "expo-application@npm:7.0.7" + peerDependencies: + expo: "*" + checksum: 10c0/70d6647b3a5055fbde16235dc050cb6381b70e6566f3d78a30a9c1dae11b669ce417433446bd44f827ea4a6a4b61feb23a9b5dd409d7ddb2c50cef60e785d657 + languageName: node + linkType: hard + "expo-modules-core@npm:^2.2.1": version: 2.5.0 resolution: "expo-modules-core@npm:2.5.0" @@ -18665,6 +19268,13 @@ __metadata: languageName: node linkType: hard +"fast-text-encoding@npm:1.0.6": + version: 1.0.6 + resolution: "fast-text-encoding@npm:1.0.6" + checksum: 10c0/e1d0381bda229c92c7906f63308f3b9caca8c78b732768b1ee16f560089ed21bc159bbe1434138ccd3815931ec8d4785bdade1ad1c45accfdf27ac6606ac67d2 + languageName: node + linkType: hard + "fast-uri@npm:^3.0.1": version: 3.1.0 resolution: "fast-uri@npm:3.1.0" @@ -19807,6 +20417,23 @@ __metadata: languageName: node linkType: hard +"h3@npm:^1.15.4": + version: 1.15.4 + resolution: "h3@npm:1.15.4" + dependencies: + cookie-es: "npm:^1.2.2" + crossws: "npm:^0.3.5" + defu: "npm:^6.1.4" + destr: "npm:^2.0.5" + iron-webcrypto: "npm:^1.2.1" + node-mock-http: "npm:^1.0.2" + radix3: "npm:^1.1.2" + ufo: "npm:^1.6.1" + uncrypto: "npm:^0.1.3" + checksum: 10c0/5182a722d01fe18af5cb62441aaa872b630f4e1ac2cf1782e1f442e65fdfddb85eb6723bf73a96184c2dc1f1e3771d713ef47c456a9a4e92c640b025ba91044c + languageName: node + linkType: hard + "handle-thing@npm:^2.0.0": version: 2.0.1 resolution: "handle-thing@npm:2.0.1" @@ -20414,6 +21041,13 @@ __metadata: languageName: node linkType: hard +"idb-keyval@npm:^6.2.1": + version: 6.2.2 + resolution: "idb-keyval@npm:6.2.2" + checksum: 10c0/b52f0d2937cc2ec9f1da536b0b5c0875af3043ca210714beaffead4ec1f44f2ad322220305fd024596203855224d9e3523aed83e971dfb62ddc21b5b1721aeef + languageName: node + linkType: hard + "ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" @@ -20661,6 +21295,13 @@ __metadata: languageName: node linkType: hard +"iron-webcrypto@npm:^1.2.1": + version: 1.2.1 + resolution: "iron-webcrypto@npm:1.2.1" + checksum: 10c0/5cf27c6e2bd3ef3b4970e486235fd82491ab8229e2ed0ac23307c28d6c80d721772a86ed4e9fe2a5cabadd710c2f024b706843b40561fb83f15afee58f809f66 + languageName: node + linkType: hard + "is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1": version: 1.2.0 resolution: "is-arguments@npm:1.2.0" @@ -21207,6 +21848,15 @@ __metadata: languageName: node linkType: hard +"isows@npm:1.0.7": + version: 1.0.7 + resolution: "isows@npm:1.0.7" + peerDependencies: + ws: "*" + checksum: 10c0/43c41fe89c7c07258d0be3825f87e12da8ac9023c5b5ae6741ec00b2b8169675c04331ea73ef8c172d37a6747066f4dc93947b17cd369f92828a3b3e741afbda + languageName: node + linkType: hard + "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" @@ -22246,6 +22896,13 @@ __metadata: languageName: node linkType: hard +"jwt-decode@npm:4.0.0": + version: 4.0.0 + resolution: "jwt-decode@npm:4.0.0" + checksum: 10c0/de75bbf89220746c388cf6a7b71e56080437b77d2edb29bae1c2155048b02c6b8c59a3e5e8d6ccdfd54f0b8bda25226e491a4f1b55ac5f8da04cfbadec4e546c + languageName: node + linkType: hard + "karma-source-map-support@npm:1.4.0": version: 1.4.0 resolution: "karma-source-map-support@npm:1.4.0" @@ -22276,6 +22933,13 @@ __metadata: languageName: node linkType: hard +"keyvaluestorage-interface@npm:^1.0.0": + version: 1.0.0 + resolution: "keyvaluestorage-interface@npm:1.0.0" + checksum: 10c0/0e028ebeda79a4e48c7e36708dbe7ced233c7a1f1bc925e506f150dd2ce43178bee8d20361c445bd915569709d9dc9ea80063b4d3c3cf5d615ab43aa31d3ec3d + languageName: node + linkType: hard + "kind-of@npm:^5.0.0": version: 5.1.0 resolution: "kind-of@npm:5.1.0" @@ -24022,6 +24686,13 @@ __metadata: languageName: node linkType: hard +"multiformats@npm:^9.4.2": + version: 9.9.0 + resolution: "multiformats@npm:9.9.0" + checksum: 10c0/1fdb34fd2fb085142665e8bd402570659b50a5fae5994027e1df3add9e1ce1283ed1e0c2584a5c63ac0a58e871b8ee9665c4a99ca36ce71032617449d48aa975 + languageName: node + linkType: hard + "mute-stream@npm:^2.0.0": version: 2.0.0 resolution: "mute-stream@npm:2.0.0" @@ -24297,6 +24968,13 @@ __metadata: languageName: node linkType: hard +"node-fetch-native@npm:^1.6.4, node-fetch-native@npm:^1.6.7": + version: 1.6.7 + resolution: "node-fetch-native@npm:1.6.7" + checksum: 10c0/8b748300fb053d21ca4d3db9c3ff52593d5e8f8a2d9fe90cbfad159676e324b954fdaefab46aeca007b5b9edab3d150021c4846444e4e8ab1f4e44cd3807be87 + languageName: node + linkType: hard + "node-fetch@npm:^2.2.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -24376,6 +25054,13 @@ __metadata: languageName: node linkType: hard +"node-mock-http@npm:^1.0.2": + version: 1.0.3 + resolution: "node-mock-http@npm:1.0.3" + checksum: 10c0/663f2a13518fc89b0dc69f96ba4442b5d1ecbbf20a833283725c8d2d92286af1b634803822432985be5999317fd5f23edbf2a62335fe6dd38d6b19dd7b107559 + languageName: node + linkType: hard + "node-releases@npm:^2.0.21": version: 2.0.23 resolution: "node-releases@npm:2.0.23" @@ -24699,6 +25384,24 @@ __metadata: languageName: node linkType: hard +"ofetch@npm:^1.4.1": + version: 1.4.1 + resolution: "ofetch@npm:1.4.1" + dependencies: + destr: "npm:^2.0.3" + node-fetch-native: "npm:^1.6.4" + ufo: "npm:^1.5.4" + checksum: 10c0/fd712e84058ad5058a5880fe805e9bb1c2084fb7f9c54afa99a2c7e84065589b4312fa6e2dcca4432865e44ad1ec13fcd055c1bf7977ced838577a45689a04fa + languageName: node + linkType: hard + +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10c0/faea2e1c9d696ecee919026c32be8d6a633a7ac1240b3b87e944a380e8a11dc9c95c4a1f8fb0568de7ab8db3823e790f12bda45296b1d111e341aad3922a0570 + languageName: node + linkType: hard + "on-finished@npm:2.4.1, on-finished@npm:^2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -24912,6 +25615,48 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.9.3": + version: 0.9.3 + resolution: "ox@npm:0.9.3" + dependencies: + "@adraffy/ens-normalize": "npm:^1.11.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/curves": "npm:1.9.1" + "@noble/hashes": "npm:^1.8.0" + "@scure/bip32": "npm:^1.7.0" + "@scure/bip39": "npm:^1.6.0" + abitype: "npm:^1.0.9" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/e04f8f5d6de4fbc65d18e8388bbb4c6a09e7b69e6d51c45985bd2ed01414fda154a78dfb8fcd46e53842794a10ef37fff2b4d8b786bd7a7476a3772e8cc8e64a + languageName: node + linkType: hard + +"ox@npm:0.9.6": + version: 0.9.6 + resolution: "ox@npm:0.9.6" + dependencies: + "@adraffy/ens-normalize": "npm:^1.11.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/curves": "npm:1.9.1" + "@noble/hashes": "npm:^1.8.0" + "@scure/bip32": "npm:^1.7.0" + "@scure/bip39": "npm:^1.6.0" + abitype: "npm:^1.0.9" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/559b39051f80a25352e1ca6e7aba6e04f60c4e29f98e4ef3ec0c8d2b0432d400004ce09d2991200eaf21745179af47367dc28c553da43403dd0b69c2453ebabe + languageName: node + linkType: hard + "oxc-resolver@npm:^11.8.3": version: 11.9.0 resolution: "oxc-resolver@npm:11.9.0" @@ -25445,6 +26190,43 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^2.0.0": + version: 2.0.0 + resolution: "pino-abstract-transport@npm:2.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10c0/02c05b8f2ffce0d7c774c8e588f61e8b77de8ccb5f8125afd4a7325c9ea0e6af7fb78168999657712ae843e4462bb70ac550dfd6284f930ee57f17f486f25a9f + languageName: node + linkType: hard + +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10c0/73e694d542e8de94445a03a98396cf383306de41fd75ecc07085d57ed7a57896198508a0dec6eefad8d701044af21eb27253ccc352586a03cf0d4a0bd25b4133 + languageName: node + linkType: hard + +"pino@npm:10.0.0": + version: 10.0.0 + resolution: "pino@npm:10.0.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^5.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + slow-redact: "npm:^0.3.0" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10c0/f95fcc51523310e9ece1822f8ef4d8e6c2b35f67eca9805fe18fdef21dfac81fa128f1ebaa3c9a11571120854391b10b3b339f2e5836f805edaf6936781c6e6f + languageName: node + linkType: hard + "pirates@npm:^4.0.1, pirates@npm:^4.0.4, pirates@npm:^4.0.6": version: 4.0.7 resolution: "pirates@npm:4.0.7" @@ -25841,6 +26623,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^5.0.0": + version: 5.0.0 + resolution: "process-warning@npm:5.0.0" + checksum: 10c0/941f48863d368ec161e0b5890ba0c6af94170078f3d6b5e915c19b36fb59edb0dc2f8e834d25e0d375a8bf368a49d490f080508842168832b93489d17843ec29 + languageName: node + linkType: hard + "process@npm:^0.11.1": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -25992,7 +26781,7 @@ __metadata: languageName: node linkType: hard -"pvtsutils@npm:^1.3.2, pvtsutils@npm:^1.3.6": +"pvtsutils@npm:^1.3.2, pvtsutils@npm:^1.3.5, pvtsutils@npm:^1.3.6": version: 1.3.6 resolution: "pvtsutils@npm:1.3.6" dependencies: @@ -26076,6 +26865,13 @@ __metadata: languageName: node linkType: hard +"quick-format-unescaped@npm:^4.0.3": + version: 4.0.4 + resolution: "quick-format-unescaped@npm:4.0.4" + checksum: 10c0/fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4 + languageName: node + linkType: hard + "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -26147,6 +26943,13 @@ __metadata: languageName: node linkType: hard +"radix3@npm:^1.1.2": + version: 1.1.2 + resolution: "radix3@npm:1.1.2" + checksum: 10c0/d4a295547f71af079868d2c2ed3814a9296ee026c5488212d58c106e6b4797c6eaec1259b46c9728913622f2240c9a944bfc8e2b3b5f6e4a5045338b1609f1e4 + languageName: node + linkType: hard + "randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -26298,6 +27101,15 @@ __metadata: languageName: node linkType: hard +"react-native-device-info@npm:^11.1.0": + version: 11.1.0 + resolution: "react-native-device-info@npm:11.1.0" + peerDependencies: + react-native: "*" + checksum: 10c0/424eb512a35264699d0f1c2f9dce2b0ca10247fbe263af37eda5563d9eb5fb333ea513c90aa58444b67d7364ac03d9a0d9cf913a0054e78ec0f2dfff22033ab0 + languageName: node + linkType: hard + "react-native-device-info@npm:^14.0.4": version: 14.1.1 resolution: "react-native-device-info@npm:14.1.1" @@ -26363,6 +27175,18 @@ __metadata: languageName: node linkType: hard +"react-native-inappbrowser-reborn@npm:^3.7.0": + version: 3.7.0 + resolution: "react-native-inappbrowser-reborn@npm:3.7.0" + dependencies: + invariant: "npm:^2.2.4" + opencollective-postinstall: "npm:^2.0.3" + peerDependencies: + react-native: ">=0.56" + checksum: 10c0/b14d564925eec2c95fc6f8931b68f9ae7c418b5ccad2494748282ee9f7460f7094eec3c6e3a6304062124277624c0f6f647f41a7950ab263b7ebb7c6b0d50d4d + languageName: node + linkType: hard + "react-native-is-edge-to-edge@npm:^1.2.1": version: 1.2.1 resolution: "react-native-is-edge-to-edge@npm:1.2.1" @@ -26445,6 +27269,16 @@ __metadata: languageName: node linkType: hard +"react-native-passkey@npm:3.3.1": + version: 3.3.1 + resolution: "react-native-passkey@npm:3.3.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/667a699f59547449d36baa622663c6e1f51bfa3dc0399a9dfb37f4080edfc9cc2649bfeb9f36ecbb49940f2e339005375f3a57d6580450c626d158e5665c56fa + languageName: node + linkType: hard + "react-native-passport-reader@npm:1.0.3": version: 1.0.3 resolution: "react-native-passport-reader@npm:1.0.3" @@ -26452,7 +27286,7 @@ __metadata: languageName: node linkType: hard -"react-native-safe-area-context@npm:5.6.1, react-native-safe-area-context@npm:^5.6.1": +"react-native-safe-area-context@npm:^5.6.1": version: 5.6.1 resolution: "react-native-safe-area-context@npm:5.6.1" peerDependencies: @@ -26536,6 +27370,42 @@ __metadata: languageName: node linkType: hard +"react-native-svg@npm:^15.14.0": + version: 15.14.0 + resolution: "react-native-svg@npm:15.14.0" + dependencies: + css-select: "npm:^5.1.0" + css-tree: "npm:^1.1.3" + warn-once: "npm:0.1.1" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/5855bee2a76313f580ac3f8c476d07bb63d1a8e5e0883154275be5a1f4224e35f2416b0ba3b03f5d4c637c3b2f9320df34ede918da27188adb6881b3b8ac96c8 + languageName: node + linkType: hard + +"react-native-url-polyfill@npm:2.0.0": + version: 2.0.0 + resolution: "react-native-url-polyfill@npm:2.0.0" + dependencies: + whatwg-url-without-unicode: "npm:8.0.0-3" + peerDependencies: + react-native: "*" + checksum: 10c0/a48e6978f5a2c9e225bb93cc5f9985a8ac0fe7d8122105bcd88ef8d8420a3b33a67ce5d2bda2b606bf5fa94af9f6d04efce354580b60900e7c967fd2ebdc42cf + languageName: node + linkType: hard + +"react-native-url-polyfill@npm:^3.0.0": + version: 3.0.0 + resolution: "react-native-url-polyfill@npm:3.0.0" + dependencies: + whatwg-url-without-unicode: "npm:8.0.0-3" + peerDependencies: + react-native: "*" + checksum: 10c0/a1e539c2a28dc48125ada8bf29f3536ee2c149e4a5e3d205858755783afafe7f871ce1de8b66cb1c4cc05e15e212c74c49e93ddde856cda63fcf660cf943522a + languageName: node + linkType: hard + "react-native-vector-icons@npm:^10.3.0": version: 10.3.0 resolution: "react-native-vector-icons@npm:10.3.0" @@ -26856,6 +27726,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10c0/23eea5623642f0477412ef8b91acd3969015a1501ed34992ada0e3af521d3c865bb2fe4cdbfec5fe4b505f6d1ef6a03e5c3652520837a8c3b53decff7e74b6a0 + languageName: node + linkType: hard + "recast@npm:^0.21.0": version: 0.21.5 resolution: "recast@npm:0.21.5" @@ -27610,6 +28487,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.3.1": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10c0/baea14971858cadd65df23894a40588ed791769db21bafb7fd7608397dbdce9c5aac60748abae9995e0fc37e15f2061980501e012cd48859740796bea2987f49 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -28037,6 +28921,13 @@ __metadata: languageName: node linkType: hard +"sha256-uint8array@npm:0.10.7, sha256-uint8array@npm:^0.10.7": + version: 0.10.7 + resolution: "sha256-uint8array@npm:0.10.7" + checksum: 10c0/b48dd49be908906d8a148ce023994e567977795f489a22a7837eede2ebab59399c8ba37d65a2b65fc43704a435e0c4add74661d4fbff31f4a07b81a35c5343ea + languageName: node + linkType: hard + "shallow-clone@npm:^1.0.0": version: 1.0.0 resolution: "shallow-clone@npm:1.0.0" @@ -28297,6 +29188,13 @@ __metadata: languageName: node linkType: hard +"slow-redact@npm:^0.3.0": + version: 0.3.2 + resolution: "slow-redact@npm:0.3.2" + checksum: 10c0/d6611e518461d918eda9a77903100e097870035c8ef8ce95eec7d7a2eafc6c0cdfc37476a1fecf9d70e0b6b36eb9d862f4ac58e931c305b3fc010939226fa803 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -28484,6 +29382,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.2.0 + resolution: "sonic-boom@npm:4.2.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10c0/ae897e6c2cd6d3cb7cdcf608bc182393b19c61c9413a85ce33ffd25891485589f39bece0db1de24381d0a38fc03d08c9862ded0c60f184f1b852f51f97af9684 + languageName: node + linkType: hard + "source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" @@ -28639,6 +29546,13 @@ __metadata: languageName: node linkType: hard +"split2@npm:^4.0.0": + version: 4.2.0 + resolution: "split2@npm:4.2.0" + checksum: 10c0/b292beb8ce9215f8c642bb68be6249c5a4c7f332fc8ecadae7be5cbdf1ea95addc95f0459ef2e7ad9d45fd1064698a097e4eb211c83e772b49bc0ee423e91534 + languageName: node + linkType: hard + "sprintf-js@npm:~1.0.2": version: 1.0.3 resolution: "sprintf-js@npm:1.0.3" @@ -29570,6 +30484,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10c0/c36118379940b77a6ef3e6f4d5dd31e97b8210c3f7b9a54eb8fe6358ab173f6d0acfaf69b9c3db024b948c0c5fd2a7df93e2e49151af02076b35ada3205ec9a6 + languageName: node + linkType: hard + "throat@npm:^5.0.0": version: 5.0.0 resolution: "throat@npm:5.0.0" @@ -29989,6 +30912,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:1.14.1, tslib@npm:^1.8.1, tslib@npm:^1.9.3": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 + languageName: node + linkType: hard + "tslib@npm:2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" @@ -30003,20 +30933,13 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.6.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.7.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.3": - version: 1.14.1 - resolution: "tslib@npm:1.14.1" - checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 - languageName: node - linkType: hard - "tsort@npm:0.0.1": version: 0.0.1 resolution: "tsort@npm:0.0.1" @@ -30093,7 +31016,7 @@ __metadata: languageName: node linkType: hard -"tsyringe@npm:^4.10.0": +"tsyringe@npm:^4.10.0, tsyringe@npm:^4.8.0": version: 4.10.0 resolution: "tsyringe@npm:4.10.0" dependencies: @@ -30372,7 +31295,7 @@ __metadata: languageName: node linkType: hard -"ufo@npm:^1.6.1": +"ufo@npm:^1.5.4, ufo@npm:^1.6.1": version: 1.6.1 resolution: "ufo@npm:1.6.1" checksum: 10c0/5a9f041e5945fba7c189d5410508cbcbefef80b253ed29aa2e1f8a2b86f4bd51af44ee18d4485e6d3468c92be9bf4a42e3a2b72dcaf27ce39ce947ec994f1e6b @@ -30388,6 +31311,15 @@ __metadata: languageName: node linkType: hard +"uint8arrays@npm:3.1.1, uint8arrays@npm:^3.0.0": + version: 3.1.1 + resolution: "uint8arrays@npm:3.1.1" + dependencies: + multiformats: "npm:^9.4.2" + checksum: 10c0/9946668e04f29b46bbb73cca3d190f63a2fbfe5452f8e6551ef4257d9d597b72da48fa895c15ef2ef772808a5335b3305f69da5f13a09f8c2924896b409565ff + languageName: node + linkType: hard + "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0" @@ -30400,6 +31332,13 @@ __metadata: languageName: node linkType: hard +"uncrypto@npm:^0.1.3": + version: 0.1.3 + resolution: "uncrypto@npm:0.1.3" + checksum: 10c0/74a29afefd76d5b77bedc983559ceb33f5bbc8dada84ff33755d1e3355da55a4e03a10e7ce717918c436b4dfafde1782e799ebaf2aadd775612b49f7b5b2998e + languageName: node + linkType: hard + "underscore@npm:1.12.1": version: 1.12.1 resolution: "underscore@npm:1.12.1" @@ -30560,6 +31499,81 @@ __metadata: languageName: node linkType: hard +"unstorage@npm:^1.9.0": + version: 1.17.1 + resolution: "unstorage@npm:1.17.1" + dependencies: + anymatch: "npm:^3.1.3" + chokidar: "npm:^4.0.3" + destr: "npm:^2.0.5" + h3: "npm:^1.15.4" + lru-cache: "npm:^10.4.3" + node-fetch-native: "npm:^1.6.7" + ofetch: "npm:^1.4.1" + ufo: "npm:^1.6.1" + peerDependencies: + "@azure/app-configuration": ^1.8.0 + "@azure/cosmos": ^4.2.0 + "@azure/data-tables": ^13.3.0 + "@azure/identity": ^4.6.0 + "@azure/keyvault-secrets": ^4.9.0 + "@azure/storage-blob": ^12.26.0 + "@capacitor/preferences": ^6.0.3 || ^7.0.0 + "@deno/kv": ">=0.9.0" + "@netlify/blobs": ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + "@planetscale/database": ^1.19.0 + "@upstash/redis": ^1.34.3 + "@vercel/blob": ">=0.27.1" + "@vercel/functions": ^2.2.12 || ^3.0.0 + "@vercel/kv": ^1.0.1 + aws4fetch: ^1.0.20 + db0: ">=0.2.1" + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + "@azure/app-configuration": + optional: true + "@azure/cosmos": + optional: true + "@azure/data-tables": + optional: true + "@azure/identity": + optional: true + "@azure/keyvault-secrets": + optional: true + "@azure/storage-blob": + optional: true + "@capacitor/preferences": + optional: true + "@deno/kv": + optional: true + "@netlify/blobs": + optional: true + "@planetscale/database": + optional: true + "@upstash/redis": + optional: true + "@vercel/blob": + optional: true + "@vercel/functions": + optional: true + "@vercel/kv": + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + checksum: 10c0/e315a0888e349f9938356c0a699a2dff5d52cf57398fbbcb07062aaf3643baf47652982d85de6557acf5dcb3a28425cd3b2f05ce851732a6e9984d18238618eb + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.1.3": version: 1.1.3 resolution: "update-browserslist-db@npm:1.1.3" @@ -30770,6 +31784,27 @@ __metadata: languageName: node linkType: hard +"viem@npm:^2.33.1": + version: 2.38.4 + resolution: "viem@npm:2.38.4" + dependencies: + "@noble/curves": "npm:1.9.1" + "@noble/hashes": "npm:1.8.0" + "@scure/bip32": "npm:1.7.0" + "@scure/bip39": "npm:1.6.0" + abitype: "npm:1.1.0" + isows: "npm:1.0.7" + ox: "npm:0.9.6" + ws: "npm:8.18.3" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/f3b7a5280306174134bfb3f10a873a911b3dc72c403af8bc211213f57462e78f09f851354ad74af87128adb8e00a923a5730d7de7316029c86a79bd60a4e58e9 + languageName: node + linkType: hard + "vite-node@npm:2.1.9": version: 2.1.9 resolution: "vite-node@npm:2.1.9" @@ -31163,6 +32198,19 @@ __metadata: languageName: node linkType: hard +"webcrypto-core@npm:^1.8.0": + version: 1.8.1 + resolution: "webcrypto-core@npm:1.8.1" + dependencies: + "@peculiar/asn1-schema": "npm:^2.3.13" + "@peculiar/json-schema": "npm:^1.1.12" + asn1js: "npm:^3.0.5" + pvtsutils: "npm:^1.3.5" + tslib: "npm:^2.7.0" + checksum: 10c0/b85a986b4f73e8505ec5eaafe8e4f1ff02574a3b655793aca91f913d02822c8b79168ad6961eaab86ae00fec00bf780ec4cef7535f64879fb866649bc2a723fa + languageName: node + linkType: hard + "webdriver-bidi-protocol@npm:0.2.11": version: 0.2.11 resolution: "webdriver-bidi-protocol@npm:0.2.11" @@ -31184,6 +32232,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^5.0.0": + version: 5.0.0 + resolution: "webidl-conversions@npm:5.0.0" + checksum: 10c0/bf31df332ed11e1114bfcae7712d9ab2c37e7faa60ba32d8fdbee785937c0b012eee235c19d2b5d84f5072db84a160e8d08dd382da7f850feec26a4f46add8ff + languageName: node + linkType: hard + "webidl-conversions@npm:^7.0.0": version: 7.0.0 resolution: "webidl-conversions@npm:7.0.0" @@ -31424,6 +32479,17 @@ __metadata: languageName: node linkType: hard +"whatwg-url-without-unicode@npm:8.0.0-3": + version: 8.0.0-3 + resolution: "whatwg-url-without-unicode@npm:8.0.0-3" + dependencies: + buffer: "npm:^5.4.3" + punycode: "npm:^2.1.1" + webidl-conversions: "npm:^5.0.0" + checksum: 10c0/c27a637ab7d01981b2e2f576fde2113b9c42247500e093d2f5ba94b515d5c86dbcf70e5cad4b21b8813185f21fa1b4846f53c79fa87995293457e28c889cc0fd + languageName: node + linkType: hard + "whatwg-url@npm:^14.0.0": version: 14.2.0 resolution: "whatwg-url@npm:14.2.0" @@ -31717,6 +32783,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.18.3, ws@npm:^8.18.0, ws@npm:^8.18.3": + version: 8.18.3 + resolution: "ws@npm:8.18.3" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53 + languageName: node + linkType: hard + "ws@npm:^6.2.3": version: 6.2.3 resolution: "ws@npm:6.2.3" @@ -31726,7 +32807,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7, ws@npm:^7.4.6, ws@npm:^7.5.10": +"ws@npm:^7, ws@npm:^7.4.6, ws@npm:^7.5.1, ws@npm:^7.5.10": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: @@ -31741,21 +32822,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.0, ws@npm:^8.18.3": - version: 8.18.3 - resolution: "ws@npm:8.18.3" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53 - languageName: node - linkType: hard - "wsl-utils@npm:^0.1.0": version: 0.1.0 resolution: "wsl-utils@npm:0.1.0"