Luu пре 11 месеци
родитељ
комит
f833767c89

+ 3 - 0
api/metroflip/metroflip_api.h

@@ -23,6 +23,7 @@ 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_app_blink_start(Metroflip* metroflip);
 
@@ -135,6 +136,8 @@ void show_ravkav_environment_info(RavKavCardEnv* environment, FuriString* parsed
 
 extern const Icon I_RFIDDolphinReceive_97x61;
 extern const Icon I_icon;
+extern const Icon I_DolphinDone_80x58;
+extern const Icon I_WarningDolphinFlip_45x42;
 
 void render_section_header(
     FuriString* str,

+ 4 - 1
api/metroflip/metroflip_api_table_i.h

@@ -9,6 +9,7 @@
 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_app_blink_start, void, (Metroflip*)),
     API_METHOD(metroflip_app_blink_stop, void, (Metroflip*)),
     API_METHOD(bit_slice_to_dec, int, (const char*, int, int)),
@@ -73,4 +74,6 @@ static constexpr auto metroflip_api_table = sort(create_array_t<sym_entry>(
     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_METHOD(mosgortrans_parse_transport_block, bool, (const MfClassicBlock*, FuriString*)),
+    API_VARIABLE(I_WarningDolphinFlip_45x42, Icon),
+    API_VARIABLE(I_DolphinDone_80x58, Icon)));

BIN
images/DolphinDone_80x58.png


BIN
images/WarningDolphinFlip_45x42.png


+ 10 - 3
metroflip.c

@@ -157,7 +157,16 @@ void metroflip_exit_widget_callback(GuiButtonType result, InputType type, void*
     UNUSED(result);
 
     if(type == InputTypeShort) {
-        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_next_scene(app->scene_manager, MetroflipSceneSave);
+    }
+}
+
+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);
     }
 }
 
@@ -304,9 +313,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);

+ 5 - 0
metroflip_i.h

@@ -73,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;

+ 303 - 0
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
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

+ 5 - 10
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);

+ 2 - 0
scenes/metroflip_scene_config.h

@@ -2,6 +2,8 @@ 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, supported, Supported)
 ADD_SCENE(metroflip, about, About)
 ADD_SCENE(metroflip, credits, Credits)

+ 66 - 41
scenes/metroflip_scene_load.c

@@ -16,6 +16,7 @@ void metroflip_scene_load_on_enter(void* context) {
     // 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;
@@ -29,51 +30,75 @@ void metroflip_scene_load_on_enter(void* context) {
         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));
-                FURI_LOG_I(TAG, "dont have card type in file, detecting..");
-                MfClassicData* mfc_data = mf_classic_alloc();
-                if(!mf_classic_load(mfc_data, format, 2)) {
-                    FURI_LOG_I(TAG, "failed");
-                } else {
-                    FURI_LOG_I(TAG, "success");
-                }
-                FURI_LOG_I(TAG, "%d", mfc_data->block[3].data[1]);
-                app->data_loaded = true;
-                CardType card_type = determine_card_type(app->nfc, mfc_data, app->data_loaded);
-                app->mfc_card_type = card_type;
-                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_UNKNOWN:
-                    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;
-                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;
+                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_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;
                 }
-                mf_classic_free(mfc_data);
-                has_card_type = true;
+                flipper_format_file_close(format);
+            } else {
+                has_card_type = false;
             }
             app->file_path = furi_string_get_cstr(file_path);
             app->data_loaded = true;
@@ -102,7 +127,7 @@ bool metroflip_scene_load_on_event(void* context, SceneManagerEvent event) {
     // 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);
+        scene_manager_next_scene(app->scene_manager, MetroflipSceneStart);
     }
     consumed = true;
 

+ 0 - 1
scenes/metroflip_scene_parse.c

@@ -63,6 +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
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(), ".metro", 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);
+}

+ 76 - 0
scenes/metroflip_scene_save_result.c

@@ -0,0 +1,76 @@
+#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.metro", 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_buffered_file_open_existing(ff, path);
+    flipper_format_write_empty_line(ff);
+    if(flipper_format_file_open_existing(ff, path)) {
+        if(flipper_format_insert_or_update_string_cstr(ff, "Card Type", app->card_type)) {
+            flipper_format_file_close(ff);
+        } else {
+            FURI_LOG_I("ting", "cant write");
+        }
+    } else {
+        FURI_LOG_I("ting", "not found");
+    }
+    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);
+}

+ 7 - 2
scenes/metroflip_scene_start.c

@@ -20,8 +20,13 @@ void metroflip_scene_start_on_enter(void* context) {
         MetroflipSceneOVC,
         metroflip_scene_start_submenu_callback,
         app);
-        
-    submenu_add_item(submenu, "Load (not working)", MetroflipSceneLoad, metroflip_scene_start_submenu_callback, app);
+
+    submenu_add_item(
+        submenu,
+        "Load (not working)",
+        MetroflipSceneLoad,
+        metroflip_scene_start_submenu_callback,
+        app);
 
     submenu_add_item(
         submenu,

+ 2 - 0
scenes/plugins/bip.c

@@ -319,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);

+ 3 - 1
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);
@@ -1270,9 +1272,9 @@ static void charliecard_on_enter(Metroflip* app) {
             }
             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);
+
             mf_classic_free(mfc_data);
             furi_string_free(parsed_data);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);

+ 44 - 16
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,45 @@ 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);
+            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) {

+ 43 - 16
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,45 @@ 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);
+            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) {

+ 2 - 0
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);

+ 43 - 16
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,45 @@ 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);
+            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) {

+ 37 - 17
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,39 @@ 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);
+            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) {

+ 2 - 0
scenes/plugins/smartrider.c

@@ -316,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);

+ 2 - 0
scenes/plugins/troika.c

@@ -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);