Selaa lähdekoodia

Merge metroflip from https://github.com/luu176/Metroflip

# Conflicts:
#	metroflip/manifest.yml
Willy-JL 10 kuukautta sitten
vanhempi
commit
a5b4cb337a
42 muutettua tiedostoa jossa 1694 lisäystä ja 332 poistoa
  1. 5 0
      metroflip/CHANGELOG.md
  2. 27 27
      metroflip/README.md
  3. 4 0
      metroflip/api/calypso/transit/ravkav.c
  4. 1 0
      metroflip/api/calypso/transit/ravkav_i.h
  5. 16 0
      metroflip/api/metroflip/metroflip_api.h
  6. 11 1
      metroflip/api/metroflip/metroflip_api_table_i.h
  7. 6 2
      metroflip/api/mosgortrans/mosgortrans_util.c
  8. 0 22
      metroflip/api/mosgortrans/mosgortrans_util.h
  9. 3 3
      metroflip/app/README.md
  10. 10 1
      metroflip/application.fam
  11. BIN
      metroflip/images/DolphinDone_80x58.png
  12. BIN
      metroflip/images/DolphinMafia_119x62.png
  13. BIN
      metroflip/images/WarningDolphinFlip_45x42.png
  14. BIN
      metroflip/images/icon.png
  15. 2 2
      metroflip/manifest.yml
  16. 27 2
      metroflip/metroflip.c
  17. 17 7
      metroflip/metroflip_i.h
  18. 303 0
      metroflip/scenes/desfire.c
  19. 5 4
      metroflip/scenes/desfire.h
  20. 191 94
      metroflip/scenes/keys.c
  21. 3 1
      metroflip/scenes/keys.h
  22. 9 11
      metroflip/scenes/metroflip_scene_auto.c
  23. 4 0
      metroflip/scenes/metroflip_scene_config.h
  24. 1 1
      metroflip/scenes/metroflip_scene_credits.c
  25. 62 0
      metroflip/scenes/metroflip_scene_delete.c
  26. 151 0
      metroflip/scenes/metroflip_scene_load.c
  27. 2 1
      metroflip/scenes/metroflip_scene_parse.c
  28. 55 0
      metroflip/scenes/metroflip_scene_save.c
  29. 69 0
      metroflip/scenes/metroflip_scene_save_result.c
  30. 3 0
      metroflip/scenes/metroflip_scene_start.c
  31. 9 9
      metroflip/scenes/metroflip_scene_supported.c
  32. 44 16
      metroflip/scenes/plugins/bip.c
  33. 52 7
      metroflip/scenes/plugins/calypso.c
  34. 42 10
      metroflip/scenes/plugins/charliecard.c
  35. 48 17
      metroflip/scenes/plugins/clipper.c
  36. 245 0
      metroflip/scenes/plugins/gocard.c
  37. 47 17
      metroflip/scenes/plugins/itso.c
  38. 44 13
      metroflip/scenes/plugins/metromoney.c
  39. 47 17
      metroflip/scenes/plugins/myki.c
  40. 41 18
      metroflip/scenes/plugins/opal.c
  41. 45 17
      metroflip/scenes/plugins/smartrider.c
  42. 43 12
      metroflip/scenes/plugins/troika.c

+ 5 - 0
metroflip/CHANGELOG.md

@@ -37,3 +37,8 @@ Big update!
 - Unified Calypso Parser: A new unified Calypso parser has been introduced (thanks to DocSystem), streamlining Calypso card support.
 - RavKav Moved to Calypso Parser: RavKav has been moved to the new unified Calypso parser (credit to luu176).
 
+## v0.6
+
+- Added a load mode and a save mode to store card info
+- Fixed a major bug due to API symbol not existing
+

+ 27 - 27
metroflip/README.md

@@ -67,38 +67,38 @@ This is a list of metro cards and transit systems that need support or have part
 
 ## ✅ Supported Cards
 
-| **Card / Agency**  | **Country / City**                          | **Card Type**     |
-|--------------------|---------------------------------------------|-------------------|
-| **Bip!**           | 🇨🇱 Santiago de Chile, Chile                | Mifare Classic    |
-| **Charliecard**    | 🇺🇸 Boston, MA, USA                        | Mifare Classic    |
-| **Clipper**        | 🇺🇸 San Francisco, CA, USA                  | Mifare DESFire    |
-| **ITSO**           | 🇬🇧 United Kingdom                          | Mifare DESFire    |
-| **Metromoney**     | 🇬🇪 Tbilisi, Georgia                        | Mifare Classic    |
-| **myki**           | 🇦🇺 Melbourne (and surrounds), VIC, Australia | Mifare DESFire    |
-| **Navigo**         | 🇫🇷 Paris, France                           | Calypso           |
-| **Opal**           | 🇦🇺 Sydney (and surrounds), NSW, Australia  | Mifare DESFire    |
-| **Opus**           | 🇨🇦 Montreal, QC, Canada                    | Calypso           |
-| **Rav-Kav**        | 🇮🇱 Israel                                  | Calypso           |
-| **SmartRider**     | :australia: Western Australia, Australia   | Mifare Classic    |
-| **Troika**         | 🇷🇺 Moscow, Russia                          | Mifare Classic    |
+| **Card / Agency**  | **City / Country**                           | **Card Type**     |
+|--------------------|----------------------------------------------|-------------------|
+| **Bip!**           | 🇨🇱 Santiago de Chile, Chile                  | MIFARE Classic    |
+| **Charliecard**    | 🇺🇸 Boston, MA, USA                           | MIFARE Classic    |
+| **Clipper**        | 🇺🇸 San Francisco, CA, USA                    | MIFARE DESFire    |
+| **ITSO**           | 🇬🇧 United Kingdom                            | MIFARE DESFire    |
+| **Metromoney**     | 🇬🇪 Tbilisi, Georgia                          | MIFARE Classic    |
+| **myki**           | 🇦🇺 Melbourne (and surrounds), VIC, Australia | MIFARE DESFire    |
+| **Navigo**         | 🇫🇷 Paris, France                             | Calypso           |
+| **Opal**           | 🇦🇺 Sydney (and surrounds), NSW, Australia    | MIFARE DESFire    |
+| **Opus**           | 🇨🇦 Montreal, QC, Canada                      | Calypso           |
+| **Rav-Kav**        | 🇮🇱 Israel                                    | Calypso           |
+| **SmartRider**     | 🇦🇺 Perth, WA, Australia                      | MIFARE Classic    |
+| **Troika**         | 🇷🇺 Moscow, Russia                            | MIFARE Classic    |
 
 
 ---
 
 # Credits
