From 633c50d6d282e136b07dfd2303984d80be99780b Mon Sep 17 00:00:00 2001 From: Come Grellard Date: Tue, 29 Oct 2024 17:04:48 +0100 Subject: [PATCH 01/25] :speech_balloon: support: LLM / LLD Update Analytics Opt In Prompt wording --- .changeset/purple-peaches-cross.md | 6 ++++++ apps/ledger-live-desktop/static/i18n/en/app.json | 10 +++++----- apps/ledger-live-mobile/src/locales/en/common.json | 10 +++++----- 3 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 .changeset/purple-peaches-cross.md diff --git a/.changeset/purple-peaches-cross.md b/.changeset/purple-peaches-cross.md new file mode 100644 index 000000000000..ba44696212f4 --- /dev/null +++ b/.changeset/purple-peaches-cross.md @@ -0,0 +1,6 @@ +--- +"ledger-live-desktop": patch +"live-mobile": patch +--- + +LLM / LLD - Update Analytics Opt In Prompt wording diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index 6ca4f50df655..0030d8503d25 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -6531,12 +6531,12 @@ "analyticsOptInPrompt": { "variantA": { "description": "Sharing your Ledger Live data helps us understand your preferences and show content that's relevant to you.", - "whatWeTrack": "With your consent, Ledger will be able to track data about how you use Ledger Live (including page visits and clicks):", + "whatWeTrack": "With your consent, Ledger will be able to collect data about how you use Ledger Live (including page visits and clicks):", "whatWeTrackList": { "1": "To measure the performance of Ledger Live and enhance both the app and your experience (Analytics).", "2": "To provide you with personalized recommendations and content tailored to your preferences, as well as to help us measure the effectiveness of our marketing campaigns (Personalization)." }, - "whatWeDoNotTrack": "Ledger will never track information regarding:", + "whatWeDoNotTrack": "Ledger will never collect information regarding:", "whatWeDoNotTrackList": { "1": "Your assets.", "2": "Your portfolio.", @@ -6578,13 +6578,13 @@ "common": { "title": "Help us improve and personalize your experience", "revokeInfo": "You can revoke your consent any time in the app settings.", - "learnMore": "Learn more about our Tracking Policy" + "learnMore": "Learn more about how we handle your data" }, "profileSettings": { "personalizedExp": "Personalized experience", "analytics": "Analytics", - "personalizedExpDesc": "Enable Ledger to track app usage data to provide personalized recommendations and content that match your preferences and to help measure the performance of our marketing campaigns.", - "analyticsDesc": "Enable Ledger to track app usage data to help measure Ledger Live’s performance and enhance both the app and your experience." + "personalizedExpDesc": "Enable Ledger to collect app usage data to provide personalized recommendations and content that match your preferences and to help measure the performance of our marketing campaigns.", + "analyticsDesc": "Enable Ledger to collect app usage data to help measure Ledger Live’s performance and enhance both the app and your experience." } }, "walletSync": { diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json index fcac11ccce4d..4f86184b6277 100644 --- a/apps/ledger-live-mobile/src/locales/en/common.json +++ b/apps/ledger-live-mobile/src/locales/en/common.json @@ -2856,9 +2856,9 @@ "developerMode": "Developer mode", "developerModeDesc": "Show developer apps in My Ledger and enable Testnet apps.", "analytics": "Analytics", - "analyticsDesc": "Enable Ledger to track app usage data to help measure Ledger Live’s performance and enhance both the app and your experience.", + "analyticsDesc": "Enable Ledger to collect app usage data to help measure Ledger Live’s performance and enhance both the app and your experience.", "personalizedRecommendations": "Personalized experience", - "personalizedRecommendationsDesc": "Enable Ledger to track app usage data to provide personalized recommendations and content that match your preferences and to help measure the performance of our marketing campaigns.", + "personalizedRecommendationsDesc": "Enable Ledger to collect app usage data to provide personalized recommendations and content that match your preferences and to help measure the performance of our marketing campaigns.", "analyticsModal": { "title": "Share analytics", "desc": "Enable analytics to help Ledger improve user experience", @@ -6789,12 +6789,12 @@ "subtitle": "Sharing your Ledger Live data helps us understand your preferences and show content that's relevant to you.", "content": { "able": { - "title": "With your consent, Ledger will be able to track data about how you use Ledger Live (including page visits and clicks):", + "title": "With your consent, Ledger will be able to collect data about how you use Ledger Live (including page visits and clicks):", "diagAndUsage": "To measure the performance of Ledger Live and enhance both the app and your experience (Analytics).", "personnalizationData": "To provide you with personalized recommendations and content tailored to your preferences, as well as to help us measure the effectiveness of our marketing campaigns (Personalization)." }, "unable": { - "title": "Ledger Live will never track information regarding:", + "title": "Ledger Live will never collect information regarding:", "adresses": "Your assets.", "balance": "Your portfolio.", "personnalInfos": "Your personal identifying information (such as your name or your email address)." @@ -6806,7 +6806,7 @@ }, "infoText": { "info": "You can revoke your consent any time in the app settings.", - "link": "Learn more about our Tracking Policy" + "link": "Learn more about how we handle your data" } } }, From 5e84c611823dd867f64e74756ecc28f52704f804 Mon Sep 17 00:00:00 2001 From: Come Grellard Date: Tue, 29 Oct 2024 17:48:43 +0100 Subject: [PATCH 02/25] :lock: support(llm): privacy info modified --- .changeset/cyan-berries-refuse.md | 5 + .../ledgerlivemobile/PrivacyInfo.xcprivacy | 105 ++++++++++++++---- 2 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 .changeset/cyan-berries-refuse.md diff --git a/.changeset/cyan-berries-refuse.md b/.changeset/cyan-berries-refuse.md new file mode 100644 index 000000000000..cda778b117bd --- /dev/null +++ b/.changeset/cyan-berries-refuse.md @@ -0,0 +1,5 @@ +--- +"live-mobile": patch +--- + +LLM - Updated iOS Privacy Info file diff --git a/apps/ledger-live-mobile/ios/ledgerlivemobile/PrivacyInfo.xcprivacy b/apps/ledger-live-mobile/ios/ledgerlivemobile/PrivacyInfo.xcprivacy index bdea2127c16a..f206435fb484 100644 --- a/apps/ledger-live-mobile/ios/ledgerlivemobile/PrivacyInfo.xcprivacy +++ b/apps/ledger-live-mobile/ios/ledgerlivemobile/PrivacyInfo.xcprivacy @@ -3,48 +3,107 @@ NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults - NSPrivacyAccessedAPITypeReasons + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeUserID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes - CA92.1 - 1C8F.1 - C56D.1 + NSPrivacyCollectedDataTypePurposeAnalytics + NSPrivacyCollectedDataTypePurposeDeveloperAdvertising + NSPrivacyCollectedDataTypePurposeProductPersonalization + NSPrivacyCollectedDataTypePurposeAppFunctionality - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryFileTimestamp - NSPrivacyAccessedAPITypeReasons + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes - C617.1 - 0A2A.1 - 3B52.1 + NSPrivacyCollectedDataTypePurposeAnalytics + NSPrivacyCollectedDataTypePurposeDeveloperAdvertising - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryDiskSpace - NSPrivacyAccessedAPITypeReasons + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeCrashData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes - E174.1 - 85F4.1 + NSPrivacyCollectedDataTypePurposeAnalytics - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategorySystemBootTime - NSPrivacyAccessedAPITypeReasons + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypePerformanceData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes - 35F9.1 + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeProductInteraction + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + NSPrivacyCollectedDataTypePurposeDeveloperAdvertising + NSPrivacyCollectedDataTypePurposeProductPersonalization + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherUsageData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + NSPrivacyCollectedDataTypePurposeDeveloperAdvertising + NSPrivacyCollectedDataTypePurposeProductPersonalization + NSPrivacyCollectedDataTypePurposeAppFunctionality - NSPrivacyCollectedDataTypes - NSPrivacyTracking + NSPrivacyTrackingDomains + From 020d2fbc8006bd9afdc9cb5040b0fa5f99f61136 Mon Sep 17 00:00:00 2001 From: "live-github-bot[bot]" <105061298+live-github-bot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:06:47 +0000 Subject: [PATCH 03/25] chore(deps): update dependency body-parser to v1.20.3 [security] --- apps/cli/package.json | 2 +- apps/ledger-live-mobile/package.json | 2 +- pnpm-lock.yaml | 41 ++++++++++++++++++++++++---- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index cf08de010816..bb7ce855fdaa 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -53,7 +53,7 @@ "asciichart": "1.5.25", "bignumber.js": "9.1.2", "bip39": "3.1.0", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "command-line-args": "5.2.1", "cors": "2.8.5", "express": "4.19.2", diff --git a/apps/ledger-live-mobile/package.json b/apps/ledger-live-mobile/package.json index a6775cb7f42a..037ab98ba701 100644 --- a/apps/ledger-live-mobile/package.json +++ b/apps/ledger-live-mobile/package.json @@ -274,7 +274,7 @@ "@types/uuid": "8.3.4", "@types/ws": "8.5.10", "babel-jest": "29.7.0", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "cors": "2.8.5", "detox": "20.26.2", "detox-allure2-adapter": "1.0.0-alpha.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cbfcedb0e61..6c19d0aff10b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,8 +205,8 @@ importers: specifier: 3.1.0 version: 3.1.0 body-parser: - specifier: 1.20.2 - version: 1.20.2 + specifier: 1.20.3 + version: 1.20.3 command-line-args: specifier: 5.2.1 version: 5.2.1 @@ -1429,8 +1429,8 @@ importers: specifier: 29.7.0 version: 29.7.0(@babel/core@7.24.3) body-parser: - specifier: 1.20.2 - version: 1.20.2 + specifier: 1.20.3 + version: 1.20.3 cors: specifier: 2.8.5 version: 2.8.5 @@ -16992,6 +16992,10 @@ packages: resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + bonjour-service@1.2.1: resolution: {integrity: sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==} @@ -26235,6 +26239,10 @@ packages: resolution: {integrity: sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==} engines: {node: '>=0.6'} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + qs@6.5.3: resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} engines: {node: '>=0.6'} @@ -35119,7 +35127,7 @@ snapshots: '@expo/metro-config': 0.10.7 '@expo/osascript': 2.0.33 '@expo/spawn-async': 1.7.2 - body-parser: 1.20.2 + body-parser: 1.20.3 chalk: 4.1.2 connect: 3.7.0 fs-extra: 9.0.0 @@ -47179,6 +47187,23 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + bonjour-service@1.2.1: dependencies: fast-deep-equal: 3.1.3 @@ -60502,6 +60527,10 @@ snapshots: dependencies: side-channel: 1.0.6 + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + qs@6.5.3: {} query-string@5.1.1: @@ -62594,7 +62623,7 @@ snapshots: servify@0.1.12: dependencies: - body-parser: 1.20.2 + body-parser: 1.20.3 cors: 2.8.5 express: 4.19.2 request: 2.88.2 From 17575afc519eb371db90f59894147411a1a0ac3c Mon Sep 17 00:00:00 2001 From: "live-github-bot[bot]" <105061298+live-github-bot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:10:05 +0000 Subject: [PATCH 04/25] chore(deps): update aws-sdk-js-v3 monorepo to v3.651.1 --- pnpm-lock.yaml | 145 +++++++++--------- tools/actions/turborepo-s3-cache/package.json | 4 +- 2 files changed, 75 insertions(+), 74 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cbfcedb0e61..b5b593d0d6f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7324,11 +7324,11 @@ importers: specifier: 6.0.0 version: 6.0.0 '@aws-sdk/client-s3': - specifier: 3.651.0 - version: 3.651.0 + specifier: 3.651.1 + version: 3.651.1 '@aws-sdk/lib-storage': - specifier: 3.651.0 - version: 3.651.0(@aws-sdk/client-s3@3.651.0) + specifier: 3.651.1 + version: 3.651.1(@aws-sdk/client-s3@3.651.1) '@types/express': specifier: 4.17.21 version: 4.17.21 @@ -7524,8 +7524,8 @@ packages: resolution: {integrity: sha512-rYBuNB7uqCO9xZc0OAwM2K6QJAo2Syt1L5OhEaf7zG7FulNMyrK6kJPg1WrvNE90tW6gUdDaTy3XsQ7lq6O7uA==} engines: {node: '>=14.0.0'} - '@aws-sdk/client-s3@3.651.0': - resolution: {integrity: sha512-37+kxxjnlOAUCb1aHpoLakW4XRG23HrkX8X3cEjxaFLQxorPUiMvfAYQEQQkYD5yggaG+5aM5GAhxkTUTqA5xw==} + '@aws-sdk/client-s3@3.651.1': + resolution: {integrity: sha512-xNm+ixNRcotyrHgjUGGEyara6kCKgDdW2EVjHBZa5T+tbmtyqezwH3UzbSDZ6MlNoLhJMfR7ozuwYTIOARoBfA==} engines: {node: '>=16.0.0'} '@aws-sdk/client-sso-oidc@3.540.0': @@ -7534,18 +7534,18 @@ packages: peerDependencies: '@aws-sdk/credential-provider-node': ^3.540.0 - '@aws-sdk/client-sso-oidc@3.650.0': - resolution: {integrity: sha512-6J7IS0f8ovhvbIAZaynOYP+jPX8344UlTjwHxjaXHgFvI8axu3+NslKtEEV5oHLhgzDvrKbinsu5lgE2n4Sqng==} + '@aws-sdk/client-sso-oidc@3.651.1': + resolution: {integrity: sha512-PKwAyTJW8pgaPIXm708haIZWBAwNycs25yNcD7OQ3NLcmgGxvrx6bSlhPEGcvwdTYwQMJsdx8ls+khlYbLqTvQ==} engines: {node: '>=16.0.0'} peerDependencies: - '@aws-sdk/client-sts': ^3.650.0 + '@aws-sdk/client-sts': ^3.651.1 '@aws-sdk/client-sso@3.540.0': resolution: {integrity: sha512-rrQZMuw4sxIo3eyAUUzPQRA336mPRnrAeSlSdVHBKZD8Fjvoy0lYry2vNhkPLpFZLso1J66KRyuIv4LzRR3v1Q==} engines: {node: '>=14.0.0'} - '@aws-sdk/client-sso@3.650.0': - resolution: {integrity: sha512-YKm14gCMChD/jlCisFlsVqB8HJujR41bl4Fup2crHwNJxhD/9LTnzwMiVVlBqlXr41Sfa6fSxczX2AMP8NM14A==} + '@aws-sdk/client-sso@3.651.1': + resolution: {integrity: sha512-Fm8PoMgiBKmmKrY6QQUGj/WW6eIiQqC1I0AiVXfO+Sqkmxcg3qex+CZBAYrTuIDnvnc/89f9N4mdL8V9DRn03Q==} engines: {node: '>=16.0.0'} '@aws-sdk/client-sts@3.540.0': @@ -7554,16 +7554,16 @@ packages: peerDependencies: '@aws-sdk/credential-provider-node': ^3.540.0 - '@aws-sdk/client-sts@3.650.0': - resolution: {integrity: sha512-ISK0ZQYA7O5/WYgslpWy956lUBudGC9d7eL0FFbiL0j50N80Gx3RUv22ezvZgxJWE0W3DqNr4CE19sPYn4Lw8g==} + '@aws-sdk/client-sts@3.651.1': + resolution: {integrity: sha512-4X2RqLqeDuVLk+Omt4X+h+Fa978Wn+zek/AM4HSPi4C5XzRBEFLRRtOQUvkETvIjbEwTYQhm0LdgzcBH4bUqIg==} engines: {node: '>=16.0.0'} '@aws-sdk/core@3.535.0': resolution: {integrity: sha512-+Yusa9HziuaEDta1UaLEtMAtmgvxdxhPn7jgfRY6PplqAqgsfa5FR83sxy5qr2q7xjQTwHtV4MjQVuOjG9JsLw==} engines: {node: '>=14.0.0'} - '@aws-sdk/core@3.649.0': - resolution: {integrity: sha512-dheG/X2y25RHE7K+TlS32kcy7TgDg1OpWV44BQRoE0OBPAWmFR1D1qjjTZ7WWrdqRPKzcnDj1qED8ncyncOX8g==} + '@aws-sdk/core@3.651.1': + resolution: {integrity: sha512-eqOq3W39K+5QTP5GAXtmP2s9B7hhM2pVz8OPe5tqob8o1xQgkwdgHerf3FoshO9bs0LDxassU/fUSz1wlwqfqg==} engines: {node: '>=16.0.0'} '@aws-sdk/credential-provider-env@3.535.0': @@ -7586,18 +7586,18 @@ packages: resolution: {integrity: sha512-igN/RbsnulIBwqXbwsWmR3srqmtbPF1dm+JteGvUY31FW65fTVvWvSr945Y/cf1UbhPmIQXntlsqESqpkhTHwg==} engines: {node: '>=14.0.0'} - '@aws-sdk/credential-provider-ini@3.650.0': - resolution: {integrity: sha512-x2M9buZxIsKuUbuDgkGHhAKYBpn0/rYdKlwuFuOhXyyAcnhvPj0lgNF2KE4ld/GF1mKr7FF/uV3G9lM6PFaYmA==} + '@aws-sdk/credential-provider-ini@3.651.1': + resolution: {integrity: sha512-yOzPC3GbwLZ8IYzke4fy70ievmunnBUni/MOXFE8c9kAIV+/RMC7IWx14nAAZm0gAcY+UtCXvBVZprFqmctfzA==} engines: {node: '>=16.0.0'} peerDependencies: - '@aws-sdk/client-sts': ^3.650.0 + '@aws-sdk/client-sts': ^3.651.1 '@aws-sdk/credential-provider-node@3.540.0': resolution: {integrity: sha512-HKQZJbLHlrHX9A0B1poiYNXIIQfy8whTjuosTCYKPDBhhUyVAQfxy/KG726j0v43IhaNPLgTGZCJve4hAsazSw==} engines: {node: '>=14.0.0'} - '@aws-sdk/credential-provider-node@3.650.0': - resolution: {integrity: sha512-uBra5YjzS/gWSekAogfqJfY6c+oKQkkou7Cjc4d/cpMNvQtF1IBdekJ7NaE1RfsDEz3uH1+Myd07YWZAJo/2Qw==} + '@aws-sdk/credential-provider-node@3.651.1': + resolution: {integrity: sha512-QKA74Qs83FTUz3jS39kBuNbLAnm6cgDqomm7XS/BkYgtUq+1lI9WL97astNIuoYvumGIS58kuIa+I3ycOA4wgw==} engines: {node: '>=16.0.0'} '@aws-sdk/credential-provider-process@3.535.0': @@ -7612,8 +7612,8 @@ packages: resolution: {integrity: sha512-tKkFqK227LF5ajc5EL6asXS32p3nkofpP8G7NRpU7zOEOQCg01KUc4JRX+ItI0T007CiN1J19yNoFqHLT/SqHg==} engines: {node: '>=14.0.0'} - '@aws-sdk/credential-provider-sso@3.650.0': - resolution: {integrity: sha512-069nkhcwximbvyGiAC6Fr2G+yrG/p1S3NQ5BZ2cMzB1hgUKo6TvgFK7nriYI4ljMQ+UWxqPwIdTqiUmn2iJmhg==} + '@aws-sdk/credential-provider-sso@3.651.1': + resolution: {integrity: sha512-7jeU+Jbn65aDaNjkjWDQcXwjNTzpYNKovkSSRmfVpP5WYiKerVS5mrfg3RiBeiArou5igCUtYcOKlRJiGRO47g==} engines: {node: '>=16.0.0'} '@aws-sdk/credential-provider-web-identity@3.540.0': @@ -7626,11 +7626,11 @@ packages: peerDependencies: '@aws-sdk/client-sts': ^3.649.0 - '@aws-sdk/lib-storage@3.651.0': - resolution: {integrity: sha512-AK41Gn/KWtOHC4X9fPjtG63FgssT+CS8CTWIhrM2k1cw4Ut+Wt8L1C4otXfyyLqTyLs/UGEFHn7ed68mGj6nAQ==} + '@aws-sdk/lib-storage@3.651.1': + resolution: {integrity: sha512-IFV7qqg9ktJAa94VD4Li1L/2MdjuskHwAo9jYYd1QZDmZb8UZG3ZrO0zzB6lc5Z4JZADscSVdUaZLSMCkz9U0g==} engines: {node: '>=16.0.0'} peerDependencies: - '@aws-sdk/client-s3': ^3.651.0 + '@aws-sdk/client-s3': ^3.651.1 '@aws-sdk/middleware-bucket-endpoint@3.535.0': resolution: {integrity: sha512-7sijlfQsc4UO9Fsl11mU26Y5f9E7g6UoNg/iJUBpC5pgvvmdBRO5UEhbB/gnqvOEPsBXyhmfzbstebq23Qdz7A==} @@ -7652,8 +7652,8 @@ packages: resolution: {integrity: sha512-rBIzldY9jjRATxICDX7t77aW6ctqmVDgnuAOgbVT5xgHftt4o7PGWKoMvl/45hYqoQgxVFnCBof9bxkqSBebVA==} engines: {node: '>=14.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.651.0': - resolution: {integrity: sha512-mGAOIjhNDcBK5+JD+W+Ky5YJL98jTNFTENJV/GiQ9t3CdqK3p02MNr/T6VwzEpzsJvJD23amogiEZeiqSQiibg==} + '@aws-sdk/middleware-flexible-checksums@3.651.1': + resolution: {integrity: sha512-cFlXSzhdRKU1vOFTIYC3HzkN7Dwwcf07rKU1sB/PrDy4ztLhGgAwvcRwj2AqErZB62C5AdN4l7peB1Iw/oSxRQ==} engines: {node: '>=16.0.0'} '@aws-sdk/middleware-host-header@3.535.0': @@ -7692,8 +7692,8 @@ packages: resolution: {integrity: sha512-/dLG/E3af6ohxkQ5GBHT8tZfuPIg6eItKxCXuulvYj0Tqgf3Mb+xTsvSkxQsJF06RS4sH7Qsg/PnB8ZfrJrXpg==} engines: {node: '>=14.0.0'} - '@aws-sdk/middleware-sdk-s3@3.649.0': - resolution: {integrity: sha512-3H8735xTAD7IxNdreT6qv2YRk4CGOGfz8ufZo5pROJYZ4N5rfcdDMvb8szMSLvQHegqS4v1DqO9nrOPgc0I2Qg==} + '@aws-sdk/middleware-sdk-s3@3.651.1': + resolution: {integrity: sha512-4BameU35aBSzrm3L/Iphc6vFLRhz6sBwgQf09mqPA2ZlX/YFqVe8HbS8wM4DG02W8A2MRTnHXRIfFoOrErp2sw==} engines: {node: '>=16.0.0'} '@aws-sdk/middleware-signing@3.535.0': @@ -7728,8 +7728,8 @@ packages: resolution: {integrity: sha512-tqCsEsEj8icW0SAh3NvyhRUq54Gz2pu4NM2tOSrFp7SO55heUUaRLSzYteNZCTOupH//AAaZvbN/UUTO/DrOog==} engines: {node: '>=14.0.0'} - '@aws-sdk/signature-v4-multi-region@3.649.0': - resolution: {integrity: sha512-feJfSHtCarFmTMZSE5k7/A+m4FrdCrmotljc/AmXArWy3wl8XFyxE5tFVW/PiUgbgeoVDN+ZLt3YYtItHfNUWQ==} + '@aws-sdk/signature-v4-multi-region@3.651.1': + resolution: {integrity: sha512-aLPCMq4c/A9DmdZLhufWOgfHN2Vgft65dB2tfbATjs6kZjusSaDFxWzjmWX3y8i2ZQ+vU0nAkkWIHFJdf+47fA==} engines: {node: '>=16.0.0'} '@aws-sdk/token-providers@3.540.0': @@ -31051,27 +31051,27 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-s3@3.651.0': + '@aws-sdk/client-s3@3.651.1': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.650.0(@aws-sdk/client-sts@3.650.0) - '@aws-sdk/client-sts': 3.650.0 - '@aws-sdk/core': 3.649.0 - '@aws-sdk/credential-provider-node': 3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))(@aws-sdk/client-sts@3.650.0) + '@aws-sdk/client-sso-oidc': 3.651.1(@aws-sdk/client-sts@3.651.1) + '@aws-sdk/client-sts': 3.651.1 + '@aws-sdk/core': 3.651.1 + '@aws-sdk/credential-provider-node': 3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1))(@aws-sdk/client-sts@3.651.1) '@aws-sdk/middleware-bucket-endpoint': 3.649.0 '@aws-sdk/middleware-expect-continue': 3.649.0 - '@aws-sdk/middleware-flexible-checksums': 3.651.0 + '@aws-sdk/middleware-flexible-checksums': 3.651.1 '@aws-sdk/middleware-host-header': 3.649.0 '@aws-sdk/middleware-location-constraint': 3.649.0 '@aws-sdk/middleware-logger': 3.649.0 '@aws-sdk/middleware-recursion-detection': 3.649.0 - '@aws-sdk/middleware-sdk-s3': 3.649.0 + '@aws-sdk/middleware-sdk-s3': 3.651.1 '@aws-sdk/middleware-ssec': 3.649.0 '@aws-sdk/middleware-user-agent': 3.649.0 '@aws-sdk/region-config-resolver': 3.649.0 - '@aws-sdk/signature-v4-multi-region': 3.649.0 + '@aws-sdk/signature-v4-multi-region': 3.651.1 '@aws-sdk/types': 3.649.0 '@aws-sdk/util-endpoints': 3.649.0 '@aws-sdk/util-user-agent-browser': 3.649.0 @@ -31159,13 +31159,13 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0)': + '@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1)': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sts': 3.650.0 - '@aws-sdk/core': 3.649.0 - '@aws-sdk/credential-provider-node': 3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))(@aws-sdk/client-sts@3.650.0) + '@aws-sdk/client-sts': 3.651.1 + '@aws-sdk/core': 3.651.1 + '@aws-sdk/credential-provider-node': 3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1))(@aws-sdk/client-sts@3.651.1) '@aws-sdk/middleware-host-header': 3.649.0 '@aws-sdk/middleware-logger': 3.649.0 '@aws-sdk/middleware-recursion-detection': 3.649.0 @@ -31247,11 +31247,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso@3.650.0': + '@aws-sdk/client-sso@3.651.1': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.649.0 + '@aws-sdk/core': 3.651.1 '@aws-sdk/middleware-host-header': 3.649.0 '@aws-sdk/middleware-logger': 3.649.0 '@aws-sdk/middleware-recursion-detection': 3.649.0 @@ -31334,13 +31334,13 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.650.0': + '@aws-sdk/client-sts@3.651.1': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.650.0(@aws-sdk/client-sts@3.650.0) - '@aws-sdk/core': 3.649.0 - '@aws-sdk/credential-provider-node': 3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))(@aws-sdk/client-sts@3.650.0) + '@aws-sdk/client-sso-oidc': 3.651.1(@aws-sdk/client-sts@3.651.1) + '@aws-sdk/core': 3.651.1 + '@aws-sdk/credential-provider-node': 3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1))(@aws-sdk/client-sts@3.651.1) '@aws-sdk/middleware-host-header': 3.649.0 '@aws-sdk/middleware-logger': 3.649.0 '@aws-sdk/middleware-recursion-detection': 3.649.0 @@ -31389,7 +31389,7 @@ snapshots: fast-xml-parser: 4.2.5 tslib: 2.6.2 - '@aws-sdk/core@3.649.0': + '@aws-sdk/core@3.651.1': dependencies: '@smithy/core': 2.4.1 '@smithy/node-config-provider': 3.1.5 @@ -31457,14 +31457,14 @@ snapshots: - '@aws-sdk/credential-provider-node' - aws-crt - '@aws-sdk/credential-provider-ini@3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))(@aws-sdk/client-sts@3.650.0)': + '@aws-sdk/credential-provider-ini@3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1))(@aws-sdk/client-sts@3.651.1)': dependencies: - '@aws-sdk/client-sts': 3.650.0 + '@aws-sdk/client-sts': 3.651.1 '@aws-sdk/credential-provider-env': 3.649.0 '@aws-sdk/credential-provider-http': 3.649.0 '@aws-sdk/credential-provider-process': 3.649.0 - '@aws-sdk/credential-provider-sso': 3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0)) - '@aws-sdk/credential-provider-web-identity': 3.649.0(@aws-sdk/client-sts@3.650.0) + '@aws-sdk/credential-provider-sso': 3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1)) + '@aws-sdk/credential-provider-web-identity': 3.649.0(@aws-sdk/client-sts@3.651.1) '@aws-sdk/types': 3.649.0 '@smithy/credential-provider-imds': 3.2.1 '@smithy/property-provider': 3.1.4 @@ -31492,14 +31492,14 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/credential-provider-node@3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))(@aws-sdk/client-sts@3.650.0)': + '@aws-sdk/credential-provider-node@3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1))(@aws-sdk/client-sts@3.651.1)': dependencies: '@aws-sdk/credential-provider-env': 3.649.0 '@aws-sdk/credential-provider-http': 3.649.0 - '@aws-sdk/credential-provider-ini': 3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))(@aws-sdk/client-sts@3.650.0) + '@aws-sdk/credential-provider-ini': 3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1))(@aws-sdk/client-sts@3.651.1) '@aws-sdk/credential-provider-process': 3.649.0 - '@aws-sdk/credential-provider-sso': 3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0)) - '@aws-sdk/credential-provider-web-identity': 3.649.0(@aws-sdk/client-sts@3.650.0) + '@aws-sdk/credential-provider-sso': 3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1)) + '@aws-sdk/credential-provider-web-identity': 3.649.0(@aws-sdk/client-sts@3.651.1) '@aws-sdk/types': 3.649.0 '@smithy/credential-provider-imds': 3.2.1 '@smithy/property-provider': 3.1.4 @@ -31540,10 +31540,10 @@ snapshots: - '@aws-sdk/credential-provider-node' - aws-crt - '@aws-sdk/credential-provider-sso@3.650.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))': + '@aws-sdk/credential-provider-sso@3.651.1(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1))': dependencies: - '@aws-sdk/client-sso': 3.650.0 - '@aws-sdk/token-providers': 3.649.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0)) + '@aws-sdk/client-sso': 3.651.1 + '@aws-sdk/token-providers': 3.649.0(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1)) '@aws-sdk/types': 3.649.0 '@smithy/property-provider': 3.1.4 '@smithy/shared-ini-file-loader': 3.1.5 @@ -31564,17 +31564,17 @@ snapshots: - '@aws-sdk/credential-provider-node' - aws-crt - '@aws-sdk/credential-provider-web-identity@3.649.0(@aws-sdk/client-sts@3.650.0)': + '@aws-sdk/credential-provider-web-identity@3.649.0(@aws-sdk/client-sts@3.651.1)': dependencies: - '@aws-sdk/client-sts': 3.650.0 + '@aws-sdk/client-sts': 3.651.1 '@aws-sdk/types': 3.649.0 '@smithy/property-provider': 3.1.4 '@smithy/types': 3.4.0 tslib: 2.6.2 - '@aws-sdk/lib-storage@3.651.0(@aws-sdk/client-s3@3.651.0)': + '@aws-sdk/lib-storage@3.651.1(@aws-sdk/client-s3@3.651.1)': dependencies: - '@aws-sdk/client-s3': 3.651.0 + '@aws-sdk/client-s3': 3.651.1 '@smithy/abort-controller': 3.1.2 '@smithy/middleware-endpoint': 3.1.1 '@smithy/smithy-client': 3.3.0 @@ -31628,7 +31628,7 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.6.2 - '@aws-sdk/middleware-flexible-checksums@3.651.0': + '@aws-sdk/middleware-flexible-checksums@3.651.1': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 @@ -31637,6 +31637,7 @@ snapshots: '@smithy/node-config-provider': 3.1.5 '@smithy/protocol-http': 4.1.1 '@smithy/types': 3.4.0 + '@smithy/util-middleware': 3.0.4 '@smithy/util-utf8': 3.0.0 tslib: 2.6.2 @@ -31704,9 +31705,9 @@ snapshots: '@smithy/util-config-provider': 2.3.0 tslib: 2.6.2 - '@aws-sdk/middleware-sdk-s3@3.649.0': + '@aws-sdk/middleware-sdk-s3@3.651.1': dependencies: - '@aws-sdk/core': 3.649.0 + '@aws-sdk/core': 3.651.1 '@aws-sdk/types': 3.649.0 '@aws-sdk/util-arn-parser': 3.568.0 '@smithy/core': 2.4.1 @@ -31786,9 +31787,9 @@ snapshots: '@smithy/types': 2.12.0 tslib: 2.6.2 - '@aws-sdk/signature-v4-multi-region@3.649.0': + '@aws-sdk/signature-v4-multi-region@3.651.1': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.649.0 + '@aws-sdk/middleware-sdk-s3': 3.651.1 '@aws-sdk/types': 3.649.0 '@smithy/protocol-http': 4.1.1 '@smithy/signature-v4': 4.1.1 @@ -31807,9 +31808,9 @@ snapshots: - '@aws-sdk/credential-provider-node' - aws-crt - '@aws-sdk/token-providers@3.649.0(@aws-sdk/client-sso-oidc@3.650.0(@aws-sdk/client-sts@3.650.0))': + '@aws-sdk/token-providers@3.649.0(@aws-sdk/client-sso-oidc@3.651.1(@aws-sdk/client-sts@3.651.1))': dependencies: - '@aws-sdk/client-sso-oidc': 3.650.0(@aws-sdk/client-sts@3.650.0) + '@aws-sdk/client-sso-oidc': 3.651.1(@aws-sdk/client-sts@3.651.1) '@aws-sdk/types': 3.649.0 '@smithy/property-provider': 3.1.4 '@smithy/shared-ini-file-loader': 3.1.5 diff --git a/tools/actions/turborepo-s3-cache/package.json b/tools/actions/turborepo-s3-cache/package.json index 5c8af2f4a02a..b491c0dc1907 100644 --- a/tools/actions/turborepo-s3-cache/package.json +++ b/tools/actions/turborepo-s3-cache/package.json @@ -14,8 +14,8 @@ "@actions/cache": "3.2.4", "@actions/core": "1.10.1", "@actions/github": "6.0.0", - "@aws-sdk/client-s3": "3.651.0", - "@aws-sdk/lib-storage": "3.651.0", + "@aws-sdk/client-s3": "3.651.1", + "@aws-sdk/lib-storage": "3.651.1", "@types/express": "4.17.21", "@types/fs-extra": "9.0.13", "express": "4.19.2", From 22f700c37f80b50c985b2e96469ea1d1567708a9 Mon Sep 17 00:00:00 2001 From: Thomas Brillard Date: Wed, 30 Oct 2024 10:11:55 +0100 Subject: [PATCH 05/25] fixed: swap history for tokens explorer link (#8227) --- .changeset/nervous-kangaroos-mix.md | 5 +++++ .../src/renderer/drawers/SwapOperationDetails/index.tsx | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/nervous-kangaroos-mix.md diff --git a/.changeset/nervous-kangaroos-mix.md b/.changeset/nervous-kangaroos-mix.md new file mode 100644 index 000000000000..730b1d01493d --- /dev/null +++ b/.changeset/nervous-kangaroos-mix.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": minor +--- + +fixed: swap history for tokens explorer link diff --git a/apps/ledger-live-desktop/src/renderer/drawers/SwapOperationDetails/index.tsx b/apps/ledger-live-desktop/src/renderer/drawers/SwapOperationDetails/index.tsx index ddfd9c564bec..e917713c05ec 100644 --- a/apps/ledger-live-desktop/src/renderer/drawers/SwapOperationDetails/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/drawers/SwapOperationDetails/index.tsx @@ -126,9 +126,9 @@ const SwapOperationDetails = ({ const theme = useTheme(); const statusColor = getStatusColor(status, theme); const { t } = useTranslation(); - const url = - fromCurrency.type === "CryptoCurrency" && - getTransactionExplorer(getDefaultExplorerView(fromCurrency), operation.hash); + const mainCurrency = + fromCurrency.type === "CryptoCurrency" ? fromCurrency : fromCurrency.parentCurrency; + const url = getTransactionExplorer(getDefaultExplorerView(mainCurrency), operation.hash); useEffect(() => { const getProvideData = async () => { From 56fa61d0b45c7d87ec762ed1c97f9b70ab0e4fce Mon Sep 17 00:00:00 2001 From: Thomas Brillard Date: Wed, 30 Oct 2024 10:12:22 +0100 Subject: [PATCH 06/25] fixed: thorswap get status send transactionId instead of LL ids (#8228) --- .changeset/ninety-moles-listen.md | 5 +++ .../src/exchange/swap/types.ts | 1 + .../exchange/swap/updateAccountSwapStatus.ts | 35 ++++++++++++++++--- 3 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 .changeset/ninety-moles-listen.md diff --git a/.changeset/ninety-moles-listen.md b/.changeset/ninety-moles-listen.md new file mode 100644 index 000000000000..97a74a88fe6e --- /dev/null +++ b/.changeset/ninety-moles-listen.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/live-common": minor +--- + +fixed: thorswap get status send transactionId instead of LL ids diff --git a/libs/ledger-live-common/src/exchange/swap/types.ts b/libs/ledger-live-common/src/exchange/swap/types.ts index 88ee733bdfee..dd051baed23e 100644 --- a/libs/ledger-live-common/src/exchange/swap/types.ts +++ b/libs/ledger-live-common/src/exchange/swap/types.ts @@ -173,6 +173,7 @@ type ValidSwapStatus = "pending" | "onhold" | "expired" | "finished" | "refunded export type SwapStatusRequest = { provider: string; swapId: string; + transactionId?: string; operationId?: string; }; export type SwapStatus = { diff --git a/libs/ledger-live-common/src/exchange/swap/updateAccountSwapStatus.ts b/libs/ledger-live-common/src/exchange/swap/updateAccountSwapStatus.ts index ceece6e8e36e..e5c86be4abd4 100644 --- a/libs/ledger-live-common/src/exchange/swap/updateAccountSwapStatus.ts +++ b/libs/ledger-live-common/src/exchange/swap/updateAccountSwapStatus.ts @@ -1,28 +1,47 @@ import { isSwapOperationPending } from "./"; import { getMultipleStatus } from "./getStatus"; -import type { SubAccount, Account, SwapOperation } from "@ledgerhq/types-live"; +import type { SubAccount, Account, SwapOperation, Operation } from "@ledgerhq/types-live"; import type { SwapStatusRequest, UpdateAccountSwapStatus } from "./types"; import { log } from "@ledgerhq/logs"; const maybeGetUpdatedSwapHistory = async ( swapHistory: SwapOperation[] | null | undefined, + operations: Operation[] | null | undefined, ): Promise => { const pendingSwapIds: SwapStatusRequest[] = []; let accountNeedsUpdating = false; let consolidatedSwapHistory: SwapOperation[] = []; + if (swapHistory) { for (const { provider, swapId, status, operationId } of swapHistory) { if (isSwapOperationPending(status)) { + const transactionId = + provider === "thorswap" + ? operations?.find(o => o.id.includes(operationId))?.hash + : undefined; pendingSwapIds.push({ provider, swapId, - ...(provider === "thorswap" && { operationId }), + transactionId, + ...(provider === "thorswap" && { operationId }), // to be removed after Thorswap is fully migrated }); } } if (pendingSwapIds.length) { - const uniquePendingSwapIdsMap = new Map(pendingSwapIds.map(item => [item.swapId, item])); + const uniquePendingSwapIdsMap = new Map(); + for (const item of pendingSwapIds) { + const existingItem = uniquePendingSwapIdsMap.get(item.swapId); + + if (!existingItem) { + uniquePendingSwapIdsMap.set(item.swapId, item); + } else { + if (item.transactionId && !existingItem.transactionId) { + uniquePendingSwapIdsMap.set(item.swapId, item); + } + } + } + const uniquePendingSwapIds = Array.from(uniquePendingSwapIdsMap.values()); if (uniquePendingSwapIds.length !== pendingSwapIds.length) { log( @@ -51,14 +70,20 @@ const maybeGetUpdatedSwapHistory = async ( }; const updateAccountSwapStatus: UpdateAccountSwapStatus = async (account: Account) => { - const swapHistoryUpdated = await maybeGetUpdatedSwapHistory(account.swapHistory); + const swapHistoryUpdated = await maybeGetUpdatedSwapHistory( + account.swapHistory, + account.operations, + ); let subAccountSwapHistoryUpdated = false; let subAccounts: SubAccount[] = []; if (account.type === "Account" && account.subAccounts?.length) { subAccounts = await Promise.all( account.subAccounts.map(async (subAccount: SubAccount): Promise => { - const updatedSwapHistory = await maybeGetUpdatedSwapHistory(subAccount.swapHistory); + const updatedSwapHistory = await maybeGetUpdatedSwapHistory( + subAccount.swapHistory, + subAccount.operations, + ); //As soon as we get one update, we will need to update the parent account if (updatedSwapHistory) subAccountSwapHistoryUpdated = true; return { From f582f1362ddd2d4339e5edee85f0a01be3dcb36e Mon Sep 17 00:00:00 2001 From: Benjamin Haramboure <105707720+bharamboure-ledger@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:25:52 +0100 Subject: [PATCH 07/25] e2e swap speculos scenarios: fine- tune error messages checks (#8222) --- .../tests/page/swap.page.ts | 29 +++++------ .../tests/specs/speculos/swap.spec.ts | 49 ++++++++++++++----- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/apps/ledger-live-desktop/tests/page/swap.page.ts b/apps/ledger-live-desktop/tests/page/swap.page.ts index 14d10fdea23b..b8332f731432 100644 --- a/apps/ledger-live-desktop/tests/page/swap.page.ts +++ b/apps/ledger-live-desktop/tests/page/swap.page.ts @@ -35,14 +35,6 @@ export class SwapPage extends AppPage { private advancedFeesSelector = this.page.getByTestId("advanced-fee-mode-selector"); private customFeeTextbox = this.page.getByTestId("currency-textbox"); - // Quote Filter Components - private centralisedQuoteFilterButton = this.page.getByTestId("centralised-quote-filter-button"); - private decentralisedQuoteFilterButton = this.page.getByTestId( - "decentralised-quote-filter-button", - ); - private floatQuoteFilterButton = this.page.getByTestId("float-quote-filter-button"); - private fixedQuoteFilterButton = this.page.getByTestId("fixed-quote-filter-button"); - // Quote Components private quoteContainer = (providerName: string, exchangeType: string) => this.page.getByTestId(`quote-container-${providerName}-${exchangeType}`); @@ -220,6 +212,8 @@ export class SwapPage extends AppPage { async fillInOriginCurrencyAmount(electronApp: ElectronApplication, amount: string) { const [, webview] = electronApp.windows(); await webview.getByTestId(this.fromAccountAmoutInput).fill(amount); + //wait for potential origin amount error to be loaded + await this.page.waitForTimeout(500); } @step("Select currency to swap to: $1") @@ -229,22 +223,23 @@ export class SwapPage extends AppPage { await this.chooseAssetDrawer.chooseFromAsset(currency); } - @step("Verify minimum swap amount error message is displayed") - async verifyMinimumSwapAmountErrorMessageIsDisplayed( + @step("Verify swap amount error message is displayed: $2") + async verifySwapAmountErrorMessageIsDisplayed( electronApp: ElectronApplication, accountToDebit: Account, + message: string | RegExp, ) { const [, webview] = electronApp.windows(); if (!accountToDebit.accountType) { - const errorMessageRegex = new RegExp( - `Minimum \\d+(\\.\\d{1,5})? ${accountToDebit.currency.ticker} needed for quotes\\.\\s*$`, - ); - const actualText = await webview.locator('span[color*="error"]').innerText(); - expect(actualText).toMatch(errorMessageRegex); - await expect(webview.getByTestId(this.numberOfQuotes)).not.toBeVisible(); + const errorSpan = await webview.locator('span[color*="error"]').textContent(); + expect(errorSpan).toMatch(message); + //that specific amount error doesn't trigger quotes + if (message instanceof RegExp) { + await expect(webview.getByTestId(this.numberOfQuotes)).not.toBeVisible(); + } } else { await expect(webview.getByTestId(this.numberOfQuotes)).toBeVisible(); - await expect(webview.locator(this.errorSpan("Not enough balance."))).toBeVisible(); + await expect(webview.locator(this.errorSpan(message))).toBeVisible(); } await expect(webview.getByTestId(`execute-button`)).not.toBeEnabled(); } diff --git a/apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts b/apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts index 0aab0bfdcae8..1dcc09800fb0 100644 --- a/apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts @@ -10,6 +10,8 @@ import { getDescription } from "tests/utils/customJsonReporter"; import { Application } from "tests/page"; import { ElectronApplication } from "@playwright/test"; +const app: AppInfos = AppInfos.EXCHANGE; + const swaps = [ { swap: new Swap( @@ -112,8 +114,6 @@ const swaps = [ }, ]; -const app: AppInfos = AppInfos.EXCHANGE; - for (const { swap, xrayTicket } of swaps) { test.describe("Swap - Accepted (without tx broadcast)", () => { test.beforeAll(async () => { @@ -308,7 +308,40 @@ for (const { swap, xrayTicket } of tooLowAmountForQuoteSwaps) { }, async ({ app, electronApp }) => { await addTmsLink(getDescription(test.info().annotations).split(", ")); - await performSwapUntilBalanceErrorMessageStep(app, electronApp, swap); + await performSwapUntilQuoteSelectionStep(app, electronApp, swap); + const errorMessage = swap.accountToDebit.accountType + ? "Not enough balance." + : new RegExp( + `Minimum \\d+(\\.\\d{1,5})? ${swap.accountToDebit.currency.ticker} needed for quotes\\.\\s*$`, + ); + await app.swap.verifySwapAmountErrorMessageIsDisplayed( + electronApp, + swap.accountToDebit, + errorMessage, + ); + //following error doesn't appear if accountToDebit has accountType erc20 + if (!swap.accountToDebit.accountType) { + await app.swap.fillInOriginCurrencyAmount(electronApp, ""); + await app.swap.fillInOriginCurrencyAmount( + electronApp, + (parseFloat(swap.amount) * 1000).toString(), + ); + await app.swap.verifySwapAmountErrorMessageIsDisplayed( + electronApp, + swap.accountToDebit, + "Not enough balance, including network fee.", + ); + await app.swap.fillInOriginCurrencyAmount(electronApp, ""); + await app.swap.fillInOriginCurrencyAmount( + electronApp, + (parseFloat(swap.amount) * 100_000_000).toString(), + ); + await app.swap.verifySwapAmountErrorMessageIsDisplayed( + electronApp, + swap.accountToDebit, + "No quotes available.", + ); + } }, ); }); @@ -319,6 +352,7 @@ async function performSwapUntilQuoteSelectionStep( electronApp: ElectronApplication, swap: Swap, ) { + //todo: remove 2 following lines after LIVE-14410 await app.layout.goToAccounts(); await app.accounts.navigateToAccountByName(swap.accountToDebit.accountName); await app.layout.goToSwap(); @@ -330,15 +364,6 @@ async function performSwapUntilQuoteSelectionStep( await app.swap.fillInOriginCurrencyAmount(electronApp, swap.amount); } -async function performSwapUntilBalanceErrorMessageStep( - app: Application, - electronApp: ElectronApplication, - swap: Swap, -) { - await performSwapUntilQuoteSelectionStep(app, electronApp, swap); - await app.swap.verifyMinimumSwapAmountErrorMessageIsDisplayed(electronApp, swap.accountToDebit); -} - async function performSwapUntilDeviceVerificationStep( app: Application, electronApp: ElectronApplication, From ed24bfd7952481adebb5385936d86f84c39d64a1 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 30 Oct 2024 11:44:22 +0100 Subject: [PATCH 08/25] =?UTF-8?q?feat(llm):=20=F0=9F=A5=85=20display=20hum?= =?UTF-8?q?an=20readable=20errors=20when=20the=20send=20flow=20fails=20(#8?= =?UTF-8?q?081)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(llm): add a Collapsible component * feat(llm): add a copy button component * feat(llm): add the SendBroadcastError screen * feat(llm): catch the send flow broadcast errors * chore: update change log * fix(llm): linting * chore(llm): import `Icons` from native-ui instead of the individual icons * chore(llm): use the LLM alias for newArch components * fix(llm): only clamp the technical error messages to 3 lines * fix(llm): lower button opacity when pressed * chore(llm): remove unecessary styles * fix(llm): use a press event on the collapsable toggle * fix(llm): show the currency info in the erorr * fix(llm): error typo * fix(llm): tx broadcast error network name * chore(llm): extract urls from `createTransactionBroadcastError` * chore(lld): extract urls from createTransactionBroadcastError --- .changeset/eighty-geckos-smash.md | 6 + apps/ledger-live-desktop/src/config/urls.ts | 7 + .../modals/Send/steps/StepConfirmation.tsx | 3 +- .../RootNavigator/SendFundsNavigator.tsx | 6 + .../RootNavigator/types/SendFundsNavigator.ts | 9 ++ .../src/const/navigation.ts | 1 + .../src/locales/en/common.json | 14 ++ .../src/logic/screenTransactionHooks.ts | 24 ++- .../newArch/components/Collapsible/index.tsx | 71 +++++++++ .../newArch/components/CopyButton/index.tsx | 58 ++++++++ .../SendFunds/07-SendBroadcastError.tsx | 138 ++++++++++++++++++ apps/ledger-live-mobile/src/utils/urls.tsx | 7 + .../src/errors/transactionBroadcastErrors.ts | 15 +- .../src/components/cta/Button/index.tsx | 2 +- 14 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 .changeset/eighty-geckos-smash.md create mode 100644 apps/ledger-live-mobile/src/newArch/components/Collapsible/index.tsx create mode 100644 apps/ledger-live-mobile/src/newArch/components/CopyButton/index.tsx create mode 100644 apps/ledger-live-mobile/src/screens/SendFunds/07-SendBroadcastError.tsx diff --git a/.changeset/eighty-geckos-smash.md b/.changeset/eighty-geckos-smash.md new file mode 100644 index 000000000000..3432442cfa65 --- /dev/null +++ b/.changeset/eighty-geckos-smash.md @@ -0,0 +1,6 @@ +--- +"live-mobile": minor +"@ledgerhq/live-common": patch +--- + +Display human readable errors when the send flow fails diff --git a/apps/ledger-live-desktop/src/config/urls.ts b/apps/ledger-live-desktop/src/config/urls.ts index 70cae363b2a4..59a8b71038e9 100644 --- a/apps/ledger-live-desktop/src/config/urls.ts +++ b/apps/ledger-live-desktop/src/config/urls.ts @@ -188,6 +188,13 @@ export const urls = { "https://shop.ledger.com?utm_source=live&utm_medium=draw&utm_campaign=ledger_sync_lns_uncompatible&utm_content=to_shop", learnMoreLedgerSync: "https://www.ledger.com/blog-ledger-sync-synchronize-your-crypto-accounts-effortless-private-and-secure", + + // Node errors + txBroadcastErrors: { + badTxns: "https://support.ledger.com/article/5129526865821-zd", + blobsLimit: "https://support.ledger.com/article/17830974229661-zd", + txnMempoolConflict: "https://support.ledger.com/article/14593285242525-zd", + }, }; export const vaultSigner = { diff --git a/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepConfirmation.tsx b/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepConfirmation.tsx index 254bec493c0d..68d7addeeb33 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepConfirmation.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/Send/steps/StepConfirmation.tsx @@ -12,6 +12,7 @@ import SuccessDisplay from "~/renderer/components/SuccessDisplay"; import { OperationDetails } from "~/renderer/drawers/OperationDetails"; import { setDrawer } from "~/renderer/drawers/Provider"; import { multiline } from "~/renderer/styles/helpers"; +import { urls } from "~/config/urls"; import { StepProps } from "../types"; import NodeError from "./Confirmation/NodeError"; import ErrorDisplay from "~/renderer/components/ErrorDisplay"; @@ -89,7 +90,7 @@ function StepConfirmation({ /> {signed ? ( + null, headerTitle: () => null }} + /> { + const currency = mainAccount.currency; + throw createTransactionBroadcastError(err, urls, { + network: currency.name, + coin: currency.ticker, + }); + }); + log( "transaction-summary", `✔️ broadcasted! optimistic operation: ${formatOperation(mainAccount)(operation)}`, @@ -282,6 +294,16 @@ export function useSignedTxHandler({ logger.critical(error as Error); } + if ( + error instanceof TransactionBroadcastError && + route.name === ScreenName.SendConnectDevice + ) { + return (navigation as StackNavigationProp<{ [key: string]: object }>).replace( + ScreenName.SendBroadcastError, + { ...route.params, error }, + ); + } + (navigation as StackNavigationProp<{ [key: string]: object }>).replace( route.name.replace("ConnectDevice", "ValidationError"), { ...route.params, error }, diff --git a/apps/ledger-live-mobile/src/newArch/components/Collapsible/index.tsx b/apps/ledger-live-mobile/src/newArch/components/Collapsible/index.tsx new file mode 100644 index 000000000000..15bc6c47dd28 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/components/Collapsible/index.tsx @@ -0,0 +1,71 @@ +import React, { memo, ReactNode, useCallback, useState } from "react"; +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + runOnJS, +} from "react-native-reanimated"; +import styled from "styled-components/native"; +import { Flex, Icons, Text } from "@ledgerhq/native-ui"; +import { FlexBoxProps } from "@ledgerhq/native-ui/lib/components/Layout/Flex/index"; + +export default memo(Collapsible); + +type Props = FlexBoxProps & { + title: ReactNode; + children: ReactNode; + collapsed?: boolean; +}; + +function Collapsible({ title, children, collapsed = false, ...titleContainerProps }: Props) { + const [isCollapsed, setIsCollapsed] = useState(collapsed); + const collapseAnimation = useSharedValue(collapsed ? 0 : 1); + + const toggleCollapsed = useCallback(() => { + const value = Math.round(collapseAnimation.value + 1) % 2; + + const { onStart, onDone }: { onStart?: () => void; onDone: () => void } = + value === 0 + ? { onDone: () => setIsCollapsed(true) } + : { onStart: () => setIsCollapsed(false), onDone: () => {} }; + + onStart?.(); + + collapseAnimation.value = withTiming(value, { duration: 200 }, finished => { + if (finished) { + runOnJS(onDone)(); + } + }); + }, [collapseAnimation]); + + const animatedChevron = useAnimatedStyle(() => ({ + transform: [{ rotate: `${collapseAnimation.value * 90}deg` }], + })); + const animateContent = useAnimatedStyle(() => ({ + maxHeight: `${collapseAnimation.value * 100}%`, + })); + + const header = typeof title === "string" ? {title} : title; + + return ( + <> + + + {header} + + + + + + + + {!isCollapsed && children} + + + ); +} + +const Toggle = styled.TouchableOpacity` + flex-direction: row; + align-items: center; +`; diff --git a/apps/ledger-live-mobile/src/newArch/components/CopyButton/index.tsx b/apps/ledger-live-mobile/src/newArch/components/CopyButton/index.tsx new file mode 100644 index 000000000000..5ac267aff549 --- /dev/null +++ b/apps/ledger-live-mobile/src/newArch/components/CopyButton/index.tsx @@ -0,0 +1,58 @@ +import Clipboard from "@react-native-clipboard/clipboard"; +import React, { memo, useCallback, useMemo } from "react"; +import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated"; +import styled from "styled-components/native"; +import { Button, Icons } from "@ledgerhq/native-ui"; +import { ButtonProps } from "@ledgerhq/native-ui/components/cta/Button"; + +export default memo(CopyButton); + +type Props = Omit & { + text: string; + transitionDuration?: number; +}; + +function CopyButton({ text, ...props }: Props) { + const transition = useSharedValue(0); + const handleCopy = useCallback(() => { + Clipboard.setString(text); + + transition.value = withTiming(1, { duration: 200 }); + setTimeout(() => (transition.value = withTiming(0, { duration: 200 })), 1200); + }, [text, transition]); + + const copyIconAnimation = useAnimatedStyle(() => ({ + opacity: 1 - transition.value, + })); + + const checkIconAnimation = useAnimatedStyle(() => ({ + opacity: transition.value, + })); + + const icon = useMemo( + () => ( + + + + + + + + + ), + [copyIconAnimation, checkIconAnimation], + ); + + return + + + ); +} + +type InformativeBannerProps = { + title: string; + description: string; + numberOfLines?: number; + children: React.ReactNode; +}; + +function InformativeBanner({ + title, + description, + numberOfLines, + children, +}: InformativeBannerProps) { + return ( + + + {title} : {description} + + {children} + + ); +} + +const InformativeBannerButton = styled(Button).attrs({ + isNewIcon: true, + iconPosition: "left", + size: "small", + activeOpacity: 0.5, +})` + background-color: ${({ theme }) => theme.colors.opacityDefault.c05}; + border-radius: 8px; +`; diff --git a/apps/ledger-live-mobile/src/utils/urls.tsx b/apps/ledger-live-mobile/src/utils/urls.tsx index ed9e3fb8d5c1..0fd754e885e0 100644 --- a/apps/ledger-live-mobile/src/utils/urls.tsx +++ b/apps/ledger-live-mobile/src/utils/urls.tsx @@ -218,4 +218,11 @@ export const urls = { learnMore: "https://support.ledger.com/article/4404389367057-zd", }, updateLedgerLive: "https://support.ledger.com/article/4410960111889-zd", + + // Node errors + txBroadcastErrors: { + badTxns: "https://support.ledger.com/article/5129526865821-zd", + blobsLimit: "https://support.ledger.com/article/17830974229661-zd", + txnMempoolConflict: "https://support.ledger.com/article/14593285242525-zd", + }, }; diff --git a/libs/ledger-live-common/src/errors/transactionBroadcastErrors.ts b/libs/ledger-live-common/src/errors/transactionBroadcastErrors.ts index 5eb24105e9ad..1d37c4e21e47 100644 --- a/libs/ledger-live-common/src/errors/transactionBroadcastErrors.ts +++ b/libs/ledger-live-common/src/errors/transactionBroadcastErrors.ts @@ -10,26 +10,31 @@ export interface TransactionBroadcastError extends Error, TxData { export const createTransactionBroadcastError = ( error: Error, + urls: { faq: string; txBroadcastErrors: Record }, data: TxData, ): TransactionBroadcastError => { - return new TransactionBroadcastError(error.message, { url: url(error.message), ...data }); + return new TransactionBroadcastError(error.message, { + url: url(error.message, urls.txBroadcastErrors) ?? urls.faq, + ...data, + }); }; -function url(message: string) { +function url(message: string, urls: Record) { if ( message.includes("-25: bad-tnxs-inputs-missingorspent") || message.includes("-25: Missing inputs") ) { - return "https://support.ledger.com/article/5129526865821-zd"; + return urls.badTxns; } if (message.includes("blobs limit in txpool is full")) { - return "https://support.ledger.com/article/17830974229661-zd"; + return urls.blobsLimit; } if (message.includes("txn-mempool-conflict")) { - return "https://support.ledger.com/article/14593285242525-zd"; + return urls.txnMempoolConflict; } } type TxData = { coin?: string; network?: string }; +type SpecificErrors = "badTxns" | "blobsLimit" | "txnMempoolConflict"; diff --git a/libs/ui/packages/native/src/components/cta/Button/index.tsx b/libs/ui/packages/native/src/components/cta/Button/index.tsx index 8dc25543548b..ed0637729981 100644 --- a/libs/ui/packages/native/src/components/cta/Button/index.tsx +++ b/libs/ui/packages/native/src/components/cta/Button/index.tsx @@ -181,10 +181,10 @@ const Button = (props: ButtonProps): React.ReactElement => { return ( From 732d0d094f64caf36e9223cb22e828192dbe1aa1 Mon Sep 17 00:00:00 2001 From: Benjamin Haramboure <105707720+bharamboure-ledger@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:07:46 +0100 Subject: [PATCH 09/25] fix: swap e2e scenarios target appropriate STG env test url (#8250) --- apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts b/apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts index 1dcc09800fb0..ba908a500063 100644 --- a/apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/speculos/swap.spec.ts @@ -118,7 +118,7 @@ for (const { swap, xrayTicket } of swaps) { test.describe("Swap - Accepted (without tx broadcast)", () => { test.beforeAll(async () => { process.env.SWAP_DISABLE_APPS_INSTALL = "true"; - process.env.SWAP_API_BASE = "https://swap-stg.ledger.com/v5"; + process.env.SWAP_API_BASE = "https://swap-stg.ledger-test.com/v5"; process.env.DISABLE_TRANSACTION_BROADCAST = "true"; }); @@ -183,7 +183,7 @@ for (const { swap, xrayTicket } of rejectedSwaps) { test.describe("Swap - Rejected on device", () => { test.beforeAll(async () => { process.env.SWAP_DISABLE_APPS_INSTALL = "true"; - process.env.SWAP_API_BASE = "https://swap-stg.ledger.com/v5"; + process.env.SWAP_API_BASE = "https://swap-stg.ledger-test.com/v5"; }); const accPair: string[] = [swap.accountToDebit, swap.accountToCredit].map(acc => @@ -273,7 +273,7 @@ for (const { swap, xrayTicket } of tooLowAmountForQuoteSwaps) { test.describe("Swap - with too low amount (throwing UI errors)", () => { test.beforeAll(async () => { process.env.SWAP_DISABLE_APPS_INSTALL = "true"; - process.env.SWAP_API_BASE = "https://swap-stg.ledger.com/v5"; + process.env.SWAP_API_BASE = "https://swap-stg.ledger-test.com/v5"; }); const accPair: string[] = [swap.accountToDebit, swap.accountToCredit].map(acc => From 96094ea41a9a7f82500e1cb91feea103cb5b4438 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Wed, 30 Oct 2024 13:56:21 +0100 Subject: [PATCH 10/25] =?UTF-8?q?feat(llm):=20=F0=9F=93=9D=20add=20Stellar?= =?UTF-8?q?=20memo=20input=20on=20the=20recipient=20selection=20step=20(#8?= =?UTF-8?q?178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(llm): add a custom memo input in the recepient step of the stellar send flow * fix(llm): do not reset the memo type to null when updating the recipient * feat(llm): validate stellar memo on recipient step * chore: update change log * chore(llm): replace the deprecated LText by Text from ledgerhq/react-ui * chore(llm): add stellar/bridge/logic to unimported * chore(llm): code improvements Co-authored-by: Martin CAYUELAS <112866305+mcayuelas-ledger@users.noreply.github.com> --------- Co-authored-by: Martin CAYUELAS <112866305+mcayuelas-ledger@users.noreply.github.com> --- .changeset/chilled-points-argue.md | 7 ++ .../src/families/stellar/MemoTagInput.tsx | 65 ++++++++++++++++++ .../src/families/stellar/MemoTypeDrawer.tsx | 68 +++++++++++++++++++ .../features/MemoTag/hooks/useMemoTagInput.ts | 6 +- .../src/newArch/features/MemoTag/types.ts | 2 +- .../screens/SendFunds/02-SelectRecipient.tsx | 10 ++- .../coin-stellar/src/types/bridge.ts | 8 ++- libs/ledger-live-common/.unimportedrc.json | 1 + .../src/families/stellar/bridge/logic.ts | 1 + .../src/families/stellar/bridge/mock.ts | 4 -- 10 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 .changeset/chilled-points-argue.md create mode 100644 apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx create mode 100644 apps/ledger-live-mobile/src/families/stellar/MemoTypeDrawer.tsx create mode 100644 libs/ledger-live-common/src/families/stellar/bridge/logic.ts diff --git a/.changeset/chilled-points-argue.md b/.changeset/chilled-points-argue.md new file mode 100644 index 000000000000..71b02c5b50a4 --- /dev/null +++ b/.changeset/chilled-points-argue.md @@ -0,0 +1,7 @@ +--- +"live-mobile": minor +"@ledgerhq/coin-stellar": patch +"@ledgerhq/live-common": patch +--- + +Add Stellar memo input on the recipient selection step diff --git a/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx new file mode 100644 index 000000000000..6c58457e5910 --- /dev/null +++ b/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx @@ -0,0 +1,65 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { isMemoValid } from "@ledgerhq/live-common/families/stellar/bridge/logic"; +import { + StellarWrongMemoFormat, + type Transaction as StellarTransaction, +} from "@ledgerhq/live-common/families/stellar/types"; +import { AnimatedInputSelect } from "@ledgerhq/native-ui"; +import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; +import { MemoTypeDrawer, MEMO_TYPES } from "./MemoTypeDrawer"; + +export default ({ onChange }: MemoTagInputProps) => { + const { t } = useTranslation(); + + const [memoType, setMemoType] = useState("NO_MEMO"); + const [memoValue, setMemoValue] = React.useState(""); + const [isOpen, setIsOpen] = useState(false); + + const handleChange = (type: MemoType, value: string) => { + const error = isMemoValid(type, value) ? undefined : new StellarWrongMemoFormat(); + const patch = { memoType: type, memoValue: value }; + onChange({ value, patch, error }); + }; + + const handleChangeType = (type: MemoType) => { + const value = type === "NO_MEMO" ? "" : memoValue; + handleChange(type, value); + + setMemoType(type); + if (value !== memoValue) setMemoValue(value); + setIsOpen(false); + }; + + const handleChangeValue = (value: string) => { + const type = memoType === "NO_MEMO" && value ? "MEMO_TEXT" : memoType; + handleChange(type, value); + + setMemoValue(value); + if (type !== memoType) setMemoType(type); + }; + + return ( + <> + setIsOpen(true), + }} + /> + + setIsOpen(false)} + value={memoType} + onChange={handleChangeType} + /> + + ); +}; + +type MemoType = Parameters<(typeof MEMO_TYPES)["get"]>[0]; diff --git a/apps/ledger-live-mobile/src/families/stellar/MemoTypeDrawer.tsx b/apps/ledger-live-mobile/src/families/stellar/MemoTypeDrawer.tsx new file mode 100644 index 000000000000..f6168494ef34 --- /dev/null +++ b/apps/ledger-live-mobile/src/families/stellar/MemoTypeDrawer.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { TouchableOpacity, TouchableOpacityProps } from "react-native"; + +import type { StellarMemoType } from "@ledgerhq/live-common/families/stellar/types"; +import { Icons, Text } from "@ledgerhq/native-ui"; +import Circle from "~/components/Circle"; +import QueuedDrawer from "~/components/QueuedDrawer"; + +export const MEMO_TYPES = new Map([ + ["NO_MEMO", "stellar.memoType.NO_MEMO"], + ["MEMO_TEXT", "stellar.memoType.MEMO_TEXT"], + ["MEMO_ID", "stellar.memoType.MEMO_ID"], + ["MEMO_HASH", "stellar.memoType.MEMO_HASH"], + ["MEMO_RETURN", "stellar.memoType.MEMO_RETURN"], +]); + +type Props = { + isOpen: boolean; + closeModal: () => void; + value: MemoType; + onChange: (value: MemoType) => void; +}; + +export function MemoTypeDrawer({ isOpen, closeModal, value, onChange }: Props) { + const { t } = useTranslation(); + return ( + + {t("send.summary.memo.type")} + + } + isRequestingToBeOpened={isOpen} + onClose={closeModal} + > + {Array.from(MEMO_TYPES).map(([type, label]) => ( + + ); +} + +type OptionProps = TouchableOpacityProps & { label: string; selected?: boolean }; +function Option({ label, selected = false, onPress }: OptionProps) { + return ( + + + {label} + + {selected && ( + + + + )} + + ); +} + +type MemoType = (typeof StellarMemoType)[number]; diff --git a/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts b/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts index 5e4daf1b278e..17c816609869 100644 --- a/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts +++ b/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts @@ -18,13 +18,15 @@ export const useMemoTagInput = ( null; const [isEmpty, setIsEmpty] = useState(true); + const [error, setError] = useState(); const handleChange = useCallback( - ({ patch, value }) => { + ({ patch, value, error }) => { setIsEmpty(!value); + setError(error); updateTransaction(patch); }, [updateTransaction], ); - return Input && { Input, isEmpty, handleChange }; + return Input && { Input, isEmpty, error, handleChange }; }; diff --git a/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts b/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts index 9687b43d5631..7fbbecc42c43 100644 --- a/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts +++ b/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts @@ -4,4 +4,4 @@ import type { AnimatedInputProps } from "@ledgerhq/native-ui/components/Form/Inp export type MemoTagInputProps = Omit< AnimatedInputProps, "value" | "onChangeText" | "onChange" -> & { onChange: (update: { patch: Partial; value: string }) => void }; +> & { onChange: (update: { patch: Partial; value: string; error?: Error }) => void }; diff --git a/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx b/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx index 42f9e64b5b3d..d42000658c03 100644 --- a/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx +++ b/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx @@ -1,5 +1,6 @@ import { isConfirmedOperation } from "@ledgerhq/coin-framework/operation"; import { RecipientRequired } from "@ledgerhq/errors"; +import { Text } from "@ledgerhq/native-ui"; import { getAccountCurrency, getMainAccount } from "@ledgerhq/live-common/account/helpers"; import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; import { @@ -27,11 +28,11 @@ import CancelButton from "~/components/CancelButton"; import { EditOperationCard } from "~/components/EditOperationCard"; import GenericErrorBottomModal from "~/components/GenericErrorBottomModal"; import KeyboardView from "~/components/KeyboardView"; -import LText from "~/components/LText"; import NavigationScrollView from "~/components/NavigationScrollView"; import RetryButton from "~/components/RetryButton"; import { SendFundsNavigatorStackParamList } from "~/components/RootNavigator/types/SendFundsNavigator"; import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; +import TranslatedError from "~/components/TranslatedError"; import { ScreenName } from "~/const"; import { accountScreenSelector } from "~/reducers/accounts"; import { currencySettingsForAccountSelector } from "~/reducers/settings"; @@ -277,7 +278,7 @@ export default function SendSelectRecipient({ navigation, route }: Props) { }, ]} /> - {} + {t("common.or")} + + + )} @@ -337,7 +341,7 @@ export default function SendSelectRecipient({ navigation, route }: Props) { testID="recipient-continue-button" type="primary" title={} - disabled={debouncedBridgePending || !!status.errors.recipient} + disabled={debouncedBridgePending || !!status.errors.recipient || memoTag?.error} pending={debouncedBridgePending} onPress={onPressContinue} /> diff --git a/libs/coin-modules/coin-stellar/src/types/bridge.ts b/libs/coin-modules/coin-stellar/src/types/bridge.ts index ed0a5f21c03b..93b5487ba362 100644 --- a/libs/coin-modules/coin-stellar/src/types/bridge.ts +++ b/libs/coin-modules/coin-stellar/src/types/bridge.ts @@ -32,7 +32,13 @@ export enum NetworkCongestionLevel { HIGH = "HIGH", } -export const StellarMemoType = ["NO_MEMO", "MEMO_TEXT", "MEMO_ID", "MEMO_HASH", "MEMO_RETURN"]; +export const StellarMemoType = [ + "NO_MEMO", + "MEMO_TEXT", + "MEMO_ID", + "MEMO_HASH", + "MEMO_RETURN", +] as const; export type StellarTransactionMode = "send" | "changeTrust"; diff --git a/libs/ledger-live-common/.unimportedrc.json b/libs/ledger-live-common/.unimportedrc.json index 534d9060f42d..58226330923f 100644 --- a/libs/ledger-live-common/.unimportedrc.json +++ b/libs/ledger-live-common/.unimportedrc.json @@ -309,6 +309,7 @@ "src/exchange/swap/const/blockchain.ts", "src/families/cardano/logic.ts", "src/families/cardano/staking.ts", + "src/families/stellar/bridge/logic.ts", "src/families/stellar/logic.ts", "src/families/tezos/logic.ts", "src/families/tezos/react.ts", diff --git a/libs/ledger-live-common/src/families/stellar/bridge/logic.ts b/libs/ledger-live-common/src/families/stellar/bridge/logic.ts new file mode 100644 index 000000000000..81f4484f6401 --- /dev/null +++ b/libs/ledger-live-common/src/families/stellar/bridge/logic.ts @@ -0,0 +1 @@ +export * from "@ledgerhq/coin-stellar/bridge/logic"; diff --git a/libs/ledger-live-common/src/families/stellar/bridge/mock.ts b/libs/ledger-live-common/src/families/stellar/bridge/mock.ts index 87346e99678a..8aed62809ca9 100644 --- a/libs/ledger-live-common/src/families/stellar/bridge/mock.ts +++ b/libs/ledger-live-common/src/families/stellar/bridge/mock.ts @@ -59,10 +59,6 @@ const createTransaction = (): Transaction => ({ }); const updateTransaction = (t, patch) => { - if ("recipient" in patch && patch.recipient !== t.recipient) { - return { ...t, ...patch, memoType: null }; - } - return { ...t, ...patch }; }; From 95b726150b3aa96cfa470483a6069d0fb225b8bd Mon Sep 17 00:00:00 2001 From: Kevin Le Seigle Date: Wed, 30 Oct 2024 14:38:27 +0100 Subject: [PATCH 11/25] fix: overflow issues in add account flow --- .changeset/lazy-mayflies-talk.md | 5 ++++ .../src/renderer/components/CurrencyBadge.tsx | 4 +-- .../renderer/components/SelectCurrency.tsx | 28 ++++++++++++++++--- 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 .changeset/lazy-mayflies-talk.md diff --git a/.changeset/lazy-mayflies-talk.md b/.changeset/lazy-mayflies-talk.md new file mode 100644 index 000000000000..aaae8aa28a7d --- /dev/null +++ b/.changeset/lazy-mayflies-talk.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": minor +--- + +Fix overflow issues in Add account flow diff --git a/apps/ledger-live-desktop/src/renderer/components/CurrencyBadge.tsx b/apps/ledger-live-desktop/src/renderer/components/CurrencyBadge.tsx index 12923e71f26f..6a4695d8020d 100644 --- a/apps/ledger-live-desktop/src/renderer/components/CurrencyBadge.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/CurrencyBadge.tsx @@ -88,9 +88,9 @@ export function CurrencyCircleIcon({ } function CurrencyBadge({ currency }: { currency: CryptoCurrency | TokenCurrency }) { return ( - + - + ({ + color: "palette.text.shade100", + ff: "Inter|SemiBold", + fontSize: 4, +}))` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; +`; + const CurrencyLabel = styled(Text).attrs(() => ({ color: "palette.text.shade60", ff: "Inter|SemiBold", @@ -179,9 +199,9 @@ export function CurrencyOption({ const isParentTagDisplayed = !hideParentTag && (currency as TokenCurrency).parentCurrency; const textContents = singleLineLayout ? ( <> - + {`${currency.name} (${currency.ticker})`} - + {isParentTagDisplayed ? ( {(currency as TokenCurrency).parentCurrency.name} ) : null} @@ -207,10 +227,10 @@ export function CurrencyOption({ ); return ( - + {textContents} - + ); } const renderOption = ({ data }: { data: C }) => ( From 42e27f229ee2c1302258b6cc27d165c7b638cf3b Mon Sep 17 00:00:00 2001 From: jiyuzhuang Date: Wed, 23 Oct 2024 11:29:03 +0200 Subject: [PATCH 12/25] feat(llc): add recoverUpsellRedirection feature flag --- .changeset/slow-buses-end.md | 6 ++++++ libs/ledger-live-common/src/featureFlags/defaultFeatures.ts | 1 + libs/ledgerjs/packages/types-live/src/feature.ts | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 .changeset/slow-buses-end.md diff --git a/.changeset/slow-buses-end.md b/.changeset/slow-buses-end.md new file mode 100644 index 000000000000..d49e20e30a09 --- /dev/null +++ b/.changeset/slow-buses-end.md @@ -0,0 +1,6 @@ +--- +"@ledgerhq/types-live": patch +"@ledgerhq/live-common": patch +--- + +Add recoverUpsellRedirection feature flag diff --git a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts index 1821b3bbbedc..c155af2a60bd 100644 --- a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts +++ b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts @@ -512,6 +512,7 @@ export const DEFAULT_FEATURES: Features = { warningVisible: true, }, }, + recoverUpsellRedirection: DEFAULT_FEATURE, }; // Firebase SDK treat JSON values as strings diff --git a/libs/ledgerjs/packages/types-live/src/feature.ts b/libs/ledgerjs/packages/types-live/src/feature.ts index 408842b58745..5833a3680011 100644 --- a/libs/ledgerjs/packages/types-live/src/feature.ts +++ b/libs/ledgerjs/packages/types-live/src/feature.ts @@ -194,6 +194,7 @@ export type Features = CurrencyFeatures & { llmMemoTag: Feature_MemoTag; lldMemoTag: Feature_MemoTag; ldmkTransport: Feature_LdmkTransport; + recoverUpsellRedirection: Feature_RecoverUpsellRedirection; }; /** @@ -554,6 +555,7 @@ export type Feature_lldNftsGalleryNewArch = DefaultFeature; export type Feature_lldnewArchOrdinals = DefaultFeature; export type Feature_SpamFilteringTx = DefaultFeature; export type Feature_MemoTag = DefaultFeature; +export type Feature_RecoverUpsellRedirection = DefaultFeature; /** * Utils types. From 739ec4f7c8946b72de969ff134eb02e880bfa4b0 Mon Sep 17 00:00:00 2001 From: jiyuzhuang Date: Wed, 23 Oct 2024 16:48:59 +0200 Subject: [PATCH 13/25] feat(llm): use new recover upsell redirection --- .changeset/green-rice-cheer.md | 5 ++ .../SyncOnboardingCompanion.tsx | 56 ++++++++++++------- 2 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 .changeset/green-rice-cheer.md diff --git a/.changeset/green-rice-cheer.md b/.changeset/green-rice-cheer.md new file mode 100644 index 000000000000..cf14d04f993f --- /dev/null +++ b/.changeset/green-rice-cheer.md @@ -0,0 +1,5 @@ +--- +"live-mobile": patch +--- + +Hide Backup section during onboarding on Flex and Stax diff --git a/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx b/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx index 13a2e30b11c5..79458d1e7f0c 100644 --- a/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx +++ b/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx @@ -148,6 +148,7 @@ export const SyncOnboardingCompanion: React.FC = ( const { t } = useTranslation(); const dispatchRedux = useDispatch(); const deviceInitialApps = useFeature("deviceInitialApps"); + const recoverUpsellRedirection = useFeature("recoverUpsellRedirection"); const productName = getDeviceModel(device.modelId).productName || device.modelId; const deviceName = device.deviceName || productName; @@ -369,17 +370,22 @@ export const SyncOnboardingCompanion: React.FC = ( if (deviceOnboardingState?.isOnboarded && !seededDeviceHandled.current) { if (deviceOnboardingState?.currentOnboardingStep === DeviceOnboardingStep.Ready) { // device was just seeded - setCompanionStepKey(CompanionStepKey.Backup); + setCompanionStepKey( + recoverUpsellRedirection?.enabled ? CompanionStepKey.Apps : CompanionStepKey.Backup, + ); seededDeviceHandled.current = true; return; } else if ( deviceOnboardingState?.currentOnboardingStep === DeviceOnboardingStep.WelcomeScreen1 ) { - // switch to the apps step - __DEV__ - ? setCompanionStepKey(CompanionStepKey.Backup) // for ease of testing in dev mode without having to reset the device - : setCompanionStepKey(CompanionStepKey.Apps); - + if (recoverUpsellRedirection?.enabled) { + // switch to the apps step + __DEV__ + ? setCompanionStepKey(CompanionStepKey.Backup) // for ease of testing in dev mode without having to reset the device + : setCompanionStepKey(CompanionStepKey.Apps); + } else { + setCompanionStepKey(CompanionStepKey.Apps); + } seededDeviceHandled.current = true; return; } @@ -433,7 +439,12 @@ export const SyncOnboardingCompanion: React.FC = ( default: break; } - }, [deviceOnboardingState, notifyEarlySecurityCheckShouldReset, shouldRestoreApps]); + }, [ + deviceOnboardingState, + notifyEarlySecurityCheckShouldReset, + recoverUpsellRedirection, + shouldRestoreApps, + ]); // When the user gets close to the seed generation step, sets the lost synchronization delay // and timers to a higher value. It avoids having a warning message while the connection is lost @@ -638,17 +649,21 @@ export const SyncOnboardingCompanion: React.FC = ( ), }, - { - key: CompanionStepKey.Backup, - title: t("syncOnboarding.backup.title"), - doneTitle: t("syncOnboarding.backup.title"), - renderBody: () => ( - setCompanionStepKey(CompanionStepKey.Apps)} - /> - ), - }, + ...(recoverUpsellRedirection?.enabled + ? [] + : [ + { + key: CompanionStepKey.Backup, + title: t("syncOnboarding.backup.title"), + doneTitle: t("syncOnboarding.backup.title"), + renderBody: () => ( + setCompanionStepKey(CompanionStepKey.Apps)} + /> + ), + }, + ]), ...(deviceInitialApps?.enabled ? [ { @@ -682,13 +697,14 @@ export const SyncOnboardingCompanion: React.FC = ( [ t, productName, - seedPathStatus, + recoverUpsellRedirection?.enabled, deviceInitialApps?.enabled, device, + seedPathStatus, + shouldRestoreApps, handleInstallAppsComplete, initialAppsToInstall, companionStepKey, - shouldRestoreApps, ], ); From 039b4460cc2a2e0ce84fc1495212a8293046c050 Mon Sep 17 00:00:00 2001 From: Olivier Freyssinet Date: Tue, 29 Oct 2024 12:04:59 +0100 Subject: [PATCH 14/25] feat(llm/post onboarding): auto redirection to Recover upsell & post onboarding --- .changeset/green-rice-cheer.md | 5 +- .../src/actions/settings.ts | 6 + apps/ledger-live-mobile/src/actions/types.ts | 3 + .../useAutoRedirectToPostOnboarding/index.ts | 39 +++ .../useOpenPostOnboardingCallback.ts | 19 ++ .../useOpenProtectUpsellCallback.ts | 73 ++++++ .../useShouldRedirect.test.ts | 226 ++++++++++++++++++ .../useShouldRedirect.ts | 38 +++ .../useNavigateToPostOnboardingHubCallback.ts | 11 +- .../src/reducers/settings.ts | 10 + apps/ledger-live-mobile/src/reducers/types.ts | 1 + .../screens/MyLedgerChooseDevice/index.tsx | 10 + .../src/screens/Onboarding/steps/pairNew.tsx | 18 +- .../src/screens/Portfolio/index.tsx | 61 +---- .../PostOnboarding/PostOnboardingHub.tsx | 13 +- .../Debug/Configuration/RecoverUpsellRow.tsx | 34 +++ .../Configuration/ResetOnboardingStateRow.tsx | 10 +- .../Settings/Debug/Configuration/index.tsx | 2 + .../SyncOnboarding/CompletionScreen.tsx | 60 ++--- .../SyncOnboardingCompanion.tsx | 24 +- .../src/hooks/recoverFeatureFlag.ts | 15 ++ .../hooks/useStartPostOnboardingCallback.ts | 6 +- 22 files changed, 556 insertions(+), 128 deletions(-) create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/index.ts create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenPostOnboardingCallback.ts create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenProtectUpsellCallback.ts create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.test.ts create mode 100644 apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts create mode 100644 apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/RecoverUpsellRow.tsx diff --git a/.changeset/green-rice-cheer.md b/.changeset/green-rice-cheer.md index cf14d04f993f..028c42b9fd23 100644 --- a/.changeset/green-rice-cheer.md +++ b/.changeset/green-rice-cheer.md @@ -2,4 +2,7 @@ "live-mobile": patch --- -Hide Backup section during onboarding on Flex and Stax +Stax/Flex onboarding: + - Hide "Backup with Recover" section + - Auto redirect to Recover upsell between the onboarding and the post onboarding + diff --git a/apps/ledger-live-mobile/src/actions/settings.ts b/apps/ledger-live-mobile/src/actions/settings.ts index 3e3065bc28ed..2e01412b6e31 100755 --- a/apps/ledger-live-mobile/src/actions/settings.ts +++ b/apps/ledger-live-mobile/src/actions/settings.ts @@ -70,6 +70,7 @@ import { SettingsAddStarredMarketcoinsPayload, SettingsRemoveStarredMarketcoinsPayload, SettingsSetFromLedgerSyncOnboardingPayload, + SettingsSetHasBeenRedirectedToPostOnboardingPayload, } from "./types"; import { ImageType } from "~/components/CustomImage/types"; @@ -264,6 +265,11 @@ export const setHasBeenUpsoldProtect = createAction( + SettingsActionTypes.SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING, + ); + export const setGeneralTermsVersionAccepted = createAction( SettingsActionTypes.SET_GENERAL_TERMS_VERSION_ACCEPTED, ); diff --git a/apps/ledger-live-mobile/src/actions/types.ts b/apps/ledger-live-mobile/src/actions/types.ts index a4e20fdd3dd9..7493911df020 100644 --- a/apps/ledger-live-mobile/src/actions/types.ts +++ b/apps/ledger-live-mobile/src/actions/types.ts @@ -273,6 +273,7 @@ export enum SettingsActionTypes { SET_FEATURE_FLAGS_BANNER_VISIBLE = "SET_FEATURE_FLAGS_BANNER_VISIBLE", SET_DEBUG_APP_LEVEL_DRAWER_OPENED = "SET_DEBUG_APP_LEVEL_DRAWER_OPENED", SET_HAS_BEEN_UPSOLD_PROTECT = "SET_HAS_BEEN_UPSOLD_PROTECT", + SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING = "SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING", SET_GENERAL_TERMS_VERSION_ACCEPTED = "SET_GENERAL_TERMS_VERSION_ACCEPTED", SET_ONBOARDING_TYPE = "SET_ONBOARDING_TYPE", SET_CLOSED_NETWORK_BANNER = "SET_CLOSED_NETWORK_BANNER", @@ -379,6 +380,8 @@ export type SettingsSetDebugAppLevelDrawerOpenedPayload = SettingsState["debugAppLevelDrawerOpened"]; export type SettingsSetHasBeenUpsoldProtectPayload = SettingsState["hasBeenUpsoldProtect"]; +export type SettingsSetHasBeenRedirectedToPostOnboardingPayload = + SettingsState["hasBeenRedirectedToPostOnboarding"]; export type SettingsCompleteOnboardingPayload = void | SettingsState["hasCompletedOnboarding"]; export type SettingsSetGeneralTermsVersionAccepted = SettingsState["generalTermsVersionAccepted"]; diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/index.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/index.ts new file mode 100644 index 000000000000..7800bc38e010 --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/index.ts @@ -0,0 +1,39 @@ +import { useEffect } from "react"; +import { useSelector } from "react-redux"; +import { lastConnectedDeviceSelector } from "~/reducers/settings"; +import { useOpenPostOnboardingCallback } from "./useOpenPostOnboardingCallback"; +import { useShouldRedirect } from "./useShouldRedirect"; +import { useOpenProtectUpsellCallback } from "./useOpenProtectUpsellCallback"; +import { useIsFocused } from "@react-navigation/core"; + +/** + * Redirects the user to the post onboarding or the protect (Ledger Recover) upsell if needed + * */ +export function useAutoRedirectToPostOnboarding() { + const focused = useIsFocused(); + const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); + + const { shouldRedirectToProtectUpsell, shouldRedirectToPostOnboarding } = useShouldRedirect(); + + const openProtectUpsell = useOpenProtectUpsellCallback(); + const openPostOnboarding = useOpenPostOnboardingCallback(); + + const isFocused = useIsFocused(); + + useEffect(() => { + if (!isFocused) return; + if (shouldRedirectToProtectUpsell) { + openProtectUpsell(); + } else if (shouldRedirectToPostOnboarding && lastConnectedDevice) { + openPostOnboarding(lastConnectedDevice.modelId); + } + }, [ + lastConnectedDevice, + openPostOnboarding, + openProtectUpsell, + shouldRedirectToPostOnboarding, + shouldRedirectToProtectUpsell, + focused, + isFocused, + ]); +} diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenPostOnboardingCallback.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenPostOnboardingCallback.ts new file mode 100644 index 000000000000..770f7e9ef74c --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenPostOnboardingCallback.ts @@ -0,0 +1,19 @@ +import { useStartPostOnboardingCallback } from "@ledgerhq/live-common/postOnboarding/hooks/useStartPostOnboardingCallback"; +import { DeviceModelId } from "@ledgerhq/types-devices"; +import { useCallback } from "react"; + +/** + * Returns a callback to open the post onboarding screen + * */ +export function useOpenPostOnboardingCallback() { + const startPostOnboarding = useStartPostOnboardingCallback(); + return useCallback( + (deviceModelId: DeviceModelId) => { + startPostOnboarding({ + deviceModelId: deviceModelId, + resetNavigationStack: false, + }); + }, + [startPostOnboarding], + ); +} diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenProtectUpsellCallback.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenProtectUpsellCallback.ts new file mode 100644 index 000000000000..6e675f3aa844 --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useOpenProtectUpsellCallback.ts @@ -0,0 +1,73 @@ +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { + Source, + useAlreadyOnboardedURI, + useHomeURI, + usePostOnboardingURI, + useTouchScreenOnboardingUpsellURI, +} from "@ledgerhq/live-common/hooks/recoverFeatureFlag"; +import { DeviceModelId } from "@ledgerhq/types-devices"; +import { useIsFocused } from "@react-navigation/core"; +import { useCallback, useEffect, useState } from "react"; +import { Linking } from "react-native"; +import { useDispatch, useSelector } from "react-redux"; +import { setHasBeenUpsoldProtect } from "~/actions/settings"; +import { internetReachable } from "~/logic/internetReachable"; +import { lastConnectedDeviceSelector, onboardingTypeSelector } from "~/reducers/settings"; +import { OnboardingType } from "~/reducers/types"; + +/** + * Returns a callback to open the Protect (Ledger Recover) upsell + * */ +export function useOpenProtectUpsellCallback() { + const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); + const onboardingType = useSelector(onboardingTypeSelector); + const protectFeature = useFeature("protectServicesMobile"); + const recoverAlreadyOnboardedURI = useAlreadyOnboardedURI(protectFeature); + const recoverPostOnboardingURI = usePostOnboardingURI(protectFeature); + const touchScreenURI = useTouchScreenOnboardingUpsellURI( + protectFeature, + Source.LLM_ONBOARDING_24, + ); + const recoverHomeURI = useHomeURI(protectFeature); + const dispatch = useDispatch(); + const [redirectionStarted, setRedirectionStarted] = useState(false); + const isFocused = useIsFocused(); + + useEffect(() => { + if (redirectionStarted && !isFocused) { + dispatch(setHasBeenUpsoldProtect(true)); + } + }, [redirectionStarted, isFocused, dispatch]); + + return useCallback(async () => { + const internetConnected = await internetReachable(); + if (internetConnected && protectFeature?.enabled) { + const redirect = (url: string) => { + Linking.openURL(url); + setRedirectionStarted(true); + }; + if ( + lastConnectedDevice && + touchScreenURI && + [DeviceModelId.stax, DeviceModelId.europa].includes(lastConnectedDevice.modelId) + ) { + redirect(touchScreenURI); + } else if (recoverPostOnboardingURI && onboardingType === OnboardingType.restore) { + redirect(recoverPostOnboardingURI); + } else if (recoverHomeURI && onboardingType === OnboardingType.setupNew) { + redirect(recoverHomeURI); + } else if (recoverAlreadyOnboardedURI) { + redirect(recoverAlreadyOnboardedURI); + } + } + }, [ + lastConnectedDevice, + onboardingType, + protectFeature?.enabled, + recoverAlreadyOnboardedURI, + recoverHomeURI, + recoverPostOnboardingURI, + touchScreenURI, + ]); +} diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.test.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.test.ts new file mode 100644 index 000000000000..340411f74f8d --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.test.ts @@ -0,0 +1,226 @@ +import { Device } from "@ledgerhq/live-common/hw/actions/types"; +import { useShouldRedirect } from "./useShouldRedirect"; +import { DeviceModelId } from "@ledgerhq/types-devices"; + +jest.mock("react-redux", () => ({ + useSelector: (fn: () => void) => fn(), +})); + +jest.mock("@ledgerhq/live-common/featureFlags/index", () => ({ + useFeature: jest.fn(), +})); + +jest.mock("~/reducers/settings", () => ({ + hasBeenUpsoldProtectSelector: jest.fn(), + hasBeenRedirectedToPostOnboardingSelector: jest.fn(), + lastConnectedDeviceSelector: jest.fn(), +})); + +const { useFeature } = jest.requireMock("@ledgerhq/live-common/featureFlags/index"); +const { + hasBeenUpsoldProtectSelector, + hasBeenRedirectedToPostOnboardingSelector, + lastConnectedDeviceSelector, +} = jest.requireMock("~/reducers/settings"); + +function mockUseFeature(value: { enabled: boolean }) { + useFeature.mockReturnValue(value); +} +function mockHasBeenUpsoldProtect(value: boolean) { + hasBeenUpsoldProtectSelector.mockReturnValue(value); +} + +function mockHasRedirectedToPostOnboarding(value: boolean) { + hasBeenRedirectedToPostOnboardingSelector.mockReturnValue(value); +} + +function mockLastConnectedDevice(value: Device) { + lastConnectedDeviceSelector.mockReturnValue(value); +} + +type Scenario = { + device: { modelId: DeviceModelId }; + featureFlagEnabled: boolean; + expected: { shouldRedirectToProtectUpsell: boolean; shouldRedirectToPostOnboarding: boolean }; +}; + +function testScenarios(scenarios: Scenario[]) { + it.each(scenarios)( + "should return $expected for $device and feature flag enabled: $featureFlagEnabled", + ({ device, featureFlagEnabled, expected }) => { + mockLastConnectedDevice(device as Device); + mockUseFeature({ enabled: featureFlagEnabled }); + + const result = useShouldRedirect(); + + expect( + [result.shouldRedirectToPostOnboarding, result.shouldRedirectToProtectUpsell].filter( + Boolean, + ).length, + ).toBeLessThanOrEqual(1); + + expect(result).toEqual(expected); + }, + ); +} + +describe("useShouldRedirect", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("user HAS NOT BEEN UPSOLD protect & HAS NOT BEEN REDIRECTED to post onboarding", () => { + beforeEach(() => { + mockHasBeenUpsoldProtect(false); + mockHasRedirectedToPostOnboarding(false); + }); + + testScenarios([ + { + device: { modelId: DeviceModelId.nanoSP }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: true }, + }, + { + device: { modelId: DeviceModelId.nanoSP }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: true }, + }, + { + device: { modelId: DeviceModelId.nanoX }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.nanoX }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.stax }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: true }, + }, + { + device: { modelId: DeviceModelId.stax }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.europa }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: true }, + }, + { + device: { modelId: DeviceModelId.europa }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + ]); + }); + + describe("user HAS BEEN UPSOLD protect & HAS NOT BEEN REDIRECTED to post onboarding", () => { + beforeEach(() => { + mockHasBeenUpsoldProtect(true); + mockHasRedirectedToPostOnboarding(false); + }); + + [ + DeviceModelId.nanoS, + DeviceModelId.nanoSP, + DeviceModelId.nanoX, + DeviceModelId.stax, + DeviceModelId.europa, + ].forEach(modelId => { + [true, false].forEach(featureFlagEnabled => + testScenarios([ + { + device: { modelId }, + featureFlagEnabled, + expected: { + shouldRedirectToProtectUpsell: false, + shouldRedirectToPostOnboarding: true, + }, + }, + ]), + ); + }); + }); + + describe("user HAS BEEN UPSOLD PROTECT & HAS BEEN REDIRECTED to post onboarding", () => { + beforeEach(() => { + mockHasBeenUpsoldProtect(true); + mockHasRedirectedToPostOnboarding(true); + }); + [ + DeviceModelId.nanoS, + DeviceModelId.nanoSP, + DeviceModelId.nanoX, + DeviceModelId.stax, + DeviceModelId.europa, + ].forEach(modelId => { + [true, false].forEach(featureFlagEnabled => + testScenarios([ + { + device: { modelId }, + featureFlagEnabled, + expected: { + shouldRedirectToProtectUpsell: false, + shouldRedirectToPostOnboarding: false, + }, + }, + ]), + ); + }); + }); + + describe("user HAS NOT BEEN UPSOLD protect & HAS BEEN REDIRECTED to post onboarding", () => { + beforeEach(() => { + mockHasBeenUpsoldProtect(false); + mockHasRedirectedToPostOnboarding(true); + }); + + testScenarios([ + { + device: { modelId: DeviceModelId.nanoSP }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.nanoSP }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.nanoX }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.nanoX }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.stax }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.stax }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.europa }, + featureFlagEnabled: false, + expected: { shouldRedirectToProtectUpsell: false, shouldRedirectToPostOnboarding: false }, + }, + { + device: { modelId: DeviceModelId.europa }, + featureFlagEnabled: true, + expected: { shouldRedirectToProtectUpsell: true, shouldRedirectToPostOnboarding: false }, + }, + ]); + }); +}); diff --git a/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts new file mode 100644 index 000000000000..1fb48978c797 --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/useAutoRedirectToPostOnboarding/useShouldRedirect.ts @@ -0,0 +1,38 @@ +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { DeviceModelId } from "@ledgerhq/types-devices"; +import { useSelector } from "react-redux"; +import { + hasBeenRedirectedToPostOnboardingSelector, + hasBeenUpsoldProtectSelector, + lastConnectedDeviceSelector, +} from "~/reducers/settings"; + +/** + * Returns whether the user should be redirected to the Protect upsell or the post onboarding + * */ +export function useShouldRedirect(): { + shouldRedirectToProtectUpsell: boolean; + shouldRedirectToPostOnboarding: boolean; +} { + const hasBeenUpsoldProtect = useSelector(hasBeenUpsoldProtectSelector); + const hasRedirectedToPostOnboarding = useSelector(hasBeenRedirectedToPostOnboardingSelector); + const recoverUpsellRedirection = useFeature("recoverUpsellRedirection"); + const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); + const eligibleDevicesForUpsell = recoverUpsellRedirection?.enabled + ? [DeviceModelId.nanoX, DeviceModelId.stax, DeviceModelId.europa] + : [DeviceModelId.nanoX]; + + const eligibleForUpsell = lastConnectedDevice?.modelId + ? eligibleDevicesForUpsell.includes(lastConnectedDevice.modelId) + : false; + + const shouldRedirectToProtectUpsell = !hasBeenUpsoldProtect && eligibleForUpsell; + + const shouldRedirectToPostOnboarding = + !shouldRedirectToProtectUpsell && !hasRedirectedToPostOnboarding; + + return { + shouldRedirectToProtectUpsell, + shouldRedirectToPostOnboarding, + }; +} diff --git a/apps/ledger-live-mobile/src/logic/postOnboarding/useNavigateToPostOnboardingHubCallback.ts b/apps/ledger-live-mobile/src/logic/postOnboarding/useNavigateToPostOnboardingHubCallback.ts index ef7952266928..20fa38013395 100644 --- a/apps/ledger-live-mobile/src/logic/postOnboarding/useNavigateToPostOnboardingHubCallback.ts +++ b/apps/ledger-live-mobile/src/logic/postOnboarding/useNavigateToPostOnboardingHubCallback.ts @@ -4,11 +4,11 @@ import { RootNavigation } from "~/components/RootNavigator/types/helpers"; import { NavigatorName, ScreenName } from "~/const"; export function useNavigateToPostOnboardingHubCallback() { - const navigation = useNavigation(); + const navigation = useNavigation(); return useCallback( (resetNavigationStack?: boolean) => { if (resetNavigationStack) { - (navigation as unknown as RootNavigation).reset({ + navigation.reset({ index: 0, routes: [ { @@ -34,8 +34,11 @@ export function useNavigateToPostOnboardingHubCallback() { ], }); } else { - navigation.navigate(NavigatorName.PostOnboarding, { - screen: ScreenName.PostOnboardingHub, + navigation.navigate(NavigatorName.Base, { + screen: NavigatorName.PostOnboarding, + params: { + screen: ScreenName.PostOnboardingHub, + }, }); } }, diff --git a/apps/ledger-live-mobile/src/reducers/settings.ts b/apps/ledger-live-mobile/src/reducers/settings.ts index b48a119e8a55..cb7f88187b23 100644 --- a/apps/ledger-live-mobile/src/reducers/settings.ts +++ b/apps/ledger-live-mobile/src/reducers/settings.ts @@ -80,6 +80,7 @@ import type { SettingsAddStarredMarketcoinsPayload, SettingsRemoveStarredMarketcoinsPayload, SettingsSetFromLedgerSyncOnboardingPayload, + SettingsSetHasBeenRedirectedToPostOnboardingPayload, } from "../actions/types"; import { SettingsActionTypes, @@ -165,6 +166,7 @@ export const INITIAL_STATE: SettingsState = { debugAppLevelDrawerOpened: false, dateFormat: "default", hasBeenUpsoldProtect: false, + hasBeenRedirectedToPostOnboarding: false, onboardingType: null, depositFlow: { hasClosedNetworkBanner: false, @@ -601,6 +603,12 @@ const handlers: ReducerMap = { ...state, hasBeenUpsoldProtect: (action as Action).payload, }), + [SettingsActionTypes.SET_HAS_BEEN_REDIRECTED_TO_POST_ONBOARDING]: (state, action) => ({ + ...state, + hasBeenRedirectedToPostOnboarding: ( + action as Action + ).payload, + }), [SettingsActionTypes.SET_GENERAL_TERMS_VERSION_ACCEPTED]: (state, action) => ({ ...state, generalTermsVersionAccepted: (action as Action).payload, @@ -874,6 +882,8 @@ export const featureFlagsBannerVisibleSelector = (state: State) => export const debugAppLevelDrawerOpenedSelector = (state: State) => state.settings.debugAppLevelDrawerOpened; export const hasBeenUpsoldProtectSelector = (state: State) => state.settings.hasBeenUpsoldProtect; +export const hasBeenRedirectedToPostOnboardingSelector = (state: State) => + state.settings.hasBeenRedirectedToPostOnboarding; export const generalTermsVersionAcceptedSelector = (state: State) => state.settings.generalTermsVersionAccepted; export const userNpsSelector = (state: State) => state.settings.userNps; diff --git a/apps/ledger-live-mobile/src/reducers/types.ts b/apps/ledger-live-mobile/src/reducers/types.ts index 7dc9c9aae716..24f3584fb53b 100644 --- a/apps/ledger-live-mobile/src/reducers/types.ts +++ b/apps/ledger-live-mobile/src/reducers/types.ts @@ -255,6 +255,7 @@ export type SettingsState = { debugAppLevelDrawerOpened: boolean; dateFormat: string; hasBeenUpsoldProtect: boolean; + hasBeenRedirectedToPostOnboarding: boolean; generalTermsVersionAccepted?: string; depositFlow: { hasClosedNetworkBanner: boolean; diff --git a/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/index.tsx b/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/index.tsx index bdb76854353b..f43ab081bdda 100644 --- a/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/index.tsx +++ b/apps/ledger-live-mobile/src/screens/MyLedgerChooseDevice/index.tsx @@ -22,6 +22,7 @@ import { MyLedgerNavigatorStackParamList } from "~/components/RootNavigator/type import { useManagerDeviceAction } from "~/hooks/deviceActions"; import ContentCardsLocation from "~/dynamicContent/ContentCardsLocation"; import { ContentCardLocation } from "~/dynamicContent/types"; +import { useAutoRedirectToPostOnboarding } from "~/hooks/useAutoRedirectToPostOnboarding"; type NavigationProps = BaseComposite< StackNavigatorProps @@ -46,6 +47,15 @@ const ChooseDevice: React.FC = ({ isFocused }) => { const navigation = useNavigation(); const { params } = useRoute(); + /** + * FIXME: + * This is here because for now the Recover upsell redirect to this screen (My Ledger) + * via a deeplink, and after the Recover upsell, we are supposed to automatically redirect + * to the post-onboarding hub. + * When the Recover webpage is fixed to only redirect to the Portfolio screen, this can be removed. + */ + useAutoRedirectToPostOnboarding(); + const onSelectDevice = (device?: Device) => { if (device) track("ManagerDeviceEntered", { diff --git a/apps/ledger-live-mobile/src/screens/Onboarding/steps/pairNew.tsx b/apps/ledger-live-mobile/src/screens/Onboarding/steps/pairNew.tsx index 9655664fc439..c93d20011526 100644 --- a/apps/ledger-live-mobile/src/screens/Onboarding/steps/pairNew.tsx +++ b/apps/ledger-live-mobile/src/screens/Onboarding/steps/pairNew.tsx @@ -1,14 +1,12 @@ import React, { useCallback, useMemo, memo } from "react"; import { useNavigation, useRoute } from "@react-navigation/native"; import { useDispatch } from "react-redux"; -import { DeviceModelId } from "@ledgerhq/devices"; -import { useStartPostOnboardingCallback } from "@ledgerhq/live-common/postOnboarding/hooks/index"; import { NavigatorName, ScreenName } from "~/const"; import BaseStepperView, { PairNew, ConnectNano } from "./setupDevice/scenes"; import { TrackScreen } from "~/analytics"; import SeedWarning from "../shared/SeedWarning"; import Illustration from "~/images/illustration/Illustration"; -import { completeOnboarding } from "~/actions/settings"; +import { completeOnboarding, setHasBeenRedirectedToPostOnboarding } from "~/actions/settings"; import { useNavigationInterceptor } from "../onboardingContext"; import useNotifications from "~/logic/notifications"; import { @@ -84,8 +82,6 @@ export default memo(function () { [isProtectFlow], ); - const startPostOnboarding = useStartPostOnboardingCallback(); - const onFinish = useCallback(() => { if (next && deviceModelId) { // only used for protect for now @@ -105,21 +101,17 @@ export default memo(function () { parentNav.popToTop(); } - startPostOnboarding({ - deviceModelId: deviceModelId as DeviceModelId, - resetNavigationStack: true, - fallbackIfNoAction: () => - navigation.replace(NavigatorName.Base, { - screen: NavigatorName.Main, - }), + navigation.replace(NavigatorName.Base, { + screen: NavigatorName.Main, }); + dispatch(setHasBeenRedirectedToPostOnboarding(false)); + triggerJustFinishedOnboardingNewDevicePushNotificationModal(); }, [ dispatch, resetCurrentStep, navigation, - startPostOnboarding, deviceModelId, triggerJustFinishedOnboardingNewDevicePushNotificationModal, next, diff --git a/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx b/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx index 38d72b130a02..ffdacc790d7d 100644 --- a/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Portfolio/index.tsx @@ -1,28 +1,14 @@ -import React, { useCallback, useMemo, useState, useEffect } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { ListRenderItemInfo, Linking, Platform } from "react-native"; +import React, { useCallback, useMemo, useState } from "react"; +import { useSelector } from "react-redux"; +import { ListRenderItemInfo, Platform } from "react-native"; import { useTranslation } from "react-i18next"; import { useFocusEffect } from "@react-navigation/native"; import { Box, Flex } from "@ledgerhq/native-ui"; import { useTheme } from "styled-components/native"; import useEnv from "@ledgerhq/live-common/hooks/useEnv"; import { ReactNavigationPerformanceView } from "@shopify/react-native-performance-navigation"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import WalletTabSafeAreaView from "~/components/WalletTab/WalletTabSafeAreaView"; -import { - useAlreadyOnboardedURI, - usePostOnboardingURI, - useHomeURI, -} from "@ledgerhq/live-common/hooks/recoverFeatureFlag"; import { useRefreshAccountsOrdering } from "~/actions/general"; -import { - // TODO: discreetMode is never used 😱 is it safe to remove - // discreetModeSelector, - hasBeenUpsoldProtectSelector, - lastConnectedDeviceSelector, - onboardingTypeSelector, -} from "~/reducers/settings"; -import { setHasBeenUpsoldProtect } from "~/actions/settings"; import Carousel from "~/components/Carousel"; import { ScreenName } from "~/const"; import FirmwareUpdateBanner from "LLM/features/FirmwareUpdate/components/UpdateBanner"; @@ -47,13 +33,12 @@ import { hasTokenAccountsNotBlackListedWithPositiveBalanceSelector, } from "~/reducers/accounts"; import PortfolioAssets from "./PortfolioAssets"; -import { internetReachable } from "~/logic/internetReachable"; import { UpdateStep } from "../FirmwareUpdate"; -import { OnboardingType } from "~/reducers/types"; import ContentCardsLocation from "~/dynamicContent/ContentCardsLocation"; import { ContentCardLocation } from "~/dynamicContent/types"; import usePortfolioAnalyticsOptInPrompt from "~/hooks/analyticsOptInPrompt/usePorfolioAnalyticsOptInPrompt"; import AddAccountDrawer from "LLM/features/Accounts/screens/AddAccount"; +import { useAutoRedirectToPostOnboarding } from "~/hooks/useAutoRedirectToPostOnboarding"; export { default as PortfolioTabIcon } from "./TabIcon"; @@ -68,19 +53,9 @@ const RefreshableCollapsibleHeaderFlatList = globalSyncRefreshControl(Collapsibl function PortfolioScreen({ navigation }: NavigationProps) { const hideEmptyTokenAccount = useEnv("HIDE_EMPTY_TOKEN_ACCOUNTS"); const { t } = useTranslation(); - // TODO: discreetMode is never used 😱 is it safe to remove - // const discreetMode = useSelector(discreetModeSelector); - const hasBeenUpsoldProtect = useSelector(hasBeenUpsoldProtectSelector); - const lastConnectedDevice = useSelector(lastConnectedDeviceSelector); const [isAddModalOpened, setAddModalOpened] = useState(false); const { colors } = useTheme(); const { isAWalletCardDisplayed } = useDynamicContent(); - const onboardingType = useSelector(onboardingTypeSelector); - const protectFeature = useFeature("protectServicesMobile"); - const recoverAlreadyOnboardedURI = useAlreadyOnboardedURI(protectFeature); - const recoverPostOnboardingURI = usePostOnboardingURI(protectFeature); - const recoverHomeURI = useHomeURI(protectFeature); - const dispatch = useDispatch(); const onBackFromUpdate = useCallback( (_updateState: UpdateStep) => { @@ -89,33 +64,7 @@ function PortfolioScreen({ navigation }: NavigationProps) { [navigation], ); - useEffect(() => { - const openProtectUpsell = async () => { - const internetConnected = await internetReachable(); - if (internetConnected && protectFeature?.enabled) { - if (recoverPostOnboardingURI && onboardingType === OnboardingType.restore) { - Linking.openURL(recoverPostOnboardingURI); - } else if (recoverHomeURI && onboardingType === OnboardingType.setupNew) { - Linking.openURL(recoverHomeURI); - } else if (recoverAlreadyOnboardedURI) { - Linking.openURL(recoverAlreadyOnboardedURI); - } - } - }; - if (!hasBeenUpsoldProtect && lastConnectedDevice?.modelId === "nanoX") { - openProtectUpsell(); - dispatch(setHasBeenUpsoldProtect(true)); - } - }, [ - onboardingType, - hasBeenUpsoldProtect, - lastConnectedDevice, - recoverPostOnboardingURI, - recoverAlreadyOnboardedURI, - recoverHomeURI, - dispatch, - protectFeature?.enabled, - ]); + useAutoRedirectToPostOnboarding(); usePortfolioAnalyticsOptInPrompt(); diff --git a/apps/ledger-live-mobile/src/screens/PostOnboarding/PostOnboardingHub.tsx b/apps/ledger-live-mobile/src/screens/PostOnboarding/PostOnboardingHub.tsx index 557a6a607b03..a8353d65306f 100644 --- a/apps/ledger-live-mobile/src/screens/PostOnboarding/PostOnboardingHub.tsx +++ b/apps/ledger-live-mobile/src/screens/PostOnboarding/PostOnboardingHub.tsx @@ -14,25 +14,22 @@ import { TrackScreen } from "~/analytics"; import Link from "~/components/wrappedUi/Link"; import { useCompletePostOnboarding } from "~/logic/postOnboarding/useCompletePostOnboarding"; import { ScrollContainer } from "@ledgerhq/native-ui"; +import { setHasBeenRedirectedToPostOnboarding } from "~/actions/settings"; const PostOnboardingHub = () => { const dispatch = useDispatch(); const { t } = useTranslation(); const { actionsState, deviceModelId } = usePostOnboardingHubState(); const closePostOnboarding = useCompletePostOnboarding(); - const clearLastActionCompleted = useCallback(() => { - dispatch(clearPostOnboardingLastActionCompleted()); - }, [dispatch]); - - useEffect( + useEffect(() => { /** * The last action context (specific title & popup) should only be visible * the 1st time the hub is navigated to after that action was completed. * So here we clear the last action completed. * */ - () => clearLastActionCompleted, - [clearLastActionCompleted], - ); + dispatch(clearPostOnboardingLastActionCompleted()); + dispatch(setHasBeenRedirectedToPostOnboarding(true)); + }, [dispatch]); const navigateToMainScreen = useCallback(() => { closePostOnboarding(); diff --git a/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/RecoverUpsellRow.tsx b/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/RecoverUpsellRow.tsx new file mode 100644 index 000000000000..a85163724364 --- /dev/null +++ b/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/RecoverUpsellRow.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import SettingsRow from "~/components/SettingsRow"; +import Switch from "~/components/Switch"; +import { useFeature, useFeatureFlags } from "@ledgerhq/live-common/featureFlags/index"; + +export function RecoverUpsellRow() { + const { overrideFeature, resetFeature } = useFeatureFlags(); + + const protectFeature = useFeature("protectServicesMobile"); + + if (protectFeature === null || protectFeature === undefined) return null; + + const currentTarget = protectFeature?.params?.protectId; + + const onChange = (enabled: boolean) => { + if (enabled) { + overrideFeature("protectServicesMobile", { + ...protectFeature, + params: { ...protectFeature?.params, protectId: "protect-prod" }, + }); + } else { + resetFeature("protectServicesMobile"); + } + }; + + return ( + + + + ); +} diff --git a/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/ResetOnboardingStateRow.tsx b/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/ResetOnboardingStateRow.tsx index a364d984d4ff..ecc1cf381937 100644 --- a/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/ResetOnboardingStateRow.tsx +++ b/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/ResetOnboardingStateRow.tsx @@ -1,7 +1,13 @@ import React, { useContext } from "react"; import { useDispatch, useSelector } from "react-redux"; import SettingsRow from "~/components/SettingsRow"; -import { completeOnboarding, setHasOrderedNano, setReadOnlyMode } from "~/actions/settings"; +import { + completeOnboarding, + setHasBeenRedirectedToPostOnboarding, + setHasBeenUpsoldProtect, + setHasOrderedNano, + setReadOnlyMode, +} from "~/actions/settings"; import { RebootContext } from "~/context/Reboot"; import { knownDevicesSelector } from "~/reducers/ble"; import { removeKnownDevices } from "~/actions/ble"; @@ -22,6 +28,8 @@ export default function ResetOnboardingStateRow() { dispatch(setHasOrderedNano(false)); dispatch(completeOnboarding(false)); dispatch(removeKnownDevices(knownDevices.map(d => d.id))); + dispatch(setHasBeenUpsoldProtect(false)); + dispatch(setHasBeenRedirectedToPostOnboarding(false)); unacceptGeneralTerms(); requestAnimationFrame(() => { reboot(); diff --git a/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/index.tsx b/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/index.tsx index 369b7308e0c0..19334be5c9ec 100644 --- a/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Settings/Debug/Configuration/index.tsx @@ -17,6 +17,7 @@ import ResetOnboardingStateRow from "./ResetOnboardingStateRow"; import NftMetadataServiceRow from "./NftMetadataServiceRow"; import HasStaxEuropaRows from "./HasStaxEuropaRows"; import SkipOnboardingRow from "./SkipOnboardingRow"; +import { RecoverUpsellRow } from "./RecoverUpsellRow"; export default function Configuration() { const navigation = useNavigation>(); @@ -40,6 +41,7 @@ export default function Configuration() { + diff --git a/apps/ledger-live-mobile/src/screens/SyncOnboarding/CompletionScreen.tsx b/apps/ledger-live-mobile/src/screens/SyncOnboarding/CompletionScreen.tsx index cd8a3b7fd76f..1e726129e92b 100644 --- a/apps/ledger-live-mobile/src/screens/SyncOnboarding/CompletionScreen.tsx +++ b/apps/ledger-live-mobile/src/screens/SyncOnboarding/CompletionScreen.tsx @@ -1,8 +1,7 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect } from "react"; import { Flex } from "@ledgerhq/native-ui"; import { StackScreenProps } from "@react-navigation/stack"; import { TouchableWithoutFeedback } from "react-native-gesture-handler"; -import { useStartPostOnboardingCallback } from "@ledgerhq/live-common/postOnboarding/hooks/useStartPostOnboardingCallback"; import { NavigatorName, ScreenName } from "~/const"; import { SyncOnboardingStackParamList } from "~/components/RootNavigator/types/SyncOnboardingNavigator"; @@ -10,6 +9,8 @@ import { BaseComposite, RootNavigation } from "~/components/RootNavigator/types/ import { DeviceModelId } from "@ledgerhq/devices"; import EuropaCompletionView from "./EuropaCompletionView"; import StaxCompletionView from "./StaxCompletionView"; +import { useDispatch } from "react-redux"; +import { setHasBeenRedirectedToPostOnboarding } from "~/actions/settings"; type Props = BaseComposite< StackScreenProps @@ -17,40 +18,41 @@ type Props = BaseComposite< const CompletionScreen = ({ navigation, route }: Props) => { const { device } = route.params; - const startPostOnboarding = useStartPostOnboardingCallback(); - - const redirectToPostOnboarding = useCallback(() => { - startPostOnboarding({ - deviceModelId: device.modelId, - resetNavigationStack: true, - fallbackIfNoAction: () => - // Resets the navigation stack to avoid allowing to go back to the onboarding welcome screen - // FIXME: bindings to react-navigation seem to have issues with composites - (navigation as unknown as RootNavigation).reset({ - index: 0, - routes: [ - { - name: NavigatorName.Base, - state: { - routes: [ - { - name: NavigatorName.Main, - }, - ], + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(setHasBeenRedirectedToPostOnboarding(false)); + }, [dispatch]); + + const hasRedirected = React.useRef(false); + + const redirectToMainScreen = useCallback(() => { + if (hasRedirected.current) return; + hasRedirected.current = true; + (navigation as unknown as RootNavigation).reset({ + index: 0, + routes: [ + { + name: NavigatorName.Base, + state: { + routes: [ + { + name: NavigatorName.Main, }, - }, - ], - }), + ], + }, + }, + ], }); - }, [device.modelId, navigation, startPostOnboarding]); + }, [navigation]); return ( - + {device.modelId === DeviceModelId.europa ? ( - + ) : ( - + )} diff --git a/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx b/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx index 79458d1e7f0c..9a3a9e9e3133 100644 --- a/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx +++ b/apps/ledger-live-mobile/src/screens/SyncOnboarding/SyncOnboardingCompanion.tsx @@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next"; import { getDeviceModel } from "@ledgerhq/devices"; import { Device } from "@ledgerhq/live-common/hw/actions/types"; import { useDispatch } from "react-redux"; -import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { SeedPhraseType, StorylyInstanceID } from "@ledgerhq/types-live"; import { DeviceModelId } from "@ledgerhq/types-devices"; @@ -149,6 +149,7 @@ export const SyncOnboardingCompanion: React.FC = ( const dispatchRedux = useDispatch(); const deviceInitialApps = useFeature("deviceInitialApps"); const recoverUpsellRedirection = useFeature("recoverUpsellRedirection"); + const hasBackupStep = !recoverUpsellRedirection?.enabled; const productName = getDeviceModel(device.modelId).productName || device.modelId; const deviceName = device.deviceName || productName; @@ -370,20 +371,19 @@ export const SyncOnboardingCompanion: React.FC = ( if (deviceOnboardingState?.isOnboarded && !seededDeviceHandled.current) { if (deviceOnboardingState?.currentOnboardingStep === DeviceOnboardingStep.Ready) { // device was just seeded - setCompanionStepKey( - recoverUpsellRedirection?.enabled ? CompanionStepKey.Apps : CompanionStepKey.Backup, - ); + setCompanionStepKey(hasBackupStep ? CompanionStepKey.Backup : CompanionStepKey.Apps); seededDeviceHandled.current = true; return; } else if ( deviceOnboardingState?.currentOnboardingStep === DeviceOnboardingStep.WelcomeScreen1 ) { - if (recoverUpsellRedirection?.enabled) { - // switch to the apps step + // device was already seeded + if (hasBackupStep) { __DEV__ ? setCompanionStepKey(CompanionStepKey.Backup) // for ease of testing in dev mode without having to reset the device : setCompanionStepKey(CompanionStepKey.Apps); } else { + // switch to the apps step setCompanionStepKey(CompanionStepKey.Apps); } seededDeviceHandled.current = true; @@ -442,7 +442,7 @@ export const SyncOnboardingCompanion: React.FC = ( }, [ deviceOnboardingState, notifyEarlySecurityCheckShouldReset, - recoverUpsellRedirection, + hasBackupStep, shouldRestoreApps, ]); @@ -649,9 +649,8 @@ export const SyncOnboardingCompanion: React.FC = ( ), }, - ...(recoverUpsellRedirection?.enabled - ? [] - : [ + ...(hasBackupStep + ? [ { key: CompanionStepKey.Backup, title: t("syncOnboarding.backup.title"), @@ -663,7 +662,8 @@ export const SyncOnboardingCompanion: React.FC = ( /> ), }, - ]), + ] + : []), ...(deviceInitialApps?.enabled ? [ { @@ -697,7 +697,7 @@ export const SyncOnboardingCompanion: React.FC = ( [ t, productName, - recoverUpsellRedirection?.enabled, + hasBackupStep, deviceInitialApps?.enabled, device, seedPathStatus, diff --git a/libs/ledger-live-common/src/hooks/recoverFeatureFlag.ts b/libs/ledger-live-common/src/hooks/recoverFeatureFlag.ts index 0ae1ac130ad4..a5c0b09a4a21 100644 --- a/libs/ledger-live-common/src/hooks/recoverFeatureFlag.ts +++ b/libs/ledger-live-common/src/hooks/recoverFeatureFlag.ts @@ -172,6 +172,8 @@ export function useCustomURI( if (source && deeplinkCampaign) { uri.searchParams.append("ajs_recover_source", source); uri.searchParams.append("ajs_recover_campaign", deeplinkCampaign); + uri.searchParams.append("ajs_prop_source", source); + uri.searchParams.append("ajs_prop_campaign", deeplinkCampaign); } return uri; @@ -190,3 +192,16 @@ export function useCustomPath( return usePath(servicesConfig, uri); } + +export enum Source { + LLM_ONBOARDING_24 = "llm-onboarding-24", + LLD_ONBOARDING_24 = "lld-onboarding-24", +} + +export function useTouchScreenOnboardingUpsellURI( + servicesConfig: Feature_ProtectServicesMobile | null, + source: Source, +): string | undefined { + const campaign = "touchscreen-onboarding"; + return useCustomURI(servicesConfig, "upsell", source, campaign); +} diff --git a/libs/ledger-live-common/src/postOnboarding/hooks/useStartPostOnboardingCallback.ts b/libs/ledger-live-common/src/postOnboarding/hooks/useStartPostOnboardingCallback.ts index fe00f2de4eaa..a39fd7591e4a 100644 --- a/libs/ledger-live-common/src/postOnboarding/hooks/useStartPostOnboardingCallback.ts +++ b/libs/ledger-live-common/src/postOnboarding/hooks/useStartPostOnboardingCallback.ts @@ -43,10 +43,8 @@ export function useStartPostOnboardingCallback(): (options: StartPostOnboardingO ); if (actions.length === 0) { - if (fallbackIfNoAction) { - dispatch(postOnboardingSetFinished()); - fallbackIfNoAction(); - } + dispatch(postOnboardingSetFinished()); + if (fallbackIfNoAction) fallbackIfNoAction(); return; } navigateToPostOnboardingHub(resetNavigationStack); From cd23bb97082be9a35432dd434444d5f4000a8b9a Mon Sep 17 00:00:00 2001 From: Luis Couto Date: Wed, 30 Oct 2024 15:46:11 +0000 Subject: [PATCH 15/25] fix(LIVE-14547): dont set ptxSwapCoreExperiment as `false` --- apps/ledger-live-desktop/src/renderer/analytics/segment.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/ledger-live-desktop/src/renderer/analytics/segment.ts b/apps/ledger-live-desktop/src/renderer/analytics/segment.ts index 5a36b3dcb6d4..e43f241e4395 100644 --- a/apps/ledger-live-desktop/src/renderer/analytics/segment.ts +++ b/apps/ledger-live-desktop/src/renderer/analytics/segment.ts @@ -89,8 +89,9 @@ const getPtxAttributes = () => { const ptxSwapLiveAppDemoThree = analyticsFeatureFlagMethod("ptxSwapLiveAppDemoThree")?.enabled; const ptxSwapExodusProvider = analyticsFeatureFlagMethod("ptxSwapExodusProvider")?.enabled; const ptxSwapCoreExperimentFlag = analyticsFeatureFlagMethod("ptxSwapCoreExperiment"); - const ptxSwapCoreExperiment = - ptxSwapCoreExperimentFlag?.enabled && ptxSwapCoreExperimentFlag?.params?.variant; + const ptxSwapCoreExperiment = ptxSwapCoreExperimentFlag?.enabled + ? ptxSwapCoreExperimentFlag?.params?.variant + : undefined; const isBatch1Enabled: boolean = !!fetchAdditionalCoins?.enabled && fetchAdditionalCoins?.params?.batch === 1; From db7b05b1a5b6a694754fde2f15d5982b27847cec Mon Sep 17 00:00:00 2001 From: Luis Couto Date: Wed, 30 Oct 2024 15:47:41 +0000 Subject: [PATCH 16/25] fix(LIVE-14547): add changeset --- .changeset/few-bikes-act.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/few-bikes-act.md diff --git a/.changeset/few-bikes-act.md b/.changeset/few-bikes-act.md new file mode 100644 index 000000000000..9aab1a5d7ed1 --- /dev/null +++ b/.changeset/few-bikes-act.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Do not set `ptxSwapCoreExperiment` as `false` in analytics From e5eb2996ee3853b6353b7febaa42f0540fd8e82a Mon Sep 17 00:00:00 2001 From: Victor Alber Date: Wed, 30 Oct 2024 16:59:24 +0100 Subject: [PATCH 17/25] =?UTF-8?q?=F0=9F=A6=BA=20Adding=20XRP3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ledger-live-desktop/tests/enum/Account.ts | 8 ++++++++ .../tests/specs/speculos/send.tx.spec.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/ledger-live-desktop/tests/enum/Account.ts b/apps/ledger-live-desktop/tests/enum/Account.ts index 41537893aec5..135a922e8c48 100644 --- a/apps/ledger-live-desktop/tests/enum/Account.ts +++ b/apps/ledger-live-desktop/tests/enum/Account.ts @@ -234,6 +234,14 @@ export class Account { 1, ); + static readonly XRP_3 = new Account( + Currency.XRP, + "XRP 3", + "rn2Z2yShWcvdTSQVo1EqjUpD1sjwBkZALb", + undefined, + 2, + ); + static readonly ADA_1 = new Account( Currency.ADA, "Cardano 1", diff --git a/apps/ledger-live-desktop/tests/specs/speculos/send.tx.spec.ts b/apps/ledger-live-desktop/tests/specs/speculos/send.tx.spec.ts index f2a04b9cac7b..cdb5b54295c9 100644 --- a/apps/ledger-live-desktop/tests/specs/speculos/send.tx.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/speculos/send.tx.spec.ts @@ -20,7 +20,7 @@ const transactionsAmountInvalid = [ xrayTicket: "B2CQA-2569", }, { - transaction: new Transaction(Account.XRP_1, Account.XRP_2, "1", Fee.MEDIUM), + transaction: new Transaction(Account.XRP_1, Account.XRP_3, "1", Fee.MEDIUM), expectedErrorMessage: "Recipient address is inactive. Send at least 10 XRP to activate it", xrayTicket: "B2CQA-2571", }, From 6def77a59680f9b75c954d32b89255ed93c52e4b Mon Sep 17 00:00:00 2001 From: Come Grellard Date: Wed, 30 Oct 2024 11:17:10 +0100 Subject: [PATCH 18/25] :heavy_minus_sign: feat(llm): remove react-native-adjust sdk and all adjust related parts from the codebase --- .changeset/clean-nails-laugh.md | 5 ++ apps/ledger-live-desktop/src/config/urls.ts | 1 - .../.env.android.prerelease | 1 - apps/ledger-live-mobile/.env.android.release | 1 - apps/ledger-live-mobile/.env.android.staging | 1 - apps/ledger-live-mobile/.env.ios.prerelease | 5 -- apps/ledger-live-mobile/.env.ios.release | 5 -- apps/ledger-live-mobile/.env.ios.staging | 5 -- apps/ledger-live-mobile/.env.mock | 1 - .../android/app/build.gradle | 4 -- .../android/app/proguard-rules.pro | 1 - .../android/app/src/main/AndroidManifest.xml | 3 +- apps/ledger-live-mobile/docs/analytics.md | 15 +----- apps/ledger-live-mobile/e2e/helpers.ts | 2 +- apps/ledger-live-mobile/ios/Podfile.lock | 12 ----- .../project.pbxproj | 2 - .../ledgerlivemobile.entitlements | 9 ---- apps/ledger-live-mobile/package.json | 1 - .../src/components/AdjustSetup.tsx | 53 ------------------- apps/ledger-live-mobile/src/index.tsx | 2 - .../src/navigation/DeeplinksProvider.tsx | 34 +----------- .../src/screens/PurchaseDevice/index.tsx | 31 +---------- pnpm-lock.yaml | 8 --- 23 files changed, 10 insertions(+), 192 deletions(-) create mode 100644 .changeset/clean-nails-laugh.md delete mode 100644 apps/ledger-live-mobile/src/components/AdjustSetup.tsx diff --git a/.changeset/clean-nails-laugh.md b/.changeset/clean-nails-laugh.md new file mode 100644 index 000000000000..d61bc854c70a --- /dev/null +++ b/.changeset/clean-nails-laugh.md @@ -0,0 +1,5 @@ +--- +"live-mobile": minor +--- + +LLM - Remove react-native-adjust SDK and all adjust related parts from the codebase diff --git a/apps/ledger-live-desktop/src/config/urls.ts b/apps/ledger-live-desktop/src/config/urls.ts index 70cae363b2a4..9f258c2289b5 100644 --- a/apps/ledger-live-desktop/src/config/urls.ts +++ b/apps/ledger-live-desktop/src/config/urls.ts @@ -177,7 +177,6 @@ export const urls = { "https://cdn.figment.io/legal/Current%20Ledger_Online%20Staking%20Delgation%20Services%20Agreement.pdf", ens: "https://support.ledger.com/article/9710787581469-zd", ledgerLiveMobile: { - storeLink: "https://r354.adj.st/?adj_t=t2esmlk&adj_campaign=Ledger_Live", appStore: "https://apps.apple.com/app/id1361671700", playStore: "https://play.google.com/store/apps/details?id=com.ledger.live", }, diff --git a/apps/ledger-live-mobile/.env.android.prerelease b/apps/ledger-live-mobile/.env.android.prerelease index bebc72147daa..e31040346200 100644 --- a/apps/ledger-live-mobile/.env.android.prerelease +++ b/apps/ledger-live-mobile/.env.android.prerelease @@ -1,7 +1,6 @@ APP_NAME="Ledger Live" SENTRY_DSN=https://ea730d6c531f40679e7306c888f7fbc5@o118392.ingest.sentry.io/6723478 ANALYTICS_TOKEN=jfUZbw28ig8JpEi9DZpTUc21dKUKu1e3 -ADJUST_APP_TOKEN=104p56owfekg BRAZE_ANDROID_API_KEY="b34d1245-04d6-4209-9580-eed4ec82cf17" BRAZE_IOS_API_KEY="93d6248d-f2e9-4255-84bc-6b551b7afcd2" BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu" \ No newline at end of file diff --git a/apps/ledger-live-mobile/.env.android.release b/apps/ledger-live-mobile/.env.android.release index 5b85a94edcb8..3beb50181f19 100644 --- a/apps/ledger-live-mobile/.env.android.release +++ b/apps/ledger-live-mobile/.env.android.release @@ -1,7 +1,6 @@ APP_NAME="Ledger Live" SENTRY_DSN=https://bfd61f321b4144ffbce693aa6c6be9d0@o118392.ingest.sentry.io/6505213 ANALYTICS_TOKEN=jfUZbw28ig8JpEi9DZpTUc21dKUKu1e3 -ADJUST_APP_TOKEN=104p56owfekg BRAZE_ANDROID_API_KEY="b34d1245-04d6-4209-9580-eed4ec82cf17" BRAZE_IOS_API_KEY="93d6248d-f2e9-4255-84bc-6b551b7afcd2" BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu" \ No newline at end of file diff --git a/apps/ledger-live-mobile/.env.android.staging b/apps/ledger-live-mobile/.env.android.staging index 777c7af8c342..cd58db3a57e8 100644 --- a/apps/ledger-live-mobile/.env.android.staging +++ b/apps/ledger-live-mobile/.env.android.staging @@ -1,7 +1,6 @@ APP_NAME="LL [STAGING]" SENTRY_DSN=https://0109819a39084e718120d031def0db38@o118392.ingest.sentry.io/6619343 ANALYTICS_TOKEN=Yc026bN2XbyBhTCPDFY0VibJugAKnjmh -ADJUST_APP_TOKEN=v88jjyrsto8w BRAZE_ANDROID_API_KEY="4ef07be0-a4ea-4f73-81a9-760e473959f3" BRAZE_IOS_API_KEY="4d6c9f5b-823e-4ea0-8158-5359bdf89618" BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu" \ No newline at end of file diff --git a/apps/ledger-live-mobile/.env.ios.prerelease b/apps/ledger-live-mobile/.env.ios.prerelease index eba9faf795f8..54cbc249144e 100644 --- a/apps/ledger-live-mobile/.env.ios.prerelease +++ b/apps/ledger-live-mobile/.env.ios.prerelease @@ -2,11 +2,6 @@ APP_NAME="Ledger Live" SENTRY_DSN=https://52a4181714f74dffa9cdf48224dba505@o118392.ingest.sentry.io/6723477 ANALYTICS_TOKEN=jfUZbw28ig8JpEi9DZpTUc21dKUKu1e3 GOOGLE_SERVICE_INFO_NAME="GoogleService-Info-Production" -ADJUST_APP_TOKEN=104p56owfekg -ADJUST_BUY_GENERIC_EVENT_ID=1sc6n8 -ADJUST_BUY_NANOX_EVENT_ID=jf9r4k -ADJUST_BUY_NANOS_EVENT_ID=qn40mj -ADJUST_BUY_NANOSP_EVENT_ID=fq8lrx BRAZE_ANDROID_API_KEY="b34d1245-04d6-4209-9580-eed4ec82cf17" BRAZE_IOS_API_KEY="93d6248d-f2e9-4255-84bc-6b551b7afcd2" BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu" \ No newline at end of file diff --git a/apps/ledger-live-mobile/.env.ios.release b/apps/ledger-live-mobile/.env.ios.release index cf5df6a8a5ed..744006c1f4cb 100644 --- a/apps/ledger-live-mobile/.env.ios.release +++ b/apps/ledger-live-mobile/.env.ios.release @@ -2,11 +2,6 @@ APP_NAME="Ledger Live" SENTRY_DSN=https://b4ac5f291b7a48529fc7ec1f34cf57ed@o118392.ingest.sentry.io/6505205 ANALYTICS_TOKEN=jfUZbw28ig8JpEi9DZpTUc21dKUKu1e3 GOOGLE_SERVICE_INFO_NAME="GoogleService-Info-Production" -ADJUST_APP_TOKEN=104p56owfekg -ADJUST_BUY_GENERIC_EVENT_ID=1sc6n8 -ADJUST_BUY_NANOX_EVENT_ID=jf9r4k -ADJUST_BUY_NANOS_EVENT_ID=qn40mj -ADJUST_BUY_NANOSP_EVENT_ID=fq8lrx BRAZE_ANDROID_API_KEY="b34d1245-04d6-4209-9580-eed4ec82cf17" BRAZE_IOS_API_KEY="93d6248d-f2e9-4255-84bc-6b551b7afcd2" BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu" \ No newline at end of file diff --git a/apps/ledger-live-mobile/.env.ios.staging b/apps/ledger-live-mobile/.env.ios.staging index f09971978085..481b4aa292c6 100644 --- a/apps/ledger-live-mobile/.env.ios.staging +++ b/apps/ledger-live-mobile/.env.ios.staging @@ -2,11 +2,6 @@ APP_NAME="LL [STAGING]" SENTRY_DSN=https://bd8b742115104e82b9ad3686fd9fd9f2@o118392.ingest.sentry.io/6619346 ANALYTICS_TOKEN=Yc026bN2XbyBhTCPDFY0VibJugAKnjmh GOOGLE_SERVICE_INFO_NAME="GoogleService-Info-Staging" -ADJUST_APP_TOKEN=v88jjyrsto8w -ADJUST_BUY_GENERIC_EVENT_ID=h7zcss -ADJUST_BUY_NANOX_EVENT_ID=8cykqm -ADJUST_BUY_NANOS_EVENT_ID=rocmaw -ADJUST_BUY_NANOSP_EVENT_ID=y0ku50 BRAZE_ANDROID_API_KEY="4ef07be0-a4ea-4f73-81a9-760e473959f3" BRAZE_IOS_API_KEY="4d6c9f5b-823e-4ea0-8158-5359bdf89618" BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu" \ No newline at end of file diff --git a/apps/ledger-live-mobile/.env.mock b/apps/ledger-live-mobile/.env.mock index 920bb6c6f5d3..f243d3f1eb60 100644 --- a/apps/ledger-live-mobile/.env.mock +++ b/apps/ledger-live-mobile/.env.mock @@ -5,7 +5,6 @@ DISABLE_YELLOW_BOX=1 GOOGLE_SERVICE_INFO_NAME="GoogleService-Info-Testing" MOCK_SCAN_RECIPIENT=bitcoin:3HX3Q4wgYi8nKakxv7kmdCgLWJFrFgcqEt?amount=0.001 FORCE_DEBUG_VISIBLE=1 -ADJUST_APP_TOKEN=cbxft2ch7wn4 BRAZE_ANDROID_API_KEY="be5e1bc8-43f1-4864-b097-076a3c693a43" BRAZE_IOS_API_KEY="e0a7dfaf-fc30-48f6-b998-01dbebbb73a4" BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu" diff --git a/apps/ledger-live-mobile/android/app/build.gradle b/apps/ledger-live-mobile/android/app/build.gradle index 156e3bb71bd5..99665ff9588f 100644 --- a/apps/ledger-live-mobile/android/app/build.gradle +++ b/apps/ledger-live-mobile/android/app/build.gradle @@ -221,10 +221,6 @@ dependencies { // implementation 'com.brentvatne.react:react-native-video' implementation project(':react-native-video') implementation "androidx.appcompat:appcompat:1.0.0" - - // Adjust - implementation 'com.google.android.gms:play-services-analytics:10.0.1' - implementation 'com.android.installreferrer:installreferrer:1.0' } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/apps/ledger-live-mobile/android/app/proguard-rules.pro b/apps/ledger-live-mobile/android/app/proguard-rules.pro index a58bfb1e8bd0..edc668ce2e67 100644 --- a/apps/ledger-live-mobile/android/app/proguard-rules.pro +++ b/apps/ledger-live-mobile/android/app/proguard-rules.pro @@ -15,7 +15,6 @@ -keep class com.facebook.hermes.unicode.** { *; } -keep class com.facebook.jni.** { *; } --keep class com.adjust.sdk.** { *; } -keep class com.google.android.gms.common.ConnectionResult { int SUCCESS; } diff --git a/apps/ledger-live-mobile/android/app/src/main/AndroidManifest.xml b/apps/ledger-live-mobile/android/app/src/main/AndroidManifest.xml index 2c0584df9886..439bf2a44219 100644 --- a/apps/ledger-live-mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/ledger-live-mobile/android/app/src/main/AndroidManifest.xml @@ -25,8 +25,7 @@ - - + 7.7.0) @@ -1112,9 +1109,6 @@ PODS: - React-Mapbuffer (0.74.6): - glog - React-debug - - react-native-adjust (4.38.0): - - Adjust (= 4.38.0) - - React-Core - react-native-biometrics (3.0.1): - React-Core - react-native-ble-plx (3.1.2): @@ -1678,7 +1672,6 @@ DEPENDENCIES: - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - - react-native-adjust (from `../node_modules/react-native-adjust`) - react-native-biometrics (from `../node_modules/react-native-biometrics`) - react-native-ble-plx (from `../node_modules/react-native-ble-plx`) - react-native-config (from `../node_modules/react-native-config`) @@ -1754,7 +1747,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - - Adjust - BrazeKit - BrazeLocation - BrazeUI @@ -1871,8 +1863,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/logger" React-Mapbuffer: :path: "../node_modules/react-native/ReactCommon" - react-native-adjust: - :path: "../node_modules/react-native-adjust" react-native-biometrics: :path: "../node_modules/react-native-biometrics" react-native-ble-plx: @@ -2019,7 +2009,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - Adjust: c98d85594a746ff9f2bafe424c5beb6db66cd6a8 boost: d3f49c53809116a5d38da093a8aa78bf551aed09 braze-react-native-sdk: 62de95dfbbfde54fa101a5d8c75e19ea639e4eda BrazeKit: ee31d3b5113646cbd6ad44eafc93b14c3c3c504c @@ -2082,7 +2071,6 @@ SPEC CHECKSUMS: React-jsitracing: df84cc252a1f4bb0970f7fe13c470451b18c2cbb React-logger: de9b65c8c7b71a663e6e99d347b1c445f5190c39 React-Mapbuffer: 766bb4d8f655d816913325b353d800debbde7209 - react-native-adjust: 801fe33f0dc0097b0474f11d08572a89fa6602ce react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc react-native-ble-plx: cd7b99ddaf7c15e652c63f87a12e96c715ee3c80 react-native-config: 8f7283449bbb048902f4e764affbbf24504454af diff --git a/apps/ledger-live-mobile/ios/ledgerlivemobile.xcodeproj/project.pbxproj b/apps/ledger-live-mobile/ios/ledgerlivemobile.xcodeproj/project.pbxproj index 4ae8af6d1d64..9cc6536b5a72 100644 --- a/apps/ledger-live-mobile/ios/ledgerlivemobile.xcodeproj/project.pbxproj +++ b/apps/ledger-live-mobile/ios/ledgerlivemobile.xcodeproj/project.pbxproj @@ -459,7 +459,6 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ledgerlivemobile/Pods-ledgerlivemobile-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/Adjust/Adjust.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/BrazeKit/BrazeKit.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/BrazeLocation/BrazeLocation.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/BrazeUI/BrazeUI.bundle", @@ -498,7 +497,6 @@ ); name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Adjust.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BrazeKit.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BrazeLocation.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/BrazeUI.bundle", diff --git a/apps/ledger-live-mobile/ios/ledgerlivemobile/ledgerlivemobile.entitlements b/apps/ledger-live-mobile/ios/ledgerlivemobile/ledgerlivemobile.entitlements index 46d384a16b33..551415101bdd 100644 --- a/apps/ledger-live-mobile/ios/ledgerlivemobile/ledgerlivemobile.entitlements +++ b/apps/ledger-live-mobile/ios/ledgerlivemobile/ledgerlivemobile.entitlements @@ -4,15 +4,6 @@ aps-environment development - com.apple.developer.associated-domains - - applinks:r354.adj.st - applinks:ledger.go.link - applinks:a4wc.adj.st - applinks:ledger-debug.go.link - applinks:fvsc.adj.st - applinks:ledger-staging.go.link - keychain-access-groups $(AppIdentifierPrefix)com.ledger.live diff --git a/apps/ledger-live-mobile/package.json b/apps/ledger-live-mobile/package.json index a6775cb7f42a..dc0e6d93c408 100644 --- a/apps/ledger-live-mobile/package.json +++ b/apps/ledger-live-mobile/package.json @@ -167,7 +167,6 @@ "react-i18next": "11.18.6", "react-is": "18.2.0", "react-native": "0.74.6", - "react-native-adjust": "4.38.0", "react-native-android-location-services-dialog-box": "2.8.2", "react-native-animatable": "1.4.0", "react-native-biometrics": "3.0.1", diff --git a/apps/ledger-live-mobile/src/components/AdjustSetup.tsx b/apps/ledger-live-mobile/src/components/AdjustSetup.tsx deleted file mode 100644 index f35a62acafd6..000000000000 --- a/apps/ledger-live-mobile/src/components/AdjustSetup.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect } from "react"; - -import { - Adjust, - AdjustEventTrackingSuccess, - AdjustEventTrackingFailure, - AdjustConfig, -} from "react-native-adjust"; -import Config from "react-native-config"; -import { useSelector } from "react-redux"; -import { trackingEnabledSelector } from "~/reducers/settings"; - -export default function AdjustSetup() { - const trackingEnabled: boolean = useSelector(trackingEnabledSelector); - - useEffect(() => { - const adjustConfig = new AdjustConfig( - Config.ADJUST_APP_TOKEN as string, - __DEV__ ? AdjustConfig.EnvironmentSandbox : AdjustConfig.EnvironmentProduction, // @TODO: Change to Production when ready - ); - adjustConfig.setDelayStart(Math.random() * 7 + 1); - if (__DEV__) { - adjustConfig.setLogLevel(AdjustConfig.LogLevelDebug); - } - if (Config.DEBUG_ADJUST_LOGS) { - adjustConfig.setEventTrackingSucceededCallbackListener( - (eventSuccess: AdjustEventTrackingSuccess) => { - // Printing all event success properties. - console.warn("Event tracking succeeded!", eventSuccess); - }, - ); - - adjustConfig.setEventTrackingFailedCallbackListener( - (eventFailure: AdjustEventTrackingFailure) => { - // Printing all event failure properties. - console.error("Event tracking failed!", eventFailure); - }, - ); - } - - Adjust.create(adjustConfig); - - return () => { - Adjust.componentWillUnmount(); - }; - }, []); - - useEffect(() => { - Adjust.setEnabled(trackingEnabled); - }, [trackingEnabled]); - - return null; -} diff --git a/apps/ledger-live-mobile/src/index.tsx b/apps/ledger-live-mobile/src/index.tsx index b682ee3fbbeb..031f7e89f66f 100644 --- a/apps/ledger-live-mobile/src/index.tsx +++ b/apps/ledger-live-mobile/src/index.tsx @@ -65,7 +65,6 @@ import Modals from "~/screens/Modals"; import NavBarColorHandler from "~/components/NavBarColorHandler"; import { FirebaseRemoteConfigProvider } from "~/components/FirebaseRemoteConfig"; import { FirebaseFeatureFlagsProvider } from "~/components/FirebaseFeatureFlags"; -import AdjustSetup from "~/components/AdjustSetup"; import { TermsAndConditionMigrateLegacyData } from "~/logic/terms"; import HookDynamicContentCards from "~/dynamicContent/useContentCards"; import PlatformAppProviderWrapper from "./PlatformAppProviderWrapper"; @@ -331,7 +330,6 @@ export default class Root extends Component { <> - diff --git a/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx b/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx index 177faea6b79c..1c2bc27933a9 100644 --- a/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx +++ b/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx @@ -97,39 +97,7 @@ const linkingOptions = () => ({ return brazeUrl ? getProxyURL(brazeUrl) : null; }, - prefixes: [ - "ledgerlive://", - "https://ledger.com", - /** - * Adjust universal links attached to iOS Bundle ID com.ledger.live - * (local debug, prod & nightly builds) - * (https://r354.adj.st/.well-known/apple-app-site-association) - * (https://ledger.go.link/.well-known/apple-app-site-association) - * - * to use these universal links, add this query parameter to the URL: adj_t=r1guxhk - * */ - "https://r354.adj.st", - "https://ledger.go.link", - /** - * Adjust universal links attached to iOS Bundle ID com.ledger.live.debug - * (https://a4wc.adj.st/.well-known/apple-app-site-association) - * (https://ledger-debug.go.link/.well-known/apple-app-site-association) - * - * to use these universal links, add this query parameter to the URL: adj_t=f1vrzvp - * */ - "https://a4wc.adj.st", - "https://ledger-debug.go.link", - /** - * Adjust universal links attached to iOS Bundle ID com.ledger.live.dev - * (staging builds) - * (https://fvsc.adj.st/.well-known/apple-app-site-association) - * (https://ledger-staging.go.link/.well-known/apple-app-site-association) - * - * to use these universal links, add this query parameter to the URL: adj_t=p72sbdr - * */ - "https://fvsc.adj.st", - "https://ledger-staging.go.link", - ], + prefixes: ["ledgerlive://", "https://ledger.com"], config: { screens: { [NavigatorName.Base]: { diff --git a/apps/ledger-live-mobile/src/screens/PurchaseDevice/index.tsx b/apps/ledger-live-mobile/src/screens/PurchaseDevice/index.tsx index 224e170ad853..af5c0b99273e 100644 --- a/apps/ledger-live-mobile/src/screens/PurchaseDevice/index.tsx +++ b/apps/ledger-live-mobile/src/screens/PurchaseDevice/index.tsx @@ -7,8 +7,6 @@ import { WebViewMessageEvent } from "react-native-webview"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import { Adjust, AdjustEvent } from "react-native-adjust"; -import Config from "react-native-config"; import Button from "~/components/wrappedUi/Button"; import logger from "../../logger"; import DebugURLDrawer from "./DebugURLDrawer"; @@ -50,32 +48,6 @@ const PurchaseDevice = () => { setURLDrawerOpen(true); }, [setURLDrawerOpen]); - const handleAdjustTracking = useCallback((data: PurchaseMessage) => { - const ids = { - nanoS: Config.ADJUST_BUY_NANOS_EVENT_ID, - nanoX: Config.ADJUST_BUY_NANOX_EVENT_ID, - nanoSP: Config.ADJUST_BUY_NANOSP_EVENT_ID, - }; - const id = data.value?.deviceId - ? ids[data.value.deviceId as keyof typeof ids] || Config.ADJUST_BUY_GENERIC_EVENT_ID - : Config.ADJUST_BUY_GENERIC_EVENT_ID; - - if (!id) { - return; - } - - const revenue = data.value?.price; - const currency = data.value?.currency; - - const adjustEvent = new AdjustEvent(id); - - if (revenue && currency) { - adjustEvent.setRevenue(revenue, currency); - } - - Adjust.trackEvent(adjustEvent); - }, []); - const handleOnboardingStates = useCallback( (data: PurchaseMessage) => { if (data.type === "ledgerLiveOrderSuccess") { @@ -93,14 +65,13 @@ const PurchaseDevice = () => { const data: PurchaseMessage = JSON.parse(event.nativeEvent.data); setMessage(data); setMessageDrawerOpen(true); - handleAdjustTracking(data); handleOnboardingStates(data); } catch (error) { logger.critical(error as Error); } } }, - [handleAdjustTracking, handleOnboardingStates], + [handleOnboardingStates], ); return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cbfcedb0e61..3e7b556722b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1112,9 +1112,6 @@ importers: react-native: specifier: 0.74.6 version: 0.74.6(@babel/core@7.24.3)(@types/react@18.2.73)(react@18.2.0) - react-native-adjust: - specifier: 4.38.0 - version: 4.38.0 react-native-android-location-services-dialog-box: specifier: 2.8.2 version: 2.8.2(react-native@0.74.6(@babel/core@7.24.3)(@types/react@18.2.73)(react@18.2.0)) @@ -26468,9 +26465,6 @@ packages: peerDependencies: react: ^0.14.9 || ^15.3.0 || ^16.0.0 - react-native-adjust@4.38.0: - resolution: {integrity: sha512-kNobWxemV4h+/iPCWd5fzE4cuT3rhqSaZQvPpKvLMzqxhpuCdEzaVm30T7SCs0a/StyFzlPk2D++0ktVZjt2SA==} - react-native-android-location-services-dialog-box@2.8.2: resolution: {integrity: sha512-zBTi0xJQoF6GdImRhXOCXezYuALlPgbctqG3eOrZu63hdrlvTBhqATkb6DB5JM9kpcMbX1skMIOI3m71zKGsPA==} peerDependencies: @@ -60774,8 +60768,6 @@ snapshots: raf: 3.4.1 react: 18.2.0 - react-native-adjust@4.38.0: {} - react-native-android-location-services-dialog-box@2.8.2(react-native@0.74.6(@babel/core@7.24.3)(@types/react@18.2.73)(react@18.2.0)): dependencies: react-native: 0.74.6(@babel/core@7.24.3)(@types/react@18.2.73)(react@18.2.0) From de397a9995848aa1dbc4b77f1542f9fe5ae55bb4 Mon Sep 17 00:00:00 2001 From: Come Grellard Date: Wed, 30 Oct 2024 15:38:23 +0100 Subject: [PATCH 19/25] :heavy_minus_sign: feat(llm): add back the ledger.go.link prefixes to the deeplinks provider to keep supporting those deeplinks when scanning a qr code on a stax or a flex --- .../ledgerlivemobile.entitlements | 9 +++++ .../src/navigation/DeeplinksProvider.tsx | 35 ++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/apps/ledger-live-mobile/ios/ledgerlivemobile/ledgerlivemobile.entitlements b/apps/ledger-live-mobile/ios/ledgerlivemobile/ledgerlivemobile.entitlements index 551415101bdd..46d384a16b33 100644 --- a/apps/ledger-live-mobile/ios/ledgerlivemobile/ledgerlivemobile.entitlements +++ b/apps/ledger-live-mobile/ios/ledgerlivemobile/ledgerlivemobile.entitlements @@ -4,6 +4,15 @@ aps-environment development + com.apple.developer.associated-domains + + applinks:r354.adj.st + applinks:ledger.go.link + applinks:a4wc.adj.st + applinks:ledger-debug.go.link + applinks:fvsc.adj.st + applinks:ledger-staging.go.link + keychain-access-groups $(AppIdentifierPrefix)com.ledger.live diff --git a/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx b/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx index 1c2bc27933a9..41a84eda32a6 100644 --- a/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx +++ b/apps/ledger-live-mobile/src/navigation/DeeplinksProvider.tsx @@ -97,7 +97,40 @@ const linkingOptions = () => ({ return brazeUrl ? getProxyURL(brazeUrl) : null; }, - prefixes: ["ledgerlive://", "https://ledger.com"], + prefixes: [ + "ledgerlive://", + "https://ledger.com", + // FIXME: We will be fixing the universal links in this epic : https://ledgerhq.atlassian.net/browse/LIVE-14732 + /** + * Adjust universal links attached to iOS Bundle ID com.ledger.live + * (local debug, prod & nightly builds) + * (https://r354.adj.st/.well-known/apple-app-site-association) + * (https://ledger.go.link/.well-known/apple-app-site-association) + * + * to use these universal links, add this query parameter to the URL: adj_t=r1guxhk + * */ + "https://r354.adj.st", + "https://ledger.go.link", + /** + * Adjust universal links attached to iOS Bundle ID com.ledger.live.debug + * (https://a4wc.adj.st/.well-known/apple-app-site-association) + * (https://ledger-debug.go.link/.well-known/apple-app-site-association) + * + * to use these universal links, add this query parameter to the URL: adj_t=f1vrzvp + * */ + "https://a4wc.adj.st", + "https://ledger-debug.go.link", + /** + * Adjust universal links attached to iOS Bundle ID com.ledger.live.dev + * (staging builds) + * (https://fvsc.adj.st/.well-known/apple-app-site-association) + * (https://ledger-staging.go.link/.well-known/apple-app-site-association) + * + * to use these universal links, add this query parameter to the URL: adj_t=p72sbdr + * */ + "https://fvsc.adj.st", + "https://ledger-staging.go.link", + ], config: { screens: { [NavigatorName.Base]: { From 901cdd5006c5a591e56819cb674d2f70e76e4a5d Mon Sep 17 00:00:00 2001 From: Jean-Christophe Chevalier Date: Wed, 30 Oct 2024 17:48:16 +0100 Subject: [PATCH 20/25] fix(xrp): return 0 as default balance (#8242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(xrp): return 0 as default balance * fix: update account info management for alpaca Signed-off-by: Stéphane Prohaszka * chore: changeset Signed-off-by: Stéphane Prohaszka * fix(xrp): sync AccountInfo model use Signed-off-by: Stéphane Prohaszka --------- Signed-off-by: Stéphane Prohaszka Co-authored-by: Stéphane Prohaszka --- .changeset/new-plums-shout.md | 5 + .../coin-xrp/src/api/index.integ.test.ts | 24 ++-- .../coin-xrp/src/bridge/synchronization.ts | 10 +- .../coin-xrp/src/logic/getBalance.test.ts | 6 +- .../coin-xrp/src/logic/getBalance.ts | 2 +- libs/coin-modules/coin-xrp/src/logic/index.ts | 3 - .../coin-xrp/src/logic/utils.test.ts | 60 +++++++++ libs/coin-modules/coin-xrp/src/logic/utils.ts | 9 +- .../coin-xrp/src/network/index.test.ts | 114 ++++++++++++++++++ .../coin-xrp/src/network/index.ts | 23 +++- .../coin-xrp/src/network/types.ts | 19 +++ libs/coin-modules/coin-xrp/src/types/index.ts | 1 + libs/coin-modules/coin-xrp/src/types/model.ts | 6 + 13 files changed, 253 insertions(+), 29 deletions(-) create mode 100644 .changeset/new-plums-shout.md create mode 100644 libs/coin-modules/coin-xrp/src/logic/utils.test.ts create mode 100644 libs/coin-modules/coin-xrp/src/network/index.test.ts create mode 100644 libs/coin-modules/coin-xrp/src/types/model.ts diff --git a/.changeset/new-plums-shout.md b/.changeset/new-plums-shout.md new file mode 100644 index 000000000000..77c94bf9f92c --- /dev/null +++ b/.changeset/new-plums-shout.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/coin-xrp": patch +--- + +Fix Cardano getAccountInfo diff --git a/libs/coin-modules/coin-xrp/src/api/index.integ.test.ts b/libs/coin-modules/coin-xrp/src/api/index.integ.test.ts index b9ad99455218..eeb6853d6409 100644 --- a/libs/coin-modules/coin-xrp/src/api/index.integ.test.ts +++ b/libs/coin-modules/coin-xrp/src/api/index.integ.test.ts @@ -5,7 +5,8 @@ import { sign } from "ripple-keypairs"; describe("Xrp Api", () => { let module: Api; - const address = "rKtXXTVno77jhu6tto1MAXjepyuaKaLcqB"; + const address = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb"; + const emptyAddress = "rKtXXTVno77jhu6tto1MAXjepyuaKaLcqB"; // Account with no transaction (at the time of this writing) const xrpPubKey = process.env["PUB_KEY"]!; const xrpSecretKey = process.env["SECRET_KEY"]!; @@ -16,7 +17,6 @@ describe("Xrp Api", () => { describe("estimateFees", () => { it("returns a default value", async () => { // Given - const address = "rDCyjRD2TcSSGUQpEcEhJGmDWfjPJpuGxu"; const amount = BigInt(100); // When @@ -56,12 +56,20 @@ describe("Xrp Api", () => { }); describe("getBalance", () => { - it("returns a list regarding address parameter", async () => { + it("returns an amount above 0 when address has transactions", async () => { // When const result = await module.getBalance(address); // Then - expect(result).toBeGreaterThan(0); + expect(result).toBeGreaterThan(BigInt(0)); + }); + + it("returns 0 when address has no transaction", async () => { + // When + const result = await module.getBalance(emptyAddress); + + // Then + expect(result).toBe(BigInt(0)); }); }); @@ -76,14 +84,16 @@ describe("Xrp Api", () => { }); // Then - expect(result.slice(0, 34)).toEqual("120000228000000024001BCDA6201B001F"); + expect(result.slice(0, 34)).toEqual("1200002280000000240002588F201B001D"); expect(result.slice(38)).toEqual( - "61400000000000000A6840000000000000018114CF30F590D7A9067B2604D80D46090FBF342EBE988314CA26FB6B0EF6859436C2037BA0A9913208A59B98", + "61400000000000000A68400000000000000181142A6ADC782DAFDDB464E434B684F01416B8A33B208314CA26FB6B0EF6859436C2037BA0A9913208A59B98", ); }); }); - describe("combine", () => { + // To enable this test, you need to fill an `.env` file at the root of this package. Example can be found in `.env.integ.test.example`. + // The value hardcoded here depends on the value filled in the `.env` file. + describe.skip("combine", () => { it("returns a signed raw transaction", async () => { // Given const rawTx = diff --git a/libs/coin-modules/coin-xrp/src/bridge/synchronization.ts b/libs/coin-modules/coin-xrp/src/bridge/synchronization.ts index 215394b51fd0..39d09e749fd0 100644 --- a/libs/coin-modules/coin-xrp/src/bridge/synchronization.ts +++ b/libs/coin-modules/coin-xrp/src/bridge/synchronization.ts @@ -3,7 +3,7 @@ import { Operation } from "@ledgerhq/types-live"; import { encodeAccountId } from "@ledgerhq/coin-framework/account/index"; import { GetAccountShape, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { getAccountInfo, getServerInfos, getTransactions } from "../network"; -import { NEW_ACCOUNT_ERROR_MESSAGE, parseAPIValue } from "../logic"; +import { parseAPIValue } from "../logic"; import { filterOperations } from "./logic"; export const getAccountShape: GetAccountShape = async info => { @@ -17,7 +17,7 @@ export const getAccountShape: GetAccountShape = async info => { }); const accountInfo = await getAccountInfo(address); - if (!accountInfo || accountInfo.error === NEW_ACCOUNT_ERROR_MESSAGE) { + if (accountInfo.isNewAccount) { return { id: accountId, xpub: address, @@ -42,10 +42,10 @@ export const getAccountShape: GetAccountShape = async info => { const minLedgerVersion = Number(ledgers[0]); const maxLedgerVersion = Number(ledgers[1]); - const trustlines = accountInfo.account_data.OwnerCount; + const trustlines = accountInfo.ownerCount; - const balance = new BigNumber(accountInfo.account_data.Balance); - const spendableBalance = new BigNumber(accountInfo.account_data.Balance) + const balance = new BigNumber(accountInfo.balance); + const spendableBalance = new BigNumber(accountInfo.balance) .minus(reserveMinXRP) .minus(reservePerTrustline.times(trustlines)); diff --git a/libs/coin-modules/coin-xrp/src/logic/getBalance.test.ts b/libs/coin-modules/coin-xrp/src/logic/getBalance.test.ts index 45eb26491265..95c74aafc207 100644 --- a/libs/coin-modules/coin-xrp/src/logic/getBalance.test.ts +++ b/libs/coin-modules/coin-xrp/src/logic/getBalance.test.ts @@ -3,7 +3,7 @@ import { getBalance } from "./getBalance"; const mockGetAccountInfo = jest.fn(); jest.mock("../network", () => ({ - getAccountInfo: (arg: unknown) => mockGetAccountInfo(arg), + getAccountInfo: (address: string) => mockGetAccountInfo(address), })); describe("getBalance", () => { @@ -16,9 +16,7 @@ describe("getBalance", () => { const balance = faker.number.bigInt(100_000_000); const address = "ACCOUNT_ADDRESS"; mockGetAccountInfo.mockResolvedValue({ - account_data: { - Balance: balance.toString(), - }, + balance, }); // When diff --git a/libs/coin-modules/coin-xrp/src/logic/getBalance.ts b/libs/coin-modules/coin-xrp/src/logic/getBalance.ts index ffc6d3925061..43906cc87d67 100644 --- a/libs/coin-modules/coin-xrp/src/logic/getBalance.ts +++ b/libs/coin-modules/coin-xrp/src/logic/getBalance.ts @@ -2,5 +2,5 @@ import { getAccountInfo } from "../network"; export async function getBalance(address: string): Promise { const accountInfo = await getAccountInfo(address); - return BigInt(accountInfo.account_data.Balance); + return BigInt(accountInfo.balance); } diff --git a/libs/coin-modules/coin-xrp/src/logic/index.ts b/libs/coin-modules/coin-xrp/src/logic/index.ts index 67681c8d3fbb..034fa644dc3b 100644 --- a/libs/coin-modules/coin-xrp/src/logic/index.ts +++ b/libs/coin-modules/coin-xrp/src/logic/index.ts @@ -13,6 +13,3 @@ export { } from "./utils"; export { parseAPIValue } from "./common"; - -//FIXME -export { NEW_ACCOUNT_ERROR_MESSAGE } from "../network"; diff --git a/libs/coin-modules/coin-xrp/src/logic/utils.test.ts b/libs/coin-modules/coin-xrp/src/logic/utils.test.ts new file mode 100644 index 000000000000..1c76d6a024c7 --- /dev/null +++ b/libs/coin-modules/coin-xrp/src/logic/utils.test.ts @@ -0,0 +1,60 @@ +import { cachedRecipientIsNew } from "./utils"; + +jest.mock("ripple-address-codec", () => ({ + isValidClassicAddress: () => true, +})); +const mockGetAccountInfo = jest.fn(); +jest.mock("../network", () => ({ + getAccountInfo: (address: string) => mockGetAccountInfo(address), +})); + +describe("cachedRecipientIsNew", () => { + afterEach(() => { + mockGetAccountInfo.mockClear(); + }); + + it("returns true when network returns a new empty account", async () => { + // Given + mockGetAccountInfo.mockResolvedValueOnce({ + isNewAccount: true, + balance: "0", + ownerCount: 0, + sequence: 0, + }); + + // When + const result = await cachedRecipientIsNew("address1"); + + // Then + expect(mockGetAccountInfo).toHaveBeenCalledTimes(1); + expect(result).toBeTruthy(); + }); + + it("returns false when network a valid AccountInfo", async () => { + // Given + mockGetAccountInfo.mockResolvedValueOnce({ + isNewAccount: false, + balance: "999441667919804", + ownerCount: 0, + sequence: 999441667919804, + }); + + // When + const result = await cachedRecipientIsNew("address2"); + + // Then + expect(mockGetAccountInfo).toHaveBeenCalledTimes(1); + expect(result).toBeFalsy(); + }); + + it("throws an error when network throws an error", async () => { + // Given + mockGetAccountInfo.mockImplementationOnce(() => { + throw new Error("Malformed address"); + }); + + // When & Then + await expect(cachedRecipientIsNew("address3")).rejects.toThrow("Malformed address"); + expect(mockGetAccountInfo).toHaveBeenCalledTimes(1); + }); +}); diff --git a/libs/coin-modules/coin-xrp/src/logic/utils.ts b/libs/coin-modules/coin-xrp/src/logic/utils.ts index ff4bdaa91b85..2afeb46e5961 100644 --- a/libs/coin-modules/coin-xrp/src/logic/utils.ts +++ b/libs/coin-modules/coin-xrp/src/logic/utils.ts @@ -1,6 +1,6 @@ import BigNumber from "bignumber.js"; import { isValidClassicAddress } from "ripple-address-codec"; -import { getAccountInfo, NEW_ACCOUNT_ERROR_MESSAGE } from "../network"; +import { getAccountInfo } from "../network"; export const UINT32_MAX = new BigNumber(2).pow(32).minus(1); @@ -15,7 +15,7 @@ export const validateTag = (tag: BigNumber) => { export const getNextValidSequence = async (address: string) => { const accInfo = await getAccountInfo(address, true); - return accInfo.account_data.Sequence; + return accInfo.sequence; }; function isRecipientValid(recipient: string): boolean { @@ -26,10 +26,7 @@ const recipientIsNew = async (recipient: string): Promise => { if (!isRecipientValid(recipient)) return false; const info = await getAccountInfo(recipient); - if (info.error === NEW_ACCOUNT_ERROR_MESSAGE) { - return true; - } - return false; + return info.isNewAccount; }; const cacheRecipientsNew: Record = {}; diff --git a/libs/coin-modules/coin-xrp/src/network/index.test.ts b/libs/coin-modules/coin-xrp/src/network/index.test.ts new file mode 100644 index 000000000000..3934cf1f4f3e --- /dev/null +++ b/libs/coin-modules/coin-xrp/src/network/index.test.ts @@ -0,0 +1,114 @@ +import network from "@ledgerhq/live-network"; +import { getAccountInfo } from "."; +import coinConfig, { type XrpCoinConfig } from "../config"; + +jest.mock("@ledgerhq/live-network"); + +describe("getAccountInfo", () => { + beforeAll(() => { + coinConfig.setCoinConfig( + () => + ({ + node: "", + }) as XrpCoinConfig, + ); + }); + + it("returns an empty AccountInfo when returns an error 'actNotFound'", async () => { + // Given + const emptyAddress = "rNCgVpHinUDjXP2vHDFDMjm7ssBwpveHya"; + (network as jest.Mock).mockResolvedValue({ + data: { + result: { + account: emptyAddress, + error: "actNotFound", + error_code: 19, + error_message: "Account not found.", + ledger_hash: "F2E6EFD279C3663B62D9DC9977106EC25BA8F89DA551C2D7AB3AE5D75B146258", + ledger_index: 91772714, + request: { + account: emptyAddress, + command: "account_info", + ledger_index: "validated", + }, + status: "error", + validated: true, + }, + }, + }); + + // When + const result = await getAccountInfo(emptyAddress); + + // Then + expect(result).toEqual({ + isNewAccount: true, + balance: "0", + ownerCount: 0, + sequence: 0, + }); + }); + + it("returns a valid AccountInfo when return a correct AccountInfo", async () => { + // Given + const address = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb"; + (network as jest.Mock).mockResolvedValue({ + data: { + result: { + account_data: { + Account: address, + Balance: "999441667919804", + Flags: 0, + LedgerEntryType: "AccountRoot", + OwnerCount: 0, + PreviousTxnID: "947F03794C982FE4C7C9FECC4C33C543BB25B82938895EBA8F9B6021CC27A571", + PreviousTxnLgrSeq: 725208, + Sequence: 153743, + index: "BC0DAE09C0BFBC4A49AA94B849266588BFD6E1F554B184B5788AC55D6E07EB95", + }, + ledger_hash: "93E952B2770233B0ABFBFBBFBC3E2E2159DCABD07FEB5F4C49174027935D9FBB", + ledger_index: 1908009, + status: "success", + validated: true, + }, + }, + }); + + // When + const result = await getAccountInfo(address); + + // Then + expect(result).toEqual({ + isNewAccount: false, + balance: "999441667919804", + ownerCount: 0, + sequence: 153743, + }); + }); + + it("throws an error when backend returns any other error", async () => { + // Given + const invalidAddress = "rNCgVpHinUDjXP2vHDFDMjm7ssBwpveHyaa"; + (network as jest.Mock).mockResolvedValue({ + result: { + error: "actMalformed", + error_code: 35, + error_message: "Account malformed.", + ledger_hash: "87DE2DD287BCAD6E81720BC6E6361EF01A66EE70A37B6BDF1EFF2E719D9410AE", + ledger_index: 91772741, + request: { + account: invalidAddress, + command: "account_info", + ledger_index: "validated", + }, + status: "error", + validated: true, + }, + }); + + // When & Then + await expect(getAccountInfo(invalidAddress)).rejects.toThrow( + "Cannot read properties of undefined (reading 'result')", + ); + }); +}); diff --git a/libs/coin-modules/coin-xrp/src/network/index.ts b/libs/coin-modules/coin-xrp/src/network/index.ts index c8e3897952e4..dace2a441708 100644 --- a/libs/coin-modules/coin-xrp/src/network/index.ts +++ b/libs/coin-modules/coin-xrp/src/network/index.ts @@ -1,9 +1,12 @@ import network from "@ledgerhq/live-network"; import coinConfig from "../config"; +import type { AccountInfo } from "../types/model"; import { + isErrorResponse, isResponseStatus, type AccountInfoResponse, type AccountTxResponse, + type ErrorResponse, type LedgerResponse, type ServerInfoResponse, type SubmitReponse, @@ -20,10 +23,10 @@ export const submit = async (signature: string): Promise => { export const getAccountInfo = async ( recipient: string, current?: boolean, -): Promise => { +): Promise => { const { data: { result }, - } = await network<{ result: AccountInfoResponse }>({ + } = await network<{ result: AccountInfoResponse | ErrorResponse }>({ method: "POST", url: getNodeUrl(), data: { @@ -41,7 +44,21 @@ export const getAccountInfo = async ( throw new Error(`couldn't fetch account info ${recipient}`); } - return result; + if (isErrorResponse(result)) { + return { + isNewAccount: true, + balance: "0", + ownerCount: 0, + sequence: 0, + }; + } else { + return { + isNewAccount: false, + balance: result.account_data.Balance, + ownerCount: result.account_data.OwnerCount, + sequence: result.account_data.Sequence, + }; + } }; export const getServerInfos = async (): Promise => { diff --git a/libs/coin-modules/coin-xrp/src/network/types.ts b/libs/coin-modules/coin-xrp/src/network/types.ts index 998d0e8cc6d9..b7fe56c3939c 100644 --- a/libs/coin-modules/coin-xrp/src/network/types.ts +++ b/libs/coin-modules/coin-xrp/src/network/types.ts @@ -193,3 +193,22 @@ export type LedgerResponse = { ledger_index: number; validated: boolean; } & ResponseStatus; + +export type ErrorResponse = { + account: string; + error: string; + error_code: number; + error_message: string; + ledger_hash: string; + ledger_index: number; + request: { + account: string; + command: string; + ledger_index: string; + }; + status: string; + validated: boolean; +}; +export function isErrorResponse(obj: object): obj is ErrorResponse { + return "status" in obj && obj.status === "error" && "error" in obj; +} diff --git a/libs/coin-modules/coin-xrp/src/types/index.ts b/libs/coin-modules/coin-xrp/src/types/index.ts index 2aa9dd916aa1..34dbbe906c79 100644 --- a/libs/coin-modules/coin-xrp/src/types/index.ts +++ b/libs/coin-modules/coin-xrp/src/types/index.ts @@ -1,2 +1,3 @@ export * from "./bridge"; +export * from "./model"; export * from "./signer"; diff --git a/libs/coin-modules/coin-xrp/src/types/model.ts b/libs/coin-modules/coin-xrp/src/types/model.ts new file mode 100644 index 000000000000..0ce2da76f469 --- /dev/null +++ b/libs/coin-modules/coin-xrp/src/types/model.ts @@ -0,0 +1,6 @@ +export type AccountInfo = { + isNewAccount: boolean; + balance: string; + ownerCount: number; + sequence: number; +}; From 10e98531932661e2693277e726f894ab61809c7e Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Thu, 31 Oct 2024 09:44:03 +0100 Subject: [PATCH 21/25] =?UTF-8?q?fix(llm):=20=F0=9F=94=A7=20memo=20input?= =?UTF-8?q?=20on=20the=20send=20flow=20of=20Solana=20(#8201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(llm): solana memo tag send flow * chore: update change log --- .changeset/dull-plums-cheer.md | 5 +++++ .../src/families/algorand/MemoTagInput.tsx | 6 +++--- .../src/families/cardano/MemoTagInput.tsx | 6 +++--- .../src/families/casper/MemoTagInput.tsx | 6 +++--- .../src/families/cosmos/MemoTagInput.tsx | 6 +++--- .../src/families/crypto_org/MemoTagInput.tsx | 6 +++--- .../src/families/hedera/MemoTagInput.tsx | 6 +++--- .../src/families/internet_computer/MemoTagInput.tsx | 6 +++--- .../src/families/solana/MemoTagInput.tsx | 11 +++++++---- .../src/families/stacks/MemoTagInput.tsx | 6 +++--- .../src/families/stellar/MemoTagInput.tsx | 2 +- .../src/families/ton/MemoTagInput.tsx | 8 +++----- .../src/families/xrp/MemoTagInput.tsx | 6 +++--- .../MemoTag/components/GenericMemoTagInput.tsx | 4 ++-- .../newArch/features/MemoTag/hooks/useMemoTagInput.ts | 9 ++++----- .../src/newArch/features/MemoTag/types.ts | 4 +++- .../src/screens/SendFunds/02-SelectRecipient.tsx | 2 +- 17 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 .changeset/dull-plums-cheer.md diff --git a/.changeset/dull-plums-cheer.md b/.changeset/dull-plums-cheer.md new file mode 100644 index 000000000000..3b47a462695f --- /dev/null +++ b/.changeset/dull-plums-cheer.md @@ -0,0 +1,5 @@ +--- +"live-mobile": patch +--- + +Fix the memo on the Solana send flow diff --git a/apps/ledger-live-mobile/src/families/algorand/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/algorand/MemoTagInput.tsx index 12305111f864..b47a97cf6172 100644 --- a/apps/ledger-live-mobile/src/families/algorand/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/algorand/MemoTagInput.tsx @@ -4,9 +4,9 @@ import type { Transaction as AlgorandTransaction } from "@ledgerhq/live-common/f import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => ( - +export default (props: MemoTagInputProps) => ( + ({ memo: value || undefined })} + valueToTxPatch={value => tx => ({ ...tx, memo: value || undefined })} /> ); diff --git a/apps/ledger-live-mobile/src/families/cardano/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/cardano/MemoTagInput.tsx index 9ba0fcc5848f..3559c2d94810 100644 --- a/apps/ledger-live-mobile/src/families/cardano/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/cardano/MemoTagInput.tsx @@ -4,9 +4,9 @@ import type { Transaction as CardanoTransaction } from "@ledgerhq/live-common/fa import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => ( - +export default (props: MemoTagInputProps) => ( + ({ memo: value || undefined })} + valueToTxPatch={value => tx => ({ ...tx, memo: value || undefined })} /> ); diff --git a/apps/ledger-live-mobile/src/families/casper/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/casper/MemoTagInput.tsx index fb56651b4a10..7c502ec01b88 100644 --- a/apps/ledger-live-mobile/src/families/casper/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/casper/MemoTagInput.tsx @@ -5,13 +5,13 @@ import type { Transaction as CasperTransaction } from "@ledgerhq/live-common/fam import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => { +export default (props: MemoTagInputProps) => { const { t } = useTranslation(); return ( - + text.replace(/\D/g, "")} - valueToTxPatch={value => ({ transferId: value || undefined })} + valueToTxPatch={value => tx => ({ ...tx, transferId: value || undefined })} placeholder={t("send.summary.transferId")} /> ); diff --git a/apps/ledger-live-mobile/src/families/cosmos/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/cosmos/MemoTagInput.tsx index 931b4f4410a0..5cb714cc53c7 100644 --- a/apps/ledger-live-mobile/src/families/cosmos/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/cosmos/MemoTagInput.tsx @@ -4,9 +4,9 @@ import type { Transaction as CosmosTransaction } from "@ledgerhq/live-common/fam import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => ( - +export default (props: MemoTagInputProps) => ( + ({ memo: value || undefined })} + valueToTxPatch={value => tx => ({ ...tx, memo: value || undefined })} /> ); diff --git a/apps/ledger-live-mobile/src/families/crypto_org/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/crypto_org/MemoTagInput.tsx index 071f056ba57b..bd48ceb79db7 100644 --- a/apps/ledger-live-mobile/src/families/crypto_org/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/crypto_org/MemoTagInput.tsx @@ -4,9 +4,9 @@ import type { Transaction as CryptoOrgTransaction } from "@ledgerhq/live-common/ import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => ( - +export default (props: MemoTagInputProps) => ( + ({ memo: value || undefined })} + valueToTxPatch={value => tx => ({ ...tx, memo: value || undefined })} /> ); diff --git a/apps/ledger-live-mobile/src/families/hedera/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/hedera/MemoTagInput.tsx index c5e0ba790714..0dc6a31f20dd 100644 --- a/apps/ledger-live-mobile/src/families/hedera/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/hedera/MemoTagInput.tsx @@ -4,9 +4,9 @@ import type { Transaction as HederaTransaction } from "@ledgerhq/live-common/fam import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => ( - +export default (props: MemoTagInputProps) => ( + ({ memo: value || undefined })} + valueToTxPatch={value => tx => ({ ...tx, memo: value || undefined })} /> ); diff --git a/apps/ledger-live-mobile/src/families/internet_computer/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/internet_computer/MemoTagInput.tsx index 632e27873f67..1f543503e926 100644 --- a/apps/ledger-live-mobile/src/families/internet_computer/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/internet_computer/MemoTagInput.tsx @@ -4,10 +4,10 @@ import { Transaction as ICPTransaction } from "@ledgerhq/live-common/families/in import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => ( - +export default (props: MemoTagInputProps) => ( + text.replace(/\D/g, "")} - valueToTxPatch={value => ({ memo: value || undefined })} + valueToTxPatch={value => tx => ({ ...tx, memo: value || undefined })} /> ); diff --git a/apps/ledger-live-mobile/src/families/solana/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/solana/MemoTagInput.tsx index f8426619131f..8a12d8d93b3f 100644 --- a/apps/ledger-live-mobile/src/families/solana/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/solana/MemoTagInput.tsx @@ -1,12 +1,15 @@ +import merge from "lodash/merge"; import React from "react"; -import { Transaction as SolanaTransaction } from "@ledgerhq/live-common/generated/types"; +import { Transaction as SolanaTransaction } from "@ledgerhq/live-common/families/solana/types"; import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => ( - +export default (props: MemoTagInputProps) => ( + ({ memo: value || undefined })} + valueToTxPatch={value => tx => + merge({}, tx, { model: { uiState: { memo: value || undefined } } }) + } /> ); diff --git a/apps/ledger-live-mobile/src/families/stacks/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/stacks/MemoTagInput.tsx index 07874cc81084..f15b17279ef1 100644 --- a/apps/ledger-live-mobile/src/families/stacks/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/stacks/MemoTagInput.tsx @@ -4,9 +4,9 @@ import type { Transaction as StacksTransaction } from "@ledgerhq/live-common/fam import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => ( - +export default (props: MemoTagInputProps) => ( + ({ memo: value || undefined })} + valueToTxPatch={value => tx => ({ ...tx, memo: value || undefined })} /> ); diff --git a/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx index 6c58457e5910..75ff1d1345ea 100644 --- a/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/stellar/MemoTagInput.tsx @@ -19,7 +19,7 @@ export default ({ onChange }: MemoTagInputProps) => { const handleChange = (type: MemoType, value: string) => { const error = isMemoValid(type, value) ? undefined : new StellarWrongMemoFormat(); - const patch = { memoType: type, memoValue: value }; + const patch = (tx: StellarTransaction) => ({ ...tx, memoType: type, memoValue: value }); onChange({ value, patch, error }); }; diff --git a/apps/ledger-live-mobile/src/families/ton/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/ton/MemoTagInput.tsx index 8caca4d628ca..e52abb191a56 100644 --- a/apps/ledger-live-mobile/src/families/ton/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/ton/MemoTagInput.tsx @@ -5,14 +5,12 @@ import type { Transaction as TonTransaction } from "@ledgerhq/live-common/famili import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => { +export default (props: MemoTagInputProps) => { const { t } = useTranslation(); return ( - + - value ? { comment: { isEncrypted: false, text: value } } : { comment: undefined } - } + valueToTxPatch={value => tx => ({ ...tx, comment: { isEncrypted: false, text: value } })} placeholder={t("send.summary.comment")} /> ); diff --git a/apps/ledger-live-mobile/src/families/xrp/MemoTagInput.tsx b/apps/ledger-live-mobile/src/families/xrp/MemoTagInput.tsx index 9f5cc1ff9eaf..29b97083b0d1 100644 --- a/apps/ledger-live-mobile/src/families/xrp/MemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/families/xrp/MemoTagInput.tsx @@ -5,13 +5,13 @@ import type { Transaction as RippleTransaction } from "@ledgerhq/live-common/fam import type { MemoTagInputProps } from "LLM/features/MemoTag/types"; import { GenericMemoTagInput } from "LLM/features/MemoTag/components/GenericMemoTagInput"; -export default (props: MemoTagInputProps) => { +export default (props: MemoTagInputProps) => { const { t } = useTranslation(); return ( - + text.replace(/\D/g, "")} - valueToTxPatch={value => ({ tag: value ? Number(value) : undefined })} + valueToTxPatch={value => tx => ({ ...tx, tag: value ? Number(value) : undefined })} placeholder={t("send.summary.tag")} /> ); diff --git a/apps/ledger-live-mobile/src/newArch/features/MemoTag/components/GenericMemoTagInput.tsx b/apps/ledger-live-mobile/src/newArch/features/MemoTag/components/GenericMemoTagInput.tsx index 8f1efc78514e..c8c2a09333c1 100644 --- a/apps/ledger-live-mobile/src/newArch/features/MemoTag/components/GenericMemoTagInput.tsx +++ b/apps/ledger-live-mobile/src/newArch/features/MemoTag/components/GenericMemoTagInput.tsx @@ -2,11 +2,11 @@ import React from "react"; import type { Transaction } from "@ledgerhq/live-common/generated/types"; import { AnimatedInput } from "@ledgerhq/native-ui"; -import { MemoTagInputProps } from "../types"; +import { MemoTagInputProps, TxPatch } from "../types"; type Props = MemoTagInputProps & { textToValue?: (text: string) => string; - valueToTxPatch: (text: string) => Partial; + valueToTxPatch: (text: string) => TxPatch; }; export function GenericMemoTagInput({ diff --git a/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts b/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts index 17c816609869..9b3bdcb9284d 100644 --- a/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts +++ b/apps/ledger-live-mobile/src/newArch/features/MemoTag/hooks/useMemoTagInput.ts @@ -4,17 +4,16 @@ import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { Transaction } from "@ledgerhq/live-common/generated/types"; import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; import perFamily from "~/generated/MemoTagInput"; -import { MemoTagInputProps } from "../types"; +import { MemoTagInputProps, TxPatch } from "../types"; export const useMemoTagInput = ( family: CryptoCurrency["family"], - updateTransaction: (patch: Partial) => void, + updateTransaction: (patch: TxPatch) => void, ) => { const featureMemoTag = useFeature("llmMemoTag"); - const Input: FC | null = + const Input = (featureMemoTag?.enabled && - family in perFamily && - perFamily[family as keyof typeof perFamily]) || + (perFamily[family as keyof typeof perFamily] as FC)) || null; const [isEmpty, setIsEmpty] = useState(true); diff --git a/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts b/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts index 7fbbecc42c43..da5da1044126 100644 --- a/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts +++ b/apps/ledger-live-mobile/src/newArch/features/MemoTag/types.ts @@ -1,7 +1,9 @@ import type { Transaction } from "@ledgerhq/live-common/generated/types"; import type { AnimatedInputProps } from "@ledgerhq/native-ui/components/Form/Input/AnimatedInput"; +export type TxPatch = (tx: T) => T; + export type MemoTagInputProps = Omit< AnimatedInputProps, "value" | "onChangeText" | "onChange" -> & { onChange: (update: { patch: Partial; value: string; error?: Error }) => void }; +> & { onChange: (update: { patch: TxPatch; value: string; error?: Error }) => void }; diff --git a/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx b/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx index d42000658c03..c3c9cabe0682 100644 --- a/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx +++ b/apps/ledger-live-mobile/src/screens/SendFunds/02-SelectRecipient.tsx @@ -137,7 +137,7 @@ export default function SendSelectRecipient({ navigation, route }: Props) { useCallback( patch => { const bridge = getAccountBridge(account, parentAccount); - setTransaction(bridge.updateTransaction(transaction, patch)); + setTransaction(bridge.updateTransaction(transaction, patch(transaction))); }, [account, parentAccount, setTransaction, transaction], ), From 272caa90ae16425707b046c712e691fdf102f681 Mon Sep 17 00:00:00 2001 From: Lucas Werey <73439207+LucasWerey@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:32:43 +0100 Subject: [PATCH 22/25] :technologist:(chore) migrate hub scope to knip (#8220) :technologist:(chore) migrate hub scope to knip --- .github/workflows/test-desktop-reusable.yml | 2 +- .github/workflows/test-mobile-reusable.yml | 2 +- CODEOWNERS | 10 +- apps/ledger-live-desktop/.unimportedrc.json | 39 ------ apps/ledger-live-desktop/package.json | 4 +- apps/ledger-live-mobile/.unimportedrc.json | 45 ------- apps/ledger-live-mobile/package.json | 2 +- knip.json | 112 ++++++++++++++++++ libs/env/.unimportedrc.json | 5 - libs/env/package.json | 2 +- .../.unimportedrc.json | 4 - libs/hw-ledger-key-ring-protocol/package.json | 2 +- .../.unimportedrc.json | 16 --- libs/ledger-key-ring-protocol/package.json | 2 +- .../.unimportedrc.json | 4 - libs/live-countervalues-react/package.json | 2 +- libs/live-countervalues/.unimportedrc.json | 4 - libs/live-countervalues/package.json | 2 +- libs/live-hooks/.unimportedrc.json | 5 - libs/live-hooks/package.json | 2 +- libs/live-nft-react/.unimportedrc.json | 4 - libs/live-nft-react/package.json | 2 +- libs/live-nft/.unimportedrc.json | 4 - libs/live-nft/package.json | 2 +- libs/live-wallet/.unimportedrc.json | 19 --- libs/live-wallet/package.json | 2 +- libs/promise/.unimportedrc.json | 5 - libs/promise/package.json | 2 +- package.json | 2 + pnpm-lock.yaml | 104 +++++++++++++++- turbo.json | 3 + 31 files changed, 233 insertions(+), 182 deletions(-) delete mode 100644 apps/ledger-live-desktop/.unimportedrc.json delete mode 100644 apps/ledger-live-mobile/.unimportedrc.json create mode 100644 knip.json delete mode 100644 libs/env/.unimportedrc.json delete mode 100644 libs/hw-ledger-key-ring-protocol/.unimportedrc.json delete mode 100644 libs/ledger-key-ring-protocol/.unimportedrc.json delete mode 100644 libs/live-countervalues-react/.unimportedrc.json delete mode 100644 libs/live-countervalues/.unimportedrc.json delete mode 100644 libs/live-hooks/.unimportedrc.json delete mode 100644 libs/live-nft-react/.unimportedrc.json delete mode 100644 libs/live-nft/.unimportedrc.json delete mode 100644 libs/live-wallet/.unimportedrc.json delete mode 100644 libs/promise/.unimportedrc.json diff --git a/.github/workflows/test-desktop-reusable.yml b/.github/workflows/test-desktop-reusable.yml index ed11840cfe59..ef97e5e9be8d 100644 --- a/.github/workflows/test-desktop-reusable.yml +++ b/.github/workflows/test-desktop-reusable.yml @@ -68,7 +68,7 @@ jobs: name: lint-desktop path: ${{ github.workspace }}/apps/ledger-live-desktop/lint-desktop.json - name: check for dead code - run: pnpm desktop unimported + run: pnpm desktop knip-check shell: bash unit-tests: diff --git a/.github/workflows/test-mobile-reusable.yml b/.github/workflows/test-mobile-reusable.yml index 8f55389d423f..db126e93fb13 100644 --- a/.github/workflows/test-mobile-reusable.yml +++ b/.github/workflows/test-mobile-reusable.yml @@ -53,7 +53,7 @@ jobs: - name: Run linter run: pnpm lint --filter="live-mobile" --api="http://127.0.0.1:${{ steps.toolchain.outputs.port }}" --token="${{ secrets.TURBOREPO_SERVER_TOKEN }}" --team="foo" -- --format="json" -o="lint-mobile.json" - name: check for dead code - run: pnpm mobile unimported + run: pnpm mobile knip-check shell: bash - name: Run code checkers run: pnpm typecheck --filter="live-mobile" --api="http://127.0.0.1:${{ steps.toolchain.outputs.port }}" --token="${{ secrets.TURBOREPO_SERVER_TOKEN }}" --team="foo" diff --git a/CODEOWNERS b/CODEOWNERS index edd3ed1398a5..487735fc58c9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -23,14 +23,9 @@ libs/live-nft-react/ @ledgerhq/live-hub libs/live-nft/ @ledgerhq/live-hub libs/promise/ @ledgerhq/live-hub libs/ui/ @ledgerhq/live-hub -libs/live-hooks/ @ledgerhq/live-hub -libs/live-countervalues/ @ledgerhq/live-hub -libs/live-countervalues-react/ @ledgerhq/live-hub -libs/live-nft/ @ledgerhq/live-hub -libs/live-nft-react/ @ledgerhq/live-hub libs/live-wallet/ @ledgerhq/live-hub -libs/trustchain/ @ledgerhq/live-hub -libs/hw-trustchain/ @ledgerhq/live-hub +libs/ledger-key-ring-protocol/ @ledgerhq/live-hub +libs/hw-ledger-key-ring-protocol/ @ledgerhq/live-hub # Blockchain team .github/**/bot-*.yml @ledgerhq/live-blockchain-support @@ -77,6 +72,7 @@ libs/ledger-live-common/src/exchange/ @ledgerhq/p libs/exchange-module/ @ledgerhq/ptx libs/wallet-api-acre-module/ @ledgerhq/ptx libs/ledgerjs/packages/hw-app-exchange/ @ledgerhq/ptx + # Wallet API team **/PlatformAppProviderWrapper.tsx @ledgerhq/wallet-api **/Web3AppWebview/ @ledgerhq/wallet-api diff --git a/apps/ledger-live-desktop/.unimportedrc.json b/apps/ledger-live-desktop/.unimportedrc.json deleted file mode 100644 index 4ea42c55028b..000000000000 --- a/apps/ledger-live-desktop/.unimportedrc.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "entry": [ - "src/index.ts", - "src/renderer/index.ts", - "src/preloader/index.ts", - "src/renderer/webworkers/workers/*.ts", - "src/webviewPreloader/dappPreloader.ts", - "src/webviewPreloader/index.ts" - ], - "extensions": [".ts", ".js", ".jsx", ".tsx"], - "ignorePatterns": ["**/node_modules/**"], - "ignoreUnresolved": ["../../../../release-notes.json", "unzip-crx-3"], - "ignoreUnimported": [ - "**/*.test.*", - "**/*.spec.*", - "**/*.d.ts", - "**/types.js", - "**/types.*", - "node_modules/**", - "src/generate-cryptoassets-md.test.ts", - "src/generate-cryptoassets-md.ts", - - "src/newArch/features/Collectibles/**", - "src/newArch/features/WalletSync/__tests__/shared.tsx", - "src/renderer/DesktopStorageProvider.ts" - ], - "ignoreUnused": [ - "@types/semver", - "@types/qrcode", - "@types/react-key-handler", - "prop-types", - "allure-commandline", - "msw", - "undici" - ], - "aliases": { - "~/*": ["./src/*"] - } -} diff --git a/apps/ledger-live-desktop/package.json b/apps/ledger-live-desktop/package.json index fe5228c266d4..85662fd51056 100644 --- a/apps/ledger-live-desktop/package.json +++ b/apps/ledger-live-desktop/package.json @@ -48,8 +48,8 @@ "test:playwright:clean": "git clean -fdX tests/artifacts", "typecheck": "node scripts/typecheck.js", "check": "pnpm lint; pnpm typecheck", - "unimported": "unimported", - "assets:replace": "zx ./scripts/replace-assets.mjs" + "assets:replace": "zx ./scripts/replace-assets.mjs", + "knip-check": "pnpm knip --directory ../.. -W apps/ledger-live-desktop" }, "dependencies": { "@braze/web-sdk": "4.10.2", diff --git a/apps/ledger-live-mobile/.unimportedrc.json b/apps/ledger-live-mobile/.unimportedrc.json deleted file mode 100644 index 812172f14cf7..000000000000 --- a/apps/ledger-live-mobile/.unimportedrc.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "entry": ["index.js"], - "extensions": [".ts", ".js", ".jsx", ".tsx", ".d.ts"], - "ignorePatterns": ["**/node_modules/**"], - "ignoreUnresolved": [], - "ignoreUnimported": [ - "**/*.test.*", - "**/*.spec.*", - "**/*.d.ts", - "src/__test__/*", - "src/__tests__/*", - "src/**/__mocks__/*", - "src/**/*.android.*", - "src/**/*.ios.*", - "src/components/RootNavigator/types.ts", - "src/logic/keyboardVisible.ts", - "src/contentCards/cards/vertical/*", - "src/**/__integrations__/*.tsx", - "src/MobileStorageProvider.ts", - "src/newArch/features/WalletSync/components/Error/index.tsx", - "src/newArch/features/WalletSync/screens/Synchronize/PinCodeDisplay.tsx", - "src/newArch/features/WalletSync/screens/Synchronize/PinCodeInput.tsx" - ], - "ignoreUnused": [ - "@react-native-masked-view/masked-view", - "@react-native/gradle-plugin", - "@react-native/metro-config", - "@segment/sovran-react-native", - "expo-file-system", - "expo-image-loader", - "expo-modules-autolinking", - "expo-modules-core", - "expo-crypto", - "prop-types", - "react-native-codegen", - "react-native-fast-pbkdf2", - "react-native-level-fs", - "react-native-navigation-bar-color", - "react-native-os", - "react-native-randombytes", - "react-native-tcp", - "react-native-tcp-socket", - "react-native-udp" - ] -} diff --git a/apps/ledger-live-mobile/package.json b/apps/ledger-live-mobile/package.json index 661065d51545..31a39e855085 100644 --- a/apps/ledger-live-mobile/package.json +++ b/apps/ledger-live-mobile/package.json @@ -68,7 +68,7 @@ "download-hermes-profile": "zx ./scripts/download-hermes-profile.mjs", "e2e:loadConfig": "ts-node ./e2e/bridge/start-server.ts", "check": "pnpm lint; pnpm typecheck", - "unimported": "unimported", + "knip-check": "pnpm knip --directory ../.. -W apps/ledger-live-mobile", "assets:replace": "zx ./scripts/replace-assets.mjs" }, "dependencies": { diff --git a/knip.json b/knip.json new file mode 100644 index 000000000000..8fa93bfc396a --- /dev/null +++ b/knip.json @@ -0,0 +1,112 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "rules": { + "binaries": "off", + "classMembers": "off", + "dependencies": "error", + "devDependencies": "off", + "optionalPeerDependencies": "off", + "duplicates": "warn", + "enumMembers": "warn", + "exports": "warn", + "files": "error", + "nsExports": "off", + "nsTypes": "off", + "types": "warn", + "unlisted": "off", + "unresolved": "off" + }, + "ignoreExportsUsedInFile": true, + "project": ["src/**"], + + "ignore": [ + "**/__integrations__/*", + "**/*.test.*", + "**/*.spec.*", + "**/tests/**", + "**/__test__/**", + "**/__tests__/**", + "**/__mocks__/**", + "**/e2e/**", + "**/*.config.*", + "**/scripts/**", + "**/tools/**" + ], + + "workspaces": { + "./apps/ledger-live-desktop": { + "entry": [ + "src/index.ts", + "src/renderer/index.ts", + "src/preloader/index.ts", + "src/renderer/webworkers/workers/*.ts", + "src/webviewPreloader/dappPreloader.ts", + "src/webviewPreloader/index.ts" + ], + "ignore": ["src/newArch/features/Collectibles/**", "**/types.*"], + "ignoreBinaries": ["eslint", "knip", "prettier", "zx", "lint"], + "ignoreDependencies": ["prop-types", "allure-commandline", "msw"] + }, + "./apps/ledger-live-mobile": { + "entry": ["index.js", "src/families/**"], + "ignore": ["src/**/*.android.*", "src/**/*.ios.*", "src/logic/keyboardVisible.ts"], + "ignoreDependencies": [ + "@react-native-masked-view/masked-view", + "@react-native/gradle-plugin", + "@react-native/metro-config", + "asyncstorage-down", + "buffer", + "expo-crypto", + "expo-file-system", + "expo-image-loader", + "expo-modules-autolinking", + "expo-modules-core", + "prop-types", + "react-native-fast-pbkdf2", + "react-native-level-fs", + "react-native-navigation-bar-color", + "react-native-randombytes", + "react-native-tcp-socket", + "react-native-udp" + ] + }, + "./libs/live-nft-react": { + "entry": ["src/index.ts", "src/tools/*", "src/hooks/*"] + }, + "./libs/live-nft": { + "entry": ["src/api/index.ts", "src/index.ts", "src/index.test.ts", "src/types.ts"] + }, + "./libs/env": { + "entry": ["src/index.ts"] + }, + "./libs/live-hooks": { + "entry": ["src/useDebounce.ts", "src/useThrottledFunction.ts"] + }, + "./libs/live-countervalues": { + "entry": ["src/logic.ts", "src/api/index.ts", "src/types.ts", "src/portfolio.ts"] + }, + "./libs/live-countervalues-react": { + "entry": ["src/index.tsx", "src/portfolio.tsx"] + }, + "./libs/live-wallet": { + "entry": [ + "src/walletsync/index.ts", + "src/cloudsync/index.ts", + "src/liveqr/cross.ts", + "src/liveqr/importAccounts.ts", + "src/ordering.ts", + "src/addAccounts.ts", + "src/store.ts", + "src/accountName.ts" + ] + }, + "./libs/ledger-key-ring-protocol": { + "entry": ["src/index.ts", "src/store.ts", "src/qrcode/index.ts"] + }, + "./libs/hw-ledger-key-ring-protocol": { + "entry": ["src/index.ts"], + "ignoreDependencies": ["@ledgerhq/logs", "@ledgerhq/live-env"] + }, + "./libs/promise": { "entry": ["src/index.ts"] } + } +} diff --git a/libs/env/.unimportedrc.json b/libs/env/.unimportedrc.json deleted file mode 100644 index cbe49f21aaf0..000000000000 --- a/libs/env/.unimportedrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entry": ["src/index.ts"], - "ignoreUnimported": [], - "ignoreUnresolved": [] -} diff --git a/libs/env/package.json b/libs/env/package.json index 9d044a31ae73..f9938b945a6d 100644 --- a/libs/env/package.json +++ b/libs/env/package.json @@ -37,6 +37,6 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported" + "unimported": "pnpm knip --directory ../.. -W libs/env" } } diff --git a/libs/hw-ledger-key-ring-protocol/.unimportedrc.json b/libs/hw-ledger-key-ring-protocol/.unimportedrc.json deleted file mode 100644 index 3ee80ec81306..000000000000 --- a/libs/hw-ledger-key-ring-protocol/.unimportedrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "entry": ["src/index.ts"], - "ignoreUnused": ["@ledgerhq/logs", "@ledgerhq/live-env"] -} diff --git a/libs/hw-ledger-key-ring-protocol/package.json b/libs/hw-ledger-key-ring-protocol/package.json index 5c041f2cace0..44a8a699338d 100644 --- a/libs/hw-ledger-key-ring-protocol/package.json +++ b/libs/hw-ledger-key-ring-protocol/package.json @@ -41,7 +41,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported", + "unimported": "pnpm knip --directory ../.. -W libs/hw-ledger-key-ring-protocol", "test": "jest" }, "typesVersions": { diff --git a/libs/ledger-key-ring-protocol/.unimportedrc.json b/libs/ledger-key-ring-protocol/.unimportedrc.json deleted file mode 100644 index 15af0c544900..000000000000 --- a/libs/ledger-key-ring-protocol/.unimportedrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "entry": ["src/index.ts", "src/store.ts", "src/qrcode/index.ts"], - "ignorePatterns": [ - "**/node_modules/**", - "**/*.test.ts", - "**/__tests__/**", - "**/test-scenarios/**", - "**/test-helpers/**" - ], - "ignoreUnused": [ - "@ledgerhq/hw-transport-mocker", - "@ledgerhq/speculos-transport", - "@ledgerhq/types-devices", - "@ledgerhq/live-env" - ] -} diff --git a/libs/ledger-key-ring-protocol/package.json b/libs/ledger-key-ring-protocol/package.json index 7e4cc770a5a5..cc1121083235 100644 --- a/libs/ledger-key-ring-protocol/package.json +++ b/libs/ledger-key-ring-protocol/package.json @@ -84,7 +84,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported", + "unimported": "pnpm knip --directory ../.. -W libs/ledger-key-ring-protocol", "test": "jest", "e2e": "ts-node scripts/e2e.ts" } diff --git a/libs/live-countervalues-react/.unimportedrc.json b/libs/live-countervalues-react/.unimportedrc.json deleted file mode 100644 index 206907d69c3f..000000000000 --- a/libs/live-countervalues-react/.unimportedrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "entry": ["src/index.tsx", "src/portfolio.tsx"], - "ignoreUnused": ["@ledgerhq/cryptoassets"] -} diff --git a/libs/live-countervalues-react/package.json b/libs/live-countervalues-react/package.json index 880eee1f3249..28f457fff87b 100644 --- a/libs/live-countervalues-react/package.json +++ b/libs/live-countervalues-react/package.json @@ -50,7 +50,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported", + "unimported": "pnpm knip --directory ../.. -W libs/live-countervalues-react", "test": "jest" }, "typesVersions": { diff --git a/libs/live-countervalues/.unimportedrc.json b/libs/live-countervalues/.unimportedrc.json deleted file mode 100644 index d118f08853dc..000000000000 --- a/libs/live-countervalues/.unimportedrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "entry": ["src/logic.ts", "src/api/index.ts", "src/types.ts", "src/portfolio.ts"], - "ignoreUnused": ["@ledgerhq/cryptoassets"] -} diff --git a/libs/live-countervalues/package.json b/libs/live-countervalues/package.json index 55bed1a9ab12..11565e5d6253 100644 --- a/libs/live-countervalues/package.json +++ b/libs/live-countervalues/package.json @@ -44,7 +44,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported", + "unimported": "pnpm knip --directory ../.. -W libs/live-countervalues", "test": "jest" }, "typesVersions": { diff --git a/libs/live-hooks/.unimportedrc.json b/libs/live-hooks/.unimportedrc.json deleted file mode 100644 index 3cf5f8f13486..000000000000 --- a/libs/live-hooks/.unimportedrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entry": ["src/useDebounce.ts", "src/useThrottledFunction.ts"], - "ignoreUnimported": [], - "ignoreUnresolved": [] -} diff --git a/libs/live-hooks/package.json b/libs/live-hooks/package.json index c16aa78068a1..0299a630a078 100644 --- a/libs/live-hooks/package.json +++ b/libs/live-hooks/package.json @@ -41,7 +41,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported", + "unimported": "pnpm knip --directory ../.. -W libs/live-hooks", "test": "jest" }, "typesVersions": { diff --git a/libs/live-nft-react/.unimportedrc.json b/libs/live-nft-react/.unimportedrc.json deleted file mode 100644 index 187a263de59b..000000000000 --- a/libs/live-nft-react/.unimportedrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "entry": ["src/index.ts", "src/tools/*", "src/hooks/*"], - "ignoreUnused": ["@ledgerhq/coin-framework/nft/nftId"] -} diff --git a/libs/live-nft-react/package.json b/libs/live-nft-react/package.json index f4b2f65afc28..30d1f7320847 100644 --- a/libs/live-nft-react/package.json +++ b/libs/live-nft-react/package.json @@ -50,7 +50,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported", + "unimported": "pnpm knip --directory ../.. -W libs/live-nft-react", "test": "jest" }, "typesVersions": { diff --git a/libs/live-nft/.unimportedrc.json b/libs/live-nft/.unimportedrc.json deleted file mode 100644 index b6725fd9c9e7..000000000000 --- a/libs/live-nft/.unimportedrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "entry": ["src/api/*", "src/index.ts", "src/index.test.ts", "src/types.ts"], - "ignoreUnused": [] -} diff --git a/libs/live-nft/package.json b/libs/live-nft/package.json index 417a9b1373e8..c272a8dc00d9 100644 --- a/libs/live-nft/package.json +++ b/libs/live-nft/package.json @@ -42,7 +42,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported", + "unimported": "pnpm knip --directory ../.. -W libs/live-nft", "test": "jest" }, "typesVersions": { diff --git a/libs/live-wallet/.unimportedrc.json b/libs/live-wallet/.unimportedrc.json deleted file mode 100644 index 38730cf69452..000000000000 --- a/libs/live-wallet/.unimportedrc.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "entry": [ - "src/walletsync/index.ts", - "src/cloudsync/index.ts", - "src/liveqr/cross.ts", - "src/liveqr/importAccounts.ts", - "src/ordering.ts", - "src/addAccounts.ts", - "src/store.ts", - "src/accountName.ts" - ], - "ignoreUnresolved": ["bufferutil", "utf-8-validate"], - "ignorePatterns": [ - "**/node_modules/**", - "**/__tests__/**", - "**/*.test.ts", - "walletsync/__mocks__/**" - ] -} diff --git a/libs/live-wallet/package.json b/libs/live-wallet/package.json index acf45f5b0c72..44a0d791d4b7 100644 --- a/libs/live-wallet/package.json +++ b/libs/live-wallet/package.json @@ -60,7 +60,7 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "typecheck": "tsc --noEmit", - "unimported": "unimported", + "unimported": "pnpm knip --directory ../.. -W libs/live-wallet", "test": "jest" }, "typesVersions": { diff --git a/libs/promise/.unimportedrc.json b/libs/promise/.unimportedrc.json deleted file mode 100644 index cbe49f21aaf0..000000000000 --- a/libs/promise/.unimportedrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entry": ["src/index.ts"], - "ignoreUnimported": [], - "ignoreUnresolved": [] -} diff --git a/libs/promise/package.json b/libs/promise/package.json index 64098bd689ed..83fc8a0f17c9 100644 --- a/libs/promise/package.json +++ b/libs/promise/package.json @@ -36,6 +36,6 @@ "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", "lint:fix": "pnpm lint --fix", "test": "jest", - "unimported": "unimported" + "unimported": "pnpm knip --directory ../.. -W libs/promise" } } diff --git a/package.json b/package.json index 51f212be3c46..2565d4b35504 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "lint": "pnpm turbo lint --no-daemon", "lint:fix": "pnpm turbo lint:fix --no-daemon", "typecheck": "pnpm turbo typecheck --no-daemon", + "knip-check": "pnpm turbo knip-check", "unimported": "pnpm turbo unimported", "desktop": "pnpm --filter ledger-live-desktop", "cli": "pnpm --filter live-cli", @@ -171,6 +172,7 @@ "eslint-config-prettier": "9.1.0", "eslint-plugin-json": "3.1.0", "eslint-plugin-prettier": "5.1.3", + "knip": "5.34.1", "nyc": "15.1.0", "prettier": "3.2.5", "rimraf": "4.4.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4688165ac2c6..b602a9b29548 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: eslint-plugin-prettier: specifier: 5.1.3 version: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5) + knip: + specifier: 5.34.1 + version: 5.34.1(typescript@5.4.3) nyc: specifier: 15.1.0 version: 15.1.0 @@ -13268,6 +13271,11 @@ packages: resolution: {integrity: sha512-OU0YllH51/CxD8iyr3UHSMwYqTGTyuxFdCMH/0F978t+iDmJseC/ttrWPb22zmYkhkrjqtipzC1xaMuax5QKIA==} engines: {node: '>=16.0.0'} + '@snyk/github-codeowners@1.1.0': + resolution: {integrity: sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw==} + engines: {node: '>=8.10'} + hasBin: true + '@soda/friendly-errors-webpack-plugin@1.8.1': resolution: {integrity: sha512-h2ooWqP8XuFqTXT+NyAFbrArzfQA7R6HTezADrvD9Re8fxMLTPPniLdqVTdDaO0eIoLaAwKT+d6w+5GeTk7Vbg==} engines: {node: '>=8.0.0'} @@ -19111,6 +19119,9 @@ packages: resolution: {integrity: sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==} engines: {node: '>=6.0.0'} + easy-table@1.2.0: + resolution: {integrity: sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==} + ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} @@ -22727,6 +22738,10 @@ packages: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true + jiti@2.3.3: + resolution: {integrity: sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==} + hasBin: true + jmespath@0.15.0: resolution: {integrity: sha512-+kHj8HXArPfpPEKGLZ+kB5ONRTCiGQXo8RQYL0hH8t6pWXUBBK5KkkQmTNOwKK4LEsd0yTsgtjJVm4UBSZea4w==} engines: {node: '>= 0.6.0'} @@ -23063,6 +23078,14 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} + knip@5.34.1: + resolution: {integrity: sha512-MwQjEWBxsi7MnTL3JpLJwYRhqD1262Fv+TylMYDyZRAVJMmEs4y0qie5zv8VjU5DBSGoEs35Ssu7I/2klNOQgw==} + engines: {node: '>=18.6.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4' + known-css-properties@0.26.0: resolution: {integrity: sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg==} @@ -25198,6 +25221,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse-numeric-range@1.3.0: resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} @@ -26027,6 +26054,10 @@ packages: resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} engines: {node: '>= 0.8'} + pretty-ms@9.1.0: + resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} + engines: {node: '>=18'} + probot@13.2.2: resolution: {integrity: sha512-wx8VoNb0ujn2k3nq0MSIUd4r7/RUn2Di3ZY07F6O5IYm3z3HNGDDX2XEreJnaZDOFjQaZ6XJbYGmiaD4SovAVQ==} engines: {node: '>=18'} @@ -27905,6 +27936,10 @@ packages: resolution: {integrity: sha512-Tv3AVvZD9sjEwBvsm1jc8to96FwmZMOYt5UL7qu3vAuA/3F/hqKe9gdRSlkvpRO4ttUAUrDKRpjM0vUnXZQhqA==} hasBin: true + smol-toml@1.3.0: + resolution: {integrity: sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA==} + engines: {node: '>= 18'} + smoldot@2.0.22: resolution: {integrity: sha512-B50vRgTY6v3baYH6uCgL15tfaag5tcS2o/P5q1OiXcKGv1axZDfz2dzzMuIkVpyMR2ug11F6EAtQlmYBQd292g==} @@ -28459,6 +28494,9 @@ packages: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} + summary@2.1.0: + resolution: {integrity: sha512-nMIjMrd5Z2nuB2RZCKJfFMjgS3fygbeyGk9PxPPaJR1RIcyN9yn4A63Isovzm3ZtQuEkLBVgMdPup8UeLH7aQw==} + superagent@6.1.0: resolution: {integrity: sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==} engines: {node: '>= 7.0.0'} @@ -30797,6 +30835,12 @@ packages: peerDependencies: zod: ^3.18.0 + zod-validation-error@3.4.0: + resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.18.0 + zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} @@ -32169,7 +32213,7 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 - picocolors: 1.0.0 + picocolors: 1.1.1 '@babel/parser@7.24.1': dependencies: @@ -41011,6 +41055,12 @@ snapshots: '@smithy/types': 3.4.0 tslib: 2.6.2 + '@snyk/github-codeowners@1.1.0': + dependencies: + commander: 4.1.1 + ignore: 5.3.1 + p-map: 4.0.0 + '@soda/friendly-errors-webpack-plugin@1.8.1(webpack@5.95.0)': dependencies: chalk: 3.0.0 @@ -46462,7 +46512,7 @@ snapshots: caniuse-lite: 1.0.30001600 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.0.0 + picocolors: 1.1.1 postcss: 8.4.32 postcss-value-parser: 4.2.0 @@ -46472,7 +46522,7 @@ snapshots: caniuse-lite: 1.0.30001600 fraction.js: 4.3.7 normalize-range: 0.1.2 - picocolors: 1.0.0 + picocolors: 1.1.1 postcss: 8.4.38 postcss-value-parser: 4.2.0 @@ -49709,6 +49759,12 @@ snapshots: easy-stack@1.0.1: {} + easy-table@1.2.0: + dependencies: + ansi-regex: 5.0.1 + optionalDependencies: + wcwidth: 1.0.1 + ecc-jsbn@0.1.2: dependencies: jsbn: 0.1.1 @@ -55531,6 +55587,8 @@ snapshots: jiti@1.21.0: {} + jiti@2.3.3: {} + jmespath@0.15.0: {} joi@17.12.2: @@ -56037,6 +56095,26 @@ snapshots: klona@2.0.6: {} + knip@5.34.1(typescript@5.4.3): + dependencies: + '@nodelib/fs.walk': 1.2.8 + '@snyk/github-codeowners': 1.1.0 + easy-table: 1.2.0 + enhanced-resolve: 5.17.1 + fast-glob: 3.3.2 + jiti: 2.3.3 + js-yaml: 4.1.0 + minimist: 1.2.8 + picocolors: 1.1.1 + picomatch: 4.0.2 + pretty-ms: 9.1.0 + smol-toml: 1.3.0 + strip-json-comments: 5.0.1 + summary: 2.1.0 + typescript: 5.4.3 + zod: 3.23.8 + zod-validation-error: 3.4.0(zod@3.23.8) + known-css-properties@0.26.0: {} koa-bodyparser@4.4.1: @@ -56162,7 +56240,7 @@ snapshots: launch-editor@2.8.0: dependencies: - picocolors: 1.0.0 + picocolors: 1.1.1 shell-quote: 1.8.1 layout-base@1.0.2: {} @@ -59162,6 +59240,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-ms@4.0.0: {} + parse-numeric-range@1.3.0: {} parse-path@7.0.0: @@ -60212,6 +60292,10 @@ snapshots: pretty-hrtime@1.0.3: {} + pretty-ms@9.1.0: + dependencies: + parse-ms: 4.0.0 + probot@13.2.2: dependencies: '@octokit/core': 5.1.0 @@ -62812,6 +62896,8 @@ snapshots: transitivePeerDependencies: - supports-color + smol-toml@1.3.0: {} + smoldot@2.0.22: dependencies: ws: 8.17.1 @@ -63591,6 +63677,8 @@ snapshots: transitivePeerDependencies: - supports-color + summary@2.1.0: {} + superagent@6.1.0: dependencies: component-emitter: 1.3.1 @@ -65239,7 +65327,7 @@ snapshots: dependencies: browserslist: 4.23.0 escalade: 3.1.2 - picocolors: 1.0.0 + picocolors: 1.1.1 update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: @@ -66131,7 +66219,7 @@ snapshots: gzip-size: 6.0.0 html-escaper: 2.0.2 opener: 1.5.2 - picocolors: 1.0.0 + picocolors: 1.1.1 sirv: 2.0.4 ws: 7.5.10 transitivePeerDependencies: @@ -67185,6 +67273,10 @@ snapshots: dependencies: zod: 3.23.8 + zod-validation-error@3.4.0(zod@3.23.8): + dependencies: + zod: 3.23.8 + zod@3.22.4: {} zod@3.23.8: {} diff --git a/turbo.json b/turbo.json index 10083face441..b20a1090b618 100644 --- a/turbo.json +++ b/turbo.json @@ -81,6 +81,9 @@ "unimported": { "cache": false }, + "knip-check": { + "cache": false + }, "android:apk:local": { "dependsOn": ["^build"] }, From 38b9030f1d80f9749fc5139c849eb1de405a14bf Mon Sep 17 00:00:00 2001 From: Victor Alber Date: Thu, 31 Oct 2024 10:35:52 +0100 Subject: [PATCH 23/25] =?UTF-8?q?tests:=20=F0=9F=8E=AD=20Adding=20TRX=20ac?= =?UTF-8?q?count=20to=20fix=20broken=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ledger-live-desktop/tests/enum/Account.ts | 8 ++++++++ .../tests/specs/speculos/receive.address.spec.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/ledger-live-desktop/tests/enum/Account.ts b/apps/ledger-live-desktop/tests/enum/Account.ts index 135a922e8c48..7009d6eb1ae5 100644 --- a/apps/ledger-live-desktop/tests/enum/Account.ts +++ b/apps/ledger-live-desktop/tests/enum/Account.ts @@ -218,6 +218,14 @@ export class Account { 1, ); + static readonly TRX_3 = new Account( + Currency.TRX, + "Tron 3", + "TWBAMUMc69Z82zJtXh1TqdPyWv7PNyud6p", + undefined, + 2, + ); + static readonly XRP_1 = new Account( Currency.XRP, "XRP 1", diff --git a/apps/ledger-live-desktop/tests/specs/speculos/receive.address.spec.ts b/apps/ledger-live-desktop/tests/specs/speculos/receive.address.spec.ts index b9b12bb744fd..5ecf99b5bc6e 100644 --- a/apps/ledger-live-desktop/tests/specs/speculos/receive.address.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/speculos/receive.address.spec.ts @@ -76,7 +76,7 @@ for (const account of accounts) { } test.describe("Receive", () => { - const account = Account.TRX_2; + const account = Account.TRX_3; test.use({ userdata: "skip-onboarding", speculosApp: account.currency.speculosApp, From 57ea64ba638f33e30a3ad1ac320a0faa085785c3 Mon Sep 17 00:00:00 2001 From: Abdurrahman Sastim <106583189+abdurrahman-ledger@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:36:40 +0100 Subject: [PATCH 24/25] [QAA] CI: fix llm notif when job fails (#8259) ci: fix llm notif when job fails --- .github/workflows/test-mobile-e2e-reusable.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-mobile-e2e-reusable.yml b/.github/workflows/test-mobile-e2e-reusable.yml index d8aa6cea9b56..a7fbb34e8a1e 100644 --- a/.github/workflows/test-mobile-e2e-reusable.yml +++ b/.github/workflows/test-mobile-e2e-reusable.yml @@ -139,7 +139,7 @@ jobs: allure-report-ios: name: "Allure Reports Export on Server" runs-on: [ledger-live-medium] - if: ${{ inputs.slack_notif || github.event_name == 'push' }} + if: ${{ always() && (inputs.slack_notif || github.event_name == 'push') }} needs: [detox-tests-ios] outputs: report-url: ${{ steps.upload.outputs.report-url }} @@ -290,7 +290,7 @@ jobs: allure-report-android: name: "Allure Reports Export on Server" runs-on: [ledger-live-medium] - if: ${{ inputs.slack_notif || github.event_name == 'push' }} + if: ${{ always() && (inputs.slack_notif || github.event_name == 'push') }} outputs: report-url: ${{ steps.upload.outputs.report-url }} result: ${{ steps.summary.outputs.test_result }} @@ -376,7 +376,7 @@ jobs: report-on-slack: runs-on: ubuntu-22.04 needs: [allure-report-android, allure-report-ios] - if: ${{ (failure() && github.event_name == 'push') || inputs.slack_notif }} + if: ${{ (failure() && github.event_name == 'push') || (always() && inputs.slack_notif) }} env: IOS_STATUS: ${{ needs.allure-report-ios.outputs.status }} IOS_REPORT_URL: ${{ needs.allure-report-ios.outputs.report-url }} From 87218b17b86eaea9daa23c4c9cdf644c7ad2f65a Mon Sep 17 00:00:00 2001 From: Martin CAYUELAS <112866305+mcayuelas-ledger@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:50:58 +0100 Subject: [PATCH 25/25] =?UTF-8?q?feat:=20=E2=9C=A8=20Add=20spamFilteringTx?= =?UTF-8?q?=20in=20LLD=20(#7911)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ✨ Add spamFilteringTx hook in global feat: ✨ Add spamFilteringTx hook in global review: Enums * feat: 💄UX/UI Improvements on HiddenCollections section (#7912) Fix some UI broken + improve SimpleHash tool * bugfix: empty state account + Check when adding new address test: ✅ Add tests for new Hooks Check when adding new address ✅ Add tests for new Hooks --- .changeset/nervous-pumpkins-remain.md | 8 + .changeset/short-hotels-invite.md | 5 + .changeset/short-spoons-notice.md | 7 + .../Nfts/BreadCrumb/useBreadCrumbModel.tsx | 22 +-- .../Collections/useNftCollectionsModel.tsx | 43 +---- .../screens/Gallery/useNftGalleryModel.tsx | 35 +--- .../src/renderer/Default.tsx | 4 + .../src/renderer/actions/settings.ts | 10 ++ .../components/Breadcrumb/NFTCrumb.tsx | 23 +-- .../components/ContextMenu/NFTContextMenu.tsx | 2 +- .../CustomImage/NFTGallerySelector.tsx | 28 +-- .../components/OperationsList/Operation.tsx | 2 +- .../Web3AppWebview/PlatformAPIWebview.tsx | 2 +- .../Web3AppWebview/WalletAPIWebview.tsx | 6 +- .../NFTViewerDrawer/ExternalViewerButton.tsx | 2 +- .../families/bitcoin/SendRecipientFields.tsx | 2 +- .../__tests__/useHideSpamCollection.test.ts | 48 +++++ .../__tests__/useSyncNFTsWitHAccount.test.ts | 90 ++++++++++ .../hooks/{ => nfts}/useHideSpamCollection.ts | 9 +- .../renderer/hooks/nfts/useNftCollections.ts | 75 ++++++++ .../renderer/hooks/{ => nfts}/useNftLinks.ts | 6 +- .../hooks/nfts/useSyncNFTsWithAccounts.ts | 101 +++++++++++ .../modals/HideNftCollection/Footer.tsx | 13 +- .../renderer/modals/SimpleHashTools/index.tsx | 12 +- .../src/renderer/reducers/settings.ts | 24 ++- .../screens/nft/Collections/Collections.tsx | 46 ++--- .../renderer/screens/nft/Gallery/Gallery.tsx | 26 +-- .../src/renderer/screens/settings/index.tsx | 2 +- .../screens/settings/sections/About/index.tsx | 4 +- .../Accounts/HiddenNFTCollections.tsx | 169 ------------------ .../Accounts/HiddenNFTCollections/helpers.ts | 2 + .../Accounts/HiddenNFTCollections/index.tsx | 114 ++++++++++++ .../Accounts/HiddenNFTCollections/row.tsx | 127 +++++++++++++ .../screens/Account/NftCollectionsList.tsx | 6 +- .../CustomImage/NFTGallerySelector.tsx | 6 +- .../src/screens/Nft/NftGallery/index.tsx | 6 +- .../screens/Nft/WalletNftGallery/index.tsx | 6 +- .../SendFunds/01b-SelectCollection.tsx | 6 +- .../src/market/utils/timers.ts | 1 + .../live-nft-react/src/hooks/helpers/const.ts | 3 + .../live-nft-react/src/hooks/helpers/index.ts | 11 ++ libs/live-nft-react/src/hooks/types.ts | 1 + .../src/hooks/useCheckNftAccount.ts | 29 ++- libs/live-nft-react/src/index.ts | 1 + libs/live-nft/src/__tests__/index.test.ts | 34 +++- libs/live-nft/src/api/simplehash.ts | 7 +- libs/live-nft/src/index.ts | 10 +- 47 files changed, 796 insertions(+), 400 deletions(-) create mode 100644 .changeset/nervous-pumpkins-remain.md create mode 100644 .changeset/short-hotels-invite.md create mode 100644 .changeset/short-spoons-notice.md create mode 100644 apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts create mode 100644 apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useSyncNFTsWitHAccount.test.ts rename apps/ledger-live-desktop/src/renderer/hooks/{ => nfts}/useHideSpamCollection.ts (68%) create mode 100644 apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts rename apps/ledger-live-desktop/src/renderer/hooks/{ => nfts}/useNftLinks.ts (96%) create mode 100644 apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts delete mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/helpers.ts create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/row.tsx create mode 100644 libs/live-nft-react/src/hooks/helpers/const.ts diff --git a/.changeset/nervous-pumpkins-remain.md b/.changeset/nervous-pumpkins-remain.md new file mode 100644 index 000000000000..91ee80c43882 --- /dev/null +++ b/.changeset/nervous-pumpkins-remain.md @@ -0,0 +1,8 @@ +--- +"@ledgerhq/types-live": patch +"ledger-live-desktop": patch +"@ledgerhq/live-common": patch +"@ledgerhq/live-nft-react": patch +--- + +Add useCheckNftAccount Hook diff --git a/.changeset/short-hotels-invite.md b/.changeset/short-hotels-invite.md new file mode 100644 index 000000000000..81c53a5bd550 --- /dev/null +++ b/.changeset/short-hotels-invite.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +UX/UI Improvements on HiddenCollections section diff --git a/.changeset/short-spoons-notice.md b/.changeset/short-spoons-notice.md new file mode 100644 index 000000000000..907408392f60 --- /dev/null +++ b/.changeset/short-spoons-notice.md @@ -0,0 +1,7 @@ +--- +"ledger-live-desktop": patch +"@ledgerhq/live-common": patch +"@ledgerhq/live-nft-react": patch +--- + +use Hook CheckNft in Default and handle global sync of NFTs every 12 hours diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/BreadCrumb/useBreadCrumbModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/BreadCrumb/useBreadCrumbModel.tsx index 00be2b1f1c1c..ea7470522ba3 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/BreadCrumb/useBreadCrumbModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/BreadCrumb/useBreadCrumbModel.tsx @@ -2,39 +2,27 @@ import { useSelector } from "react-redux"; import { useHistory, useParams } from "react-router-dom"; import { State } from "~/renderer/reducers"; import { accountSelector } from "~/renderer/reducers/accounts"; -import { nftsByCollections } from "@ledgerhq/live-nft"; import { useCallback, useMemo } from "react"; import { ProtoNFT } from "@ledgerhq/types-live"; import { DropDownItemType } from "~/renderer/components/DropDownSelector"; import { setTrackingSource } from "~/renderer/analytics/TrackPage"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const useBreadCrumbModel = () => { const history = useHistory(); const { id, collectionAddress } = useParams<{ id?: string; collectionAddress?: string }>(); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; const account = useSelector((state: State) => - id ? accountSelector(state, { accountId: id }) : null, + id ? accountSelector(state, { accountId: id }) : undefined, ); - const { nfts } = useNftGalleryFilter({ - nftsOwned: account?.nfts || [], - addresses: String(account?.freshAddress), - chains: [String(account?.currency.id)], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + const { collections } = useNftCollections({ + account, }); - const collections = useMemo( - () => nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account?.nfts), - [account?.nfts, nfts, nftsFromSimplehashFeature], - ); - const items: DropDownItemType[] = useMemo( () => - Object.entries(collections).map(([contract, nfts]: [string, ProtoNFT[]]) => ({ + collections.map(([contract, nfts]: [string, ProtoNFT[]]) => ({ key: contract, label: contract, content: nfts[0], diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/Collections/useNftCollectionsModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/Collections/useNftCollectionsModel.tsx index d14d62a8e132..ae215c1c4490 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/Collections/useNftCollectionsModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/Collections/useNftCollectionsModel.tsx @@ -1,16 +1,10 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { Account, ProtoNFT } from "@ledgerhq/types-live"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { useHistory } from "react-router-dom"; import { openModal } from "~/renderer/actions/modals"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; -import { nftsByCollections } from "@ledgerhq/live-nft/index"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; -import { - filterHiddenCollections, - mapCollectionsToStructure, -} from "LLD/features/Collectibles/utils/collectionUtils"; +import { mapCollectionsToStructure } from "LLD/features/Collectibles/utils/collectionUtils"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; type NftsInTheCollections = { contract: string; @@ -27,9 +21,6 @@ const INCREMENT = 5; export const useNftCollectionsModel = ({ account }: Props) => { const history = useHistory(); const dispatch = useDispatch(); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); const [numberOfVisibleCollections, setNumberOfVisibleCollections] = useState(INCREMENT); const [displayShowMore, setDisplayShowMore] = useState(false); @@ -53,20 +44,10 @@ export const useNftCollectionsModel = ({ account }: Props) => { history.push(`/account/${account.id}/nft-collection`); }, [account.id, history]); - const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ - nftsOwned: account.nfts || [], - addresses: account.freshAddress, - chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + const { fetchNextPage, hasNextPage, collections, collectionsLength } = useNftCollections({ + account, }); - const collections = useMemo( - () => nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account.nfts), - [account.nfts, nfts, nftsFromSimplehashFeature], - ); - - const collectionsLength = Object.keys(collections).length; - const onShowMore = useCallback(() => { setNumberOfVisibleCollections(numberOfVisibleCollections => Math.min(numberOfVisibleCollections + INCREMENT, collectionsLength), @@ -74,21 +55,15 @@ export const useNftCollectionsModel = ({ account }: Props) => { if (hasNextPage) fetchNextPage(); }, [collectionsLength, fetchNextPage, hasNextPage]); - const filteredCollections = useMemo( - () => filterHiddenCollections(collections, hiddenNftCollections, account.id), - [account.id, collections, hiddenNftCollections], - ); - const nftsInTheCollection: NftsInTheCollections[] = useMemo( - () => - mapCollectionsToStructure(filteredCollections, numberOfVisibleCollections, onOpenCollection), - [filteredCollections, numberOfVisibleCollections, onOpenCollection], + () => mapCollectionsToStructure(collections, numberOfVisibleCollections, onOpenCollection), + [collections, numberOfVisibleCollections, onOpenCollection], ); useEffect(() => { - const moreToShow = numberOfVisibleCollections < filteredCollections.length; + const moreToShow = numberOfVisibleCollections < collections.length; setDisplayShowMore(moreToShow); - }, [numberOfVisibleCollections, filteredCollections.length]); + }, [numberOfVisibleCollections, collections.length]); return { nftsInTheCollection, diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx index e22136ad0620..d58c3b864797 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx @@ -2,14 +2,10 @@ import { useHistory, useParams } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import { State } from "~/renderer/reducers"; import { accountSelector } from "~/renderer/reducers/accounts"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; -import { useNftGalleryFilter, isThresholdValid } from "@ledgerhq/live-nft-react"; -import { nftsByCollections } from "@ledgerhq/live-nft"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { openModal } from "~/renderer/actions/modals"; import { useOnScreen } from "LLD/hooks/useOnScreen"; -import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; -import { ChainsEnum } from "LLD/features/Collectibles/types/enum/Chains"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const defaultNumberOfVisibleNfts = 10; @@ -18,31 +14,17 @@ const useNftGalleryModel = () => { const history = useHistory(); const { id } = useParams<{ id: string }>(); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const threshold = nftsFromSimplehashFeature?.params?.threshold; - const listFooterRef = useRef(null); const [maxVisibleNFTs, setMaxVisibleNFTs] = useState(defaultNumberOfVisibleNfts); - const { account, hiddenNftCollections } = useSelector((state: State) => ({ + const { account } = useSelector((state: State) => ({ account: accountSelector(state, { accountId: id }), - hiddenNftCollections: hiddenNftCollectionsSelector(state), })); - const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ - nftsOwned: account?.nfts || [], - addresses: account?.freshAddress || "", - chains: [account?.currency.id ?? ChainsEnum.ETHEREUM], - threshold: isThresholdValid(threshold) ? Number(threshold) : 75, + const { fetchNextPage, hasNextPage, collections, allNfts } = useNftCollections({ + account, }); - const collections = useMemo(() => { - const allNfts = nftsFromSimplehashFeature?.enabled ? nfts : account?.nfts; - return Object.entries(nftsByCollections(allNfts)).filter( - ([contract]) => !hiddenNftCollections.includes(`${account?.id}|${contract}`), - ); - }, [account?.id, account?.nfts, hiddenNftCollections, nfts, nftsFromSimplehashFeature?.enabled]); - useEffect(() => { if (collections.length < 1) { history.push(`/account/${account?.id}/`); @@ -70,13 +52,13 @@ const useNftGalleryModel = () => { }, [hasNextPage, fetchNextPage]); useOnScreen({ - enabled: maxVisibleNFTs < nfts?.length, + enabled: maxVisibleNFTs < allNfts?.length, onIntersect: updateMaxVisibleNtfs, target: listFooterRef, threshold: 0.5, }); - const nftsByCollection = nfts.reduce( + const nftsByCollection = allNfts.reduce( (acc, nft) => { const collectionKey = nft.contract || "-"; if (!acc[collectionKey]) { @@ -85,12 +67,11 @@ const useNftGalleryModel = () => { acc[collectionKey].push(nft); return acc; }, - {} as Record, + {} as Record, ); return { account, - hiddenNftCollections, nftsByCollection, listFooterRef, collections, diff --git a/apps/ledger-live-desktop/src/renderer/Default.tsx b/apps/ledger-live-desktop/src/renderer/Default.tsx index 0bed65b6e48a..421e5089469b 100644 --- a/apps/ledger-live-desktop/src/renderer/Default.tsx +++ b/apps/ledger-live-desktop/src/renderer/Default.tsx @@ -56,6 +56,7 @@ import { isLocked as isLockedSelector } from "~/renderer/reducers/application"; import { useAutoDismissPostOnboardingEntryPoint } from "@ledgerhq/live-common/postOnboarding/hooks/index"; import { setShareAnalytics, setSharePersonalizedRecommendations } from "./actions/settings"; import useEnv from "@ledgerhq/live-common/hooks/useEnv"; +import { useSyncNFTsWithAccounts } from "./hooks/nfts/useSyncNFTsWithAccounts"; const PlatformCatalog = lazy(() => import("~/renderer/screens/platform")); const Dashboard = lazy(() => import("~/renderer/screens/dashboard")); @@ -202,12 +203,15 @@ export default function Default() { useRecoverRestoreOnboarding(); useAutoDismissPostOnboardingEntryPoint(); + useSyncNFTsWithAccounts(); + const analyticsFF = useFeature("lldAnalyticsOptInPrompt"); const hasSeenAnalyticsOptInPrompt = useSelector(hasSeenAnalyticsOptInPromptSelector); const nftReworked = useFeature("lldNftsGalleryNewArch"); const isLocked = useSelector(isLockedSelector); const dispatch = useDispatch(); const isNftReworkedEnabled = nftReworked?.enabled; + useEffect(() => { if ( !isLocked && diff --git a/apps/ledger-live-desktop/src/renderer/actions/settings.ts b/apps/ledger-live-desktop/src/renderer/actions/settings.ts index e90520966f32..bc199fe40776 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/settings.ts @@ -218,6 +218,12 @@ export const hideNftCollection = (collectionId: string) => ({ type: "HIDE_NFT_COLLECTION", payload: collectionId, }); + +export const whitelistNftCollection = (collectionId: string) => ({ + type: "WHITELIST_NFT_COLLECTION", + payload: collectionId, +}); + export const hideOrdinalsAsset = (inscriptionId: string) => ({ type: "HIDE_ORDINALS_ASSET", payload: inscriptionId, @@ -252,6 +258,10 @@ export const unhideNftCollection = (collectionId: string) => ({ type: "UNHIDE_NFT_COLLECTION", payload: collectionId, }); +export const unwhitelistNftCollection = (collectionId: string) => ({ + type: "UNWHITELIST_NFT_COLLECTION", + payload: collectionId, +}); export const unhideOrdinalsAsset = (inscriptionId: string) => ({ type: "UNHIDE_ORDINALS_ASSET", payload: inscriptionId, diff --git a/apps/ledger-live-desktop/src/renderer/components/Breadcrumb/NFTCrumb.tsx b/apps/ledger-live-desktop/src/renderer/components/Breadcrumb/NFTCrumb.tsx index 58d168582774..f26dd379142f 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Breadcrumb/NFTCrumb.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Breadcrumb/NFTCrumb.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, memo } from "react"; import { useHistory, useParams } from "react-router-dom"; import { useSelector } from "react-redux"; -import { nftsByCollections } from "@ledgerhq/live-nft"; import { accountSelector } from "~/renderer/reducers/accounts"; import DropDownSelector, { DropDownItemType } from "~/renderer/components/DropDownSelector"; import Button from "~/renderer/components/Button"; @@ -14,8 +13,7 @@ import { setTrackingSource } from "~/renderer/analytics/TrackPage"; import CollectionName from "~/renderer/components/Nft/CollectionName"; import { ProtoNFT } from "@ledgerhq/types-live"; import { State } from "~/renderer/reducers"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const LabelWithMeta = ({ item, @@ -38,38 +36,29 @@ const LabelWithMeta = ({ const NFTCrumb = () => { const history = useHistory(); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; const { id, collectionAddress } = useParams<{ id?: string; collectionAddress?: string }>(); const account = useSelector((state: State) => id ? accountSelector(state, { accountId: id, }) - : null, + : undefined, ); - const { nfts } = useNftGalleryFilter({ - nftsOwned: account?.nfts || [], - addresses: String(account?.freshAddress), - chains: [String(account?.currency.id)], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + const { collections } = useNftCollections({ + account, }); - const collections = useMemo( - () => nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account?.nfts), - [account?.nfts, nfts, nftsFromSimplehashFeature], - ); - const items: DropDownItemType[] = useMemo( () => - Object.entries(collections).map(([contract, nfts]: [string, ProtoNFT[]]) => ({ + collections.map(([contract, nfts]: [string, ProtoNFT[]]) => ({ key: contract, label: contract, content: nfts[0], })), [collections], ); + const activeItem: DropDownItemType | undefined | null = useMemo( () => items.find(item => item.key === collectionAddress) || items[0], [collectionAddress, items], diff --git a/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTContextMenu.tsx b/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTContextMenu.tsx index bbd3e824f31a..70f1af44ee04 100644 --- a/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTContextMenu.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/ContextMenu/NFTContextMenu.tsx @@ -1,7 +1,7 @@ import React, { memo } from "react"; import ContextMenuItem from "./ContextMenuItem"; import { Account, ProtoNFT, NFTMetadata } from "@ledgerhq/types-live"; -import useNftLinks from "~/renderer/hooks/useNftLinks"; +import useNftLinks from "~/renderer/hooks/nfts/useNftLinks"; type Props = { account: Account; diff --git a/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx b/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx index 74f75add2433..39b28538b99b 100644 --- a/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx @@ -2,16 +2,15 @@ import React, { useMemo, useRef, useState } from "react"; import { useSelector } from "react-redux"; import { Flex, Grid, InfiniteLoader, Text } from "@ledgerhq/react-ui"; import { NFTMetadata } from "@ledgerhq/types-live"; -import { accountsSelector, orderedVisibleNftsSelector } from "../../reducers/accounts"; +import { accountsSelector, orderedVisibleNftsSelector } from "~/renderer/reducers/accounts"; import NftGalleryEmptyState from "./NftGalleryEmptyState"; import isEqual from "lodash/isEqual"; import NFTItem from "./NFTItem"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; import { useOnScreen } from "~/renderer/screens/nft/useOnScreen"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; import { getEnv } from "@ledgerhq/live-env"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const ScrollContainer = styled(Flex).attrs({ flexDirection: "column", @@ -32,8 +31,6 @@ type Props = { const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const threshold = nftsFromSimplehashFeature?.params?.threshold; const accounts = useSelector(accountsSelector); const nftsOrdered = useSelector(orderedVisibleNftsSelector, isEqual); @@ -47,26 +44,19 @@ const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { [accounts], ); - const { - nfts: nftsFiltered, - fetchNextPage, - hasNextPage, - } = useNftGalleryFilter({ - nftsOwned: nftsOrdered || [], + const { fetchNextPage, hasNextPage, allNfts } = useNftCollections({ + nftsOwned: nftsOrdered, addresses: addresses, chains: SUPPORTED_NFT_CURRENCIES, - threshold: isThresholdValid(threshold) ? Number(threshold) : 75, }); - const nfts = nftsFromSimplehashFeature?.enabled ? nftsFiltered : nftsOrdered; - const { t } = useTranslation(); const [displayedCount, setDisplayedCount] = useState(10); const content = useMemo( () => - nfts.slice(0, displayedCount).map((nft, index) => { + allNfts.slice(0, displayedCount).map((nft, index) => { const { id } = nft; return ( { /> ); }), - [nfts, displayedCount, selectedNftId, handlePickNft], + [allNfts, displayedCount, selectedNftId, handlePickNft], ); const loaderContainerRef = useRef(null); @@ -91,13 +81,13 @@ const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { }; useOnScreen({ - enabled: displayedCount < nfts.length, + enabled: displayedCount < allNfts.length, onIntersect: updateDisplayable, target: loaderContainerRef, threshold: 0.5, }); - if (nfts.length <= 0) return ; + if (allNfts.length <= 0) return ; return ( @@ -108,7 +98,7 @@ const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { {content} - {displayedCount < nfts.length ? ( + {displayedCount < allNfts.length ? ( diff --git a/apps/ledger-live-desktop/src/renderer/components/OperationsList/Operation.tsx b/apps/ledger-live-desktop/src/renderer/components/OperationsList/Operation.tsx index 8b962a07f3df..79cf5176108d 100644 --- a/apps/ledger-live-desktop/src/renderer/components/OperationsList/Operation.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/OperationsList/Operation.tsx @@ -15,7 +15,7 @@ import { confirmationsNbForCurrencySelector } from "~/renderer/reducers/settings import { isConfirmedOperation } from "@ledgerhq/live-common/operation"; import { useAccountUnit } from "~/renderer/hooks/useAccountUnit"; import { State } from "~/renderer/reducers"; -import { useAccountName } from "../../reducers/wallet"; +import { useAccountName } from "~/renderer/reducers/wallet"; const OperationRow = styled(Box).attrs(() => ({ horizontal: true, diff --git a/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/PlatformAPIWebview.tsx b/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/PlatformAPIWebview.tsx index 42edf84b62ae..d6139d5825f1 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/PlatformAPIWebview.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/PlatformAPIWebview.tsx @@ -25,7 +25,7 @@ import { } from "@ledgerhq/live-common/platform/react"; import trackingWrapper from "@ledgerhq/live-common/platform/tracking"; import { openModal } from "../../actions/modals"; -import { flattenAccountsSelector } from "../../reducers/accounts"; +import { flattenAccountsSelector } from "~/renderer/reducers/accounts"; import BigSpinner from "../BigSpinner"; import { track } from "~/renderer/analytics/segment"; import { diff --git a/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/WalletAPIWebview.tsx b/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/WalletAPIWebview.tsx index c1237d6124bb..a2a5016f063d 100644 --- a/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/WalletAPIWebview.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/Web3AppWebview/WalletAPIWebview.tsx @@ -35,9 +35,9 @@ import { setDrawer } from "~/renderer/drawers/Provider"; import { shareAnalyticsSelector } from "~/renderer/reducers/settings"; import { walletSelector } from "~/renderer/reducers/wallet"; import { getStoreValue, setStoreValue } from "~/renderer/store"; -import { updateAccountWithUpdater } from "../../actions/accounts"; -import { openModal } from "../../actions/modals"; -import { flattenAccountsSelector } from "../../reducers/accounts"; +import { updateAccountWithUpdater } from "~/renderer/actions/accounts"; +import { openModal } from "~/renderer/actions/modals"; +import { flattenAccountsSelector } from "~/renderer/reducers/accounts"; import BigSpinner from "../BigSpinner"; import { NetworkErrorScreen } from "./NetworkError"; import { NoAccountOverlay } from "./NoAccountOverlay"; diff --git a/apps/ledger-live-desktop/src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.tsx b/apps/ledger-live-desktop/src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.tsx index 365f1b619c92..d2ba072556b2 100644 --- a/apps/ledger-live-desktop/src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.tsx +++ b/apps/ledger-live-desktop/src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.tsx @@ -5,7 +5,7 @@ import Box from "~/renderer/components/Box"; import Button from "~/renderer/components/Button"; import DropDownSelector, { DropDownItem } from "~/renderer/components/DropDownSelector"; import IconExternal from "~/renderer/icons/ExternalLink"; -import useNftLinks from "~/renderer/hooks/useNftLinks"; +import useNftLinks from "~/renderer/hooks/nfts/useNftLinks"; import { setDrawer } from "~/renderer/drawers/Provider"; import { Account, ProtoNFT, NFTMetadata } from "@ledgerhq/types-live"; import { Icons } from "@ledgerhq/react-ui"; diff --git a/apps/ledger-live-desktop/src/renderer/families/bitcoin/SendRecipientFields.tsx b/apps/ledger-live-desktop/src/renderer/families/bitcoin/SendRecipientFields.tsx index d856fdfe9827..2f41c9d6ffaf 100644 --- a/apps/ledger-live-desktop/src/renderer/families/bitcoin/SendRecipientFields.tsx +++ b/apps/ledger-live-desktop/src/renderer/families/bitcoin/SendRecipientFields.tsx @@ -7,7 +7,7 @@ import { connect } from "react-redux"; import Alert from "~/renderer/components/Alert"; import TranslatedError from "~/renderer/components/TranslatedError"; import { State } from "~/renderer/reducers"; -import { confirmationsNbForCurrencySelector } from "../../reducers/settings"; +import { confirmationsNbForCurrencySelector } from "~/renderer/reducers/settings"; // FIXME: ConfirmationNB seems to be specific. // So we can't do diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts new file mode 100644 index 000000000000..f6406bd162c4 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts @@ -0,0 +1,48 @@ +import { hideNftCollection } from "~/renderer/actions/settings"; +import { useHideSpamCollection } from "../useHideSpamCollection"; +import { renderHook } from "tests/testUtils"; +import { INITIAL_STATE } from "~/renderer/reducers/settings"; +import { useDispatch } from "react-redux"; + +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: jest.fn(), +})); + +const mockDispatch = jest.fn(); + +describe("useHideSpamCollection", () => { + beforeEach(() => { + (useDispatch as jest.Mock).mockReturnValue(mockDispatch); + mockDispatch.mockClear(); + }); + + it("should dispatch hideNftCollection action if collection is not whitelisted", () => { + const { result } = renderHook(() => useHideSpamCollection(), { + initialState: { + settings: { + ...INITIAL_STATE, + whitelistedNftCollections: ["collectionA", "collectionB"], + hiddenNftCollections: [], + }, + }, + }); + result.current.hideSpamCollection("collectionC"); + + expect(mockDispatch).toHaveBeenCalledWith(hideNftCollection("collectionC")); + }); + + it("should not dispatch hideNftCollection action if collection is whitelisted", () => { + const { result } = renderHook(() => useHideSpamCollection(), { + initialState: { + settings: { + hiddenNftCollections: [], + whitelistedNftCollections: ["collectionA", "collectionB"], + }, + }, + }); + result.current.hideSpamCollection("collectionA"); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useSyncNFTsWitHAccount.test.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useSyncNFTsWitHAccount.test.ts new file mode 100644 index 000000000000..d43721130372 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useSyncNFTsWitHAccount.test.ts @@ -0,0 +1,90 @@ +import { useSelector } from "react-redux"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { useCheckNftAccount } from "@ledgerhq/live-nft-react"; +import { useHideSpamCollection } from "../useHideSpamCollection"; +import { useSyncNFTsWithAccounts } from "../useSyncNFTsWithAccounts"; + +import { accountsSelector, orderedVisibleNftsSelector } from "~/renderer/reducers/accounts"; +import { renderHook } from "@testing-library/react"; + +jest.mock("react-redux", () => ({ + useSelector: jest.fn(), +})); + +jest.mock("@ledgerhq/live-common/featureFlags/index", () => ({ + useFeature: jest.fn(), +})); + +jest.mock("../useHideSpamCollection", () => ({ + useHideSpamCollection: jest.fn(), +})); + +jest.mock("@ledgerhq/live-nft-react", () => ({ + useCheckNftAccount: jest.fn(), + isThresholdValid: jest.fn(), + getThreshold: jest.fn(), +})); + +describe("useSyncNFTsWithAccounts", () => { + const mockUseSelector = useSelector as jest.Mock; + const mockUseFeature = useFeature as jest.Mock; + const mockUseHideSpamCollection = useHideSpamCollection as jest.Mock; + const mockUseCheckNftAccount = useCheckNftAccount as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should refetch periodically based on TIMER", () => { + const mockRefetch = jest.fn(); + const mockAccounts = [{ freshAddress: "0x123" }, { freshAddress: "0x456" }]; + + mockUseFeature.mockReturnValue({ enabled: true }); + mockUseHideSpamCollection.mockReturnValue({ enabled: true, hideSpamCollection: jest.fn() }); + mockUseSelector.mockImplementation(selector => { + if (selector === accountsSelector) return mockAccounts; + if (selector === orderedVisibleNftsSelector) return []; + return []; + }); + mockUseCheckNftAccount.mockReturnValue({ refetch: mockRefetch }); + + renderHook(() => useSyncNFTsWithAccounts()); + + jest.advanceTimersByTime(5 * 60 * 60 * 1000); + + expect(mockRefetch).toHaveBeenCalledTimes(1); + }); + + it("should refetch immediately when a new account is added", () => { + const mockRefetch = jest.fn(); + const initialAccounts = [{ freshAddress: "0x123" }]; + const updatedAccounts = [...initialAccounts, { freshAddress: "0x789" }]; + + mockUseFeature.mockReturnValue({ enabled: true }); + mockUseHideSpamCollection.mockReturnValue({ enabled: true, hideSpamCollection: jest.fn() }); + mockUseSelector + .mockImplementationOnce(selector => { + if (selector === accountsSelector) return initialAccounts; + if (selector === orderedVisibleNftsSelector) return []; + return []; + }) + .mockImplementationOnce(selector => { + if (selector === accountsSelector) return updatedAccounts; + if (selector === orderedVisibleNftsSelector) return []; + return []; + }); + + mockUseCheckNftAccount.mockReturnValue({ refetch: mockRefetch }); + + const { rerender } = renderHook(() => useSyncNFTsWithAccounts()); + + rerender(); + + expect(mockRefetch).toHaveBeenCalledTimes(2); // 1 for initial render & 1 for adding new account + }); +}); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useHideSpamCollection.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts similarity index 68% rename from apps/ledger-live-desktop/src/renderer/hooks/useHideSpamCollection.ts rename to apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts index e4ac73c9cc1a..7772d51dddee 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useHideSpamCollection.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts @@ -1,20 +1,21 @@ import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { whitelistedNftCollectionsSelector } from "~/renderer/reducers/settings"; import { hideNftCollection } from "~/renderer/actions/settings"; -import { hiddenNftCollectionsSelector } from "../reducers/settings"; export function useHideSpamCollection() { const spamFilteringTxFeature = useFeature("spamFilteringTx"); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); + const dispatch = useDispatch(); const hideSpamCollection = useCallback( (collection: string) => { - if (!hiddenNftCollections.includes(collection)) { + if (!whitelistedNftCollections.includes(collection)) { dispatch(hideNftCollection(collection)); } }, - [dispatch, hiddenNftCollections], + [dispatch, whitelistedNftCollections], ); return { diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts new file mode 100644 index 000000000000..16832100f6e5 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollections.ts @@ -0,0 +1,75 @@ +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { decodeCollectionId, getThreshold, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { nftsByCollections } from "@ledgerhq/live-nft/index"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { Account, ProtoNFT } from "@ledgerhq/types-live"; +import { useMemo } from "react"; +import { useSelector } from "react-redux"; +import { + hiddenNftCollectionsSelector, + whitelistedNftCollectionsSelector, +} from "~/renderer/reducers/settings"; + +export function useNftCollections({ + account, + nftsOwned, + addresses, + chains, +}: { + account?: Account; + nftsOwned?: ProtoNFT[]; + addresses?: string; + chains?: string[]; +}) { + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); + const threshold = nftsFromSimplehashFeature?.params?.threshold; + const simplehashEnabled = nftsFromSimplehashFeature?.enabled; + + const whitelistNft = useSelector(whitelistedNftCollectionsSelector); + const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + + const nftsOwnedToCheck = useMemo(() => account?.nfts ?? nftsOwned, [account?.nfts, nftsOwned]); + + const whitelistedNfts = useMemo( + () => + nftsOwnedToCheck?.filter(nft => + whitelistNft + .map(collection => decodeCollectionId(collection).contractAddress) + .includes(nft.contract), + ) ?? [], + [nftsOwnedToCheck, whitelistNft], + ); + + const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ + nftsOwned: account?.nfts ?? nftsOwned ?? [], + addresses: account?.freshAddress ?? addresses ?? "", + chains: account + ? [account.currency.id || BlockchainEVM.Ethereum] + : chains ?? [BlockchainEVM.Ethereum], + threshold: getThreshold(threshold), + }); + + const allNfts = useMemo( + () => (simplehashEnabled ? [...nfts, ...whitelistedNfts] : account?.nfts || nftsOwned || []), + [simplehashEnabled, nfts, whitelistedNfts, account, nftsOwned], + ); + + const collections = useMemo( + () => + Object.entries(nftsByCollections(allNfts)).filter( + ([contract]) => !hiddenNftCollections.includes(`${account?.id}|${contract}`), + ), + [account?.id, allNfts, hiddenNftCollections], + ); + + const collectionsLength = Object.keys(collections).length; + + return { + collections, + collectionsLength, + fetchNextPage, + hasNextPage, + nfts, + allNfts, + }; +} diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useNftLinks.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts similarity index 96% rename from apps/ledger-live-desktop/src/renderer/hooks/useNftLinks.ts rename to apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts index f6dc75342e95..f3ef4a4977e0 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useNftLinks.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftLinks.ts @@ -10,11 +10,11 @@ import IconOpensea from "~/renderer/icons/Opensea"; import IconRarible from "~/renderer/icons/Rarible"; import { openURL } from "~/renderer/linking"; import { getMetadataMediaTypes } from "~/helpers/nft"; -import { setDrawer } from "../drawers/Provider"; +import { setDrawer } from "../../drawers/Provider"; import CustomImage from "~/renderer/screens/customImage"; import NFTViewerDrawer from "~/renderer/drawers/NFTViewerDrawer"; -import { ContextMenuItemType } from "../components/ContextMenu/ContextMenuWrapper"; -import { devicesModelListSelector } from "../reducers/settings"; +import { ContextMenuItemType } from "../../components/ContextMenu/ContextMenuWrapper"; +import { devicesModelListSelector } from "~/renderer/reducers/settings"; function safeList(items: (ContextMenuItemType | "" | undefined)[]): ContextMenuItemType[] { return items.filter(Boolean) as ContextMenuItemType[]; diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts new file mode 100644 index 000000000000..b795bb62fc0c --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts @@ -0,0 +1,101 @@ +import { useEffect, useMemo, useState } from "react"; +import { useHideSpamCollection } from "./useHideSpamCollection"; +import { getThreshold, useCheckNftAccount } from "@ledgerhq/live-nft-react"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { useSelector } from "react-redux"; +import { accountsSelector, orderedVisibleNftsSelector } from "~/renderer/reducers/accounts"; +import isEqual from "lodash/isEqual"; +import { getEnv } from "@ledgerhq/live-env"; + +/** + * Represents the size of groups for batching address fetching. + * @constant {number} + */ +const GROUP_SIZE = 20; + +/** + * Represents the timer duration for updating address groups. + * 5 hours = 18,000,000 ms. + * @constant {number} + */ +const TIMER = 5 * 60 * 60 * 1000; // 5 hours = 18000000 ms + +/** + * A React hook that synchronizes NFT accounts by fetching their data in groups. + * It utilizes address batching and manages updates based on a timer. + * + * @returns {void} + * + * @example + * import { useSyncNFTsWithAccounts } from './path/to/hook'; + * + * const MyComponent = () => { + * useSyncNFTsWithAccounts(); + * return
Syncing NFT Accounts...
; + * }; + */ + +export function useSyncNFTsWithAccounts() { + const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); + const threshold = getThreshold(nftsFromSimplehashFeature?.params?.threshold); + + const { enabled, hideSpamCollection } = useHideSpamCollection(); + + const accounts = useSelector(accountsSelector); + const nftsOwned = useSelector(orderedVisibleNftsSelector, isEqual); + + const addressGroups = useMemo(() => { + const uniqueAddresses = [ + ...new Set( + accounts.map(account => account.freshAddress).filter(addr => addr.startsWith("0x")), + ), + ]; + + return uniqueAddresses.reduce((acc, _, i, arr) => { + if (i % GROUP_SIZE === 0) { + acc.push(arr.slice(i, i + GROUP_SIZE)); + } + return acc; + }, []); + }, [accounts]); + + const [groupToFetch, setGroupToFetch] = useState( + addressGroups.length > 0 ? addressGroups[0] : [], + ); + const [, setCurrentIndex] = useState(0); + + const { refetch } = useCheckNftAccount({ + addresses: groupToFetch.join(","), + nftsOwned, + chains: SUPPORTED_NFT_CURRENCIES, + threshold, + action: hideSpamCollection, + enabled, + }); + + // Refetch with new last group when addressGroups length changes + useEffect(() => { + if (enabled) { + const newIndex = addressGroups.length - 1; + setCurrentIndex(newIndex); + setGroupToFetch(addressGroups[newIndex] || []); + refetch(); + } + }, [addressGroups.length, addressGroups, refetch, enabled]); + + // Regular interval-based rotation through groups + useEffect(() => { + if (!enabled) return; + + const interval = setInterval(() => { + setCurrentIndex(prevIndex => { + const nextIndex = (prevIndex + 1) % addressGroups.length; + setGroupToFetch(addressGroups[nextIndex]); + return nextIndex; + }); + }, TIMER); + + return () => clearInterval(interval); + }, [addressGroups, enabled]); +} diff --git a/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Footer.tsx b/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Footer.tsx index 5d07f94bdf86..da297a4fed5f 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Footer.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/HideNftCollection/Footer.tsx @@ -2,15 +2,22 @@ import React, { useCallback } from "react"; import Button from "~/renderer/components/Button"; import { Trans } from "react-i18next"; import Box from "~/renderer/components/Box"; -import { useDispatch } from "react-redux"; -import { hideNftCollection } from "~/renderer/actions/settings"; +import { useDispatch, useSelector } from "react-redux"; +import { hideNftCollection, unwhitelistNftCollection } from "~/renderer/actions/settings"; +import { whitelistedNftCollectionsSelector } from "~/renderer/reducers/settings"; const Footer = ({ onClose, collectionId }: { onClose: () => void; collectionId: string }) => { const dispatch = useDispatch(); + const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); + const confirmHideNftCollection = useCallback( (collectionId: string) => { + if (whitelistedNftCollections.includes(collectionId)) { + dispatch(unwhitelistNftCollection(collectionId)); + } + dispatch(hideNftCollection(collectionId)); }, - [dispatch], + [dispatch, whitelistedNftCollections], ); return ( diff --git a/apps/ledger-live-desktop/src/renderer/modals/SimpleHashTools/index.tsx b/apps/ledger-live-desktop/src/renderer/modals/SimpleHashTools/index.tsx index ec88031582f3..0ab69a72fea4 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/SimpleHashTools/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/SimpleHashTools/index.tsx @@ -11,7 +11,7 @@ import RefreshMetadata, { HookResult as RefreshHookResult, useHook as useHookRefresh, } from "~/renderer/screens/settings/sections/Developer/SimpleHashTools/RefreshMetadata"; -import { Flex } from "@ledgerhq/react-ui"; +import { Flex, InfiniteLoader } from "@ledgerhq/react-ui"; import Button from "~/renderer/components/ButtonV3"; import SpamScore, { HookResult as SpamScoreHookResult, @@ -35,6 +35,7 @@ const getItems = ( cta: t("settings.developer.debugSimpleHash.debugSpamNft.report"), closeInfo: hooks.spam.closeInfo, displayInfo: hooks.spam.displayInfo, + isLoading: hooks.spam.spamReportMutation.isPending, }, { key: "refresh", @@ -44,6 +45,7 @@ const getItems = ( cta: t("settings.developer.debugSimpleHash.debugRefreshMetadata.refresh"), closeInfo: hooks.refresh.closeInfo, displayInfo: hooks.refresh.displayInfo, + isLoading: hooks.refresh.refreshMutation.isPending, }, { key: "check", @@ -53,6 +55,7 @@ const getItems = ( cta: t("settings.developer.debugSimpleHash.debugCheckSpamScore.check"), closeInfo: hooks.check.closeInfo, displayInfo: hooks.check.displayInfo, + isLoading: hooks.check.checkSpamScore.isLoading, }, ]; @@ -80,6 +83,7 @@ const SimpleHashToolsDebugger = () => { ( { height={15} /> - + {activeItem.value} @@ -109,7 +113,9 @@ const SimpleHashToolsDebugger = () => { )} renderFooter={() => ( <> - {displayInfo ? ( + {activeItem.isLoading ? ( + + ) : displayInfo ? ( diff --git a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts index 38cabc8fb56e..deb7e81dfdbe 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts @@ -82,6 +82,7 @@ export type SettingsState = { starredAccountIds?: string[]; blacklistedTokenIds: string[]; hiddenNftCollections: string[]; + whitelistedNftCollections: string[]; hiddenOrdinalsAsset: string[]; deepLinkUrl: string | undefined | null; lastSeenCustomImage: { @@ -182,6 +183,7 @@ export const INITIAL_STATE: SettingsState = { latestFirmware: null, blacklistedTokenIds: [], hiddenNftCollections: [], + whitelistedNftCollections: [], hiddenOrdinalsAsset: [], deepLinkUrl: null, firstTimeLend: false, @@ -228,6 +230,8 @@ type HandlersPayloads = { BLACKLIST_TOKEN: string; UNHIDE_NFT_COLLECTION: string; HIDE_NFT_COLLECTION: string; + WHITELIST_NFT_COLLECTION: string; + UNWHITELIST_NFT_COLLECTION: string; UNHIDE_ORDINALS_ASSET: string; HIDE_ORDINALS_ASSET: string; LAST_SEEN_DEVICE_INFO: { @@ -334,9 +338,25 @@ const handlers: SettingsHandlers = { const collections = state.hiddenNftCollections; return { ...state, - hiddenNftCollections: [...collections, collectionId], + hiddenNftCollections: [...new Set(collections.concat(collectionId))], }; }, + + UNWHITELIST_NFT_COLLECTION: (state, { payload: collectionId }) => { + const ids = state.whitelistedNftCollections; + return { + ...state, + whitelistedNftCollections: ids.filter(id => id !== collectionId), + }; + }, + WHITELIST_NFT_COLLECTION: (state, { payload: collectionId }) => { + const collections = state.whitelistedNftCollections; + return { + ...state, + whitelistedNftCollections: [...new Set(collections.concat(collectionId))], + }; + }, + UNHIDE_ORDINALS_ASSET: (state, { payload: inscriptionId }) => { const ids = state.hiddenOrdinalsAsset; return { @@ -761,6 +781,8 @@ export const enableLearnPageStagingUrlSelector = (state: State) => state.settings.enableLearnPageStagingUrl; export const blacklistedTokenIdsSelector = (state: State) => state.settings.blacklistedTokenIds; export const hiddenNftCollectionsSelector = (state: State) => state.settings.hiddenNftCollections; +export const whitelistedNftCollectionsSelector = (state: State) => + state.settings.whitelistedNftCollections; export const hiddenOrdinalsAssetSelector = (state: State) => state.settings.hiddenOrdinalsAsset; export const hasCompletedOnboardingSelector = (state: State) => state.settings.hasCompletedOnboarding || getEnv("SKIP_ONBOARDING"); diff --git a/apps/ledger-live-desktop/src/renderer/screens/nft/Collections/Collections.tsx b/apps/ledger-live-desktop/src/renderer/screens/nft/Collections/Collections.tsx index 345a47071e1d..f28a12c3c182 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/nft/Collections/Collections.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/nft/Collections/Collections.tsx @@ -1,14 +1,12 @@ import React, { useState, useCallback, useEffect, useMemo, memo } from "react"; -import { nftsByCollections } from "@ledgerhq/live-nft"; import { Account, NFT, ProtoNFT } from "@ledgerhq/types-live"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import { useHistory } from "react-router-dom"; import styled from "styled-components"; import { TokenShowMoreIndicator, IconAngleDown } from "~/renderer/screens/account/TokensList"; import TableContainer, { TableHeader } from "~/renderer/components/TableContainer"; import LabelWithExternalIcon from "~/renderer/components/LabelWithExternalIcon"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; import { supportLinkByTokenType } from "~/config/urls"; import { openModal } from "~/renderer/actions/modals"; import { track } from "~/renderer/analytics/segment"; @@ -19,9 +17,7 @@ import Text from "~/renderer/components/Text"; import { openURL } from "~/renderer/linking"; import Box from "~/renderer/components/Box"; import Row from "./Row"; -import { isThresholdValid, useCheckNftAccount } from "@ledgerhq/live-nft-react"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { useHideSpamCollection } from "~/renderer/hooks/useHideSpamCollection"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const INCREMENT = 5; const EmptyState = styled.div` @@ -46,13 +42,15 @@ type Props = { account: Account; }; const Collections = ({ account }: Props) => { - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; - const dispatch = useDispatch(); const { t } = useTranslation(); const history = useHistory(); const [numberOfVisibleCollections, setNumberOfVisibleCollections] = useState(INCREMENT); + + const { fetchNextPage, hasNextPage, collectionsLength, collections } = useNftCollections({ + account, + }); + const onOpenGallery = useCallback(() => { history.push(`/account/${account.id}/nft-collection`); }, [account.id, history]); @@ -72,21 +70,6 @@ const Collections = ({ account }: Props) => { [account.id, history], ); - const { enabled, hideSpamCollection } = useHideSpamCollection(); - - const { nfts, fetchNextPage, hasNextPage } = useCheckNftAccount({ - nftsOwned: account.nfts || [], - addresses: account.freshAddress, - chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, - ...(enabled && { action: hideSpamCollection }), - }); - - const collections = useMemo( - () => nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account.nfts), - [account.nfts, nfts, nftsFromSimplehashFeature], - ); - const collectionsLength = Object.keys(collections).length; const onShowMore = useCallback(() => { setNumberOfVisibleCollections(numberOfVisibleCollections => Math.min(numberOfVisibleCollections + INCREMENT, collectionsLength), @@ -95,17 +78,10 @@ const Collections = ({ account }: Props) => { fetchNextPage(); } }, [collectionsLength, fetchNextPage, hasNextPage]); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); - const filteredCollections = useMemo( - () => - Object.entries(collections).filter( - ([contract]) => !hiddenNftCollections.includes(`${account.id}|${contract}`), - ), - [account.id, collections, hiddenNftCollections], - ); + const visibleCollections = useMemo( () => - filteredCollections + collections .slice(0, numberOfVisibleCollections) .map(([contract, nfts]: [string, (ProtoNFT | NFT)[]]) => ( { nfts={nfts} /> )), - [account, filteredCollections, numberOfVisibleCollections, onOpenCollection], + [account, collections, numberOfVisibleCollections, onOpenCollection], ); useEffect(() => { @@ -169,7 +145,7 @@ const Collections = ({ account }: Props) => { )} - {filteredCollections.length > numberOfVisibleCollections ? ( + {collections.length > numberOfVisibleCollections ? ( diff --git a/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx b/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx index 2659c6c90afc..3a2d838c84ed 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx @@ -4,8 +4,6 @@ import { useSelector, useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import { accountSelector } from "~/renderer/reducers/accounts"; import { openModal } from "~/renderer/actions/modals"; -import { nftsByCollections } from "@ledgerhq/live-nft"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; import styled from "styled-components"; import IconSend from "~/renderer/icons/Send"; import CollectionName from "~/renderer/components/Nft/CollectionName"; @@ -20,8 +18,7 @@ import { State } from "~/renderer/reducers"; import { ProtoNFT } from "@ledgerhq/types-live"; import theme from "@ledgerhq/react-ui/styles/theme"; import { useOnScreen } from "../useOnScreen"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; -import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; const SpinnerContainer = styled.div` display: flex; @@ -52,9 +49,7 @@ const Footer = styled.footer` `; const Gallery = () => { - const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; - + const history = useHistory(); const { t } = useTranslation(); const dispatch = useDispatch(); const { id } = useParams<{ id: string }>(); @@ -63,24 +58,11 @@ const Gallery = () => { accountId: id, }), ); - const history = useHistory(); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); - const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ - nftsOwned: account?.nfts || [], - addresses: account?.freshAddress || "", - chains: [account?.currency.id ?? "ethereum"], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + const { fetchNextPage, hasNextPage, collections } = useNftCollections({ + account, }); - const collections = useMemo( - () => - Object.entries( - nftsByCollections(nftsFromSimplehashFeature?.enabled ? nfts : account?.nfts), - ).filter(([contract]) => !hiddenNftCollections.includes(`${account?.id}|${contract}`)), - [account?.id, account?.nfts, hiddenNftCollections, nfts, nftsFromSimplehashFeature?.enabled], - ); - // Should redirect to the account page if there is not NFT anymore in the page. useEffect(() => { if (collections.length <= 0) { diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/index.tsx index 4c91b144986b..7ca6a725fb62 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/index.tsx @@ -13,7 +13,7 @@ import SectionAccounts from "./sections/Accounts"; import SectionAbout from "./sections/About"; import SectionHelp from "./sections/Help"; import { setTrackingSource } from "~/renderer/analytics/TrackPage"; -import { developerModeSelector } from "../../reducers/settings"; +import { developerModeSelector } from "~/renderer/reducers/settings"; const getItems = (t: (a: string) => string, devMode?: boolean) => { const items = [ diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/About/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/About/index.tsx index 914561ff0f32..590424444728 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/About/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/About/index.tsx @@ -5,11 +5,11 @@ import { getEnv } from "@ledgerhq/live-env"; import { SettingsSectionBody as Body, SettingsSectionRow as Row } from "../../SettingsSection"; import RowItem from "../../RowItem"; import ReleaseNotesButton from "./ReleaseNotesButton"; -import { setDeveloperMode } from "../../../../actions/settings"; +import { setDeveloperMode } from "~/renderer/actions/settings"; import { useDispatch, useSelector } from "react-redux"; import { useToasts } from "@ledgerhq/live-common/notifications/ToastProvider/index"; import { v4 as uuidv4 } from "uuid"; -import { developerModeSelector } from "../../../../reducers/settings"; +import { developerModeSelector } from "~/renderer/reducers/settings"; import { useLocalizedUrl } from "~/renderer/hooks/useLocalizedUrls"; import { urls } from "~/config/urls"; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections.tsx deleted file mode 100644 index 31c56de3dc5c..000000000000 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import React, { useCallback, useState, useMemo } from "react"; -import styled from "styled-components"; -import { useTranslation } from "react-i18next"; -import { useDispatch, useSelector } from "react-redux"; -import { useNftMetadata, useNftCollectionMetadata } from "@ledgerhq/live-nft-react"; -import { SettingsSection as Section, SettingsSectionRow as Row } from "../../SettingsSection"; -import Text from "~/renderer/components/Text"; -import Box from "~/renderer/components/Box"; -import Media from "~/renderer/components/Nft/Media"; -import Skeleton from "~/renderer/components/Nft/Skeleton"; -import { unhideNftCollection } from "~/renderer/actions/settings"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; -import { accountSelector } from "~/renderer/reducers/accounts"; -import Track from "~/renderer/analytics/Track"; -import { State } from "~/renderer/reducers"; -import { NFTMetadata } from "@ledgerhq/types-live"; -import { Icons } from "@ledgerhq/react-ui"; - -const HiddenNftCollectionRow = ({ - contractAddress, - accountId, - onUnhide, -}: { - contractAddress: string; - accountId: string; - onUnhide: () => void; -}) => { - const account = useSelector((state: State) => - accountSelector(state, { - accountId, - }), - ); - const firstNft = account?.nfts?.find(nft => nft.contract === contractAddress); - const { metadata: nftMetadata, status: nftStatus } = useNftMetadata( - contractAddress, - firstNft?.tokenId, - firstNft?.currencyId, - ); - const { metadata: collectionMetadata, status: collectionStatus } = useNftCollectionMetadata( - contractAddress, - firstNft?.currencyId, - ); - const loading = useMemo( - () => nftStatus === "loading" || collectionStatus === "loading", - [collectionStatus, nftStatus], - ); - - return ( - - - {nftMetadata && firstNft && ( - - )} - - - {collectionMetadata?.tokenName || contractAddress} - - - - - - ); -}; -export default function HiddenNftCollections() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - const [sectionVisible, setSectionVisible] = useState(false); - const onUnhideCollection = useCallback( - (collectionId: string) => { - dispatch(unhideNftCollection(collectionId)); - }, - [dispatch], - ); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); - const toggleCurrencySection = useCallback(() => { - setSectionVisible(prevState => !prevState); - }, [setSectionVisible]); - return ( -
- - - {hiddenNftCollections.length ? ( - - - {t("settings.accounts.hiddenNftCollections.count", { - count: hiddenNftCollections.length, - })} - - - - - - ) : null} - - - {sectionVisible && ( - - {hiddenNftCollections.map(collectionId => { - const [accountId, contractAddress] = collectionId.split("|"); - return ( - onUnhideCollection(collectionId)} - /> - ); - })} - - )} -
- ); -} -const IconContainer = styled.div` - color: ${p => p.theme.colors.palette.text.shade60}; - text-align: center; - &:hover { - cursor: pointer; - color: ${p => p.theme.colors.palette.text.shade40}; - } -`; -const HiddenNftCollectionRowContainer = styled(Box).attrs({ - alignItems: "center", - horizontal: true, - flow: 1, - py: 1, -})` - margin: 0px; - &:not(:last-child) { - border-bottom: 1px solid ${p => p.theme.colors.palette.text.shade10}; - } - padding: 14px 6px; -`; -const Body = styled(Box)` - &:not(:empty) { - padding: 0 20px; - } -`; - -const Show = styled(Box).attrs<{ visible?: boolean }>({})<{ visible?: boolean }>` - transform: rotate(${p => (p.visible ? 0 : 270)}deg); -`; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/helpers.ts b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/helpers.ts new file mode 100644 index 000000000000..161739c68aaa --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/helpers.ts @@ -0,0 +1,2 @@ +export const splitAddress = (str: string, length: number) => + `${str.slice(0, length)}...${str.slice(-length)}`; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx new file mode 100644 index 000000000000..3c8bbd83d1fa --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx @@ -0,0 +1,114 @@ +import React, { useState, useCallback, useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; +import { unhideNftCollection, whitelistNftCollection } from "~/renderer/actions/settings"; +import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; + +import { SettingsSection as Section, SettingsSectionRow as Row } from "../../../SettingsSection"; +import Box from "~/renderer/components/Box"; +import Track from "~/renderer/analytics/Track"; +import ShowMore from "LLD/features/Collectibles/components/Collection/ShowMore"; + +import IconAngleDown from "~/renderer/icons/AngleDown"; +import { HiddenNftCollectionRow } from "./row"; +import { decodeCollectionId } from "@ledgerhq/live-nft-react"; + +// Styled components and layout + +const Body = styled(Box)` + &:not(:empty) { + padding: 0 20px; + } +`; + +const Show = styled(Box).attrs<{ visible?: boolean }>({})<{ visible?: boolean }>` + transform: rotate(${p => (p.visible ? 0 : 270)}deg); +`; + +const Collections = styled(Box)` + &:hover { + cursor: pointer; + } +`; + +const INCREMENT = 10; + +export default function HiddenNftCollections() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const [sectionVisible, setSectionVisible] = useState(false); + + const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + + const [numberOfVisibleCollections, setNumberOfVisibleCollections] = useState(INCREMENT); + + const onUnhideCollection = useCallback( + (collectionId: string) => { + dispatch(unhideNftCollection(collectionId)); + dispatch(whitelistNftCollection(collectionId)); + }, + [dispatch], + ); + + const toggleHiddenCollectionsSection = useCallback(() => { + setSectionVisible(prevState => !prevState); + }, []); + + const onShowMore = useCallback(() => { + setNumberOfVisibleCollections(numberOfVisibleCollections => + Math.min(numberOfVisibleCollections + INCREMENT, hiddenNftCollections.length), + ); + }, [hiddenNftCollections.length]); + + const visibleCollections = useMemo( + () => hiddenNftCollections.slice(0, numberOfVisibleCollections), + + [hiddenNftCollections, numberOfVisibleCollections], + ); + + const displayShowMore = numberOfVisibleCollections < hiddenNftCollections.length; + + return ( +
+ + + {hiddenNftCollections.length ? ( + + + {t("settings.accounts.hiddenNftCollections.count", { + count: hiddenNftCollections.length, + })} + + + + + + ) : null} + + + {sectionVisible && ( + + {visibleCollections.map(collectionId => { + const { accountId, contractAddress } = decodeCollectionId(collectionId); + return ( + onUnhideCollection(collectionId)} + /> + ); + })} + + {displayShowMore && } + + )} +
+ ); +} diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/row.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/row.tsx new file mode 100644 index 000000000000..477f1958481f --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/row.tsx @@ -0,0 +1,127 @@ +import React, { useState, useMemo, useCallback } from "react"; +import { useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { useNftMetadata, useNftCollectionMetadata } from "@ledgerhq/live-nft-react"; +import { accountSelector } from "~/renderer/reducers/accounts"; +import { State } from "~/renderer/reducers"; +import { NFTMetadata } from "@ledgerhq/types-live"; +import { clipboard } from "electron"; + +import Text from "~/renderer/components/Text"; +import Media from "~/renderer/components/Nft/Media"; +import Skeleton from "~/renderer/components/Nft/Skeleton"; +import { Flex, Icons } from "@ledgerhq/react-ui"; + +import IconCross from "~/renderer/icons/Cross"; +import styled from "styled-components"; +import { splitAddress } from "./helpers"; + +export const HiddenNftCollectionRow = ({ + contractAddress, + accountId, + onUnhide, +}: { + contractAddress: string; + accountId: string; + onUnhide: () => void; +}) => { + const { t } = useTranslation(); + const account = useSelector((state: State) => accountSelector(state, { accountId })); + const firstNft = useMemo( + () => account?.nfts?.find(nft => nft.contract === contractAddress), + [account, contractAddress], + ); + + const { metadata: nftMetadata, status: nftStatus } = useNftMetadata( + contractAddress, + firstNft?.tokenId, + firstNft?.currencyId, + ); + const { metadata: collectionMetadata, status: collectionStatus } = useNftCollectionMetadata( + contractAddress, + firstNft?.currencyId, + ); + + const loading = useMemo( + () => nftStatus === "loading" || collectionStatus === "loading", + [nftStatus, collectionStatus], + ); + + const [copyFeedback, setCopyFeedback] = useState(false); + const onCopy = useCallback(() => { + clipboard.writeText(contractAddress); + setCopyFeedback(true); + + setTimeout(() => setCopyFeedback(false), 1e3); + }, [contractAddress]); + + return ( + + + + {nftMetadata && firstNft && ( + + )} + + + + + {collectionMetadata?.tokenName || contractAddress} + + + + + {splitAddress(contractAddress, 8)} + + + {!copyFeedback ? ( + + ) : ( + <> + + + {t("common.addressCopied")} + + + )} + + + + + + + + + ); +}; + +const IconContainer = styled(Flex)` + color: ${p => p.theme.colors.palette.text.shade60}; + &:hover { + cursor: pointer; + color: ${p => p.theme.colors.palette.text.shade40}; + } +`; + +const HiddenNftCollectionRowContainer = styled(Flex).attrs({ + alignItems: "center", + justifyContent: "space-between", + py: 1, +})` + margin: 0px; + &:not(:last-child) { + border-bottom: 1px solid ${p => p.theme.colors.palette.text.shade10}; + } + padding: 14px 6px; +`; + +const StyledFlex = styled(Flex)` + &:hover { + cursor: pointer; + color: ${p => p.theme.colors.primary.c80}; + } +`; diff --git a/apps/ledger-live-mobile/src/screens/Account/NftCollectionsList.tsx b/apps/ledger-live-mobile/src/screens/Account/NftCollectionsList.tsx index 6b2e1ee0d59b..435ade26db8a 100644 --- a/apps/ledger-live-mobile/src/screens/Account/NftCollectionsList.tsx +++ b/apps/ledger-live-mobile/src/screens/Account/NftCollectionsList.tsx @@ -17,7 +17,7 @@ import Button from "~/components/wrappedUi/Button"; import Touchable from "~/components/Touchable"; import SectionTitle from "../WalletCentricSections/SectionTitle"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; -import { useNftGalleryFilter, isThresholdValid } from "@ledgerhq/live-nft-react"; +import { useNftGalleryFilter, getThreshold } from "@ledgerhq/live-nft-react"; const MAX_COLLECTIONS_TO_SHOW = 3; @@ -28,7 +28,7 @@ type Props = { export default function NftCollectionsList({ account }: Props) { useEnv("HIDE_EMPTY_TOKEN_ACCOUNTS"); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const nftsFromSimplehashEnabled = nftsFromSimplehashFeature?.enabled; const { colors } = useTheme(); const { t } = useTranslation(); @@ -38,7 +38,7 @@ export default function NftCollectionsList({ account }: Props) { nftsOwned: account.nfts || [], addresses: account.freshAddress, chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); diff --git a/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx b/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx index dc28ce0c4137..11a7ab520db3 100644 --- a/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx @@ -14,7 +14,7 @@ import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/t import { CustomImageNavigatorParamList } from "~/components/RootNavigator/types/CustomImageNavigator"; import { TrackScreen } from "~/analytics"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { getThreshold, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; import { getEnv } from "@ledgerhq/live-env"; const NB_COLUMNS = 2; @@ -34,7 +34,7 @@ const NFTGallerySelector = ({ navigation, route }: NavigationProps) => { const nftsOrdered = useSelector(orderedVisibleNftsSelector, isEqual); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const nftsFromSimplehashEnabled = nftsFromSimplehashFeature?.enabled; const accounts = useSelector(accountsSelector); @@ -52,7 +52,7 @@ const NFTGallerySelector = ({ navigation, route }: NavigationProps) => { nftsOwned: nftsOrdered || [], addresses: addresses, chains: SUPPORTED_NFT_CURRENCIES, - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const nfts = nftsFromSimplehashEnabled ? filteredNfts : nftsOrdered; diff --git a/apps/ledger-live-mobile/src/screens/Nft/NftGallery/index.tsx b/apps/ledger-live-mobile/src/screens/Nft/NftGallery/index.tsx index 6a9037476d34..82478272b6e3 100644 --- a/apps/ledger-live-mobile/src/screens/Nft/NftGallery/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Nft/NftGallery/index.tsx @@ -21,7 +21,7 @@ import { AccountsNavigatorParamList } from "~/components/RootNavigator/types/Acc import InfoModal from "~/modals/Info"; import { notAvailableModalInfo } from "../NftInfoNotAvailable"; import invariant from "invariant"; -import { useNftGalleryFilter, isThresholdValid } from "@ledgerhq/live-nft-react"; +import { useNftGalleryFilter, getThreshold } from "@ledgerhq/live-nft-react"; const MAX_COLLECTIONS_FIRST_RENDER = 12; const COLLECTIONS_TO_ADD_ON_LIST_END_REACHED = 6; @@ -32,7 +32,7 @@ type NavigationProps = BaseComposite< const NftGallery = () => { const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const nftsFromSimplehashEnabled = nftsFromSimplehashFeature?.enabled; const navigation = useNavigation(); const { t } = useTranslation(); @@ -55,7 +55,7 @@ const NftGallery = () => { nftsOwned: account.nfts || [], addresses: account.freshAddress, chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); diff --git a/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx b/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx index 0cbcae827846..682c80317b8b 100644 --- a/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx @@ -9,7 +9,7 @@ import { accountsSelector, filteredNftsSelector, hasNftsSelector } from "~/reduc import isEqual from "lodash/isEqual"; import { galleryChainFiltersSelector } from "~/reducers/nft"; -import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { getThreshold, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; const WalletNftGallery = () => { @@ -17,7 +17,7 @@ const WalletNftGallery = () => { const hasNFTs = useSelector(hasNftsSelector); const accounts = useSelector(accountsSelector); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const chainFilters = useSelector(galleryChainFiltersSelector); const nftsOwned = useSelector(filteredNftsSelector, isEqual); @@ -44,7 +44,7 @@ const WalletNftGallery = () => { addresses, chains, nftsOwned, - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const useSimpleHash = Boolean(nftsFromSimplehashFeature?.enabled); diff --git a/apps/ledger-live-mobile/src/screens/SendFunds/01b-SelectCollection.tsx b/apps/ledger-live-mobile/src/screens/SendFunds/01b-SelectCollection.tsx index 7a5c57246770..3a00e9b53d50 100644 --- a/apps/ledger-live-mobile/src/screens/SendFunds/01b-SelectCollection.tsx +++ b/apps/ledger-live-mobile/src/screens/SendFunds/01b-SelectCollection.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useMemo, useState, memo } from "react"; import { View, StyleSheet, FlatList, TouchableOpacity, Platform } from "react-native"; import { nftsByCollections } from "@ledgerhq/live-nft"; import { - isThresholdValid, + getThreshold, useNftCollectionMetadata, useNftGalleryFilter, useNftMetadata, @@ -91,7 +91,7 @@ const SendFundsSelectCollection = ({ route }: Props) => { const { account } = params; const { colors } = useTheme(); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); - const thresold = nftsFromSimplehashFeature?.params?.threshold; + const threshold = nftsFromSimplehashFeature?.params?.threshold; const nftsFromSimplehashEnabled = nftsFromSimplehashFeature?.enabled; const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); @@ -99,7 +99,7 @@ const SendFundsSelectCollection = ({ route }: Props) => { nftsOwned: account.nfts || [], addresses: account.freshAddress, chains: [account.currency.id], - threshold: isThresholdValid(thresold) ? Number(thresold) : 75, + threshold: getThreshold(threshold), }); const [collectionsCount, setCollectionsCount] = useState(MAX_COLLECTIONS_FIRST_RENDER); diff --git a/libs/ledger-live-common/src/market/utils/timers.ts b/libs/ledger-live-common/src/market/utils/timers.ts index 2e9bac6e03b4..1a01e24cbb1f 100644 --- a/libs/ledger-live-common/src/market/utils/timers.ts +++ b/libs/ledger-live-common/src/market/utils/timers.ts @@ -3,3 +3,4 @@ export const REFETCH_TIME_ONE_MINUTE = 60 * 1000; export const BASIC_REFETCH = 3; // nb minutes export const ONE_DAY = 24 * 60 * 60 * 1000; +export const HALF_DAY = ONE_DAY / 2; diff --git a/libs/live-nft-react/src/hooks/helpers/const.ts b/libs/live-nft-react/src/hooks/helpers/const.ts new file mode 100644 index 000000000000..d74658dfd200 --- /dev/null +++ b/libs/live-nft-react/src/hooks/helpers/const.ts @@ -0,0 +1,3 @@ +const DEFAULT_THRESHOLD = 75; + +export { DEFAULT_THRESHOLD }; diff --git a/libs/live-nft-react/src/hooks/helpers/index.ts b/libs/live-nft-react/src/hooks/helpers/index.ts index 55657dcfe669..717b05437ac3 100644 --- a/libs/live-nft-react/src/hooks/helpers/index.ts +++ b/libs/live-nft-react/src/hooks/helpers/index.ts @@ -1,3 +1,5 @@ +import { DEFAULT_THRESHOLD } from "./const"; + export function hashProtoNFT(contract: string, tokenId: string, currencyId: string): string { return `${contract}|${tokenId}|${currencyId}`; } @@ -5,3 +7,12 @@ export function hashProtoNFT(contract: string, tokenId: string, currencyId: stri export function isThresholdValid(threshold?: string | number): boolean { return Number(threshold) >= 0 && Number(threshold) <= 100; } + +export function decodeCollectionId(collectionId: string) { + const [accountId, contractAddress] = collectionId.split("|"); + return { accountId, contractAddress }; +} + +export function getThreshold(threshold?: string | number): number { + return isThresholdValid(threshold) ? Number(threshold) : DEFAULT_THRESHOLD; +} diff --git a/libs/live-nft-react/src/hooks/types.ts b/libs/live-nft-react/src/hooks/types.ts index 8a33820aa55c..051f88385404 100644 --- a/libs/live-nft-react/src/hooks/types.ts +++ b/libs/live-nft-react/src/hooks/types.ts @@ -19,6 +19,7 @@ export type HookProps = { chains: string[]; threshold: number; action?: (collection: string) => void; + enabled?: boolean; }; export type PartialProtoNFT = Partial; diff --git a/libs/live-nft-react/src/hooks/useCheckNftAccount.ts b/libs/live-nft-react/src/hooks/useCheckNftAccount.ts index 4eaad5874cbd..6d67b3f56ef4 100644 --- a/libs/live-nft-react/src/hooks/useCheckNftAccount.ts +++ b/libs/live-nft-react/src/hooks/useCheckNftAccount.ts @@ -9,19 +9,32 @@ import { nftsByCollections } from "@ledgerhq/live-nft/index"; import { hashProtoNFT } from "./helpers"; /** - * useCheckNftAccount() will apply a spam filtering on top of existing NFT data. - * - addresses: a list of wallet addresses separated by a "," - * - nftOwned: the array of all nfts as found by all user's account on Ledger Live - * - chains: a list of selected network to search for NFTs - * - action: custom action to handle collections - * NB: for performance, make sure that addresses, nftOwned and chains are memoized + * A React hook that checks NFT accounts against specified criteria and provides filtering functionality for managing NFT collections. + * + * @param {Object} params - The parameters for the hook. + * @param {string} params.addresses - A comma-separated string of NFT addresses to check. + * @param {Array} params.nftsOwned - An array of owned NFTs. + * @param {Array} params.chains - An array representing the blockchain chains. + * @param {number} params.threshold - A numeric threshold for filtering NFTs. + * @param {Function} params.action - A callback function to execute when spam is detected. + * @param {boolean} [params.enabled=false] - A flag to enable or disable the hook's functionality. + * + * @returns {Object} The result of the hook. + * @returns {Array} returns.nfts - An array of filtered NFTs. + * @returns {Object} returns.queryResult - The result of the infinite query, containing pagination and loading states. + * */ + +export const ONE_DAY = 24 * 60 * 60 * 1000; +export const HALF_DAY = ONE_DAY / 2; + export function useCheckNftAccount({ addresses, nftsOwned, chains, threshold, action, + enabled, }: HookProps): NftsFilterResult { // for performance, we hashmap the list of nfts by hash. const nftsWithProperties = useMemo( @@ -36,7 +49,9 @@ export function useCheckNftAccount({ fetchNftsFromSimpleHash({ addresses, chains, cursor: pageParam, threshold }), initialPageParam: undefined, getNextPageParam: lastPage => lastPage.next_cursor, - enabled: addresses.length > 0, + enabled: enabled && addresses.length > 0, + refetchInterval: HALF_DAY, + staleTime: HALF_DAY, }); useEffect(() => { diff --git a/libs/live-nft-react/src/index.ts b/libs/live-nft-react/src/index.ts index bbe9948fc0b9..21922170f011 100644 --- a/libs/live-nft-react/src/index.ts +++ b/libs/live-nft-react/src/index.ts @@ -9,4 +9,5 @@ export * from "./hooks/useFetchOrdinalByTokenId"; export * from "./hooks/helpers/ordinals"; export * from "./hooks/useCheckNftAccount"; export * from "./hooks/helpers/index"; +export * from "./hooks/helpers/const"; export * from "./hooks/types"; diff --git a/libs/live-nft/src/__tests__/index.test.ts b/libs/live-nft/src/__tests__/index.test.ts index 16dc7888f79b..d355c0a1eac0 100644 --- a/libs/live-nft/src/__tests__/index.test.ts +++ b/libs/live-nft/src/__tests__/index.test.ts @@ -3,7 +3,16 @@ import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; import { Account, NFTStandard, ProtoNFT } from "@ledgerhq/types-live"; import { encodeNftId } from "@ledgerhq/coin-framework/nft/nftId"; import { genAccount } from "@ledgerhq/coin-framework/mocks/account"; -import { getNFT, getNftCollectionKey, getNftKey, groupByCurrency, orderByLastReceived } from ".."; +import { + getNFT, + getNftCollectionKey, + getNftKey, + groupByCurrency, + mapChain, + mapChains, + orderByLastReceived, +} from ".."; +import { BlockchainEVM } from "../supported"; const NFT_1 = { id: encodeNftId("js:2:0ddkdlsPmds", "contract", "nft.tokenId", "ethereum"), @@ -46,24 +55,35 @@ const accounts: Account[] = [ genAccount("mocked-account-2", { currency: ETH, withNft: true }), ]; -describe("helpers", () => { - it("getNftKey", () => { +describe("Helpers functions", () => { + it("should return Nft key", () => { expect(getNftKey("contract", "tokenId", "currencyId")).toEqual("currencyId-contract-tokenId"); }); - it("getNftCollectionKey", () => { + it("should get NftCollection Key", () => { expect(getNftCollectionKey("contract", "currencyId")).toEqual("currencyId-contract"); }); - it("getNFT", () => { + it("should ge NFT", () => { expect(getNFT("contract", "nft.tokenId", NFTs)).toEqual(NFT_1); }); - it("groupByCurrency", () => { + it("should group by Currency", () => { expect(groupByCurrency(NFTs)).toEqual([NFT_1, NFT_4, NFT_2, NFT_3]); }); - it("orderByLastReceived", () => { + it("should order By last received", () => { expect(orderByLastReceived(accounts, NFTs)).toHaveLength(0); const NFTs_TEST = accounts.map(a => a.nfts).flat() as ProtoNFT[]; expect(orderByLastReceived(accounts, NFTs.concat(NFTs_TEST)).length).toBeGreaterThanOrEqual(1); }); + + it("should map a single chain", () => { + expect(mapChain(BlockchainEVM.Avalanche)).toEqual("avalanche"); + }); + + it("should map all chains", () => { + expect(mapChains([BlockchainEVM.Avalanche, BlockchainEVM.Ethereum])).toEqual([ + "avalanche", + "ethereum", + ]); + }); }); diff --git a/libs/live-nft/src/api/simplehash.ts b/libs/live-nft/src/api/simplehash.ts index 9ba69abfc812..c4df41b4918e 100644 --- a/libs/live-nft/src/api/simplehash.ts +++ b/libs/live-nft/src/api/simplehash.ts @@ -5,8 +5,7 @@ import { SimpleHashSpamReportResponse, } from "./types"; import { getEnv } from "@ledgerhq/live-env"; -import { replacements } from "../supported"; -import { mapChains } from ".."; +import { mapChain, mapChains } from ".."; /** * @@ -79,7 +78,7 @@ const defaultOpts = { export async function fetchNftsFromSimpleHash(opts: NftFetchOpts): Promise { const { chains, addresses, limit, filters, cursor, threshold } = { ...defaultOpts, ...opts }; - const chainsMapped = mapChains(chains, replacements); + const chainsMapped = mapChains(chains); const enrichedFilters = buildFilters(filters, { threshold: String(threshold) }); const { data } = await network({ @@ -139,7 +138,7 @@ export async function reportSpamNtf( }, data: JSON.stringify({ contract_address: opts.contractAddress, - chain_id: opts.chainId, + chain_id: mapChain(opts.chainId), token_id: opts.tokenId, collection_id: opts.collectionId, event_type: opts.eventType, diff --git a/libs/live-nft/src/index.ts b/libs/live-nft/src/index.ts index 501b87d06a04..6165681be904 100644 --- a/libs/live-nft/src/index.ts +++ b/libs/live-nft/src/index.ts @@ -2,6 +2,7 @@ import { getEnv } from "@ledgerhq/live-env"; import { groupAccountsOperationsByDay } from "@ledgerhq/coin-framework/account/index"; import type { Operation, ProtoNFT, NFT, Account } from "@ledgerhq/types-live"; import { NFTResource } from "./types"; +import { replacements } from "./supported"; export const GENESIS_PASS_COLLECTION_CONTRACT = "0x33c6Eec1723B12c46732f7AB41398DE45641Fa42"; export const INFINITY_PASS_COLLECTION_CONTRACT = "0xfe399E9a4B0bE4087a701fF0B1c89dABe7ce5425"; @@ -130,6 +131,11 @@ export const isNftTransaction = (transaction: T | undefined | null): boolean return false; }; -export const mapChains = (chains: string[], replacements: { [key: string]: string }) => { - return chains.map(chain => replacements[chain] || chain); +export const mapChains = (chains: string[]) => { + return chains.map(mapChain); +}; + +export const mapChain = (chain?: string) => { + if (!chain) return; + return replacements[chain] || chain; };