Просмотр исходного кода

Merge branch 'dev' into 0.5+load+suica

Zinong Li 10 месяцев назад
Родитель
Сommit
a622965dfb

+ 5 - 0
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.
 - 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).
 - 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
+

+ 4 - 0
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) {
 void show_ravkav_contract_info(RavKavCardContract* contract, FuriString* parsed_data) {
     // Core contract validity period
     // 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: ");
     furi_string_cat_printf(parsed_data, "Valid from: ");
     locale_format_datetime_cat(parsed_data, &contract->start_date, false);
     locale_format_datetime_cat(parsed_data, &contract->start_date, false);
     if(contract->end_date_available) {
     if(contract->end_date_available) {

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

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

+ 12 - 0
api/metroflip/metroflip_api.h

@@ -23,6 +23,8 @@ extern "C" {
 // metroflip
 // metroflip
 
 
 void metroflip_exit_widget_callback(GuiButtonType result, InputType type, void* context);
 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);
 void metroflip_app_blink_start(Metroflip* metroflip);
 
 
@@ -135,6 +137,16 @@ void show_ravkav_environment_info(RavKavCardEnv* environment, FuriString* parsed
 
 
 extern const Icon I_RFIDDolphinReceive_97x61;
 extern const Icon I_RFIDDolphinReceive_97x61;
 extern const Icon I_icon;
 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);
 
 
 extern const Icon I_Suica_AsakusaA;
 extern const Icon I_Suica_AsakusaA;
 extern const Icon I_Suica_BigStar;
 extern const Icon I_Suica_BigStar;

+ 7 - 1
api/metroflip/metroflip_api_table_i.h

@@ -9,6 +9,8 @@
 static constexpr auto metroflip_api_table = sort(create_array_t<sym_entry>(
 static constexpr auto metroflip_api_table = sort(create_array_t<sym_entry>(
     // metroflip stuff
     // metroflip stuff
     API_METHOD(metroflip_exit_widget_callback, void, (GuiButtonType, InputType, void*)),
     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_start, void, (Metroflip*)),
     API_METHOD(metroflip_app_blink_stop, void, (Metroflip*)),
     API_METHOD(metroflip_app_blink_stop, void, (Metroflip*)),
     API_METHOD(bit_slice_to_dec, int, (const char*, int, int)),
     API_METHOD(bit_slice_to_dec, int, (const char*, int, int)),
@@ -72,6 +74,7 @@ static constexpr auto metroflip_api_table = sort(create_array_t<sym_entry>(
 
 
     API_VARIABLE(I_RFIDDolphinReceive_97x61, Icon),
     API_VARIABLE(I_RFIDDolphinReceive_97x61, Icon),
     API_VARIABLE(I_icon, Icon),
     API_VARIABLE(I_icon, Icon),
+
     // Suica
     // Suica
     API_VARIABLE(I_Suica_AsakusaA, Icon),
     API_VARIABLE(I_Suica_AsakusaA, Icon),
     API_VARIABLE(I_Suica_BigStar, Icon),
     API_VARIABLE(I_Suica_BigStar, Icon),
@@ -163,4 +166,7 @@ static constexpr auto metroflip_api_table = sort(create_array_t<sym_entry>(
     API_VARIABLE(I_Suica_UnknownIcon, Icon),
     API_VARIABLE(I_Suica_UnknownIcon, Icon),
     
     
     API_METHOD(render_section_header, void, (FuriString*, const char*, uint8_t, uint8_t)),
     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),
+    API_VARIABLE(I_DolphinMafia_119x62, Icon)));

+ 13 - 2
application.fam

@@ -5,7 +5,7 @@ App(
     entry_point="metroflip",
     entry_point="metroflip",
     stack_size=2 * 1024,
     stack_size=2 * 1024,
     fap_category="NFC",
     fap_category="NFC",
-    fap_version="0.5",
+    fap_version="0.6",
     fap_icon="icon.png",
     fap_icon="icon.png",
     fap_description="An implementation of metrodroid on the flipper",
     fap_description="An implementation of metrodroid on the flipper",
     fap_author="luu176",
     fap_author="luu176",
@@ -37,6 +37,7 @@ App(
     entry_point="metromoney_plugin_ep",
     entry_point="metromoney_plugin_ep",
     requires=["metroflip"],
     requires=["metroflip"],
     sources=["scenes/plugins/metromoney.c"],
     sources=["scenes/plugins/metromoney.c"],
+    fal_embedded=True,
 )
 )
 
 
 App(
 App(
@@ -45,6 +46,7 @@ App(
     entry_point="bip_plugin_ep",
     entry_point="bip_plugin_ep",
     requires=["metroflip"],
     requires=["metroflip"],
     sources=["scenes/plugins/bip.c"],
     sources=["scenes/plugins/bip.c"],
+    fal_embedded=True,
 )
 )
 
 
 App(
 App(
@@ -108,4 +110,13 @@ App(
     requires=["metroflip"],
     requires=["metroflip"],
     sources=["scenes/plugins/suica.c"],
     sources=["scenes/plugins/suica.c"],
     fal_embedded=True,
     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
images/DolphinDone_80x58.png


BIN
images/DolphinMafia_119x62.png


BIN
images/WarningDolphinFlip_45x42.png


+ 2 - 2
manifest.yml

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

+ 19 - 2
metroflip.c

@@ -158,6 +158,25 @@ void metroflip_exit_widget_callback(GuiButtonType result, InputType type, void*
 
 
     if(type == InputTypeShort) {
     if(type == InputTypeShort) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         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);
     }
     }
 }
 }
 
 
@@ -304,9 +323,7 @@ KeyfileManager manage_keyfiles(
             return SUCCESSFUL;
             return SUCCESSFUL;
         }
         }
     } else {
     } else {
-        FURI_LOG_I("TAG", "testing 1");
         size_t source_file_length = storage_file_size(source);
         size_t source_file_length = storage_file_size(source);
-        FURI_LOG_I("TAG", "testing 2");
 
 
         storage_file_close(source);
         storage_file_close(source);
         mf_classic_key_cache_load(instance, uid, uid_len);
         mf_classic_key_cache_load(instance, uid, uid_len);

+ 7 - 1
metroflip_i.h

@@ -53,7 +53,7 @@
 
 
 
 
 #define KEY_MASK_BIT_CHECK(key_mask_1, key_mask_2) (((key_mask_1) & (key_mask_2)) == (key_mask_1))
 #define KEY_MASK_BIT_CHECK(key_mask_1, key_mask_2) (((key_mask_1) & (key_mask_2)) == (key_mask_1))
-#define METROFLIP_FILE_EXTENSION                   ".metro"
+#define METROFLIP_FILE_EXTENSION                   ".nfc"
 typedef struct {
 typedef struct {
     Gui* gui;
     Gui* gui;
     SceneManager* scene_manager;
     SceneManager* scene_manager;
@@ -75,6 +75,11 @@ typedef struct {
     MfClassicKeyCache* mfc_key_cache;
     MfClassicKeyCache* mfc_key_cache;
     NfcDetectedProtocols* detected_protocols;
     NfcDetectedProtocols* detected_protocols;
     DesfireCardType desfire_card_type;
     DesfireCardType desfire_card_type;
+    MfDesfireData* mfdes_data;
+    MfClassicData* mfc_data;
+
+    // save stuff
+    char save_buf[248];
 
 
     //plugin manager
     //plugin manager
     PluginManager* plugin_manager;
     PluginManager* plugin_manager;
@@ -94,6 +99,7 @@ typedef struct {
     CardType mfc_card_type;
     CardType mfc_card_type;
     NfcProtocol protocol;
     NfcProtocol protocol;
     const char* file_path;
     const char* file_path;
+    char delete_file_path[256];
 
 
     // Calypso specific context
     // Calypso specific context
     CalypsoContext* calypso_context;
     CalypsoContext* calypso_context;

+ 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
 #define DESFIRE_H
 
 
 #include "../metroflip_i.h"
 #include "../metroflip_i.h"
+#include <lib/nfc/protocols/mf_desfire/mf_desfire.h>
 
 
 typedef enum {
 typedef enum {
     CARD_TYPE_ITSO,
     CARD_TYPE_ITSO,
@@ -11,9 +12,9 @@ typedef enum {
     CARD_TYPE_DESFIRE_UNKNOWN
     CARD_TYPE_DESFIRE_UNKNOWN
 } DesfireCardType;
 } 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
 #endif // DESFIRE_H

+ 37 - 0
scenes/keys.c

@@ -33,6 +33,13 @@ const MfClassicKeyPair metromoney_1k_verify_key[] = {
     {.a = 0x9C616585E26D},
     {.a = 0x9C616585E26D},
 };
 };
 
 
+
+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) {
 static bool charliecard_verify(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
     bool verified = false;
     bool verified = false;
     FURI_LOG_I(TAG, "verifying charliecard..");
     FURI_LOG_I(TAG, "verifying charliecard..");
@@ -244,6 +251,34 @@ static bool troika_verify(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
            troika_verify_type(nfc, mfc_data, data_loaded, MfClassicType4k);
            troika_verify_type(nfc, mfc_data, data_loaded, MfClassicType4k);
 }
 }
 
 
+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;
+        }
+    } while(false);
+
+    return verified;
+}
+
+
 CardType determine_card_type(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
 CardType determine_card_type(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded) {
     FURI_LOG_I(TAG, "checking keys..");
     FURI_LOG_I(TAG, "checking keys..");
     UNUSED(bip_verify);
     UNUSED(bip_verify);
@@ -258,6 +293,8 @@ CardType determine_card_type(Nfc* nfc, MfClassicData* mfc_data, bool data_loaded
         return CARD_TYPE_TROIKA;
         return CARD_TYPE_TROIKA;
     } else if(charliecard_verify(nfc, mfc_data, data_loaded)) {
     } else if(charliecard_verify(nfc, mfc_data, data_loaded)) {
         return CARD_TYPE_CHARLIECARD;
         return CARD_TYPE_CHARLIECARD;
+    } else if(gocard_verify(mfc_data, data_loaded)) {
+        return CARD_TYPE_GOCARD;
     } else {
     } else {
         FURI_LOG_I(TAG, "its unknown");
         FURI_LOG_I(TAG, "its unknown");
         return CARD_TYPE_UNKNOWN;
         return CARD_TYPE_UNKNOWN;

+ 3 - 1
scenes/keys.h

@@ -9,6 +9,7 @@ typedef enum {
     CARD_TYPE_CHARLIECARD,
     CARD_TYPE_CHARLIECARD,
     CARD_TYPE_SMARTRIDER,
     CARD_TYPE_SMARTRIDER,
     CARD_TYPE_TROIKA,
     CARD_TYPE_TROIKA,
+    CARD_TYPE_GOCARD,
     CARD_TYPE_UNKNOWN
     CARD_TYPE_UNKNOWN
 } CardType;
 } CardType;
 
 
@@ -28,9 +29,10 @@ typedef struct {
 
 
 extern const MfClassicKeyPair troika_1k_keys[16];
 extern const MfClassicKeyPair troika_1k_keys[16];
 extern const MfClassicKeyPair troika_4k_keys[40];
 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 charliecard_1k_keys[16];
 extern const MfClassicKeyPair bip_1k_keys[16];
 extern const MfClassicKeyPair bip_1k_keys[16];
 extern const MfClassicKeyPair metromoney_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
 #endif // KEYS_H

+ 5 - 10
scenes/metroflip_scene_auto.c

@@ -20,28 +20,24 @@ static NfcCommand
     Metroflip* app = context;
     Metroflip* app = context;
     NfcCommand command = NfcCommandContinue;
     NfcCommand command = NfcCommandContinue;
 
 
-    FuriString* parsed_data = furi_string_alloc();
-    furi_string_reset(app->text_box_store);
     const MfDesfirePollerEvent* mf_desfire_event = event.event_data;
     const MfDesfirePollerEvent* mf_desfire_event = event.event_data;
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
             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(
             view_dispatcher_send_custom_event(
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
             app->desfire_card_type = CARD_TYPE_CLIPPER;
             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(
             view_dispatcher_send_custom_event(
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
             app->desfire_card_type = CARD_TYPE_ITSO;
             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(
             view_dispatcher_send_custom_event(
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
             app->desfire_card_type = CARD_TYPE_MYKI;
             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);
             furi_string_reset(app->text_box_store);
             view_dispatcher_send_custom_event(
             view_dispatcher_send_custom_event(
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
@@ -52,7 +48,6 @@ static NfcCommand
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
                 app->view_dispatcher, MetroflipCustomEventPollerSuccess);
             app->desfire_card_type = CARD_TYPE_DESFIRE_UNKNOWN;
             app->desfire_card_type = CARD_TYPE_DESFIRE_UNKNOWN;
         }
         }
-        furi_string_free(parsed_data);
         command = NfcCommandStop;
         command = NfcCommandStop;
     } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
     } else if(mf_desfire_event->type == MfDesfirePollerEventTypeReadFailed) {
         view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);
         view_dispatcher_send_custom_event(app->view_dispatcher, MetroflipCustomEventPollerSuccess);

+ 3 - 0
scenes/metroflip_scene_config.h

@@ -2,6 +2,9 @@ ADD_SCENE(metroflip, start, Start)
 ADD_SCENE(metroflip, auto, Auto)
 ADD_SCENE(metroflip, auto, Auto)
 ADD_SCENE(metroflip, parse, Parse)
 ADD_SCENE(metroflip, parse, Parse)
 ADD_SCENE(metroflip, ovc, OVC)
 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, supported, Supported)
 ADD_SCENE(metroflip, about, About)
 ADD_SCENE(metroflip, about, About)
 ADD_SCENE(metroflip, credits, Credits)
 ADD_SCENE(metroflip, credits, Credits)

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

+ 80 - 44
scenes/metroflip_scene_load.c

@@ -17,6 +17,7 @@ void metroflip_scene_load_on_enter(void* context) {
     // The same string we will use to direct parse scene which plugin to call
     // The same string we will use to direct parse scene which plugin to call
     // Extracted from the file
     // Extracted from the file
     FuriString* card_type_str = furi_string_alloc();
     FuriString* card_type_str = furi_string_alloc();
+    FuriString* device_type = furi_string_alloc();
 
 
     // All the app_data browser stuff. Don't worry about this
     // All the app_data browser stuff. Don't worry about this
     DialogsFileBrowserOptions browser_options;
     DialogsFileBrowserOptions browser_options;
@@ -30,58 +31,92 @@ void metroflip_scene_load_on_enter(void* context) {
         FlipperFormat* format = flipper_format_file_alloc(storage);
         FlipperFormat* format = flipper_format_file_alloc(storage);
         do {
         do {
             if(!flipper_format_file_open_existing(format, furi_string_get_cstr(file_path))) break;
             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_str)) {
             if(!flipper_format_read_string(format, "Card Type", card_type_str)) {
                 flipper_format_file_close(format);
                 flipper_format_file_close(format);
                 flipper_format_file_open_existing(format, furi_string_get_cstr(file_path));
                 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_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 {
+                    if(furi_string_equal_str(card_type_str, "suica")) {
+                        FURI_LOG_I(TAG, "Detected: Suica");
+                        load_suica_data(app, format);
+                    }
+                    has_card_type = true;
                 }
                 }
-                mf_classic_free(mfc_data);
-                has_card_type = true;
+                flipper_format_file_close(format);
             } else {
             } else {
-                if(furi_string_equal_str(card_type_str, "suica")) {
-                    load_suica_data(app, format);
-                }
+                has_card_type = false;
             }
             }
-
             app->file_path = furi_string_get_cstr(file_path);
             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;
             app->data_loaded = true;
         } while(0);
         } while(0);
         flipper_format_free(format);
         flipper_format_free(format);
@@ -93,6 +128,7 @@ void metroflip_scene_load_on_enter(void* context) {
             app->card_type = furi_string_get_cstr(card_type_str);
             app->card_type = furi_string_get_cstr(card_type_str);
             has_card_type = false;
             has_card_type = false;
         }
         }
+        scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_next_scene(app->scene_manager, MetroflipSceneParse);
         scene_manager_next_scene(app->scene_manager, MetroflipSceneParse);
     } else {
     } else {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);

+ 0 - 1
scenes/metroflip_scene_parse.c

@@ -61,6 +61,5 @@ void metroflip_scene_parse_on_exit(void* context) {
         plugin_manager_free(app->plugin_manager);
         plugin_manager_free(app->plugin_manager);
         composite_api_resolver_free(app->resolver);
         composite_api_resolver_free(app->resolver);
     }
     }
-    app->card_type = "unknown";
     app->data_loaded = false;
     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(), 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
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 - 2
scenes/metroflip_scene_start.c

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

+ 4 - 0
scenes/plugins/bip.c

@@ -319,6 +319,8 @@ static NfcCommand bip_poller_callback(NfcGenericEvent event, void* context) {
 
 
         widget_add_button_element(
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -357,6 +359,8 @@ static void bip_on_enter(Metroflip* app) {
 
 
             widget_add_button_element(
             widget_add_button_element(
                 widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
                 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);
             mf_classic_free(mfc_data);
             furi_string_free(parsed_data);
             furi_string_free(parsed_data);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);

+ 51 - 6
scenes/plugins/calypso.c

@@ -321,6 +321,8 @@ void metroflip_next_button_widget_callback(GuiButtonType result, InputType type,
             ctx->page_id = 0;
             ctx->page_id = 0;
             scene_manager_search_and_switch_to_previous_scene(
             scene_manager_search_and_switch_to_previous_scene(
                 app->scene_manager, MetroflipSceneStart);
                 app->scene_manager, MetroflipSceneStart);
+            scene_manager_set_scene_state(
+                app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
             return;
             return;
         }
         }
         if(ctx->page_id < 10) {
         if(ctx->page_id < 10) {
@@ -352,6 +354,8 @@ void metroflip_next_button_widget_callback(GuiButtonType result, InputType type,
                 ctx->page_id = 0;
                 ctx->page_id = 0;
                 scene_manager_search_and_switch_to_previous_scene(
                 scene_manager_search_and_switch_to_previous_scene(
                     app->scene_manager, MetroflipSceneStart);
                     app->scene_manager, MetroflipSceneStart);
+                scene_manager_set_scene_state(
+                    app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
                 return;
                 return;
             }
             }
             ctx->page_id += 1;
             ctx->page_id += 1;
@@ -359,6 +363,8 @@ void metroflip_next_button_widget_callback(GuiButtonType result, InputType type,
             ctx->page_id = 0;
             ctx->page_id = 0;
             scene_manager_search_and_switch_to_previous_scene(
             scene_manager_search_and_switch_to_previous_scene(
                 app->scene_manager, MetroflipSceneStart);
                 app->scene_manager, MetroflipSceneStart);
+            scene_manager_set_scene_state(
+                app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
             return;
             return;
         }
         }
 
 
@@ -1783,8 +1789,17 @@ static NfcCommand calypso_poller_callback(NfcGenericEvent event, void* context)
                     if(card->card_type == CALYPSO_CARD_RAVKAV) {
                     if(card->card_type == CALYPSO_CARD_RAVKAV) {
                         card->ravkav = malloc(sizeof(RavKavCardData));
                         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(
                         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) {
                         if(error != 0) {
                             FURI_LOG_E(TAG, "Failed to select app for contracts");
                             FURI_LOG_E(TAG, "Failed to select app for contracts");
                             break;
                             break;
@@ -1793,15 +1808,44 @@ static NfcCommand calypso_poller_callback(NfcGenericEvent event, void* context)
                         // Check the response after selecting app
                         // Check the response after selecting app
                         if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                         if(check_response(rx_buffer, app, &stage, &response_length) != 0) {
                             FURI_LOG_E(
                             FURI_LOG_E(
-                                TAG, "Failed to check response after selecting app for contracts");
+                                TAG, "Failed to check response after selecting app for counter");
                             break;
                             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;
                             break;
                         }
                         }
 
 
@@ -2477,6 +2521,7 @@ static bool calypso_on_event(Metroflip* app, SceneManagerEvent event) {
         }
         }
     } else if(event.type == SceneManagerEventTypeBack) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }
 
 

+ 6 - 1
scenes/plugins/charliecard.c

@@ -1235,6 +1235,8 @@ static NfcCommand
 
 
         widget_add_button_element(
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -1270,9 +1272,11 @@ static void charliecard_on_enter(Metroflip* app) {
             }
             }
             widget_add_text_scroll_element(
             widget_add_text_scroll_element(
                 widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
                 widget, 0, 0, 128, 64, furi_string_get_cstr(parsed_data));
-
             widget_add_button_element(
             widget_add_button_element(
                 widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
                 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);
             mf_classic_free(mfc_data);
             furi_string_free(parsed_data);
             furi_string_free(parsed_data);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -1316,6 +1320,7 @@ static bool charliecard_on_event(Metroflip* app, SceneManagerEvent event) {
         }
         }
     } else if(event.type == SceneManagerEventTypeBack) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }
 
 

+ 47 - 16
scenes/plugins/clipper.c

@@ -232,15 +232,12 @@ static int16_t get_i16be(const uint8_t* field) {
         return raw;
         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);
     furi_assert(parsed_data);
 
 
     bool parsed = false;
     bool parsed = false;
 
 
     do {
     do {
-        const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
-
         const MfDesfireApplication* app = NULL;
         const MfDesfireApplication* app = NULL;
         const char* device_description = 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) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
             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_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
             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_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         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) {
 static void clipper_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
     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) {
 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) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }
 
 

+ 296 - 0
scenes/plugins/gocard.c

@@ -0,0 +1,296 @@
+
+#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;
+
+bool hasTravelPassAvailable = false;
+
+// 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(int block, int offset, const MfClassicData* data, FuriString* parsed_data) {
+    //byte to start at
+    int num_bytes = 4;
+    char gocard_date_bit_representation[num_bytes * 8 + 1];
+    memset(gocard_date_bit_representation, 0, sizeof(gocard_date_bit_representation));
+
+    for(int i = (offset + num_bytes - 1), j = 0; i >= offset;
+        i--, j++) { // Reverse the order of bytes and converty to binary
+        char bits[9];
+        byte_to_binary(data->block[block].data[i], bits);
+        memcpy(&gocard_date_bit_representation[j * 8], bits, 8);
+    }
+    gocard_date_bit_representation[num_bytes * 8] = '\0';
+
+    int len = strlen(gocard_date_bit_representation);
+    FURI_LOG_I(TAG, "len %d", len); // I get 34
+
+    if(len != 32 && len != 33) {
+        FURI_LOG_I(TAG, "Invalid input length");
+        return;
+    }
+
+    // Field layout (from rightmost bit):
+    // - Day: 5 bits
+    // - Month: 4 bits
+    // - Year: 6 bits (years since 2000)
+    // - Minutes: 11 bits (minutes from midnight)
+    // Extract values from right to left using bit_slice_to_dec
+    uint32_t day = bit_slice_to_dec(gocard_date_bit_representation, len - 5, len);
+    uint32_t month = bit_slice_to_dec(gocard_date_bit_representation, len - 9, len - 6);
+    uint32_t year = bit_slice_to_dec(gocard_date_bit_representation, len - 15, len - 10);
+    uint32_t minutes = bit_slice_to_dec(gocard_date_bit_representation, 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);
+}
+
+void parse_gocard_topup_info(FuriString* parsed_data, const MfClassicData* data) {
+    furi_string_cat_printf(parsed_data, "\n\e#Top-Up Info:");
+    bool fully_empty = true;
+    int block_num = 8;
+    for(int i = block_num; i < block_num + 3; i++) {
+        /******* Check if it's empty ******/
+        bool is_block_empty = true;
+
+        for(int j = 2; j < 8; j++) {
+            if(data->block[i].data[j] != 0) {
+                FURI_LOG_I(TAG, "Not 0, proceeding");
+                is_block_empty = false;
+                break;
+            }
+        }
+
+        if(is_block_empty) {
+            FURI_LOG_I(TAG, "Block %d is empty", i);
+            continue;
+        } else {
+            fully_empty = false;
+        }
+
+        /**** If not fully empty, proceed ******/
+        unsigned short creditcents =
+            byteArrayToIntReversed(data->block[i].data[6], data->block[i].data[7]);
+        creditcents &= 0x7FFF;
+        double credit = creditcents / 100.0;
+
+        furi_string_cat_printf(parsed_data, "\nCredit Added: A$%.2f\nTime: ", credit);
+        parse_gocard_time(i, 2, data, parsed_data);
+    }
+
+    if(fully_empty) {
+        FURI_LOG_I(TAG, "All checked blocks are empty, returning.");
+    }
+}
+
+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
+
+        hasTravelPassAvailable = (data->block[balance_slot].data[7] != 0x00) ? true : false;
+        int start_index = 4; //byte to start at
+        int config_block = 6; //block number containing card configuration
+
+        furi_string_cat_printf(parsed_data, "Expiry:\n");
+        parse_gocard_time(config_block, start_index, data, parsed_data);
+
+        //concession type:
+
+        unsigned short concession_type = byteArrayToIntReversed(
+            data->block[config_block].data[8], data->block[config_block].data[9]);
+
+        printConcessionType(concession_type, parsed_data);
+
+        parse_gocard_topup_info(parsed_data, 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;
+}

+ 46 - 16
scenes/plugins/itso.c

@@ -28,15 +28,12 @@ uint64_t swap_uint64(uint64_t val) {
     return (val << 32) | (val >> 32);
     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);
     furi_assert(parsed_data);
 
 
     bool parsed = false;
     bool parsed = false;
 
 
     do {
     do {
-        const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
-
         const MfDesfireApplication* app = mf_desfire_get_application(data, &itso_app_id);
         const MfDesfireApplication* app = mf_desfire_get_application(data, &itso_app_id);
         if(app == NULL) break;
         if(app == NULL) break;
 
 
@@ -126,7 +123,8 @@ static NfcCommand itso_poller_callback(NfcGenericEvent event, void* context) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
             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_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
             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_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         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) {
 static void itso_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
     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) {
 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) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }
 
 

+ 8 - 1
scenes/plugins/metromoney.c

@@ -124,6 +124,8 @@ static NfcCommand metromoney_poller_callback(NfcGenericEvent event, void* contex
 
 
         widget_add_button_element(
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -139,10 +141,11 @@ static NfcCommand metromoney_poller_callback(NfcGenericEvent event, void* contex
 
 
 static void metromoney_on_enter(Metroflip* app) {
 static void metromoney_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
     dolphin_deed(DolphinDeedNfcRead);
-
+    FURI_LOG_I(TAG, "open metromoney");
     app->sec_num = 0;
     app->sec_num = 0;
 
 
     if(app->data_loaded) {
     if(app->data_loaded) {
+        FURI_LOG_I(TAG, "tbilisi loaded");
         Storage* storage = furi_record_open(RECORD_STORAGE);
         Storage* storage = furi_record_open(RECORD_STORAGE);
         FlipperFormat* ff = flipper_format_file_alloc(storage);
         FlipperFormat* ff = flipper_format_file_alloc(storage);
         if(flipper_format_file_open_existing(ff, app->file_path)) {
         if(flipper_format_file_open_existing(ff, app->file_path)) {
@@ -162,12 +165,15 @@ static void metromoney_on_enter(Metroflip* app) {
 
 
             widget_add_button_element(
             widget_add_button_element(
                 widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
                 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);
             mf_classic_free(mfc_data);
             furi_string_free(parsed_data);
             furi_string_free(parsed_data);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         }
         }
         flipper_format_free(ff);
         flipper_format_free(ff);
     } else {
     } else {
+        FURI_LOG_I(TAG, "tbilisi not loaded");
         // Setup view
         // Setup view
         Popup* popup = app->popup;
         Popup* popup = app->popup;
         popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
         popup_set_header(popup, "Apply\n card to\nthe back", 68, 30, AlignLeft, AlignTop);
@@ -205,6 +211,7 @@ static bool metromoney_on_event(Metroflip* app, SceneManagerEvent event) {
         }
         }
     } else if(event.type == SceneManagerEventTypeBack) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }
 
 

+ 46 - 16
scenes/plugins/myki.c

@@ -36,15 +36,12 @@ static uint8_t myki_calculate_luhn(uint64_t number) {
     return (10 - (sum % 10)) % 10;
     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);
     furi_assert(parsed_data);
 
 
     bool parsed = false;
     bool parsed = false;
 
 
     do {
     do {
-        const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
-
         const MfDesfireApplication* app = mf_desfire_get_application(data, &myki_app_id);
         const MfDesfireApplication* app = mf_desfire_get_application(data, &myki_app_id);
         if(app == NULL) break;
         if(app == NULL) break;
 
 
@@ -109,7 +106,8 @@ static NfcCommand myki_poller_callback(NfcGenericEvent event, void* context) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
             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_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
             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_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         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) {
 static void myki_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
     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) {
 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) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }
 
 

+ 40 - 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;
     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);
     furi_assert(parsed_data);
 
 
-    const MfDesfireData* data = nfc_device_get_data(device, NfcProtocolMfDesfire);
-
     bool parsed = false;
     bool parsed = false;
 
 
     do {
     do {
@@ -230,7 +226,8 @@ static NfcCommand opal_poller_callback(NfcGenericEvent event, void* context) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
     if(mf_desfire_event->type == MfDesfirePollerEventTypeReadSuccess) {
         nfc_device_set_data(
         nfc_device_set_data(
             app->nfc_device, NfcProtocolMfDesfire, nfc_poller_get_data(app->poller));
             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_string_reset(app->text_box_store);
             FURI_LOG_I(TAG, "Unknown card type");
             FURI_LOG_I(TAG, "Unknown card type");
             furi_string_printf(parsed_data, "\e#Unknown card\n");
             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_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         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) {
 static void opal_on_enter(Metroflip* app) {
     dolphin_deed(DolphinDeedNfcRead);
     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) {
 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) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }
 
 

+ 5 - 0
scenes/plugins/smartrider.c

@@ -316,6 +316,8 @@ static NfcCommand smartrider_poller_callback(NfcGenericEvent event, void* contex
 
 
         widget_add_button_element(
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -355,6 +357,8 @@ static void smartrider_on_enter(Metroflip* app) {
 
 
             widget_add_button_element(
             widget_add_button_element(
                 widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
                 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);
             mf_classic_free(mfc_data);
             furi_string_free(parsed_data);
             furi_string_free(parsed_data);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -398,6 +402,7 @@ static bool smartrider_on_event(Metroflip* app, SceneManagerEvent event) {
         }
         }
     } else if(event.type == SceneManagerEventTypeBack) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }
 
 

+ 5 - 0
scenes/plugins/troika.c

@@ -221,6 +221,8 @@ static NfcCommand troika_poller_callback(NfcGenericEvent event, void* context) {
 
 
         widget_add_button_element(
         widget_add_button_element(
             widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
             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);
         furi_string_free(parsed_data);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
         view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -259,6 +261,8 @@ static void troika_on_enter(Metroflip* app) {
 
 
             widget_add_button_element(
             widget_add_button_element(
                 widget, GuiButtonTypeRight, "Exit", metroflip_exit_widget_callback, app);
                 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);
             mf_classic_free(mfc_data);
             furi_string_free(parsed_data);
             furi_string_free(parsed_data);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
             view_dispatcher_switch_to_view(app->view_dispatcher, MetroflipViewWidget);
@@ -302,6 +306,7 @@ static bool troika_on_event(Metroflip* app, SceneManagerEvent event) {
         }
         }
     } else if(event.type == SceneManagerEventTypeBack) {
     } else if(event.type == SceneManagerEventTypeBack) {
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
         scene_manager_search_and_switch_to_previous_scene(app->scene_manager, MetroflipSceneStart);
+        scene_manager_set_scene_state(app->scene_manager, MetroflipSceneStart, MetroflipSceneAuto);
         consumed = true;
         consumed = true;
     }
     }