-- **App Author**: [@luu176](https://github.com/luu176)
-- **Charliecard Parser**: [@zacharyweiss](https://github.com/zacharyweiss)
-- **Rav-Kav Parser**: [@luu176](https://github.com/luu176)
-- **Navigo Parser**: [@luu176](https://github.com/luu176), [@DocSystem](https://github.com/DocSystem)
-- **Opus Parser**: [@DocSystem](https://github.com/DocSystem)
-- **Metromoney Parser**: [@Leptopt1los](https://github.com/Leptopt1los)
-- **Bip! Parser**: [@rbasoalto](https://github.com/rbasoalto), [@gornekich](https://github.com/gornekich)
-- **Clipper Parser**: [@ke6jjj](https://github.com/ke6jjj)
-- **Troika Parser**: [@gornekich](https://github.com/gornekich)
-- **Myki Parser**: [@gornekich](https://github.com/gornekich)
-- **Opal Parser**: [@gornekich](https://github.com/gornekich)
-- **ITSO Parser**: [@gsp8181](https://github.com/gsp8181), [@hedger](https://github.com/hedger), [@gornekich](https://github.com/gornekich)
-- **Info Slaves**: [@equipter](https://github.com/equipter), [TheDingo8MyBaby](https://github.com/TheDingo8MyBaby)
+- **App Author:** [@luu176](https://github.com/luu176)
+- **Info Slaves:** [@equipter](https://github.com/equipter), [TheDingo8MyBaby](https://github.com/TheDingo8MyBaby)
+- **Bip! Parser:** [@rbasoalto](https://github.com/rbasoalto), [@gornekich](https://github.com/gornekich)
+- **Charliecard Parser:** [@zacharyweiss](https://github.com/zacharyweiss)
+- **Clipper Parser:** [@ke6jjj](https://github.com/ke6jjj)
+- **ITSO Parser:** [@gsp8181](https://github.com/gsp8181), [@hedger](https://github.com/hedger), [@gornekich](https://github.com/gornekich)
+- **Metromoney Parser:** [@Leptopt1los](https://github.com/Leptopt1los)
+- **myki Parser:** [@gornekich](https://github.com/gornekich)
+- **Navigo Parser:** [@luu176](https://github.com/luu176), [@DocSystem](https://github.com/DocSystem)
+- **Opal Parser:** [@gornekich](https://github.com/gornekich)
+- **Opus Parser:** [@DocSystem](https://github.com/DocSystem)
+- **Rav-Kav Parser:** [@luu176](https://github.com/luu176)
+- **Troika Parser:** [@gornekich](https://github.com/gornekich)
 
 ---
 

+ 4 - 0
metroflip/api/calypso/transit/ravkav.c

@@ -20,6 +20,10 @@ const char* get_ravkav_issuer(int issuer) {
 
 void show_ravkav_contract_info(RavKavCardContract* contract, FuriString* parsed_data) {
     // Core contract validity period
+    if(contract->balance != 0.0f) {
+        furi_string_cat_printf(parsed_data, "Balance: %.2f ILS\n", (double)contract->balance);
+    }
+
     furi_string_cat_printf(parsed_data, "Valid from: ");
     locale_format_datetime_cat(parsed_data, &contract->start_date, false);
     if(contract->end_date_available) {

+ 1 - 0
metroflip/api/calypso/transit/ravkav_i.h

@@ -23,6 +23,7 @@ typedef struct {
     bool end_date_available;
 
     bool present;
+    float balance;
 } RavKavCardContract;
 
 typedef struct {

+ 16 - 0
metroflip/api/metroflip/metroflip_api.h

@@ -23,6 +23,8 @@ extern "C" {
 // metroflip
 
 void metroflip_exit_widget_callback(GuiButtonType result, InputType type, void* context);
+void metroflip_save_widget_callback(GuiButtonType result, InputType type, void* context);
+void metroflip_delete_widget_callback(GuiButtonType result, InputType type, void* context);
 
 void metroflip_app_blink_start(Metroflip* metroflip);
 
@@ -132,6 +134,20 @@ void show_ravkav_event_info(RavKavCardEvent* event, FuriString* parsed_data);
 void show_ravkav_contract_info(RavKavCardContract* contract, FuriString* parsed_data);
 
 void show_ravkav_environment_info(RavKavCardEnv* environment, FuriString* parsed_data);
+
+extern const Icon I_RFIDDolphinReceive_97x61;
+extern const Icon I_icon;
+extern const Icon I_DolphinDone_80x58;
+extern const Icon I_WarningDolphinFlip_45x42;
+extern const Icon I_DolphinMafia_119x62;
+
+void render_section_header(
+    FuriString* str,
+    const char* name,
+    uint8_t prefix_separator_cnt,
+    uint8_t suffix_separator_cnt);
+bool mosgortrans_parse_transport_block(const MfClassicBlock* block, FuriString* result);
+
 /*******************/
 #ifdef __cplusplus
 }

+ 11 - 1
metroflip/api/metroflip/metroflip_api_table_i.h

@@ -9,6 +9,8 @@
 static constexpr auto metroflip_api_table = sort(create_array_t<sym_entry>(
     // metroflip stuff
     API_METHOD(metroflip_exit_widget_callback, void, (GuiButtonType, InputType, void*)),
+    API_METHOD(metroflip_save_widget_callback, void, (GuiButtonType, InputType, void*)),
+    API_METHOD(metroflip_delete_widget_callback, void, (GuiButtonType, InputType, void*)),
     API_METHOD(metroflip_app_blink_start, void, (Metroflip*)),
     API_METHOD(metroflip_app_blink_stop, void, (Metroflip*)),
     API_METHOD(bit_slice_to_dec, int, (const char*, int, int)),
@@ -68,4 +70,12 @@ static constexpr auto metroflip_api_table = sort(create_array_t<sym_entry>(
     API_METHOD(get_ravkav_env_holder_structure, CalypsoApp*, ()),
     API_METHOD(show_ravkav_event_info, void, (RavKavCardEvent*, FuriString*)),
     API_METHOD(show_ravkav_contract_info, void, (RavKavCardContract*, FuriString*)),
-    API_METHOD(show_ravkav_environment_info, void, (RavKavCardEnv*, FuriString*))));
+    API_METHOD(show_ravkav_environment_info, void, (RavKavCardEnv*, FuriString*)),
+
+    API_VARIABLE(I_RFIDDolphinReceive_97x61, Icon),
+    API_VARIABLE(I_icon, Icon),
+    API_METHOD(render_section_header, void, (FuriString*, const char*, uint8_t, uint8_t)),
+    API_METHOD(mosgortrans_parse_transport_block, bool, (const MfClassicBlock*, FuriString*)),
+    API_VARIABLE(I_WarningDolphinFlip_45x42, Icon),
+    API_VARIABLE(I_DolphinDone_80x58, Icon),
+    API_VARIABLE(I_DolphinMafia_119x62, Icon)));

+ 6 - 2
metroflip/api/mosgortrans/mosgortrans_util.c

@@ -1,5 +1,9 @@
-#include "mosgortrans_util.h"
-
+#include "../metroflip/metroflip_api.h"
+#include <bit_lib.h>
+#include <datetime.h>
+#include <furi/core/string.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+#include <furi_hal_rtc.h>
 #define TAG "Metroflip:Scene:Mosgortrans"
 
 void render_section_header(

+ 0 - 22
metroflip/api/mosgortrans/mosgortrans_util.h

@@ -1,22 +0,0 @@
-#pragma once
-
-#include <bit_lib.h>
-#include <datetime.h>
-#include <furi/core/string.h>
-#include <nfc/protocols/mf_classic/mf_classic.h>
-#include <furi_hal_rtc.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-void render_section_header(
-    FuriString* str,
-    const char* name,
-    uint8_t prefix_separator_cnt,
-    uint8_t suffix_separator_cnt);
-bool mosgortrans_parse_transport_block(const MfClassicBlock* block, FuriString* result);
-
-#ifdef __cplusplus
-}
-#endif

+ 3 - 3
metroflip/app/README.md

@@ -14,7 +14,7 @@ This is a list of metro cards and transit systems that are supported.
 - **Clipper**  
 - **ITSO**  
 - **Metromoney**  
-- **Myki**  
+- **myki**  
 - **Navigo**  
 - **Opal**  
 - **Opus**  
@@ -32,8 +32,8 @@ More coming soon!
 - **Info Slaves**: equipter, TheDingo8MyBaby  
 - **ITSO Parser**: gsp8181, hedger, gornekich  
 - **Metromoney Parser**: Leptopt1los  
-- **Myki Parser**: gornekich  
+- **myki Parser**: gornekich  
 - **Navigo Parser**: luu176, DocSystem  
 - **Opal Parser**: gornekich  
 - **Rav-Kav Parser**: luu176  
-- **Troika Parser**: gornekich  
+- **Troika Parser**: gornekich  

+ 10 - 1
metroflip/application.fam

@@ -5,7 +5,7 @@ App(
     entry_point="metroflip",
     stack_size=2 * 1024,
     fap_category="NFC",
-    fap_version="0.5",
+    fap_version="0.6",
     fap_icon="icon.png",
     fap_description="An implementation of metrodroid on the flipper",
     fap_author="luu176",
@@ -100,3 +100,12 @@ App(
     sources=["scenes/plugins/troika.c"],
     fal_embedded=True,
 )
+
+App(
+    appid="gocard_plugin",
+    apptype=FlipperAppType.PLUGIN,
+    entry_point="gocard_plugin_ep",
+    requires=["metroflip"],
+    sources=["scenes/plugins/gocard.c"],
+    fal_embedded=True,
+)

BIN
metroflip/images/DolphinDone_80x58.png


BIN
metroflip/images/DolphinMafia_119x62.png


BIN
metroflip/images/WarningDolphinFlip_45x42.png


BIN
metroflip/images/icon.png


+ 2 - 2
metroflip/manifest.yml

@@ -15,8 +15,8 @@ screenshots:
 short_description: 'An implementation of Metrodroid on the Flipper Zero'
 sourcecode:
   location:
-    commit_sha: 9bb6a0ac17640a08df176e340c1cc57663d6fe42
+    commit_sha: c4e1ed2304ad96a3565f83a58b3c7f8529f9d651
     origin: https://github.com/luu176/Metroflip
     subdir:
   type: git
-version: 0.5
+version: 0.6

+ 27 - 2
metroflip/metroflip.c

@@ -78,6 +78,10 @@ Metroflip* metroflip_alloc() {
         app->view_dispatcher, MetroflipViewTextBox, text_box_get_view(app->text_box));
     app->text_box_store = furi_string_alloc();
 
+    // Dialog for loading
+    app->dialogs = furi_record_open(RECORD_DIALOGS);
+
+    app->data_loaded = false;
     return app;
 }
 
@@ -121,6 +125,10 @@ void metroflip_free(Metroflip* app) {
 
     // Records
     furi_record_close(RECORD_GUI);
+
+    // Dialogs
+    furi_record_close(RECORD_DIALOGS);
+
     free(app);
 }
 
@@ -150,6 +158,25 @@ void metroflip_exit_widget_callback(GuiButtonType result, InputType type, void*
 
     if(type == InputTypeShort) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
+    }
+}
+
+void metroflip_save_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Metroflip* app = context;
+    UNUSED(result);
+
+    if(type == InputTypeShort) {
+        scene_manager_next_scene(app->scene_manager, MetroflipSceneSave);
+    }
+}
+
+void metroflip_delete_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Metroflip* app = context;
+    UNUSED(result);
+
+    if(type == InputTypeShort) {
+        scene_manager_next_scene(app->scene_manager, MetroflipSceneDelete);
     }
 }
 
@@ -296,9 +323,7 @@ KeyfileManager manage_keyfiles(
             return SUCCESSFUL;
         }
     } else {
-        FURI_LOG_I("TAG", "testing 1");
         size_t source_file_length = storage_file_size(source);
-        FURI_LOG_I("TAG", "testing 2");
 
         storage_file_close(source);
         mf_classic_key_cache_load(instance, uid, uid_len);

+ 17 - 7
metroflip/metroflip_i.h

@@ -9,11 +9,6 @@
 #include <gui/view_dispatcher.h>
 #include <gui/scene_manager.h>
 #include "api/nfc/mf_classic_key_cache.h"
-#if __has_include(<assets_icons.h>)
-#include <assets_icons.h>
-#else
-extern const Icon I_RFIDDolphinReceive_97x61;
-#endif
 #include <flipper_application/plugins/composite_resolver.h>
 #include <loader/firmware_api/firmware_api.h>
 #include <flipper_application/plugins/plugin_manager.h>
@@ -48,13 +43,15 @@ extern const Icon I_RFIDDolphinReceive_97x61;
 #include <strings.h>
 #include <flipper_application/flipper_application.h>
 #include <loader/firmware_api/firmware_api.h>
+#include <applications/services/storage/storage.h>
+#include <applications/services/dialogs/dialogs.h>
 
 #include "scenes/metroflip_scene.h"
 
 #include "api/calypso/calypso_i.h"
 
 #define KEY_MASK_BIT_CHECK(key_mask_1, key_mask_2) (((key_mask_1) & (key_mask_2)) == (key_mask_1))
-
+#define METROFLIP_FILE_EXTENSION                   ".nfc"
 typedef struct {
     Gui* gui;
     SceneManager* scene_manager;
@@ -76,6 +73,11 @@ typedef struct {
     MfClassicKeyCache* mfc_key_cache;
     NfcDetectedProtocols* detected_protocols;
     DesfireCardType desfire_card_type;
+    MfDesfireData* mfdes_data;
+    MfClassicData* mfc_data;
+
+    // save stuff
+    char save_buf[248];
 
     //plugin manager
     PluginManager* plugin_manager;
@@ -94,9 +96,16 @@ typedef struct {
     bool auto_mode;
     CardType mfc_card_type;
     NfcProtocol protocol;
+    const char* file_path;
+    char delete_file_path[256];
 
     // Calypso specific context
     CalypsoContext* calypso_context;
+
+    DialogsApp* dialogs;
+
+    bool data_loaded;
+
 } Metroflip;
 
 enum MetroflipCustomEvent {
@@ -137,6 +146,7 @@ typedef enum {
     MetroflipViewTextBox,
     MetroflipViewWidget,
     MetroflipViewUart,
+    MetroflipViewCanvas,
 } MetroflipView;
 
 typedef enum {
@@ -145,7 +155,7 @@ typedef enum {
     MISSING_KEYFILE
 } KeyfileManager;
 
-CardType determine_card_type(Nfc* nfc);
+CardType determine_card_type(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded);
 
 #ifdef FW_ORIGIN_Official
 #define submenu_add_lockable_item(                                             \

+ 303 - 0
metroflip/scenes/desfire.c

@@ -0,0 +1,303 @@
+#include <lib/nfc/protocols/mf_desfire/mf_desfire.h>
+#include "../metroflip_i.h"
+#include "desfire.h"
+#include <lib/toolbox/strint.h>
+#include <stdio.h>
+
+static const MfDesfireApplicationId opal_verify_app_id = {.data = {0x31, 0x45, 0x53}};
+
+static const MfDesfireFileId opal_verify_file_id = 0x07;
+
+static const MfDesfireApplicationId myki_verify_app_id = {.data = {0x00, 0x11, 0xf2}};
+
+static const MfDesfireFileId myki_verify_file_id = 0x0f;
+
+static const MfDesfireApplicationId itso_verify_app_id = {.data = {0x16, 0x02, 0xa0}};
+
+static const MfDesfireFileId itso_verify_file_id = 0x0f;
+
+uint64_t itso_swap_uint64(uint64_t val) {
+    val = ((val << 8) & 0xFF00FF00FF00FF00ULL) | ((val >> 8) & 0x00FF00FF00FF00FFULL);
+    val = ((val << 16) & 0xFFFF0000FFFF0000ULL) | ((val >> 16) & 0x0000FFFF0000FFFFULL);
+    return (val << 32) | (val >> 32);
+}
+
+static const struct {
+    const MfDesfireApplicationId app;
+    const char* type;
+} clipper_verify_types[] = {
+    // Application advertised on classic, plastic cards.
+    {.app = {.data = {0x90, 0x11, 0xf2}}, .type = "Card"},
+    // Application advertised on a mobile device.
+    {.app = {.data = {0x91, 0x11, 0xf2}}, .type = "Mobile Device"},
+};
+
+static const size_t kNumCardVerifyTypes =
+    sizeof(clipper_verify_types) / sizeof(clipper_verify_types[0]);
+
+// File ids of important files on the card.
+static const MfDesfireFileId clipper_ecash_file_id = 2;
+static const MfDesfireFileId clipper_histidx_file_id = 6;
+static const MfDesfireFileId clipper_identity_file_id = 8;
+static const MfDesfireFileId clipper_history_file_id = 14;
+
+static bool get_file_contents(
+    const MfDesfireApplication* app,
+    const MfDesfireFileId* id,
+    MfDesfireFileType type,
+    size_t min_size,
+    const uint8_t** out) {
+    const MfDesfireFileSettings* settings = mf_desfire_get_file_settings(app, id);
+    if(settings == NULL) return false;
+    if(settings->type != type) return false;
+
+    const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, id);
+
+    if(file_data == NULL) return false;
+
+    if(simple_array_get_count(file_data->data) < min_size) return false;
+
+    *out = simple_array_cget_data(file_data->data);
+
+    return true;
+}
+
+struct ClipperVerifyCardInfo_struct {
+    uint32_t serial_number;
+    uint16_t counter;
+    uint16_t last_txn_id;
+    uint32_t last_updated_tm_1900;
+    uint16_t last_terminal_id;
+    int16_t balance_cents;
+};
+typedef struct ClipperVerifyCardInfo_struct ClipperVerifyCardInfo;
+
+// Opal file 0x7 structure. Assumes a little-endian CPU.
+typedef struct FURI_PACKED {
+    uint32_t serial         : 32;
+    uint8_t check_digit     : 4;
+    bool blocked            : 1;
+    uint16_t txn_number     : 16;
+    int32_t balance         : 21;
+    uint16_t days           : 15;
+    uint16_t minutes        : 11;
+    uint8_t mode            : 3;
+    uint16_t usage          : 4;
+    bool auto_topup         : 1;
+    uint8_t weekly_journeys : 4;
+    uint16_t checksum       : 16;
+} OpalVerifyFile;
+
+static_assert(sizeof(OpalVerifyFile) == 16, "OpalFile");
+
+bool opal_verify(const MfDesfireData* data) {
+    // Check if the card has the expected application
+    const MfDesfireApplication* app = mf_desfire_get_application(data, &opal_verify_app_id);
+    if(app == NULL) {
+        return false;
+    }
+
+    // Verify the file settings: must be of type standard and have the expected size
+    const MfDesfireFileSettings* file_settings =
+        mf_desfire_get_file_settings(app, &opal_verify_file_id);
+    if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard ||
+       file_settings->data.size != sizeof(OpalVerifyFile)) {
+        return false;
+    }
+
+    // Check that the file data exists
+    const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &opal_verify_file_id);
+    if(file_data == NULL) {
+        return false;
+    }
+
+    // Retrieve the opal file from the file data
+    const OpalVerifyFile* opal_file = simple_array_cget_data(file_data->data);
+    if(opal_file == NULL) {
+        return false;
+    }
+
+    // Ensure the check digit is valid (i.e. 0..9)
+    if(opal_file->check_digit > 9) {
+        return false;
+    }
+
+    // All checks passed, return true
+    return true;
+}
+
+bool myki_verify(const MfDesfireData* data) {
+    // Check if the card contains the expected Myki application.
+    const MfDesfireApplication* app = mf_desfire_get_application(data, &myki_verify_app_id);
+    if(app == NULL) {
+        return false;
+    }
+
+    // Define the structure for Myki file data.
+    typedef struct {
+        uint32_t top;
+        uint32_t bottom;
+    } mykiFile;
+
+    // Verify file settings: must be present, of the correct type, and large enough to contain a mykiFile.
+    const MfDesfireFileSettings* file_settings =
+        mf_desfire_get_file_settings(app, &myki_verify_file_id);
+    if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard ||
+       file_settings->data.size < sizeof(mykiFile)) {
+        return false;
+    }
+
+    // Verify that the file data is available.
+    const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &myki_verify_file_id);
+    if(file_data == NULL) {
+        return false;
+    }
+
+    // Retrieve the Myki file data from the file data array.
+    const mykiFile* myki_file = simple_array_cget_data(file_data->data);
+    if(myki_file == NULL) {
+        return false;
+    }
+
+    // Check that Myki card numbers are prefixed with "308425".
+    if(myki_file->top != 308425UL) {
+        return false;
+    }
+
+    // Card numbers are always 15 digits in length.
+    // The bottom field must be within [10000000, 100000000) to meet this requirement.
+    if(myki_file->bottom < 10000000UL || myki_file->bottom >= 100000000UL) {
+        return false;
+    }
+
+    // All checks passed.
+    return true;
+}
+
+bool itso_verify(const MfDesfireData* data) {
+    // Check if the card contains the expected ITSO application.
+    const MfDesfireApplication* app = mf_desfire_get_application(data, &itso_verify_app_id);
+    if(app == NULL) {
+        return false;
+    }
+
+    // Define the structure for ITSO file data.
+    typedef struct {
+        uint64_t part1;
+        uint64_t part2;
+        uint64_t part3;
+        uint64_t part4;
+    } ItsoFile;
+
+    // Verify file settings: must exist, be of standard type,
+    // and have a data size at least as large as an ItsoFile.
+    const MfDesfireFileSettings* file_settings =
+        mf_desfire_get_file_settings(app, &itso_verify_file_id);
+    if(file_settings == NULL || file_settings->type != MfDesfireFileTypeStandard ||
+       file_settings->data.size < sizeof(ItsoFile)) {
+        return false;
+    }
+
+    // Verify that the file data is available.
+    const MfDesfireFileData* file_data = mf_desfire_get_file_data(app, &itso_verify_file_id);
+    if(file_data == NULL) {
+        return false;
+    }
+
+    // Retrieve the ITSO file from the file data.
+    const ItsoFile* itso_file = simple_array_cget_data(file_data->data);
+    if(itso_file == NULL) {
+        return false;
+    }
+
+    // Swap bytes for the first two parts.
+    uint64_t x1 = itso_swap_uint64(itso_file->part1);
+    uint64_t x2 = itso_swap_uint64(itso_file->part2);
+
+    // Prepare buffers for card and date strings.
+    char cardBuff[32];
+    char dateBuff[18];
+
+    // Format the hex strings.
+    snprintf(cardBuff, sizeof(cardBuff), "%llx%llx", x1, x2);
+    snprintf(dateBuff, sizeof(dateBuff), "%llx", x2);
+
+    // Get pointer to the card number substring (skipping the first 4 characters).
+    char* cardp = cardBuff + 4;
+    cardp[18] = '\0'; // Ensure the substring is null-terminated.
+
+    // Verify that all ITSO card numbers are prefixed with "633597".
+    if(strncmp(cardp, "633597", 6) != 0) {
+        return false;
+    }
+
+    // Prepare the date string by advancing 12 characters.
+    char* datep = dateBuff + 12;
+    dateBuff[17] = '\0'; // Ensure termination of the date string.
+
+    // Convert the date portion (in hexadecimal) to a date stamp.
+    uint32_t dateStamp;
+    if(strint_to_uint32(datep, NULL, &dateStamp, 16) != StrintParseNoError) {
+        return false;
+    }
+
+    // (Optional) Calculate the Unix timestamp if needed:
+    // uint32_t unixTimestamp = dateStamp * 24 * 60 * 60 + 852076800U;
+
+    // All checks passed.
+    return true;
+}
+
+bool clipper_verify(const MfDesfireData* data) {
+    bool verified = false;
+
+    do {
+        FURI_LOG_I("clipper verify", "verifying..");
+        const MfDesfireApplication* app = NULL;
+
+        // Try each card type until a matching application is found.
+        for(size_t i = 0; i < kNumCardVerifyTypes; i++) {
+            app = mf_desfire_get_application(data, &clipper_verify_types[i].app);
+            if(app != NULL) {
+                break;
+            }
+        }
+        // If no matching application was found, verification fails.
+        if(app == NULL) {
+            break;
+        }
+
+        const uint8_t* id_data;
+        if(!get_file_contents(
+               app, &clipper_identity_file_id, MfDesfireFileTypeStandard, 5, &id_data)) {
+            break;
+        }
+
+        // Get the ecash file contents.
+        const uint8_t* cash_data;
+        if(!get_file_contents(
+               app, &clipper_ecash_file_id, MfDesfireFileTypeBackup, 32, &cash_data)) {
+            break;
+        }
+
+        // Retrieve ride history file contents.
+        const uint8_t* history_index;
+        const uint8_t* history;
+        if(!get_file_contents(
+               app, &clipper_histidx_file_id, MfDesfireFileTypeBackup, 16, &history_index)) {
+            break;
+        }
+        if(!get_file_contents(
+               app, &clipper_history_file_id, MfDesfireFileTypeStandard, 512, &history)) {
+            break;
+        }
+
+        // Use a dummy string to verify that the ride history can be decoded.
+        FuriString* dummy_str = furi_string_alloc();
+        furi_string_free(dummy_str);
+
+        verified = true;
+    } while(false);
+
+    return verified;
+}

+ 5 - 4
metroflip/scenes/desfire.h

@@ -2,6 +2,7 @@
 #define DESFIRE_H
 
 #include "../metroflip_i.h"
+#include <lib/nfc/protocols/mf_desfire/mf_desfire.h>
 
 typedef enum {
     CARD_TYPE_ITSO,
@@ -11,9 +12,9 @@ typedef enum {
     CARD_TYPE_DESFIRE_UNKNOWN
 } DesfireCardType;
 
-bool itso_parse(const NfcDevice* device, FuriString* parsed_data);
-bool opal_parse(const NfcDevice* device, FuriString* parsed_data);
-bool clipper_parse(const NfcDevice* device, FuriString* parsed_data);
-bool myki_parse(const NfcDevice* device, FuriString* parsed_data);
+bool itso_verify(const MfDesfireData* data);
+bool opal_verify(const MfDesfireData* data);
+bool clipper_verify(const MfDesfireData* data);
+bool myki_verify(const MfDesfireData* data);
 
 #endif // DESFIRE_H

+ 191 - 94
metroflip/scenes/keys.c

@@ -9,11 +9,11 @@
 
 #define TAG "keys_check"
 
-const MfClassicKeyPair troika_1k_key[1] = {
+const MfClassicKeyPair troika_1k_key[] = {
     {.a = 0x08b386463229},
 };
 
-const MfClassicKeyPair troika_4k_key[1] = {
+const MfClassicKeyPair troika_4k_key[] = {
     {.a = 0xA73F5DC1D333},
 };
 
@@ -25,111 +25,163 @@ const MfClassicKeyPair charliecard_1k_verify_key[] = {
     {.a = 0x5EC39B022F2B},
 };
 
-const MfClassicKeyPair bip_1k_verify_key[1] = {
+const MfClassicKeyPair bip_1k_verify_key[] = {
     {.a = 0x3a42f33af429},
 };
 
-const MfClassicKeyPair metromoney_1k_verify_key[1] = {
+const MfClassicKeyPair metromoney_1k_verify_key[] = {
     {.a = 0x9C616585E26D},
 };
 
-static bool charliecard_verify(Nfc* nfc) {
+const uint8_t gocard_verify_data[1][14] = {
+    {0x16, 0x18, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x5A, 0x5B, 0x20, 0x21, 0x22, 0x23}};
+
+const uint8_t gocard_verify_data2[1][14] = {
+    {0x16, 0x18, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x01, 0x01}};
+
+static bool charliecard_verify(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
     bool verified = false;
     FURI_LOG_I(TAG, "verifying charliecard..");
-
+    const uint8_t verify_sector = 1;
     do {
-        const uint8_t verify_sector = 1;
-        const uint8_t verify_block = mf_classic_get_first_block_num_of_sector(verify_sector) + 1;
-        FURI_LOG_I(TAG, "Verifying sector %u", verify_sector);
-
-        MfClassicKey key = {0};
-        bit_lib_num_to_bytes_be(charliecard_1k_verify_key[0].a, COUNT_OF(key.data), key.data);
-
-        MfClassicAuthContext auth_context;
-        MfClassicError error =
-            mf_classic_poller_sync_auth(nfc, verify_block, &key, MfClassicKeyTypeA, &auth_context);
-        if(error != MfClassicErrorNone) {
-            FURI_LOG_I(TAG, "Failed to read block %u: %d", verify_block, error);
-            break;
+        if(!data_loaded) {
+            const uint8_t verify_block =
+                mf_classic_get_first_block_num_of_sector(verify_sector) + 1;
+            FURI_LOG_I(TAG, "Verifying sector %u", verify_sector);
+
+            MfClassicKey key = {0};
+            bit_lib_num_to_bytes_be(charliecard_1k_verify_key[0].a, COUNT_OF(key.data), key.data);
+
+            MfClassicAuthContext auth_context;
+            MfClassicError error = mf_classic_poller_sync_auth(
+                nfc, verify_block, &key, MfClassicKeyTypeA, &auth_context);
+            if(error != MfClassicErrorNone) {
+                FURI_LOG_I(TAG, "Failed to read block %u: %d", verify_block, error);
+                break;
+            }
+
+            verified = true;
+        } else {
+            MfClassicSectorTrailer* sec_tr =
+                mf_classic_get_sector_trailer_by_sector(mfc_data, verify_sector);
+            FURI_LOG_I(TAG, "%2x", sec_tr->key_a.data[1]);
+            uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
+            if(key != charliecard_1k_verify_key[0].a) {
+                FURI_LOG_I(TAG, "not equall");
+                break;
+            }
+
+            verified = true;
         }
-
-        verified = true;
     } while(false);
 
     return verified;
 }
 
-bool bip_verify(Nfc* nfc) {
+bool bip_verify(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
     bool verified = false;
 
     do {
-        const uint8_t verify_sector = 0;
-        uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
-        FURI_LOG_I(TAG, "Verifying sector %u", verify_sector);
+        if(!data_loaded) {
+            const uint8_t verify_sector = 0;
+            uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
+            FURI_LOG_I(TAG, "Verifying sector %u", verify_sector);
 
-        MfClassicKey key = {};
-        bit_lib_num_to_bytes_be(bip_1k_verify_key[0].a, COUNT_OF(key.data), key.data);
+            MfClassicKey key = {};
+            bit_lib_num_to_bytes_be(bip_1k_verify_key[0].a, COUNT_OF(key.data), key.data);
 
-        MfClassicAuthContext auth_ctx = {};
-        MfClassicError error =
-            mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
+            MfClassicAuthContext auth_ctx = {};
+            MfClassicError error =
+                mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
 
-        if(error != MfClassicErrorNone) {
-            FURI_LOG_I(TAG, "Failed to read block %u: %d", block_num, error);
-            break;
-        }
+            if(error != MfClassicErrorNone) {
+                FURI_LOG_I(TAG, "Failed to read block %u: %d", block_num, error);
+                break;
+            }
+
+            verified = true;
+        } else {
+            MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(mfc_data, 0);
 
-        verified = true;
+            uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
+            if(key != bip_1k_verify_key[0].a) {
+                break;
+            }
+
+            verified = true;
+        }
     } while(false);
 
     return verified;
 }
 
-static bool metromoney_verify(Nfc* nfc) {
+static bool metromoney_verify(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
     bool verified = false;
-
+    const uint8_t ticket_sector_number = 1;
     do {
-        const uint8_t ticket_sector_number = 1;
-        const uint8_t ticket_block_number =
-            mf_classic_get_first_block_num_of_sector(ticket_sector_number) + 1;
-        FURI_LOG_D(TAG, "Verifying sector %u", ticket_sector_number);
-
-        MfClassicKey key = {0};
-        bit_lib_num_to_bytes_be(metromoney_1k_verify_key[0].a, COUNT_OF(key.data), key.data);
-
-        MfClassicAuthContext auth_context;
-        MfClassicError error = mf_classic_poller_sync_auth(
-            nfc, ticket_block_number, &key, MfClassicKeyTypeA, &auth_context);
-        if(error != MfClassicErrorNone) {
-            FURI_LOG_D(TAG, "Failed to read block %u: %d", ticket_block_number, error);
-            break;
+        if(!data_loaded) {
+            const uint8_t ticket_block_number =
+                mf_classic_get_first_block_num_of_sector(ticket_sector_number) + 1;
+            FURI_LOG_D(TAG, "Verifying sector %u", ticket_sector_number);
+
+            MfClassicKey key = {0};
+            bit_lib_num_to_bytes_be(metromoney_1k_verify_key[0].a, COUNT_OF(key.data), key.data);
+
+            MfClassicAuthContext auth_context;
+            MfClassicError error = mf_classic_poller_sync_auth(
+                nfc, ticket_block_number, &key, MfClassicKeyTypeA, &auth_context);
+            if(error != MfClassicErrorNone) {
+                FURI_LOG_D(TAG, "Failed to read block %u: %d", ticket_block_number, error);
+                break;
+            }
+
+            verified = true;
+        } else {
+            MfClassicSectorTrailer* sec_tr =
+                mf_classic_get_sector_trailer_by_sector(mfc_data, ticket_sector_number);
+
+            uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
+            if(key != metromoney_1k_verify_key[0].a) {
+                break;
+            }
+
+            verified = true;
         }
-
-        verified = true;
     } while(false);
 
     return verified;
 }
 
-static bool smartrider_verify(Nfc* nfc) {
+static bool smartrider_verify(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
     bool verified = false;
 
     do {
-        const uint8_t block_number = mf_classic_get_first_block_num_of_sector(0) + 1;
-        FURI_LOG_D(TAG, "Verifying sector 0");
-
-        MfClassicKey key = {0};
-        bit_lib_num_to_bytes_be(smartrider_verify_key[0].a, COUNT_OF(key.data), key.data);
-
-        MfClassicAuthContext auth_context;
-        MfClassicError error =
-            mf_classic_poller_sync_auth(nfc, block_number, &key, MfClassicKeyTypeA, &auth_context);
-        if(error != MfClassicErrorNone) {
-            FURI_LOG_D(TAG, "Failed to read block %u: %d", block_number, error);
-            break;
+        if(!data_loaded) {
+            const uint8_t block_number = mf_classic_get_first_block_num_of_sector(0) + 1;
+            FURI_LOG_D(TAG, "Verifying sector 0");
+
+            MfClassicKey key = {0};
+            bit_lib_num_to_bytes_be(smartrider_verify_key[0].a, COUNT_OF(key.data), key.data);
+
+            MfClassicAuthContext auth_context;
+            MfClassicError error = mf_classic_poller_sync_auth(
+                nfc, block_number, &key, MfClassicKeyTypeA, &auth_context);
+            if(error != MfClassicErrorNone) {
+                FURI_LOG_D(TAG, "Failed to read block %u: %d", block_number, error);
+                break;
+            }
+
+            verified = true;
+        } else {
+            MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(mfc_data, 0);
+
+            uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
+            if(key != smartrider_verify_key[0].a) {
+                break;
+            }
+
+            verified = true;
         }
-
-        verified = true;
     } while(false);
 
     return verified;
@@ -151,51 +203,96 @@ static bool troika_get_card_config(TroikaCardConfig* config, MfClassicType type)
     return success;
 }
 
-static bool troika_verify_type(Nfc* nfc, MfClassicType type) {
+static bool
+    troika_verify_type(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded, MfClassicType type) {
     bool verified = false;
 
     do {
-        TroikaCardConfig cfg = {};
-        if(!troika_get_card_config(&cfg, type)) break;
+        if(!data_loaded) {
+            TroikaCardConfig cfg = {};
+            if(!troika_get_card_config(&cfg, type)) break;
+
+            const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
+            FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector);
+
+            MfClassicKey key = {0};
+            bit_lib_num_to_bytes_be(cfg.keys[0].a, COUNT_OF(key.data), key.data);
+
+            MfClassicAuthContext auth_context;
+            MfClassicError error = mf_classic_poller_sync_auth(
+                nfc, block_num, &key, MfClassicKeyTypeA, &auth_context);
+            if(error != MfClassicErrorNone) {
+                FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
+                break;
+            }
+            FURI_LOG_D(TAG, "Verify success!");
+            verified = true;
+        } else {
+            TroikaCardConfig cfg = {};
+            if(!troika_get_card_config(&cfg, type)) break;
+            MfClassicSectorTrailer* sec_tr =
+                mf_classic_get_sector_trailer_by_sector(mfc_data, cfg.data_sector);
+
+            uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
+            if(key != cfg.keys[0].a) {
+                break;
+            }
+
+            verified = true;
+        }
+    } while(false);
 
-        const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
-        FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector);
+    return verified;
+}
 
-        MfClassicKey key = {0};
-        bit_lib_num_to_bytes_be(cfg.keys[0].a, COUNT_OF(key.data), key.data);
+static bool troika_verify(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
+    return troika_verify_type(nfc, mfc_data, data_loaded, MfClassicType1k) ||
+           troika_verify_type(nfc, mfc_data, data_loaded, MfClassicType4k);
+}
 
-        MfClassicAuthContext auth_context;
-        MfClassicError error =
-            mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_context);
-        if(error != MfClassicErrorNone) {
-            FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
-            break;
+static bool gocard_verify(MfClassicData* mfc_data, bool data_loaded) {
+    bool verified = false;
+    FURI_LOG_I(TAG, "verifying charliecard..");
+    do {
+        if(data_loaded) {
+            uint8_t* buffer = &mfc_data->block[1].data[1];
+            size_t buffer_size = 14;
+
+            if(memcmp(buffer, gocard_verify_data[0], buffer_size) == 0) {
+                FURI_LOG_I(TAG, "Match!");
+            } else {
+                FURI_LOG_I(TAG, "No match.");
+                if(memcmp(buffer, gocard_verify_data2[0], buffer_size) == 0) {
+                    FURI_LOG_I(TAG, "Match!");
+                } else {
+                    FURI_LOG_I(TAG, "No match.");
+                    break;
+                }
+            }
+
+            verified = true;
         }
-        FURI_LOG_D(TAG, "Verify success!");
-        verified = true;
     } while(false);
 
     return verified;
 }
 
-static bool troika_verify(Nfc* nfc) {
-    return troika_verify_type(nfc, MfClassicType1k) || troika_verify_type(nfc, MfClassicType4k);
-}
-
-CardType determine_card_type(Nfc* nfc) {
+CardType determine_card_type(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
     FURI_LOG_I(TAG, "checking keys..");
     UNUSED(bip_verify);
 
-    if(bip_verify(nfc)) {
-        return CARD_TYPE_METROMONEY;
-    } else if(metromoney_verify(nfc)) {
+    if(bip_verify(nfc, mfc_data, data_loaded)) {
+        return CARD_TYPE_BIP;
+    } else if(metromoney_verify(nfc, mfc_data, data_loaded)) {
         return CARD_TYPE_METROMONEY;
-    } else if(smartrider_verify(nfc)) {
+    } else if(smartrider_verify(nfc, mfc_data, data_loaded)) {
         return CARD_TYPE_SMARTRIDER;
-    } else if(troika_verify(nfc)) {
+    } else if(troika_verify(nfc, mfc_data, data_loaded)) {
         return CARD_TYPE_TROIKA;
-    } else if(charliecard_verify(nfc)) {
+    } else if(charliecard_verify(nfc, mfc_data, data_loaded)) {
         return CARD_TYPE_CHARLIECARD;
+    } else if(gocard_verify(mfc_data, data_loaded)) {
+        return CARD_TYPE_GOCARD;
     } else {
         FURI_LOG_I(TAG, "its unknown");
         return CARD_TYPE_UNKNOWN;

+ 3 - 1
metroflip/scenes/keys.h

@@ -9,6 +9,7 @@ typedef enum {
     CARD_TYPE_CHARLIECARD,
     CARD_TYPE_SMARTRIDER,
     CARD_TYPE_TROIKA,
+    CARD_TYPE_GOCARD,
     CARD_TYPE_UNKNOWN
 } CardType;
 
@@ -28,9 +29,10 @@ typedef struct {
 
 extern const MfClassicKeyPair troika_1k_keys[16];
 extern const MfClassicKeyPair troika_4k_keys[40];
-extern const uint8_t SMARTRIDER_STANDARD_KEYS[3][6];
 extern const MfClassicKeyPair charliecard_1k_keys[16];
 extern const MfClassicKeyPair bip_1k_keys[16];
 extern const MfClassicKeyPair metromoney_1k_keys[16];
+extern const uint8_t gocard_verify_data[1][14];
+extern const uint8_t gocard_verify_data2[1][14];
 
 #endif // KEYS_H

+ 9 - 11
metroflip/scenes/metroflip_scene_auto.c

@@ -20,28 +20,24 @@ static NfcCommand
     Metroflip* app = context;
     NfcCommand command = NfcCommandContinue;
 
-    FuriString* parsed_data = furi_string_alloc();
-    furi_string_reset(app->text_box_store);
     const MfDesfirePollerEvent* mf_desfire_event = event.event_data;
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
-        if(clipper_parse(app->nfc_device, parsed_data)) {
-            furi_string_reset(app->text_box_store);
+        const MfDesfireData* data = nfc_device_get_data(app->nfc_device, NfcProtocolMfDesfire);
+        if(clipper_verify(data)) {
             view_dispatcher_send_custom_event(
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
             app->desfire_card_type = CARD_TYPE_CLIPPER;
-        } else if(itso_parse(app->nfc_device, parsed_data)) {
-            furi_string_reset(app->text_box_store);
+        } else if(itso_verify(data)) {
             view_dispatcher_send_custom_event(
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
             app->desfire_card_type = CARD_TYPE_ITSO;
-        } else if(myki_parse(app->nfc_device, parsed_data)) {
-            furi_string_reset(app->text_box_store);
+        } else if(myki_verify(data)) {
             view_dispatcher_send_custom_event(
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
             app->desfire_card_type = CARD_TYPE_MYKI;
-        } else if(opal_parse(app->nfc_device, parsed_data)) {
+        } else if(opal_verify(data)) {
             furi_string_reset(app->text_box_store);
             view_dispatcher_send_custom_event(
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
@@ -52,7 +48,6 @@ static NfcCommand
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
             app->desfire_card_type = CARD_TYPE_DESFIRE_UNKNOWN;
         }
-        furi_string_free(parsed_data);
         command = NfcCommandStop;
     } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
         view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);
@@ -160,7 +155,10 @@ bool metroflip_scene_auto_on_event(void* context, SceneManagerEvent event) {
             app->auto_mode = true;
             if(nfc_detected_protocols_get_protocol(app->detected_protocols, 0) ==
                NfcProtocolMfClassic) {
-                CardType card_type = determine_card_type(app->nfc);
+                MfClassicData* mfc_data = mf_classic_alloc();
+                app->data_loaded = false;
+                CardType card_type = determine_card_type(app->nfc, mfc_data, app->data_loaded);
+                mf_classic_free(mfc_data);
                 app->mfc_card_type = card_type;
                 Popup* popup = app->popup;
                 UNUSED(popup);

+ 4 - 0
metroflip/scenes/metroflip_scene_config.h

@@ -2,6 +2,10 @@ ADD_SCENE(metroflip, start, Start)
 ADD_SCENE(metroflip, auto, Auto)
 ADD_SCENE(metroflip, parse, Parse)
 ADD_SCENE(metroflip, ovc, OVC)
+ADD_SCENE(metroflip, save, Save)
+ADD_SCENE(metroflip, save_result, SaveResult)
+ADD_SCENE(metroflip, delete, Delete)
 ADD_SCENE(metroflip, supported, Supported)
 ADD_SCENE(metroflip, about, About)
 ADD_SCENE(metroflip, credits, Credits)
+ADD_SCENE(metroflip, load, Load)

+ 1 - 1
metroflip/scenes/metroflip_scene_credits.c

@@ -17,6 +17,7 @@ void metroflip_scene_credits_on_enter(void* context) {
     furi_string_cat_printf(str, "Created by luu176\n");
     furi_string_cat_printf(str, "Inspired by Metrodroid\n\n");
     furi_string_cat_printf(str, "Special Thanks:\n willyjl\n");
+    furi_string_cat_printf(str, "Info Slaves:\n Equip, TheDingo8MyBaby\n\n");
     furi_string_cat_printf(str, "\e#Parser Credits:\n\n");
     furi_string_cat_printf(str, "Bip! Parser:\n rbasoalto, gornekich\n\n");
     furi_string_cat_printf(str, "CharlieCard Parser:\n zacharyweiss\n\n");
@@ -29,7 +30,6 @@ void metroflip_scene_credits_on_enter(void* context) {
     furi_string_cat_printf(str, "Opus Parser: DocSystem\n\n");
     furi_string_cat_printf(str, "Rav-Kav Parser: luu176\n\n");
     furi_string_cat_printf(str, "Troika Parser:\n gornekich\n\n");
-    furi_string_cat_printf(str, "Info Slaves:\n Equip, TheDingo8MyBaby\n\n");
 
     widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(str));
 

+ 62 - 0
metroflip/scenes/metroflip_scene_delete.c

@@ -0,0 +1,62 @@
+#include "../metroflip_i.h"
+#include "../api/metroflip/metroflip_api.h"
+#include <stdio.h>
+enum PopupEvent {
+    PopupEventExit,
+};
+
+static void metroflip_scene_delete_popup_callback(void* context) {
+    Metroflip* app = context;
+
+    view_dispatcher_send_custom_event(app->view_dispatcher, PopupEventExit);
+}
+
+void metroflip_scene_delete_on_enter(void* context) {
+    Metroflip* app = context;
+    Popup* popup = app->popup;
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FURI_LOG_I("PATH", "PATH: %s", app->delete_file_path);
+    bool success = storage_simply_remove(storage, app->delete_file_path);
+    furi_record_close(RECORD_STORAGE);
+    if(success) {
+        popup_set_icon(popup, 0, 2, &I_DolphinMafia_119x62);
+        popup_set_header(popup, "Deleted", 80, 19, AlignLeft, AlignBottom);
+        popup_enable_timeout(popup);
+    } else {
+        popup_set_icon(popup, 69, 15, &I_WarningDolphinFlip_45x42);
+        popup_set_header(popup, "Error!", 13, 22, AlignLeft, AlignBottom);
+        popup_disable_timeout(popup);
+    }
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, app);
+    popup_set_callback(popup, metroflip_scene_delete_popup_callback);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+}
+
+bool metroflip_scene_delete_on_event(void* context, SceneManagerEvent event) {
+    Metroflip* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+        switch(event.event) {
+        case PopupEventExit:
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, MetroflipSceneStart);
+            break;
+        default:
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void metroflip_scene_delete_on_exit(void* context) {
+    Metroflip* app = context;
+    app->delete_file_path[0] = '\0';
+
+    popup_reset(app->popup);
+}

+ 151 - 0
metroflip/scenes/metroflip_scene_load.c

@@ -0,0 +1,151 @@
+#include "../metroflip_i.h"
+#include <dolphin/dolphin.h>
+#include <furi.h>
+#include <bit_lib.h>
+#include <lib/nfc/protocols/nfc_protocol.h>
+#include "../api/metroflip/metroflip_api.h"
+#define TAG "Metroflip:Scene:Load"
+#include "keys.h"
+#include <nfc/protocols/mf_classic/mf_classic.h>
+
+void metroflip_scene_load_on_enter(void* context) {
+    Metroflip* app = (Metroflip*)context;
+    // We initialized this to be false every time we enter
+    app->data_loaded = false;
+    bool has_card_type = false;
+    // The same string we will use to direct parse scene which plugin to call
+    // Extracted from the file
+    FuriString* card_type = furi_string_alloc();
+    FuriString* device_type = furi_string_alloc();
+
+    // All the app_data browser stuff. Don't worry about this
+    DialogsFileBrowserOptions browser_options;
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    storage_simply_mkdir(storage, STORAGE_APP_DATA_PATH_PREFIX);
+    dialog_file_browser_set_basic_options(&browser_options, METROFLIP_FILE_EXTENSION, &I_icon);
+    browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX;
+    FuriString* file_path = furi_string_alloc_set(browser_options.base_path);
+
+    if(dialog_file_browser_show(app->dialogs, file_path, file_path, &browser_options)) {
+        FlipperFormat* format = flipper_format_file_alloc(storage);
+        do {
+            if(!flipper_format_file_open_existing(format, furi_string_get_cstr(file_path))) break;
+            if(!flipper_format_read_string(format, "Device type", device_type)) break;
+            const char* protocol_name = furi_string_get_cstr(device_type);
+            if(!flipper_format_read_string(format, "Card Type", card_type)) {
+                flipper_format_file_close(format);
+                flipper_format_file_open_existing(format, furi_string_get_cstr(file_path));
+
+                if(strcmp(protocol_name, "Mifare Classic") == 0) {
+                    MfClassicData* mfc_data = mf_classic_alloc();
+                    if(!mf_classic_load(mfc_data, format, 2)) break;
+                    app->data_loaded = true;
+                    CardType card_type = determine_card_type(app->nfc, mfc_data, app->data_loaded);
+                    app->mfc_card_type = card_type;
+                    has_card_type = true;
+                    switch(card_type) {
+                    case CARD_TYPE_METROMONEY:
+                        app->card_type = "metromoney";
+                        FURI_LOG_I(TAG, "Detected: Metromoney\n");
+                        break;
+                    case CARD_TYPE_CHARLIECARD:
+                        app->card_type = "charliecard";
+                        FURI_LOG_I(TAG, "Detected: CharlieCard\n");
+                        break;
+                    case CARD_TYPE_SMARTRIDER:
+                        app->card_type = "smartrider";
+                        FURI_LOG_I(TAG, "Detected: SmartRider\n");
+                        break;
+                    case CARD_TYPE_TROIKA:
+                        app->card_type = "troika";
+                        FURI_LOG_I(TAG, "Detected: Troika\n");
+                        break;
+                    case CARD_TYPE_GOCARD:
+                        app->card_type = "gocard";
+                        FURI_LOG_I(TAG, "Detected: go card\n");
+                        break;
+                    case CARD_TYPE_UNKNOWN:
+                        app->card_type = "unknown";
+                        //popup_set_header(popup, "Unsupported\n card", 58, 31, AlignLeft, AlignTop);
+                        break;
+                    default:
+                        app->card_type = "unknown";
+                        FURI_LOG_I(TAG, "Detected: Unknown card type\n");
+                        //popup_set_header(popup, "Unsupported\n card", 58, 31, AlignLeft, AlignTop);
+                        break;
+                    }
+                    mf_classic_free(mfc_data);
+                } else if(strcmp(protocol_name, "Mifare DESFire") == 0) {
+                    MfDesfireData* data = mf_desfire_alloc();
+                    if(!mf_desfire_load(data, format, 2)) break;
+                    app->data_loaded = true;
+                    if(clipper_verify(data)) {
+                        app->card_type = "clipper";
+                        FURI_LOG_I(TAG, "Detected: Clipper");
+                    } else if(itso_verify(data)) {
+                        app->card_type = "itso";
+                        FURI_LOG_I(TAG, "Detected: ITSO");
+                    } else if(myki_verify(data)) {
+                        app->card_type = "myki";
+                        FURI_LOG_I(TAG, "Detected: Myki");
+                    } else if(opal_verify(data)) {
+                        app->card_type = "opal";
+                        FURI_LOG_I(TAG, "Detected: Opal");
+                    } else {
+                        app->card_type = "unknown";
+                        FURI_LOG_I(TAG, "Detected: none");
+                    }
+                    mf_desfire_free(data);
+                    has_card_type = true;
+                } else {
+                    has_card_type = true;
+                }
+                flipper_format_file_close(format);
+            } else {
+                has_card_type = false;
+            }
+            app->file_path = furi_string_get_cstr(file_path);
+            strncpy(
+                app->delete_file_path,
+                furi_string_get_cstr(file_path),
+                sizeof(app->delete_file_path) - 1);
+            app->delete_file_path[sizeof(app->delete_file_path) - 1] = '\0';
+
+            app->data_loaded = true;
+        } while(0);
+        flipper_format_free(format);
+    }
+
+    if(app->data_loaded) {
+        // Direct to the parsing screen just like the auto scene does
+        if(!has_card_type) {
+            app->card_type = furi_string_get_cstr(card_type);
+            has_card_type = false;
+        }
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_next_scene(app->scene_manager, MetroflipSceneParse);
+    } else {
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+    }
+    furi_string_free(file_path);
+    furi_record_close(RECORD_STORAGE);
+}
+
+bool metroflip_scene_load_on_event(void* context, SceneManagerEvent event) {
+    Metroflip* app = context;
+    UNUSED(event);
+    bool consumed = false;
+    // If they don't select any file in the brwoser and press back button,
+    // the data is not loaded
+    if(!app->data_loaded) {
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+    }
+    consumed = true;
+
+    return consumed;
+}
+
+void metroflip_scene_load_on_exit(void* context) {
+    Metroflip* app = context;
+    UNUSED(app);
+}

+ 2 - 1
metroflip/scenes/metroflip_scene_parse.c

@@ -1,6 +1,7 @@
 #include "../metroflip_i.h"
 #include <furi.h>
 #include "../metroflip_plugins.h"
+#include "../api/metroflip/metroflip_api.h"
 #define TAG "Metroflip:Scene:Parse"
 #include <stdio.h>
 
@@ -62,5 +63,5 @@ void metroflip_scene_parse_on_exit(void* context) {
         plugin_manager_free(app->plugin_manager);
         composite_api_resolver_free(app->resolver);
     }
-    app->card_type = "unknown";
+    app->data_loaded = false;
 }

+ 55 - 0
metroflip/scenes/metroflip_scene_save.c

@@ -0,0 +1,55 @@
+#include "../metroflip_i.h"
+
+enum TextInputResult {
+    TextInputResultOk,
+};
+
+static void metroflip_scene_save_text_input_callback(void* context) {
+    Metroflip* app = context;
+
+    view_dispatcher_send_custom_event(app->view_dispatcher, TextInputResultOk);
+}
+
+void metroflip_scene_save_on_enter(void* context) {
+    Metroflip* app = context;
+    TextInput* text_input = app->text_input;
+
+    text_input_set_header_text(text_input, "Save the NFC tag:");
+
+    text_input_set_result_callback(
+        text_input,
+        metroflip_scene_save_text_input_callback,
+        app,
+        app->save_buf,
+        sizeof(app->save_buf),
+        true);
+
+    ValidatorIsFile* validator_is_file =
+        validator_is_file_alloc_init(APP_DATA_PATH(), METROFLIP_FILE_EXTENSION, NULL);
+    text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewTextInput);
+}
+
+bool metroflip_scene_save_on_event(void* context, SceneManagerEvent event) {
+    Metroflip* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+        switch(event.event) {
+        case TextInputResultOk:
+            scene_manager_next_scene(app->scene_manager, MetroflipSceneSaveResult);
+            break;
+        default:
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void metroflip_scene_save_on_exit(void* context) {
+    Metroflip* app = context;
+    text_input_reset(app->text_input);
+}

+ 69 - 0
metroflip/scenes/metroflip_scene_save_result.c

@@ -0,0 +1,69 @@
+#include "../metroflip_i.h"
+#include "../api/metroflip/metroflip_api.h"
+#include <stdio.h>
+enum PopupEvent {
+    PopupEventExit,
+};
+
+static void metroflip_scene_save_result_popup_callback(void* context) {
+    Metroflip* app = context;
+
+    view_dispatcher_send_custom_event(app->view_dispatcher, PopupEventExit);
+}
+
+void metroflip_scene_save_result_on_enter(void* context) {
+    Metroflip* app = context;
+    Popup* popup = app->popup;
+
+    char path[280];
+    snprintf(path, sizeof(path), "/ext/apps_data/metroflip/%s.nfc", app->save_buf);
+    FURI_LOG_I("path", "path: %s", path);
+    bool success = nfc_device_save(app->nfc_device, path);
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FlipperFormat* ff = flipper_format_file_alloc(storage);
+    flipper_format_write_empty_line(ff);
+    flipper_format_file_open_existing(ff, path);
+    flipper_format_insert_or_update_string_cstr(ff, "Card Type", app->card_type);
+    flipper_format_file_close(ff);
+    flipper_format_free(ff);
+    furi_record_close(RECORD_STORAGE);
+
+    if(success) {
+        popup_set_icon(popup, 36, 5, &I_DolphinDone_80x58);
+        popup_set_header(popup, "Saved!", 13, 22, AlignLeft, AlignBottom);
+        popup_enable_timeout(popup);
+    } else {
+        popup_set_icon(popup, 69, 15, &I_WarningDolphinFlip_45x42);
+        popup_set_header(popup, "Error!", 13, 22, AlignLeft, AlignBottom);
+        popup_disable_timeout(popup);
+    }
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, app);
+    popup_set_callback(popup, metroflip_scene_save_result_popup_callback);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+}
+
+bool metroflip_scene_save_result_on_event(void* context, SceneManagerEvent event) {
+    Metroflip* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        consumed = true;
+        switch(event.event) {
+        case PopupEventExit:
+            scene_manager_search_and_switch_to_previous_scene(
+                app->scene_manager, MetroflipSceneStart);
+            break;
+        default:
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void metroflip_scene_save_result_on_exit(void* context) {
+    Metroflip* app = context;
+    popup_reset(app->popup);
+}

+ 3 - 0
metroflip/scenes/metroflip_scene_start.c

@@ -21,6 +21,9 @@ void metroflip_scene_start_on_enter(void* context) {
         metroflip_scene_start_submenu_callback,
         app);
 
+    submenu_add_item(
+        submenu, "Saved", MetroflipSceneLoad, metroflip_scene_start_submenu_callback, app);
+
     submenu_add_item(
         submenu,
         "Supported Cards",

+ 9 - 9
metroflip/scenes/metroflip_scene_supported.c

@@ -15,23 +15,23 @@ void metroflip_scene_supported_on_enter(void* context) {
 
     furi_string_printf(str, "\e#Supported Cards:\n\n");
     furi_string_cat_printf(
-        str, " - Bip!:\nSantiago de Chile, Chile\nProtocol: Mifare Classic\n\n");
-    furi_string_cat_printf(str, " - Charliecard:\nBoston, MA, USA\nProtocol: Mifare Classic\n\n");
+        str, " - Bip!:\nSantiago de Chile, Chile\nProtocol: MIFARE Classic\n\n");
+    furi_string_cat_printf(str, " - Charliecard:\nBoston, MA, USA\nProtocol: MIFARE Classic\n\n");
     furi_string_cat_printf(
-        str, " - Clipper:\nSan Francisco, CA, USA\nProtocol: Mifare DESFire\n\n");
-    furi_string_cat_printf(str, " - ITSO:\nUnited Kingdom\nProtocol: Mifare DESFire\n\n");
-    furi_string_cat_printf(str, " - Metromoney:\nTbilisi, Georgia\nProtocol: Mifare Classic\n\n");
+        str, " - Clipper:\nSan Francisco, CA, USA\nProtocol: MIFARE DESFire\n\n");
+    furi_string_cat_printf(str, " - ITSO:\nUnited Kingdom\nProtocol: MIFARE DESFire\n\n");
+    furi_string_cat_printf(str, " - Metromoney:\nTbilisi, Georgia\nProtocol: MIFARE Classic\n\n");
     furi_string_cat_printf(
         str,
-        " - myki:\n🇦🇺 Melbourne (and surrounds), VIC, Australia\nProtocol: Mifare DESFire\n\n");
+        " - myki:\nMelbourne (and surrounds), VIC, Australia\nProtocol: MIFARE DESFire\n\n");
     furi_string_cat_printf(str, " - Navigo:\nParis, France\nProtocol: Calypso\n\n");
     furi_string_cat_printf(
-        str, " - Opal:\nSydney (and surrounds), NSW, Australia\nProtocol: Mifare DESFire\n\n");
+        str, " - Opal:\nSydney (and surrounds), NSW, Australia\nProtocol: MIFARE DESFire\n\n");
     furi_string_cat_printf(str, " - Opus:\nMontreal, QC, Canada\nProtocol: Calypso\n\n");
     furi_string_cat_printf(str, " - Rav-Kav:\nIsrael\nProtocol: Calypso\n\n");
     furi_string_cat_printf(
-        str, " - SmartRider:\nWestern Australia, Australia\nProtocol: Mifare Classic\n\n");
-    furi_string_cat_printf(str, " - Troika:\nMoscow, Russia\nProtocol: Mifare Classic\n\n");
+        str, " - SmartRider:\nPerth, WA, Australia\nProtocol: MIFARE Classic\n\n");
+    furi_string_cat_printf(str, " - Troika:\nMoscow, Russia\nProtocol: MIFARE Classic\n\n");
 
     widget_add_text_scroll_element(widget, 0, 0, 128, 64, furi_string_get_cstr(str));
 

+ 44 - 16
metroflip/scenes/plugins/bip.c

@@ -133,9 +133,7 @@ static bool is_bip_block_empty(const MfClassicBlock* block) {
     return true;
 }
 
-static bool
-    bip_parse(const NfcDevice* device, FuriString* parsed_data, const MfClassicData* data) {
-    furi_assert(device);
+static bool bip_parse(FuriString* parsed_data, const MfClassicData* data) {
     furi_assert(parsed_data);
 
     struct {
@@ -311,7 +309,7 @@ static NfcCommand bip_poller_callback(NfcGenericEvent event, void* context) {
 
         dolphin_deed(DolphinDeedNfcReadSuccess);
         furi_string_reset(app->text_box_store);
-        if(!bip_parse(app->nfc_device, parsed_data, mfc_data)) {
+        if(!bip_parse(parsed_data, mfc_data)) {
             furi_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
@@ -321,6 +319,8 @@ static NfcCommand bip_poller_callback(NfcGenericEvent event, void* context) {
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -339,18 +339,46 @@ static void bip_on_enter(Metroflip* app) {
 
     app->sec_num = 0;
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
-
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
-    nfc_poller_start(app->poller, bip_poller_callback, app);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfClassicData* mfc_data = mf_classic_alloc();
+            mf_classic_load(mfc_data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
 
-    metroflip_app_blink_start(app);
+            furi_string_reset(app->text_box_store);
+            if(!bip_parse(parsed_data, mfc_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            mf_classic_free(mfc_data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
+        nfc_poller_start(app->poller, bip_poller_callback, app);
+
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool bip_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -385,7 +413,7 @@ static bool bip_on_event(Metroflip* app, SceneManagerEvent event) {
 static void bip_on_exit(Metroflip* app) {
     widget_reset(app->widget);
 
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 52 - 7
metroflip/scenes/plugins/calypso.c

@@ -321,6 +321,8 @@ void metroflip_next_button_widget_callback(GuiButtonType result, InputType type,
             ctx->page_id = 0;
             scene_manager_search_and_switch_to_previous_scene(
                 app->scene_manager, MetroflipSceneStart);
+            scene_manager_set_scene_state(
+                app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
             return;
         }
         if(ctx->page_id < 10) {
@@ -352,6 +354,8 @@ void metroflip_next_button_widget_callback(GuiButtonType result, InputType type,
                 ctx->page_id = 0;
                 scene_manager_search_and_switch_to_previous_scene(
                     app->scene_manager, MetroflipSceneStart);
+                scene_manager_set_scene_state(
+                    app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
                 return;
             }
             ctx->page_id += 1;
@@ -359,6 +363,8 @@ void metroflip_next_button_widget_callback(GuiButtonType result, InputType type,
             ctx->page_id = 0;
             scene_manager_search_and_switch_to_previous_scene(
                 app->scene_manager, MetroflipSceneStart);
+            scene_manager_set_scene_state(
+                app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
             return;
         }
 
@@ -1783,8 +1789,17 @@ static NfcCommand calypso_poller_callback(NfcGenericEvent event, void* context)
                     if(card->card_type == CALYPSO_CARD_RAVKAV) {
                         card->ravkav = malloc(sizeof(RavKavCardData));
 
+                        // Prepare calypso structure
+
+                        CalypsoApp* RavKavContractStructure = get_ravkav_contract_structure();
+                        if(!RavKavContractStructure) {
+                            FURI_LOG_E(TAG, "Failed to load RavKav Contract structure");
+                            break;
+                        }
+
+                        //get balance
                         error = select_new_app(
-                            0x20, 0x20, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                            0x20, 0x2A, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
                         if(error != 0) {
                             FURI_LOG_E(TAG, "Failed to select app for contracts");
                             break;
@@ -1793,15 +1808,44 @@ static NfcCommand calypso_poller_callback(NfcGenericEvent event, void* context)
                         // Check the response after selecting app
                         if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                             FURI_LOG_E(
-                                TAG, "Failed to check response after selecting app for contracts");
+                                TAG, "Failed to check response after selecting app for counter");
                             break;
                         }
 
-                        // Prepare calypso structure
+                        error = read_new_file(
+                            1, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                        if(error != 0) {
+                            FURI_LOG_E(TAG, "Failed to read counter %d", 1);
+                            break;
+                        }
 
-                        CalypsoApp* RavKavContractStructure = get_ravkav_contract_structure();
-                        if(!RavKavContractStructure) {
-                            FURI_LOG_E(TAG, "Failed to load RavKav Contract structure");
+                        // Check the response after reading the file
+                        if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                            FURI_LOG_E(
+                                TAG, "Failed to check response after reading counter %d", 1);
+                            break;
+                        }
+
+                        uint32_t value = 0;
+                        for(uint8_t i = 0; i < 3; i++) {
+                            value = (value << 8) | bit_buffer_get_byte(rx_buffer, i);
+                        }
+                        float result = value / 100.0f;
+                        FURI_LOG_I(TAG, "Value: %.2f ILS", (double)result);
+
+                        card->ravkav->contracts[0].balance = result;
+
+                        error = select_new_app(
+                            0x20, 0x20, tx_buffer, rx_buffer, iso14443_4b_poller, app, &stage);
+                        if(error != 0) {
+                            FURI_LOG_E(TAG, "Failed to select app for contracts");
+                            break;
+                        }
+
+                        // Check the response after selecting app
+                        if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
+                            FURI_LOG_E(
+                                TAG, "Failed to check response after selecting app for contracts");
                             break;
                         }
 
@@ -2477,6 +2521,7 @@ static bool calypso_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -2484,7 +2529,7 @@ static bool calypso_on_event(Metroflip* app, SceneManagerEvent event) {
 }
 
 static void calypso_on_exit(Metroflip* app) {
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 42 - 10
metroflip/scenes/plugins/charliecard.c

@@ -1235,6 +1235,8 @@ static NfcCommand
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -1253,17 +1255,46 @@ static void charliecard_on_enter(Metroflip* app) {
 
     app->sec_num = 0;
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfClassicData* mfc_data = mf_classic_alloc();
+            mf_classic_load(mfc_data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
+
+            furi_string_reset(app->text_box_store);
+            if(!charliecard_parse(parsed_data, mfc_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+
+            mf_classic_free(mfc_data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
 
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
-    nfc_poller_start(app->poller, metroflip_scene_charlicard_poller_callback, app);
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
+        nfc_poller_start(app->poller, metroflip_scene_charlicard_poller_callback, app);
 
-    metroflip_app_blink_start(app);
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool charliecard_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -1289,6 +1320,7 @@ static bool charliecard_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -1298,7 +1330,7 @@ static bool charliecard_on_event(Metroflip* app, SceneManagerEvent event) {
 static void charliecard_on_exit(Metroflip* app) {
     widget_reset(app->widget);
 
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 48 - 17
metroflip/scenes/plugins/clipper.c

@@ -232,15 +232,12 @@ static int16_t get_i16be(const uint8_t* field) {
         return raw;
 }
 
-bool clipper_parse(const NfcDevice* device, FuriString* parsed_data) {
-    furi_assert(device);
+bool clipper_parse(const MfDesfireData* data, FuriString* parsed_data) {
     furi_assert(parsed_data);
 
     bool parsed = false;
 
     do {
-        const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
-
         const MfDesfireApplication* app = NULL;
         const char* device_description = NULL;
 
@@ -579,7 +576,9 @@ static NfcCommand clipper_poller_callback(NfcGenericEvent event, void* context)
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
-        if(!clipper_parse(app->nfc_device, parsed_data)) {
+        const MfDesfireData* data = nfc_device_get_data(app->nfc_device, NfcProtocolMfDesfire);
+
+        if(!clipper_parse(data, parsed_data)) {
             furi_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
@@ -588,6 +587,8 @@ static NfcCommand clipper_poller_callback(NfcGenericEvent event, void* context)
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -604,18 +605,47 @@ static NfcCommand clipper_poller_callback(NfcGenericEvent event, void* context)
 static void clipper_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
-
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
-    nfc_poller_start(app->poller, clipper_poller_callback, app);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfDesfireData* data = mf_desfire_alloc();
+            mf_desfire_load(data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
 
-    metroflip_app_blink_start(app);
+            furi_string_reset(app->text_box_store);
+            if(!clipper_parse(data, parsed_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            mf_desfire_free(data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        nfc_scanner_alloc(app->nfc);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
+        nfc_poller_start(app->poller, clipper_poller_callback, app);
+
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool clipper_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -641,6 +671,7 @@ static bool clipper_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -651,7 +682,7 @@ static void clipper_on_exit(Metroflip* app) {
     widget_reset(app->widget);
     metroflip_app_blink_stop(app);
 
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 245 - 0
metroflip/scenes/plugins/gocard.c

@@ -0,0 +1,245 @@
+
+#include <flipper_application.h>
+#include "../../metroflip_i.h"
+
+#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
+#include <nfc/protocols/mf_classic/mf_classic.h>
+#include <nfc/protocols/mf_classic/mf_classic_poller.h>
+
+#include <dolphin/dolphin.h>
+#include <bit_lib.h>
+#include <furi_hal.h>
+#include <nfc/nfc.h>
+#include <nfc/nfc_device.h>
+#include <nfc/nfc_listener.h>
+#include "../../api/metroflip/metroflip_api.h"
+#include "../../metroflip_plugins.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#define TAG "Metroflip:Scene:gocard"
+
+typedef enum {
+    CHILD = 2051, // 0x803
+    ADULT = 3073 // 0xc01
+} ConcessionType;
+
+// Function to print concession type
+void printConcessionType(unsigned short concession_type, FuriString* parsed_data) {
+    switch(concession_type) {
+    case CHILD:
+        furi_string_cat_printf(parsed_data, "Concession Type: Child\n");
+        break;
+    case ADULT:
+        furi_string_cat_printf(parsed_data, "Concession Type: Adult\n");
+        break;
+    default:
+        furi_string_cat_printf(parsed_data, "Concession Type: 0x%X\n", concession_type);
+        break;
+    }
+}
+
+unsigned short byteArrayToIntReversed(unsigned int dec1, unsigned int dec2) {
+    unsigned char byte1 = (unsigned char)dec1;
+    unsigned char byte2 = (unsigned char)dec2;
+    return ((unsigned short)byte2 << 8) | byte1;
+}
+
+// Function to extract a substring and convert binary to decimal
+uint32_t extract_and_convert(const char* str, int start, int length) {
+    uint32_t value = 0;
+    for(int i = 0; i < length; i++) {
+        if(str[start + i] == '1') {
+            value |= (1U << (length - 1 - i));
+        }
+    }
+    return value;
+}
+
+void parse_gocard_time(const char* bin_str, FuriString* parsed_data) {
+    int len = strlen(bin_str);
+    if(len != 32 && len != 33) {
+        FURI_LOG_I(TAG, "Invalid input length");
+        return;
+    }
+
+    // Extract values from right to left using bit_slice_to_dec
+    uint32_t day = bit_slice_to_dec(bin_str, len - 5, len);
+    uint32_t month = bit_slice_to_dec(bin_str, len - 9, len - 6);
+    uint32_t year = bit_slice_to_dec(bin_str, len - 15, len - 10);
+    uint32_t minutes = bit_slice_to_dec(bin_str, len - 26, len - 16);
+
+    // Convert year from offset 2000
+    year += 2000;
+
+    // Convert minutes since midnight to HH:MM
+    uint32_t hours = minutes / 60;
+    uint32_t mins = minutes % 60;
+
+    // Format output string: "YYYY-MM-DD HH:MM"
+    furi_string_cat_printf(
+        parsed_data, "%04lu-%02lu-%02lu %02lu:%02lu\n", year, month, day, hours, mins);
+}
+
+static bool gocard_parse(FuriString* parsed_data, const MfClassicData* data) {
+    bool parsed = false;
+
+    do {
+        int balance_slot = 4;
+
+        if(data->block[balance_slot].data[13] <= data->block[balance_slot + 1].data[13])
+            balance_slot++;
+
+        unsigned short balancecents = byteArrayToIntReversed(
+            data->block[balance_slot].data[2], data->block[balance_slot].data[3]);
+
+        // Check if the sign flag is set in 'balance'
+        if((balancecents & 0x8000) == 0x8000) {
+            balancecents = balancecents & 0x7fff; // Clear the sign flag.
+            balancecents *= -1; // Negate the balance.
+        }
+        // Otherwise, check the sign flag in data->block[4].data[1]
+        else if((data->block[balance_slot].data[1] & 0x80) == 0x80) {
+            // seq_go uses a sign flag in an adjacent byte.
+            balancecents *= -1;
+        }
+
+        double balance = balancecents / 100.0;
+        furi_string_printf(parsed_data, "\e#go card\nValue: A$%.2f\n", balance); //show balance
+
+        int start_index = 4; //byte to start at
+        int end_index = 7; // byte to end at
+        int config_block = 6; //block number containing card configuration
+        int num_bytes = end_index - start_index + 1;
+        char config_bit_representation[num_bytes * 8 + 1];
+
+        for(int i = end_index, j = 0; i >= start_index;
+            i--, j++) { // Reverse the order of bytes and converty to binary
+            char bits[9];
+            byte_to_binary(data->block[config_block].data[i], bits);
+            memcpy(&config_bit_representation[j * 8], bits, 8);
+        }
+
+        config_bit_representation[num_bytes * 8] = '\0'; //add a null terminator as always
+        furi_string_cat_printf(parsed_data, "Expiry:\n");
+        parse_gocard_time(config_bit_representation, parsed_data);
+        FURI_LOG_I(TAG, "bitrepr: %s", config_bit_representation);
+
+        //concession type:
+
+        unsigned short concession_type = byteArrayToIntReversed(
+            data->block[config_block].data[8], data->block[config_block].data[9]);
+
+        printConcessionType(concession_type, parsed_data);
+
+        parsed = true;
+    } while(false);
+
+    return parsed;
+}
+
+static void gocard_on_enter(Metroflip* app) {
+    dolphin_deed(DolphinDeedNfcRead);
+
+    app->sec_num = 0;
+
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfClassicData* mfc_data = mf_classic_alloc();
+            mf_classic_load(mfc_data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
+
+            furi_string_reset(app->text_box_store);
+            if(!gocard_parse(parsed_data, mfc_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            mf_classic_free(mfc_data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "unsupported", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+    }
+}
+
+static bool gocard_on_event(Metroflip* app, SceneManagerEvent event) {
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == MetroflipCustomEventCardDetected) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "DON'T\nMOVE", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventCardLost) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "Card \n lost", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventWrongCard) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "WRONG \n CARD", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == MetroflipCustomEventPollerFail) {
+            Popup* popup = app->popup;
+            popup_set_header(popup, "Failed", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+static void gocard_on_exit(Metroflip* app) {
+    widget_reset(app->widget);
+
+    if(app->poller && !app->data_loaded) {
+        nfc_poller_stop(app->poller);
+        nfc_poller_free(app->poller);
+    }
+
+    // Clear view
+    popup_reset(app->popup);
+
+    metroflip_app_blink_stop(app);
+}
+
+/* Actual implementation of app<>plugin interface */
+static const MetroflipPlugin gocard_plugin = {
+    .card_name = "gocard",
+    .plugin_on_enter = gocard_on_enter,
+    .plugin_on_event = gocard_on_event,
+    .plugin_on_exit = gocard_on_exit,
+
+};
+
+/* Plugin descriptor to comply with basic plugin specification */
+static const FlipperAppPluginDescriptor gocard_plugin_descriptor = {
+    .appid = METROFLIP_SUPPORTED_CARD_PLUGIN_APP_ID,
+    .ep_api_version = METROFLIP_SUPPORTED_CARD_PLUGIN_API_VERSION,
+    .entry_point = &gocard_plugin,
+};
+
+/* Plugin entry point - must return a pointer to const descriptor  */
+const FlipperAppPluginDescriptor* gocard_plugin_ep(void) {
+    return &gocard_plugin_descriptor;
+}

+ 47 - 17
metroflip/scenes/plugins/itso.c

@@ -28,15 +28,12 @@ uint64_t swap_uint64(uint64_t val) {
     return (val << 32) | (val >> 32);
 }
 
-bool itso_parse(const NfcDevice* device, FuriString* parsed_data) {
-    furi_assert(device);
+bool itso_parse(const MfDesfireData* data, FuriString* parsed_data) {
     furi_assert(parsed_data);
 
     bool parsed = false;
 
     do {
-        const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
-
         const MfDesfireApplication* app = mf_desfire_get_application(data, &itso_app_id);
         if(app == NULL) break;
 
@@ -126,7 +123,8 @@ static NfcCommand itso_poller_callback(NfcGenericEvent event, void* context) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
-        if(!itso_parse(app->nfc_device, parsed_data)) {
+        const MfDesfireData* data = nfc_device_get_data(app->nfc_device, NfcProtocolMfDesfire);
+        if(!itso_parse(data, parsed_data)) {
             furi_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
@@ -135,6 +133,8 @@ static NfcCommand itso_poller_callback(NfcGenericEvent event, void* context) {
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -151,18 +151,47 @@ static NfcCommand itso_poller_callback(NfcGenericEvent event, void* context) {
 static void itso_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfDesfireData* data = mf_desfire_alloc();
+            mf_desfire_load(data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
 
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
-    nfc_poller_start(app->poller, itso_poller_callback, app);
-
-    metroflip_app_blink_start(app);
+            furi_string_reset(app->text_box_store);
+            if(!itso_parse(data, parsed_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            mf_desfire_free(data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        nfc_scanner_alloc(app->nfc);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
+        nfc_poller_start(app->poller, itso_poller_callback, app);
+
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool itso_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -188,6 +217,7 @@ static bool itso_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -197,7 +227,7 @@ static bool itso_on_event(Metroflip* app, SceneManagerEvent event) {
 static void itso_on_exit(Metroflip* app) {
     widget_reset(app->widget);
     metroflip_app_blink_stop(app);
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 44 - 13
metroflip/scenes/plugins/metromoney.c

@@ -124,6 +124,8 @@ static NfcCommand metromoney_poller_callback(NfcGenericEvent event, void* contex
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -142,18 +144,46 @@ static void metromoney_on_enter(Metroflip* app) {
 
     app->sec_num = 0;
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
-
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
-    nfc_poller_start(app->poller, metromoney_poller_callback, app);
-
-    metroflip_app_blink_start(app);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfClassicData* mfc_data = mf_classic_alloc();
+            mf_classic_load(mfc_data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
+
+            furi_string_reset(app->text_box_store);
+            if(!metromoney_parse(parsed_data, mfc_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            mf_classic_free(mfc_data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
+        nfc_poller_start(app->poller, metromoney_poller_callback, app);
+
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool metromoney_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -179,6 +209,7 @@ static bool metromoney_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -188,7 +219,7 @@ static bool metromoney_on_event(Metroflip* app, SceneManagerEvent event) {
 static void metromoney_on_exit(Metroflip* app) {
     widget_reset(app->widget);
 
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 47 - 17
metroflip/scenes/plugins/myki.c

@@ -36,15 +36,12 @@ static uint8_t myki_calculate_luhn(uint64_t number) {
     return (10 - (sum % 10)) % 10;
 }
 
-bool myki_parse(const NfcDevice* device, FuriString* parsed_data) {
-    furi_assert(device);
+bool myki_parse(const MfDesfireData* data, FuriString* parsed_data) {
     furi_assert(parsed_data);
 
     bool parsed = false;
 
     do {
-        const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
-
         const MfDesfireApplication* app = mf_desfire_get_application(data, &myki_app_id);
         if(app == NULL) break;
 
@@ -109,7 +106,8 @@ static NfcCommand myki_poller_callback(NfcGenericEvent event, void* context) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
-        if(!myki_parse(app->nfc_device, parsed_data)) {
+        const MfDesfireData* data = nfc_device_get_data(app->nfc_device, NfcProtocolMfDesfire);
+        if(!myki_parse(data, parsed_data)) {
             furi_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
@@ -118,6 +116,8 @@ static NfcCommand myki_poller_callback(NfcGenericEvent event, void* context) {
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -134,18 +134,47 @@ static NfcCommand myki_poller_callback(NfcGenericEvent event, void* context) {
 static void myki_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfDesfireData* data = mf_desfire_alloc();
+            mf_desfire_load(data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
 
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
-    nfc_poller_start(app->poller, myki_poller_callback, app);
-
-    metroflip_app_blink_start(app);
+            furi_string_reset(app->text_box_store);
+            if(!myki_parse(data, parsed_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            mf_desfire_free(data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        nfc_scanner_alloc(app->nfc);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
+        nfc_poller_start(app->poller, myki_poller_callback, app);
+
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool myki_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -171,6 +200,7 @@ static bool myki_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -180,7 +210,7 @@ static bool myki_on_event(Metroflip* app, SceneManagerEvent event) {
 static void myki_on_exit(Metroflip* app) {
     widget_reset(app->widget);
     metroflip_app_blink_stop(app);
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 41 - 18
metroflip/scenes/plugins/opal.c

@@ -118,13 +118,9 @@ static void opal_days_minutes_to_datetime(uint16_t days, uint16_t minutes, DateT
 
     out->day = days;
 }
-
-bool opal_parse(const NfcDevice* device, FuriString* parsed_data) {
-    furi_assert(device);
+bool opal_parse(const MfDesfireData* data, FuriString* parsed_data) {
     furi_assert(parsed_data);
 
-    const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
-
     bool parsed = false;
 
     do {
@@ -230,7 +226,8 @@ static NfcCommand opal_poller_callback(NfcGenericEvent event, void* context) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
-        if(!opal_parse(app->nfc_device, parsed_data)) {
+        const MfDesfireData* data = nfc_device_get_data(app->nfc_device, NfcProtocolMfDesfire);
+        if(!opal_parse(data, parsed_data)) {
             furi_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
@@ -239,6 +236,8 @@ static NfcCommand opal_poller_callback(NfcGenericEvent event, void* context) {
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -255,18 +254,41 @@ static NfcCommand opal_poller_callback(NfcGenericEvent event, void* context) {
 static void opal_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
-
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
-    nfc_poller_start(app->poller, opal_poller_callback, app);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            mf_desfire_load(app->mfdes_data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
 
-    metroflip_app_blink_start(app);
+            furi_string_reset(app->text_box_store);
+            opal_parse(app->mfdes_data, parsed_data);
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        nfc_scanner_alloc(app->nfc);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfDesfire);
+        nfc_poller_start(app->poller, opal_poller_callback, app);
+
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool opal_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -292,6 +314,7 @@ static bool opal_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -301,7 +324,7 @@ static bool opal_on_event(Metroflip* app, SceneManagerEvent event) {
 static void opal_on_exit(Metroflip* app) {
     widget_reset(app->widget);
     metroflip_app_blink_stop(app);
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 45 - 17
metroflip/scenes/plugins/smartrider.c

@@ -134,10 +134,8 @@ static void calculate_date(uint32_t timestamp, char* date_str, size_t date_str_s
     }
 }
 
-static bool smartrider_parse(const NfcDevice* device, FuriString* parsed_data) {
-    furi_assert(device);
+static bool smartrider_parse(FuriString* parsed_data, const MfClassicData* data) {
     furi_assert(parsed_data);
-    const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
     SmartRiderData sr_data = {0};
 
     if(data->type != MfClassicType1k) {
@@ -307,10 +305,10 @@ static NfcCommand smartrider_poller_callback(NfcGenericEvent event, void* contex
     } else if(mfc_event->type == MfClassicPollerEventTypeSuccess) {
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfClassic, nfc_poller_get_data(app->poller));
-
+        const MfClassicData* mfc_data = nfc_device_get_data(app->nfc_device, NfcProtocolMfClassic);
         dolphin_deed(DolphinDeedNfcReadSuccess);
         furi_string_reset(app->text_box_store);
-        if(!smartrider_parse(app->nfc_device, parsed_data)) {
+        if(!smartrider_parse(parsed_data, mfc_data)) {
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
         }
@@ -318,6 +316,8 @@ static NfcCommand smartrider_poller_callback(NfcGenericEvent event, void* contex
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -333,23 +333,50 @@ static NfcCommand smartrider_poller_callback(NfcGenericEvent event, void* contex
 }
 
 static void smartrider_on_enter(Metroflip* app) {
-    FURI_LOG_I(TAG, "entered smartrider");
     dolphin_deed(DolphinDeedNfcRead);
 
     mf_classic_key_cache_reset(app->mfc_key_cache);
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfClassicData* mfc_data = mf_classic_alloc();
+            mf_classic_load(mfc_data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
+
+            furi_string_reset(app->text_box_store);
+            if(!smartrider_parse(parsed_data, mfc_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            mf_classic_free(mfc_data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
 
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
-    nfc_poller_start(app->poller, smartrider_poller_callback, app);
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
+        nfc_poller_start(app->poller, smartrider_poller_callback, app);
 
-    metroflip_app_blink_start(app);
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool smartrider_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -375,6 +402,7 @@ static bool smartrider_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -384,7 +412,7 @@ static bool smartrider_on_event(Metroflip* app, SceneManagerEvent event) {
 static void smartrider_on_exit(Metroflip* app) {
     widget_reset(app->widget);
 
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }

+ 43 - 12
metroflip/scenes/plugins/troika.c

@@ -4,7 +4,7 @@
 #include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
 #include <nfc/protocols/mf_classic/mf_classic.h>
 #include <nfc/protocols/mf_classic/mf_classic_poller.h>
-#include "../../api/mosgortrans/mosgortrans_util.h"
+#include "../../api/metroflip/metroflip_api.h"
 
 #include <dolphin/dolphin.h>
 #include <bit_lib.h>
@@ -221,6 +221,8 @@ static NfcCommand troika_poller_callback(NfcGenericEvent event, void* context) {
 
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+        widget_add_button_element(
+            widget, GuiButtonTypeCenter, "Save", metroflip_save_widget_callback, app);
 
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -239,18 +241,46 @@ static void troika_on_enter(Metroflip* app) {
 
     app->sec_num = 0;
 
-    // Setup view
-    Popup* popup = app->popup;
-    popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
-    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+    if(app->data_loaded) {
+        Storage* storage = furi_record_open(RECORD_STORAGE);
+        FlipperFormat* ff = flipper_format_file_alloc(storage);
+        if(flipper_format_file_open_existing(ff, app->file_path)) {
+            MfClassicData* mfc_data = mf_classic_alloc();
+            mf_classic_load(mfc_data, ff, 2);
+            FuriString* parsed_data = furi_string_alloc();
+            Widget* widget = app->widget;
+
+            furi_string_reset(app->text_box_store);
+            if(!troika_parse(parsed_data, mfc_data)) {
+                furi_string_reset(app->text_box_store);
+                FURI_LOG_I(TAG, "Unknown card type");
+                furi_string_printf(parsed_data, "\e#Unknown card\n");
+            }
+            widget_add_text_scroll_element(
+                widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
+
+            widget_add_button_element(
+                widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
+            widget_add_button_element(
+                widget, GuiButtonTypeCenter, "Delete", metroflip_delete_widget_callback, app);
+            mf_classic_free(mfc_data);
+            furi_string_free(parsed_data);
+            view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
+        }
+        flipper_format_free(ff);
+    } else {
+        // Setup view
+        Popup* popup = app->popup;
+        popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
 
-    // Start worker
-    view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
-    nfc_scanner_alloc(app->nfc);
-    app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
-    nfc_poller_start(app->poller, troika_poller_callback, app);
+        // Start worker
+        view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewPopup);
+        app->poller = nfc_poller_alloc(app->nfc, NfcProtocolMfClassic);
+        nfc_poller_start(app->poller, troika_poller_callback, app);
 
-    metroflip_app_blink_start(app);
+        metroflip_app_blink_start(app);
+    }
 }
 
 static bool troika_on_event(Metroflip* app, SceneManagerEvent event) {
@@ -276,6 +306,7 @@ static bool troika_on_event(Metroflip* app, SceneManagerEvent event) {
         }
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
     }
 
@@ -285,7 +316,7 @@ static bool troika_on_event(Metroflip* app, SceneManagerEvent event) {
 static void troika_on_exit(Metroflip* app) {
     widget_reset(app->widget);
 
-    if(app->poller) {
+    if(app->poller && !app->data_loaded) {
         nfc_poller_stop(app->poller);
         nfc_poller_free(app->poller);
     }