Explorar o código

Add flip_store from https://github.com/jblanked/FlipStore

git-subtree-dir: flip_store
git-subtree-mainline: 01c7af915cb2eb714ebc51b8e5e2d69332d107c1
git-subtree-split: 517ff24054bc121375269f646612edd3992bb463
Willy-JL hai 1 ano
pai
achega
235c632379
Modificáronse 35 ficheiros con 5521 adicións e 0 borrados
  1. BIN=BIN
      flip_store/.DS_Store
  2. 2 0
      flip_store/.gitattributes
  3. 2 0
      flip_store/.gitignore
  4. 1 0
      flip_store/.gitsubtree
  5. 53 0
      flip_store/README.md
  6. 35 0
      flip_store/app.c
  7. BIN=BIN
      flip_store/app.png
  8. 14 0
      flip_store/application.fam
  9. BIN=BIN
      flip_store/assets/.DS_Store
  10. BIN=BIN
      flip_store/assets/01-main.png
  11. BIN=BIN
      flip_store/assets/02-list.png
  12. BIN=BIN
      flip_store/assets/03-success.png
  13. BIN=BIN
      flip_store/assets/04-app-folder.png
  14. BIN=BIN
      flip_store/assets/ButtonBACK_10x8.png
  15. BIN=BIN
      flip_store/assets/ButtonLeft_4x7.png
  16. BIN=BIN
      flip_store/assets/ButtonOK_7x7.png
  17. BIN=BIN
      flip_store/assets/ButtonRight_4x7.png
  18. BIN=BIN
      flip_store/assets/ButtonUp_7x4.png
  19. 8 0
      flip_store/assets/CHANGELOG.md
  20. BIN=BIN
      flip_store/assets/KeyBackspaceSelected_16x9.png
  21. BIN=BIN
      flip_store/assets/KeyBackspace_16x9.png
  22. BIN=BIN
      flip_store/assets/KeySaveSelected_24x11.png
  23. BIN=BIN
      flip_store/assets/KeySave_24x11.png
  24. 54 0
      flip_store/assets/README.md
  25. BIN=BIN
      flip_store/assets/WarningDolphin_45x42.png
  26. 593 0
      flip_store/easy_flipper.h
  27. 534 0
      flip_store/flip_store_apps.h
  28. 393 0
      flip_store/flip_store_callback.h
  29. 144 0
      flip_store/flip_store_e.h
  30. 148 0
      flip_store/flip_store_free.h
  31. 207 0
      flip_store/flip_store_i.h
  32. 317 0
      flip_store/flip_store_storage.h
  33. 1350 0
      flip_store/flipper_http.h
  34. 863 0
      flip_store/jsmn.h
  35. 803 0
      flip_store/uart_text_input.h

BIN=BIN
flip_store/.DS_Store


+ 2 - 0
flip_store/.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 2 - 0
flip_store/.gitignore

@@ -0,0 +1,2 @@
+
+.DS_Store

+ 1 - 0
flip_store/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/jblanked/FlipStore main /

+ 53 - 0
flip_store/README.md

@@ -0,0 +1,53 @@
+# FlipStore
+Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no longer need another device to install apps. FlipStore uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP
+
+## Features
+- App Catalog
+- Install Apps
+- Delete Apps (coming soon)
+- Install Custom Apps (coming soon)
+- Install Devboard Flashes (coming soon)
+- Install Official Firmware (coming soon)
+
+## Installation
+1. Flash your WiFi Devboard: https://github.com/jblanked/FlipperHTTP
+2. Install the app.
+3. Enjoy :D
+
+## Roadmap
+**v0.2**
+- Stability Patch
+- App Categories
+
+**v0.3**
+- Caching
+- Stability Patch 2
+- App Catalog Patch (add in required functionalility)
+
+**v0.4**
+- Delete Apps
+
+**v0.5**
+- App short description
+- App version
+
+**v0.6**
+- Download flash firmware (Marauder, Black Magic, FlipperHTTP)
+
+**v0.7**
+- Download custom apps from a GitHub URL
+
+**v0.8**
+- App Icons
+
+**1.0**
+- Download Official Firmware/Firmware Updates
+
+## Contribution
+This is a big task, and I welcome all contributors, especially developers interested in animations and graphics. Fork the repository, create a pull request, and I will review your edits.
+
+## Known Bugs
+1. Clicking the catalog results in an "Out of Memory" error.
+   - This issue has been addressed, but it may still occur. If it does, restart the app.
+2. The app file is corrupted.
+   - This is likely due to an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view. If this happens more than three times, the current version of FlipStore may not be able to download that app successfully.

+ 35 - 0
flip_store/app.c

@@ -0,0 +1,35 @@
+#include <flip_store_e.h>
+#include <flip_store_storage.h>
+#include <flip_store_callback.h>
+#include <flip_store_i.h>
+#include <flip_store_free.h>
+
+// Entry point for the Hello World application
+int32_t main_flip_store(void *p)
+{
+    // Suppress unused parameter warning
+    UNUSED(p);
+
+    // Initialize the Hello World application
+    FlipStoreApp *app = flip_store_app_alloc();
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate FlipStoreApp");
+        return -1;
+    }
+
+    if (!flipper_http_ping())
+    {
+        FURI_LOG_E(TAG, "Failed to ping the device");
+        return -1;
+    }
+
+    // Run the view dispatcher
+    view_dispatcher_run(app->view_dispatcher);
+
+    // Free the resources used by the Hello World application
+    flip_store_app_free(app);
+
+    // Return 0 to indicate success
+    return 0;
+}

BIN=BIN
flip_store/app.png


+ 14 - 0
flip_store/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="flip_store",
+    name="FlipStore",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="main_flip_store",
+    stack_size=4 * 1024,
+    fap_icon="app.png",
+    fap_category="GPIO",
+    fap_icon_assets="assets",
+    fap_description="Download apps via WiFi directly to your Flipper Zero",
+    fap_author="JBlanked",
+    fap_weburl="https://github.com/jblanked/FlipStore",
+    fap_version="0.2",
+)

BIN=BIN
flip_store/assets/.DS_Store


BIN=BIN
flip_store/assets/01-main.png


BIN=BIN
flip_store/assets/02-list.png


BIN=BIN
flip_store/assets/03-success.png


BIN=BIN
flip_store/assets/04-app-folder.png


BIN=BIN
flip_store/assets/ButtonBACK_10x8.png


BIN=BIN
flip_store/assets/ButtonLeft_4x7.png


BIN=BIN
flip_store/assets/ButtonOK_7x7.png


BIN=BIN
flip_store/assets/ButtonRight_4x7.png


BIN=BIN
flip_store/assets/ButtonUp_7x4.png


+ 8 - 0
flip_store/assets/CHANGELOG.md

@@ -0,0 +1,8 @@
+## v0.2
+- Refactored using the Easy Flipper library.
+- Added categories: Users can now navigate through categories, and when FlipStore installs a selected app, it will download directly to the corresponding category's folder in the apps directory.
+- Improved memory allocation to prevent "Out of Memory" warnings
+- Updated installation messages
+
+## v0.1
+- Initial release

BIN=BIN
flip_store/assets/KeyBackspaceSelected_16x9.png


BIN=BIN
flip_store/assets/KeyBackspace_16x9.png


BIN=BIN
flip_store/assets/KeySaveSelected_24x11.png


BIN=BIN
flip_store/assets/KeySave_24x11.png


+ 54 - 0
flip_store/assets/README.md

@@ -0,0 +1,54 @@
+# FlipStore
+Download Flipper Zero apps directly to your Flipper Zero using WiFi. You no longer need another device to install apps. FlipStore uses the FlipperHTTP flash for the WiFi Devboard, first introduced in the WebCrawler app: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP
+
+## Features
+- App Catalog
+- Install Apps
+- Delete Apps (coming soon)
+- Install Custom Apps (coming soon)
+- Install Devboard Flashes (coming soon)
+- Install Official Firmware (coming soon)
+
+## Installation
+1. Flash your WiFi Devboard: https://github.com/jblanked/WebCrawler-FlipperZero/tree/main/assets/FlipperHTTP
+2. Install the app.
+3. Enjoy :D
+
+## Roadmap
+**v0.2**
+- Stability Patch
+- App Categories
+
+**v0.3**
+- Caching
+- App Catalog Patch (add in required functionalility)
+
+**v0.4**
+- Delete Apps
+
+**v0.5**
+- App short description
+- App version
+
+**v0.6**
+- Download flash firmware (Marauder, Black Magic, FlipperHTTP)
+
+**v0.7**
+- Download custom apps from a GitHub URL
+
+**v0.8**
+- App Icons
+
+**1.0**
+- Download Official Firmware/Firmware Updates
+
+## Contribution
+This is a big task, and I welcome all contributors, especially developers interested in animations and graphics. Fork the repository, create a pull request, and I will review your edits.
+
+## Known Bugs
+1. When clicking the Catalog, I get an "out of memory" error.
+   - This has been addressed but may still occur. If it does, just restart the app.
+2. The app file is corrupted.
+   - It's likely there was an error parsing the data. Restart the app and wait until the green LED light turns off after downloading the app before exiting the view.
+3. The app is stuck on "receiving".
+   - Restart your Flipper Zero with your WiFi Devboard plugged in.

BIN=BIN
flip_store/assets/WarningDolphin_45x42.png


+ 593 - 0
flip_store/easy_flipper.h

@@ -0,0 +1,593 @@
+#ifndef EASY_FLIPPER_H
+#define EASY_FLIPPER_H
+
+#include <malloc.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/modules/submenu.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/menu.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/modules/dialog_ex.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/loading.h>
+#include <uart_text_input.h>
+
+#define EASY_TAG "EasyFlipper"
+
+/**
+ * @brief Navigation callback for exiting the application
+ * @param context The context - unused
+ * @return next view id (VIEW_NONE to exit the app)
+ */
+uint32_t easy_flipper_callback_exit_app(void *context)
+{
+    // Exit the application
+    if (!context)
+    {
+        FURI_LOG_E(EASY_TAG, "Context is NULL");
+        return VIEW_NONE;
+    }
+    UNUSED(context);
+    return VIEW_NONE; // Return VIEW_NONE to exit the app
+}
+
+/**
+ * @brief Initialize a buffer
+ * @param buffer The buffer to initialize
+ * @param buffer_size The size of the buffer
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_buffer(char **buffer, uint32_t buffer_size)
+{
+    if (!buffer)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_buffer");
+        return false;
+    }
+    *buffer = (char *)malloc(buffer_size);
+    if (!*buffer)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate buffer");
+        return false;
+    }
+    *buffer[0] = '\0';
+    return true;
+}
+
+/**
+ * @brief Initialize a View object
+ * @param view The View object to initialize
+ * @param view_id The ID/Index of the view
+ * @param draw_callback The draw callback function (set to NULL if not needed)
+ * @param input_callback The input callback function (set to NULL if not needed)
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_view(
+    View **view,
+    int32_t view_id,
+    void draw_callback(Canvas *, void *),
+    bool input_callback(InputEvent *, void *),
+    uint32_t (*previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!view || !view_dispatcher)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_view");
+        return false;
+    }
+    *view = view_alloc();
+    if (!*view)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate View");
+        return false;
+    }
+    if (draw_callback)
+    {
+        view_set_draw_callback(*view, draw_callback);
+    }
+    if (input_callback)
+    {
+        view_set_input_callback(*view, input_callback);
+    }
+    if (context)
+    {
+        view_set_context(*view, context);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(*view, previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, *view);
+    return true;
+}
+
+/**
+ * @brief Initialize a ViewDispatcher object
+ * @param view_dispatcher The ViewDispatcher object to initialize
+ * @param gui The GUI object
+ * @param context The context to pass to the event callback
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_view_dispatcher(ViewDispatcher **view_dispatcher, Gui *gui, void *context)
+{
+    if (!view_dispatcher)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_view_dispatcher");
+        return false;
+    }
+    *view_dispatcher = view_dispatcher_alloc();
+    if (!*view_dispatcher)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate ViewDispatcher");
+        return false;
+    }
+    view_dispatcher_attach_to_gui(*view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    if (context)
+    {
+        view_dispatcher_set_event_callback_context(*view_dispatcher, context);
+    }
+    return true;
+}
+
+/**
+ * @brief Initialize a Submenu object
+ * @note This does not set the items in the submenu
+ * @param submenu The Submenu object to initialize
+ * @param view_id The ID/Index of the view
+ * @param title The title/header of the submenu
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_submenu(
+    Submenu **submenu,
+    int32_t view_id,
+    char *title,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!submenu)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_submenu");
+        return false;
+    }
+    *submenu = submenu_alloc();
+    if (!*submenu)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Submenu");
+        return false;
+    }
+    if (title)
+    {
+        submenu_set_header(*submenu, title);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(submenu_get_view(*submenu), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, submenu_get_view(*submenu));
+    return true;
+}
+/**
+ * @brief Initialize a Menu object
+ * @note This does not set the items in the menu
+ * @param menu The Menu object to initialize
+ * @param view_id The ID/Index of the view
+ * @param item_callback The item callback function
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_menu(
+    Menu **menu,
+    int32_t view_id,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!menu)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_menu");
+        return false;
+    }
+    *menu = menu_alloc();
+    if (!*menu)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Menu");
+        return false;
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(menu_get_view(*menu), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, menu_get_view(*menu));
+    return true;
+}
+
+/**
+ * @brief Initialize a Widget object
+ * @param widget The Widget object to initialize
+ * @param view_id The ID/Index of the view
+ * @param text The text to display in the widget
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_widget(
+    Widget **widget,
+    int32_t view_id,
+    char *text,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!widget)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_widget");
+        return false;
+    }
+    *widget = widget_alloc();
+    if (!*widget)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Widget");
+        return false;
+    }
+    if (text)
+    {
+        widget_add_text_scroll_element(*widget, 0, 0, 128, 64, text);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(widget_get_view(*widget), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, widget_get_view(*widget));
+    return true;
+}
+
+/**
+ * @brief Initialize a VariableItemList object
+ * @note This does not set the items in the VariableItemList
+ * @param variable_item_list The VariableItemList object to initialize
+ * @param view_id The ID/Index of the view
+ * @param enter_callback The enter callback function (can be set to NULL)
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @param context The context to pass to the enter callback (usually the app)
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_variable_item_list(
+    VariableItemList **variable_item_list,
+    int32_t view_id,
+    void (*enter_callback)(void *, uint32_t),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!variable_item_list)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_variable_item_list");
+        return false;
+    }
+    *variable_item_list = variable_item_list_alloc();
+    if (!*variable_item_list)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate VariableItemList");
+        return false;
+    }
+    if (enter_callback)
+    {
+        variable_item_list_set_enter_callback(*variable_item_list, enter_callback, context);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(variable_item_list_get_view(*variable_item_list), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, variable_item_list_get_view(*variable_item_list));
+    return true;
+}
+
+/**
+ * @brief Initialize a TextInput object
+ * @param text_input The TextInput object to initialize
+ * @param view_id The ID/Index of the view
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_text_input(
+    TextInput **text_input,
+    int32_t view_id,
+    char *header_text,
+    char *text_input_temp_buffer,
+    uint32_t text_input_buffer_size,
+    void (*result_callback)(void *),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!text_input)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_text_input");
+        return false;
+    }
+    *text_input = text_input_alloc();
+    if (!*text_input)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate TextInput");
+        return false;
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(text_input_get_view(*text_input), previous_callback);
+    }
+    if (header_text)
+    {
+        text_input_set_header_text(*text_input, header_text);
+    }
+    if (text_input_temp_buffer && text_input_buffer_size && result_callback)
+    {
+        text_input_set_result_callback(*text_input, result_callback, context, text_input_temp_buffer, text_input_buffer_size, false);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, text_input_get_view(*text_input));
+    return true;
+}
+
+/**
+ * @brief Initialize a UART_TextInput object
+ * @param uart_text_input The UART_TextInput object to initialize
+ * @param view_id The ID/Index of the view
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_uart_text_input(
+    UART_TextInput **uart_text_input,
+    int32_t view_id,
+    char *header_text,
+    char *uart_text_input_temp_buffer,
+    uint32_t uart_text_input_buffer_size,
+    void (*result_callback)(void *),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!uart_text_input)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_uart_text_input");
+        return false;
+    }
+    *uart_text_input = uart_text_input_alloc();
+    if (!*uart_text_input)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate UART_TextInput");
+        return false;
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(uart_text_input_get_view(*uart_text_input), previous_callback);
+    }
+    if (header_text)
+    {
+        uart_text_input_set_header_text(*uart_text_input, header_text);
+    }
+    if (uart_text_input_temp_buffer && uart_text_input_buffer_size && result_callback)
+    {
+        uart_text_input_set_result_callback(*uart_text_input, result_callback, context, uart_text_input_temp_buffer, uart_text_input_buffer_size, false);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, uart_text_input_get_view(*uart_text_input));
+    return true;
+}
+
+/**
+ * @brief Initialize a DialogEx object
+ * @param dialog_ex The DialogEx object to initialize
+ * @param view_id The ID/Index of the view
+ * @param header The header of the dialog
+ * @param header_x The x coordinate of the header
+ * @param header_y The y coordinate of the header
+ * @param text The text of the dialog
+ * @param text_x The x coordinate of the dialog
+ * @param text_y The y coordinate of the dialog
+ * @param left_button_text The text of the left button
+ * @param right_button_text The text of the right button
+ * @param center_button_text The text of the center button
+ * @param result_callback The result callback function
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @param context The context to pass to the result callback
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_dialog_ex(
+    DialogEx **dialog_ex,
+    int32_t view_id,
+    char *header,
+    uint16_t header_x,
+    uint16_t header_y,
+    char *text,
+    uint16_t text_x,
+    uint16_t text_y,
+    char *left_button_text,
+    char *right_button_text,
+    char *center_button_text,
+    void (*result_callback)(DialogExResult, void *),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!dialog_ex)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_dialog_ex");
+        return false;
+    }
+    *dialog_ex = dialog_ex_alloc();
+    if (!*dialog_ex)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate DialogEx");
+        return false;
+    }
+    if (header)
+    {
+        dialog_ex_set_header(*dialog_ex, header, header_x, header_y, AlignLeft, AlignTop);
+    }
+    if (text)
+    {
+        dialog_ex_set_text(*dialog_ex, text, text_x, text_y, AlignLeft, AlignTop);
+    }
+    if (left_button_text)
+    {
+        dialog_ex_set_left_button_text(*dialog_ex, left_button_text);
+    }
+    if (right_button_text)
+    {
+        dialog_ex_set_right_button_text(*dialog_ex, right_button_text);
+    }
+    if (center_button_text)
+    {
+        dialog_ex_set_center_button_text(*dialog_ex, center_button_text);
+    }
+    if (result_callback)
+    {
+        dialog_ex_set_result_callback(*dialog_ex, result_callback);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(dialog_ex_get_view(*dialog_ex), previous_callback);
+    }
+    if (context)
+    {
+        dialog_ex_set_context(*dialog_ex, context);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, dialog_ex_get_view(*dialog_ex));
+    return true;
+}
+
+/**
+ * @brief Initialize a Popup object
+ * @param popup The Popup object to initialize
+ * @param view_id The ID/Index of the view
+ * @param header The header of the dialog
+ * @param header_x The x coordinate of the header
+ * @param header_y The y coordinate of the header
+ * @param text The text of the dialog
+ * @param text_x The x coordinate of the dialog
+ * @param text_y The y coordinate of the dialog
+ * @param result_callback The result callback function
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @param context The context to pass to the result callback
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_popup(
+    Popup **popup,
+    int32_t view_id,
+    char *header,
+    uint16_t header_x,
+    uint16_t header_y,
+    char *text,
+    uint16_t text_x,
+    uint16_t text_y,
+    void (*result_callback)(void *),
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher,
+    void *context)
+{
+    if (!popup)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_popup");
+        return false;
+    }
+    *popup = popup_alloc();
+    if (!*popup)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Popup");
+        return false;
+    }
+    if (header)
+    {
+        popup_set_header(*popup, header, header_x, header_y, AlignLeft, AlignTop);
+    }
+    if (text)
+    {
+        popup_set_text(*popup, text, text_x, text_y, AlignLeft, AlignTop);
+    }
+    if (result_callback)
+    {
+        popup_set_callback(*popup, result_callback);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(popup_get_view(*popup), previous_callback);
+    }
+    if (context)
+    {
+        popup_set_context(*popup, context);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, popup_get_view(*popup));
+    return true;
+}
+
+/**
+ * @brief Initialize a Loading object
+ * @param loading The Loading object to initialize
+ * @param view_id The ID/Index of the view
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_loading(
+    Loading **loading,
+    int32_t view_id,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!loading)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_loading");
+        return false;
+    }
+    *loading = loading_alloc();
+    if (!*loading)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate Loading");
+        return false;
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(loading_get_view(*loading), previous_callback);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, loading_get_view(*loading));
+    return true;
+}
+
+/**
+ * @brief Set a char butter to a FuriString
+ * @param furi_string The FuriString object
+ * @param buffer The buffer to copy the string to
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer)
+{
+    if (!furi_string)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_buffer_to_furi_string");
+        return false;
+    }
+    *furi_string = furi_string_alloc();
+    if (!furi_string)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate FuriString");
+        return false;
+    }
+    furi_string_set_str(*furi_string, buffer);
+    return true;
+}
+
+#endif // EASY_FLIPPER_H

+ 534 - 0
flip_store/flip_store_apps.h

@@ -0,0 +1,534 @@
+#ifndef FLIP_STORE_APPS_H
+#define FLIP_STORE_APPS_H
+
+// Define maximum limits
+#define MAX_APP_NAME_LENGTH 32
+#define MAX_ID_LENGTH 32
+#define MAX_APP_COUNT 100
+
+typedef struct
+{
+    char app_name[MAX_APP_NAME_LENGTH];
+    char app_id[MAX_APP_NAME_LENGTH];
+    char app_build_id[MAX_ID_LENGTH];
+} FlipStoreAppInfo;
+
+static FlipStoreAppInfo *flip_catalog = NULL;
+
+static uint32_t app_selected_index = 0;
+static bool flip_store_sent_request = false;
+static bool flip_store_success = false;
+static bool flip_store_saved_data = false;
+static bool flip_store_saved_success = false;
+static uint32_t flip_store_category_index = 0;
+
+enum ObjectState
+{
+    OBJECT_EXPECT_KEY,
+    OBJECT_EXPECT_COLON,
+    OBJECT_EXPECT_VALUE,
+    OBJECT_EXPECT_COMMA_OR_END
+};
+
+static void flip_catalog_free()
+{
+    if (flip_catalog)
+    {
+        free(flip_catalog);
+        flip_catalog = NULL;
+    }
+}
+
+static bool flip_catalog_alloc()
+{
+    if (!flip_catalog)
+    {
+        flip_catalog = (FlipStoreAppInfo *)malloc(MAX_APP_COUNT * sizeof(FlipStoreAppInfo));
+    }
+    if (!flip_catalog)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog.");
+        return false;
+    }
+    return true;
+}
+
+// Utility function to parse JSON incrementally from a file
+static bool flip_store_process_app_list(const char *file_path)
+{
+    if (file_path == NULL)
+    {
+        FURI_LOG_E(TAG, "JSON file path is NULL.");
+        return false;
+    }
+
+    // initialize the flip_catalog
+    if (!flip_catalog_alloc())
+    {
+        FURI_LOG_E(TAG, "Failed to allocate memory for flip_catalog.");
+        return false;
+    }
+
+    Storage *_storage = NULL;
+    File *_file = NULL;
+    char buffer[BUFFER_SIZE];
+    size_t bytes_read;
+    bool in_string = false;
+    bool is_escaped = false;
+    bool reading_key = false;
+    bool reading_value = false;
+    bool inside_app_object = false;
+    bool found_name = false;
+    bool found_id = false;
+    bool found_build_id = false;
+    char current_key[MAX_KEY_LENGTH] = {0};
+    size_t key_index = 0;
+    char current_value[MAX_VALUE_LENGTH] = {0};
+    size_t value_index = 0;
+    int app_count = 0;
+    enum ObjectState object_state = OBJECT_EXPECT_KEY; // Initialize object_state
+
+    // Initialize parser state
+    enum
+    {
+        STATE_SEARCH_APPS_KEY,
+        STATE_SEARCH_ARRAY_START,
+        STATE_READ_ARRAY_ELEMENTS,
+        STATE_DONE
+    } state = STATE_SEARCH_APPS_KEY;
+
+    // Open storage and file
+    _storage = furi_record_open(RECORD_STORAGE);
+    if (!_storage)
+    {
+        FURI_LOG_E(TAG, "Failed to open storage.");
+        return false;
+    }
+
+    _file = storage_file_alloc(_storage);
+    if (!_file)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate file.");
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    if (!storage_file_open(_file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        FURI_LOG_E(TAG, "Failed to open JSON file for reading.");
+        storage_file_free(_file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    while ((bytes_read = storage_file_read(_file, buffer, BUFFER_SIZE)) > 0 && state != STATE_DONE)
+    {
+        for (size_t i = 0; i < bytes_read; ++i)
+        {
+            char c = buffer[i];
+
+            if (is_escaped)
+            {
+                is_escaped = false;
+                if (reading_key)
+                {
+                    if (key_index < MAX_KEY_LENGTH - 1)
+                    {
+                        current_key[key_index++] = c;
+                    }
+                }
+                else if (reading_value)
+                {
+                    if (value_index < MAX_VALUE_LENGTH - 1)
+                    {
+                        current_value[value_index++] = c;
+                    }
+                }
+                continue;
+            }
+
+            if (c == '\\')
+            {
+                is_escaped = true;
+                continue;
+            }
+
+            if (c == '\"')
+            {
+                in_string = !in_string;
+
+                if (in_string)
+                {
+                    // Start of a string
+                    if (!reading_key && !reading_value)
+                    {
+                        if (state == STATE_SEARCH_APPS_KEY)
+                        {
+                            reading_key = true;
+                            key_index = 0;
+                            current_key[0] = '\0';
+                        }
+                        else if (inside_app_object)
+                        {
+                            if (object_state == OBJECT_EXPECT_KEY)
+                            {
+                                reading_key = true;
+                                key_index = 0;
+                                current_key[0] = '\0';
+                            }
+                            else if (object_state == OBJECT_EXPECT_VALUE)
+                            {
+                                reading_value = true;
+                                value_index = 0;
+                                current_value[0] = '\0';
+                            }
+                        }
+                    }
+                }
+                else
+                {
+                    // End of a string
+                    if (reading_key)
+                    {
+                        reading_key = false;
+                        current_key[key_index] = '\0';
+
+                        if (state == STATE_SEARCH_APPS_KEY && strcmp(current_key, "apps") == 0)
+                        {
+                            state = STATE_SEARCH_ARRAY_START;
+                        }
+                        else if (inside_app_object)
+                        {
+                            object_state = OBJECT_EXPECT_COLON;
+                        }
+                    }
+                    else if (reading_value)
+                    {
+                        reading_value = false;
+                        current_value[value_index] = '\0';
+
+                        if (inside_app_object)
+                        {
+                            if (strcmp(current_key, "name") == 0)
+                            {
+                                strncpy(flip_catalog[app_count].app_name, current_value, MAX_APP_NAME_LENGTH - 1);
+                                flip_catalog[app_count].app_name[MAX_APP_NAME_LENGTH - 1] = '\0';
+                                found_name = true;
+                            }
+                            else if (strcmp(current_key, "id") == 0)
+                            {
+                                strncpy(flip_catalog[app_count].app_id, current_value, MAX_APP_NAME_LENGTH - 1);
+                                flip_catalog[app_count].app_id[MAX_APP_NAME_LENGTH - 1] = '\0';
+                                found_id = true;
+                            }
+                            else if (strcmp(current_key, "build_id") == 0)
+                            {
+                                strncpy(flip_catalog[app_count].app_build_id, current_value, MAX_APP_NAME_LENGTH - 1);
+                                flip_catalog[app_count].app_build_id[MAX_ID_LENGTH - 1] = '\0';
+                                found_build_id = true;
+                            }
+
+                            // After processing value, expect comma or end
+                            object_state = OBJECT_EXPECT_COMMA_OR_END;
+
+                            // Check if both name and id are found
+                            if (found_name && found_id && found_build_id)
+                            {
+                                app_count++;
+                                if (app_count >= MAX_APP_COUNT)
+                                {
+                                    FURI_LOG_I(TAG, "Reached maximum app count.");
+                                    state = STATE_DONE;
+                                    break;
+                                }
+
+                                // Reset for next app
+                                found_name = false;
+                                found_id = false;
+                                found_build_id = false;
+                            }
+                        }
+                    }
+                }
+                continue;
+            }
+
+            if (in_string)
+            {
+                if (reading_key)
+                {
+                    if (key_index < MAX_KEY_LENGTH - 1)
+                    {
+                        current_key[key_index++] = c;
+                    }
+                }
+                else if (reading_value)
+                {
+                    if (value_index < MAX_VALUE_LENGTH - 1)
+                    {
+                        current_value[value_index++] = c;
+                    }
+                }
+                continue;
+            }
+
+            // Not inside a string
+
+            if (state == STATE_SEARCH_ARRAY_START)
+            {
+                if (c == '[')
+                {
+                    state = STATE_READ_ARRAY_ELEMENTS;
+                }
+                continue;
+            }
+
+            if (state == STATE_READ_ARRAY_ELEMENTS)
+            {
+                if (c == '{')
+                {
+                    inside_app_object = true;
+                    found_name = false;
+                    found_id = false;
+                    found_build_id = false;
+                    object_state = OBJECT_EXPECT_KEY;
+                }
+                else if (c == '}')
+                {
+                    inside_app_object = false;
+                    object_state = OBJECT_EXPECT_KEY;
+                }
+                else if (c == ']')
+                {
+                    state = STATE_DONE;
+                    break;
+                }
+                else if (c == ':')
+                {
+                    if (inside_app_object && object_state == OBJECT_EXPECT_COLON)
+                    {
+                        object_state = OBJECT_EXPECT_VALUE;
+                    }
+                }
+                else if (c == ',')
+                {
+                    if (inside_app_object && object_state == OBJECT_EXPECT_COMMA_OR_END)
+                    {
+                        object_state = OBJECT_EXPECT_KEY;
+                    }
+                    // Else, separator between objects or values
+                }
+                // Ignore other characters like whitespace, etc.
+                continue;
+            }
+        }
+    }
+
+    // Clean up
+    storage_file_close(_file);
+    storage_file_free(_file);
+    furi_record_close(RECORD_STORAGE);
+
+    if (app_count == 0)
+    {
+        FURI_LOG_E(TAG, "No valid apps were parsed.");
+        return false;
+    }
+    return true;
+}
+
+static bool flip_store_get_fap_file(char *build_id, char *target, char *api)
+{
+    is_compile_app_request = true;
+    char url[164];
+    snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=%s&api=%s", build_id, target, api);
+    return flipper_http_get_request_bytes(url, jsmn("Content-Type", "application/octet-stream"));
+}
+
+static void flip_store_request_error(Canvas *canvas)
+{
+    if (fhttp.received_data == NULL)
+    {
+        if (fhttp.last_response != NULL)
+        {
+            if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
+            {
+                canvas_clear(canvas);
+                canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+                canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+                canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+            }
+            else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
+            {
+                canvas_clear(canvas);
+                canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+                canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+                canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+            }
+            else
+            {
+                FURI_LOG_E(TAG, "Received an error: %s", fhttp.last_response);
+                canvas_draw_str(canvas, 0, 42, "Unusual error...");
+                canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+                canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+            }
+        }
+        else
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error.");
+            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+    }
+    else
+    {
+        canvas_clear(canvas);
+        canvas_draw_str(canvas, 0, 10, "Failed to receive data.");
+        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+    }
+}
+// function to handle the entire installation process "asynchronously"
+static bool flip_store_install_app(Canvas *canvas, char *category)
+{
+    // create /apps/FlipStore directory if it doesn't exist
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/%s", category);
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    // Adjusted to access flip_catalog as an array of structures
+    char *app_name = flip_catalog[app_selected_index].app_name;
+    char installing_text[128];
+    snprintf(installing_text, sizeof(installing_text), "Installing %s", app_name);
+    char bin_path[256];
+    snprintf(bin_path, sizeof(bin_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s.fap", category, flip_catalog[app_selected_index].app_id);
+    strncpy(fhttp.file_path, bin_path, sizeof(fhttp.file_path) - 1);
+    canvas_draw_str(canvas, 0, 10, installing_text);
+    canvas_draw_str(canvas, 0, 20, "Sending request..");
+    if (fhttp.state != INACTIVE && flip_store_get_fap_file(flip_catalog[app_selected_index].app_build_id, "f7", "73.0"))
+    {
+        canvas_draw_str(canvas, 0, 30, "Request sent.");
+        fhttp.state = RECEIVING;
+        // furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        canvas_draw_str(canvas, 0, 40, "Receiving...");
+    }
+    else
+    {
+        FURI_LOG_E(TAG, "Failed to send the request");
+        flip_store_success = false;
+        return false;
+    }
+    while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0)
+    {
+        // Wait for the feed to be received
+        furi_delay_ms(10);
+    }
+    // furi_timer_stop(fhttp.get_timeout_timer);
+    if (fhttp.state == ISSUE || fhttp.received_data == NULL)
+    {
+        flip_store_request_error(canvas);
+        flip_store_success = false;
+        return false;
+    }
+    flip_store_success = true;
+    return true;
+}
+
+// process the app list and return view
+static int32_t flip_store_handle_app_list(FlipStoreApp *app, int32_t success_view, char *category, Submenu **submenu)
+{
+    // reset the flip_catalog
+    if (flip_catalog)
+    {
+        flip_catalog_free();
+    }
+
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return FlipStoreViewPopup;
+    }
+    char url[128];
+    is_compile_app_request = false;
+    // append the category to the end of the url
+    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/flipper/apps/%s/extended/", category);
+    // async call to the app list with timer
+    if (fhttp.state != INACTIVE && flipper_http_get_request_with_headers(url, jsmn("Content-Type", "application/json")))
+    {
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+    }
+    else
+    {
+        FURI_LOG_E(TAG, "Failed to send the request");
+        return FlipStoreViewPopup;
+    }
+    while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0)
+    {
+        // Wait for the feed to be received
+        furi_delay_ms(100);
+    }
+    furi_timer_stop(fhttp.get_timeout_timer);
+    if (fhttp.state == ISSUE || fhttp.received_data == NULL)
+    {
+        if (fhttp.received_data == NULL)
+        {
+            FURI_LOG_E(TAG, "Failed to receive data");
+            if (fhttp.last_response != NULL)
+            {
+                if (strstr(fhttp.last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
+                {
+                    popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
+                }
+                else if (strstr(fhttp.last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
+                {
+                    popup_set_text(app->popup, "[ERROR] WiFi Disconnected.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
+                }
+                else
+                {
+                    popup_set_text(app->popup, fhttp.last_response, 0, 10, AlignLeft, AlignTop);
+                }
+            }
+            else
+            {
+                popup_set_text(app->popup, "[ERROR] Unknown Error.\n\n\nUpdate your WiFi settings.\nPress BACK to return.", 0, 10, AlignLeft, AlignTop);
+            }
+            return FlipStoreViewPopup;
+        }
+        else
+        {
+            FURI_LOG_E(TAG, "Failed to receive data");
+            popup_set_text(app->popup, "Failed to received data.", 0, 10, AlignLeft, AlignTop);
+            return FlipStoreViewPopup;
+        }
+    }
+    else
+    {
+        // process the app list
+        const char *output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt";
+        if (flip_store_process_app_list(output_file_path))
+        {
+            submenu_reset(*submenu);
+            // add each app name to submenu
+            for (int i = 0; i < MAX_APP_COUNT; i++)
+            {
+                if (strlen(flip_catalog[i].app_name) > 0)
+                {
+                    submenu_add_item(*submenu, flip_catalog[i].app_name, FlipStoreSubmenuIndexStartAppList + i, callback_submenu_choices, app);
+                }
+            }
+            return success_view;
+        }
+        else
+        {
+            FURI_LOG_E(TAG, "Failed to process the app list");
+            popup_set_text(app->popup, "Failed to process the app list", 0, 10, AlignLeft, AlignTop);
+            return FlipStoreViewPopup;
+        }
+    }
+}
+
+#endif // FLIP_STORE_APPS_H

+ 393 - 0
flip_store/flip_store_callback.h

@@ -0,0 +1,393 @@
+#ifndef FLIP_STORE_CALLBACK_H
+#define FLIP_STORE_CALLBACK_H
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <stdbool.h>
+#include <flip_store_apps.h>
+
+// Callback for drawing the main screen
+static void flip_store_view_draw_callback_main(Canvas *canvas, void *model)
+{
+    UNUSED(model);
+    canvas_set_font(canvas, FontSecondary);
+
+    if (fhttp.state == INACTIVE)
+    {
+        canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected.");
+        canvas_draw_str(canvas, 0, 17, "Please connect to the board.");
+        canvas_draw_str(canvas, 0, 32, "If your board is connected,");
+        canvas_draw_str(canvas, 0, 42, "make sure you have flashed");
+        canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the");
+        canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash.");
+        return;
+    }
+
+    if (!flip_store_sent_request)
+    {
+        flip_store_sent_request = true;
+
+        if (!flip_store_install_app(canvas, categories[flip_store_category_index]))
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "Failed to install app.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+    }
+    else
+    {
+        if (flip_store_success)
+        {
+            if (fhttp.state == RECEIVING)
+            {
+                canvas_clear(canvas);
+                canvas_draw_str(canvas, 0, 10, "Downloading app...");
+                canvas_draw_str(canvas, 0, 60, "Please wait...");
+                return;
+            }
+            else if (fhttp.state == IDLE)
+            {
+                canvas_clear(canvas);
+                canvas_draw_str(canvas, 0, 10, "App installed successfully.");
+                canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+            }
+        }
+        else
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "Failed to install app.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+    }
+}
+
+static void flip_store_view_draw_callback_app_list(Canvas *canvas, void *model)
+{
+    UNUSED(model);
+    canvas_clear(canvas);
+    canvas_set_font(canvas, FontPrimary);
+    // Adjusted to access flip_catalog as an array of structures
+    canvas_draw_str(canvas, 0, 10, flip_catalog[app_selected_index].app_name);
+    // canvas_draw_icon(canvas, 0, 53, &I_ButtonLeft_4x7); (future implementation)
+    //  canvas_draw_str_aligned(canvas, 7, 54, AlignLeft, AlignTop, "Delete");  (future implementation)
+    canvas_draw_icon(canvas, 0, 53, &I_ButtonBACK_10x8);
+    canvas_draw_str_aligned(canvas, 12, 54, AlignLeft, AlignTop, "Back");
+    canvas_draw_icon(canvas, 90, 53, &I_ButtonRight_4x7);
+    canvas_draw_str_aligned(canvas, 97, 54, AlignLeft, AlignTop, "Install");
+}
+
+static bool flip_store_input_callback(InputEvent *event, void *context)
+{
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return false;
+    }
+    if (event->type == InputTypeShort)
+    {
+        // Future implementation
+        // if (event->key == InputKeyLeft)
+        //{
+        // Left button clicked, delete the app with DialogEx confirmation
+        // view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppDelete);
+        //    return true;
+        //}
+        if (event->key == InputKeyRight)
+        {
+            // Right button clicked, download the app
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain);
+            return true;
+        }
+    }
+    else if (event->type == InputTypePress)
+    {
+        if (event->key == InputKeyBack)
+        {
+            // Back button clicked, switch to the previous view.
+            view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static void flip_store_text_updated_ssid(void *context)
+{
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return;
+    }
+
+    // store the entered text
+    strncpy(app->uart_text_input_buffer_ssid, app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid);
+
+    // Ensure null-termination
+    app->uart_text_input_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0';
+
+    // update the variable item text
+    if (app->variable_item_ssid)
+    {
+        variable_item_set_current_value_text(app->variable_item_ssid, app->uart_text_input_buffer_ssid);
+    }
+
+    // save the settings
+    save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass);
+
+    // if SSID and PASS are not empty, connect to the WiFi
+    if (strlen(app->uart_text_input_buffer_ssid) > 0 && strlen(app->uart_text_input_buffer_pass) > 0)
+    {
+        // save wifi settings
+        if (!flipper_http_save_wifi(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass))
+        {
+            FURI_LOG_E(TAG, "Failed to save WiFi settings");
+        }
+    }
+
+    // switch to the settings view
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings);
+}
+static void flip_store_text_updated_pass(void *context)
+{
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return;
+    }
+
+    // store the entered text
+    strncpy(app->uart_text_input_buffer_pass, app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_size_pass);
+
+    // Ensure null-termination
+    app->uart_text_input_buffer_pass[app->uart_text_input_buffer_size_pass - 1] = '\0';
+
+    // update the variable item text
+    if (app->variable_item_pass)
+    {
+        variable_item_set_current_value_text(app->variable_item_pass, app->uart_text_input_buffer_pass);
+    }
+
+    // save the settings
+    save_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass);
+
+    // if SSID and PASS are not empty, connect to the WiFi
+    if (strlen(app->uart_text_input_buffer_ssid) > 0 && strlen(app->uart_text_input_buffer_pass) > 0)
+    {
+        // save wifi settings
+        if (!flipper_http_save_wifi(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_pass))
+        {
+            FURI_LOG_E(TAG, "Failed to save WiFi settings");
+        }
+    }
+
+    // switch to the settings view
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings);
+}
+
+static uint32_t callback_to_submenu(void *context)
+{
+    if (!context)
+    {
+        FURI_LOG_E(TAG, "Context is NULL");
+        return VIEW_NONE;
+    }
+    UNUSED(context);
+    return FlipStoreViewSubmenu;
+}
+
+static uint32_t callback_to_app_list(void *context)
+{
+    if (!context)
+    {
+        FURI_LOG_E(TAG, "Context is NULL");
+        return VIEW_NONE;
+    }
+    UNUSED(context);
+    flip_store_sent_request = false;
+    flip_store_success = false;
+    flip_store_saved_data = false;
+    flip_store_saved_success = false;
+    return FlipStoreViewAppList;
+}
+
+static void settings_item_selected(void *context, uint32_t index)
+{
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return;
+    }
+    switch (index)
+    {
+    case 0: // Input SSID
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInputSSID);
+        break;
+    case 1: // Input Password
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewTextInputPass);
+        break;
+    default:
+        FURI_LOG_E(TAG, "Unknown configuration item index");
+        break;
+    }
+}
+
+void dialog_callback(DialogExResult result, void *context)
+{
+    furi_assert(context);
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    if (result == DialogExResultLeft) // No
+    {
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
+    }
+    else if (result == DialogExResultRight)
+    {
+        // delete the app then return to the app list
+
+        // pop up a message
+        popup_set_header(app->popup, "Success", 0, 0, AlignLeft, AlignTop);
+        popup_set_text(app->popup, "App deleted successfully.", 0, 60, AlignLeft, AlignTop);
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewPopup);
+        furi_delay_ms(2000); // delay for 2 seconds
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
+    }
+}
+
+void popup_callback(void *context)
+{
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return;
+    }
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu);
+}
+
+/**
+ * @brief Navigation callback for exiting the application
+ * @param context The context - unused
+ * @return next view id (VIEW_NONE to exit the app)
+ */
+static uint32_t callback_exit_app(void *context)
+{
+    // Exit the application
+    if (!context)
+    {
+        FURI_LOG_E(TAG, "Context is NULL");
+        return VIEW_NONE;
+    }
+    UNUSED(context);
+    return VIEW_NONE; // Return VIEW_NONE to exit the app
+}
+
+static void callback_submenu_choices(void *context, uint32_t index)
+{
+    FlipStoreApp *app = (FlipStoreApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return;
+    }
+    switch (index)
+    {
+    case FlipStoreSubmenuIndexMain:
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewMain);
+        break;
+    case FlipStoreSubmenuIndexAbout:
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAbout);
+        break;
+    case FlipStoreSubmenuIndexSettings:
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSettings);
+        break;
+    case FlipStoreSubmenuIndexAppList:
+        flip_store_category_index = 0;
+        view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppList);
+        break;
+    case FlipStoreSubmenuIndexAppListBluetooth:
+        flip_store_category_index = 0;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListBluetooth, "Bluetooth", &app->submenu_app_list_bluetooth));
+        break;
+    case FlipStoreSubmenuIndexAppListGames:
+        flip_store_category_index = 1;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListGames, "Games", &app->submenu_app_list_games));
+        break;
+    case FlipStoreSubmenuIndexAppListGPIO:
+        flip_store_category_index = 2;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListGPIO, "GPIO", &app->submenu_app_list_gpio));
+        break;
+    case FlipStoreSubmenuIndexAppListInfrared:
+        flip_store_category_index = 3;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListInfrared, "Infrared", &app->submenu_app_list_infrared));
+        break;
+    case FlipStoreSubmenuIndexAppListiButton:
+        flip_store_category_index = 4;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListiButton, "iButton", &app->submenu_app_list_ibutton));
+        break;
+    case FlipStoreSubmenuIndexAppListMedia:
+        flip_store_category_index = 5;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListMedia, "Media", &app->submenu_app_list_media));
+        break;
+    case FlipStoreSubmenuIndexAppListNFC:
+        flip_store_category_index = 6;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListNFC, "NFC", &app->submenu_app_list_nfc));
+        break;
+    case FlipStoreSubmenuIndexAppListRFID:
+        flip_store_category_index = 7;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListRFID, "RFID", &app->submenu_app_list_rfid));
+        break;
+    case FlipStoreSubmenuIndexAppListSubGHz:
+        flip_store_category_index = 8;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListSubGHz, "Sub-GHz", &app->submenu_app_list_subghz));
+        break;
+    case FlipStoreSubmenuIndexAppListTools:
+        flip_store_category_index = 9;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListTools, "Tools", &app->submenu_app_list_tools));
+        break;
+    case FlipStoreSubmenuIndexAppListUSB:
+        flip_store_category_index = 10;
+        view_dispatcher_switch_to_view(app->view_dispatcher, flip_store_handle_app_list(app, FlipStoreViewAppListUSB, "USB", &app->submenu_app_list_usb));
+        break;
+    default:
+        // Check if the index is within the app list range
+        if (index >= FlipStoreSubmenuIndexStartAppList && index < FlipStoreSubmenuIndexStartAppList + MAX_APP_COUNT)
+        {
+            // Get the app index
+            uint32_t app_index = index - FlipStoreSubmenuIndexStartAppList;
+
+            // Check if the app index is valid
+            if ((int)app_index >= 0 && app_index < MAX_APP_COUNT)
+            {
+                // Get the app name
+                char *app_name = flip_catalog[app_index].app_name;
+
+                // Check if the app name is valid
+                if (app_name != NULL && strlen(app_name) > 0)
+                {
+                    app_selected_index = app_index;
+                    view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewAppInfo);
+                }
+                else
+                {
+                    FURI_LOG_E(TAG, "Invalid app name");
+                }
+            }
+            else
+            {
+                FURI_LOG_E(TAG, "Invalid app index");
+            }
+        }
+        else
+        {
+            FURI_LOG_E(TAG, "Unknown submenu index");
+        }
+        break;
+    }
+}
+
+#endif // FLIP_STORE_CALLBACK_H

+ 144 - 0
flip_store/flip_store_e.h

@@ -0,0 +1,144 @@
+#ifndef FLIP_STORE_E_H
+#define FLIP_STORE_E_H
+#include <uart_text_input.h>
+#include <flipper_http.h>
+#include <easy_flipper.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/modules/submenu.h>
+#include <gui/view_dispatcher.h>
+#include <notification/notification.h>
+#include <dialogs/dialogs.h>
+#include <jsmn.h>
+#define TAG "FlipStore"
+
+// define the list of categories
+char *categories[] = {
+    "Bluetooth",
+    "Games",
+    "GPIO",
+    "Infrared",
+    "iButton",
+    "Media",
+    "NFC",
+    "RFID",
+    "Sub-GHz",
+    "Tools",
+    "USB",
+};
+
+// Define the submenu items for our FlipStore application
+typedef enum
+{
+    FlipStoreSubmenuIndexMain, // Click to start downloading the selected app
+    FlipStoreSubmenuIndexAbout,
+    FlipStoreSubmenuIndexSettings,
+    FlipStoreSubmenuIndexAppList,
+    //
+    FlipStoreSubmenuIndexAppListBluetooth,
+    FlipStoreSubmenuIndexAppListGames,
+    FlipStoreSubmenuIndexAppListGPIO,
+    FlipStoreSubmenuIndexAppListInfrared,
+    FlipStoreSubmenuIndexAppListiButton,
+    FlipStoreSubmenuIndexAppListMedia,
+    FlipStoreSubmenuIndexAppListNFC,
+    FlipStoreSubmenuIndexAppListRFID,
+    FlipStoreSubmenuIndexAppListSubGHz,
+    FlipStoreSubmenuIndexAppListTools,
+    FlipStoreSubmenuIndexAppListUSB,
+    //
+    FlipStoreSubmenuIndexStartAppList
+} FlipStoreSubmenuIndex;
+
+// Define a single view for our FlipStore application
+typedef enum
+{
+    FlipStoreViewMain,          // The main screen
+    FlipStoreViewSubmenu,       // The submenu
+    FlipStoreViewAbout,         // The about screen
+    FlipStoreViewSettings,      // The settings screen
+    FlipStoreViewTextInputSSID, // The text input screen for SSID
+    FlipStoreViewTextInputPass, // The text input screen for password
+    //
+    FlipStoreViewPopup, // The popup screen
+    //
+    FlipStoreViewAppList,     // The app list screen
+    FlipStoreViewAppInfo,     // The app info screen (widget) of the selected app
+    FlipStoreViewAppDownload, // The app download screen (widget) of the selected app
+    FlipStoreViewAppDelete,   // The app delete screen (DialogEx) of the selected app
+    //
+    FlipStoreViewAppListBluetooth, // the app list screen for Bluetooth
+    FlipStoreViewAppListGames,     // the app list screen for Games
+    FlipStoreViewAppListGPIO,      // the app list screen for GPIO
+    FlipStoreViewAppListInfrared,  // the app list screen for Infrared
+    FlipStoreViewAppListiButton,   // the app list screen for iButton
+    FlipStoreViewAppListMedia,     // the app list screen for Media
+    FlipStoreViewAppListNFC,       // the app list screen for NFC
+    FlipStoreViewAppListRFID,      // the app list screen for RFID
+    FlipStoreViewAppListSubGHz,    // the app list screen for Sub-GHz
+    FlipStoreViewAppListTools,     // the app list screen for Tools
+    FlipStoreViewAppListUSB,       // the app list screen for USB
+} FlipStoreView;
+
+// Each screen will have its own view
+typedef struct
+{
+    ViewDispatcher *view_dispatcher; // Switches between our views
+    View *view_main;                 // The main screen for downloading apps
+    View *view_app_info;             // The app info screen (view) of the selected app
+    Submenu *submenu;                // The submenu (main)
+    //
+    Submenu *submenu_app_list; // The submenu (app list) for the selected category
+    //
+    Submenu *submenu_app_list_bluetooth; // The submenu (app list) for Bluetooth
+    Submenu *submenu_app_list_games;     // The submenu (app list) for Games
+    Submenu *submenu_app_list_gpio;      // The submenu (app list) for GPIO
+    Submenu *submenu_app_list_infrared;  // The submenu (app list) for Infrared
+    Submenu *submenu_app_list_ibutton;   // The submenu (app list) for iButton
+    Submenu *submenu_app_list_media;     // The submenu (app list) for Media
+    Submenu *submenu_app_list_nfc;       // The submenu (app list) for NFC
+    Submenu *submenu_app_list_rfid;      // The submenu (app list) for RFID
+    Submenu *submenu_app_list_subghz;    // The submenu (app list) for Sub-GHz
+    Submenu *submenu_app_list_tools;     // The submenu (app list) for Tools
+    Submenu *submenu_app_list_usb;       // The submenu (app list) for USB
+    //
+    Widget *widget;                       // The widget
+    Popup *popup;                         // The popup
+    DialogEx *dialog_delete;              // The dialog for deleting an app
+    VariableItemList *variable_item_list; // The variable item list (settngs)
+    VariableItem *variable_item_ssid;     // The variable item
+    VariableItem *variable_item_pass;     // The variable item
+    UART_TextInput *uart_text_input_ssid; // The text input
+    UART_TextInput *uart_text_input_pass; // The text input
+
+    char *uart_text_input_buffer_ssid;         // Buffer for the text input
+    char *uart_text_input_temp_buffer_ssid;    // Temporary buffer for the text input
+    uint32_t uart_text_input_buffer_size_ssid; // Size of the text input buffer
+
+    char *uart_text_input_buffer_pass;         // Buffer for the text input
+    char *uart_text_input_temp_buffer_pass;    // Temporary buffer for the text input
+    uint32_t uart_text_input_buffer_size_pass; // Size of the text input buffer
+} FlipStoreApp;
+
+// include strndup (otherwise NULL pointer dereference)
+char *strndup(const char *s, size_t n)
+{
+    char *result;
+    size_t len = strlen(s);
+
+    if (n < len)
+        len = n;
+
+    result = (char *)malloc(len + 1);
+    if (!result)
+        return NULL;
+
+    result[len] = '\0';
+    return (char *)memcpy(result, s, len);
+}
+
+static void callback_submenu_choices(void *context, uint32_t index);
+
+#endif // FLIP_STORE_E_H

+ 148 - 0
flip_store/flip_store_free.h

@@ -0,0 +1,148 @@
+#ifndef FLIP_STORE_FREE_H
+#define FLIP_STORE_FREE_H
+
+// Function to free the resources used by FlipStoreApp
+static void flip_store_app_free(FlipStoreApp *app)
+{
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipStoreApp is NULL");
+        return;
+    }
+
+    // Free View(s)
+    if (app->view_main)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewMain);
+        view_free(app->view_main);
+    }
+    if (app->view_app_info)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppInfo);
+        view_free(app->view_app_info);
+    }
+
+    // Free Submenu(s)
+    if (app->submenu)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSubmenu);
+        submenu_free(app->submenu);
+    }
+    if (app->submenu_app_list)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppList);
+        submenu_free(app->submenu_app_list);
+    }
+    if (app->submenu_app_list_bluetooth)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListBluetooth);
+        submenu_free(app->submenu_app_list_bluetooth);
+    }
+    if (app->submenu_app_list_games)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListGames);
+        submenu_free(app->submenu_app_list_games);
+    }
+    if (app->submenu_app_list_gpio)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListGPIO);
+        submenu_free(app->submenu_app_list_gpio);
+    }
+    if (app->submenu_app_list_infrared)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListInfrared);
+        submenu_free(app->submenu_app_list_infrared);
+    }
+    if (app->submenu_app_list_ibutton)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListiButton);
+        submenu_free(app->submenu_app_list_ibutton);
+    }
+    if (app->submenu_app_list_media)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListMedia);
+        submenu_free(app->submenu_app_list_media);
+    }
+    if (app->submenu_app_list_nfc)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListNFC);
+        submenu_free(app->submenu_app_list_nfc);
+    }
+    if (app->submenu_app_list_rfid)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListRFID);
+        submenu_free(app->submenu_app_list_rfid);
+    }
+    if (app->submenu_app_list_subghz)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListSubGHz);
+        submenu_free(app->submenu_app_list_subghz);
+    }
+    if (app->submenu_app_list_tools)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListTools);
+        submenu_free(app->submenu_app_list_tools);
+    }
+    if (app->submenu_app_list_usb)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppListUSB);
+        submenu_free(app->submenu_app_list_usb);
+    }
+
+    // Free Widget(s)
+    if (app->widget)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAbout);
+        widget_free(app->widget);
+    }
+
+    // Free Variable Item List(s)
+    if (app->variable_item_list)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewSettings);
+        variable_item_list_free(app->variable_item_list);
+    }
+
+    // Free Text Input(s)
+    if (app->uart_text_input_ssid)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewTextInputSSID);
+        uart_text_input_free(app->uart_text_input_ssid);
+    }
+    if (app->uart_text_input_pass)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewTextInputPass);
+        uart_text_input_free(app->uart_text_input_pass);
+    }
+
+    // Free popup
+    if (app->popup)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewPopup);
+        popup_free(app->popup);
+    }
+
+    // Free dialog
+    if (app->dialog_delete)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipStoreViewAppDelete);
+        dialog_ex_free(app->dialog_delete);
+    }
+
+    // Free the flip catalog
+    flip_catalog_free();
+
+    // deinitalize flipper http
+    flipper_http_deinit();
+
+    // free the view dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+
+    // close the gui
+    furi_record_close(RECORD_GUI);
+
+    // free the app
+    free(app);
+}
+
+#endif // FLIP_STORE_FREE_H

+ 207 - 0
flip_store/flip_store_i.h

@@ -0,0 +1,207 @@
+#ifndef FLIP_STORE_I_H
+#define FLIP_STORE_I_H
+
+// Function to allocate resources for the FlipStoreApp
+static FlipStoreApp *flip_store_app_alloc()
+{
+    FlipStoreApp *app = (FlipStoreApp *)malloc(sizeof(FlipStoreApp));
+
+    Gui *gui = furi_record_open(RECORD_GUI);
+
+    if (!flipper_http_init(flipper_http_rx_callback, app))
+    {
+        FURI_LOG_E(TAG, "Failed to initialize flipper http");
+        return NULL;
+    }
+
+    // Allocate the text input buffer
+    app->uart_text_input_buffer_size_ssid = 64;
+    app->uart_text_input_buffer_size_pass = 64;
+    if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_buffer(&app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_size_pass))
+    {
+        return NULL;
+    }
+
+    // Allocate ViewDispatcher
+    if (!easy_flipper_set_view_dispatcher(&app->view_dispatcher, gui, app))
+    {
+        return NULL;
+    }
+
+    // Main view
+    if (!easy_flipper_set_view(&app->view_main, FlipStoreViewMain, flip_store_view_draw_callback_main, NULL, callback_to_app_list, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_view(&app->view_app_info, FlipStoreViewAppInfo, flip_store_view_draw_callback_app_list, flip_store_input_callback, callback_to_app_list, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+
+    // Widget
+    if (!easy_flipper_set_widget(
+            &app->widget,
+            FlipStoreViewAbout,
+            "Welcome to the FlipStore!\n------\nDownload apps via WiFi and\nrun them on your Flipper!\n------\nwww.github.com/jblanked",
+            callback_to_submenu,
+            &app->view_dispatcher))
+    {
+        return NULL;
+    }
+
+    // Popup
+    if (!easy_flipper_set_popup(&app->popup, FlipStoreViewPopup, "Failed", 0, 0, "You are not connected to Wifi.\n\nIf you have the FlipperHTTP\nflash installed, then update\nyour WiFi credentials.", 0, 10, popup_callback, callback_to_submenu, &app->view_dispatcher, app))
+    {
+        FURI_LOG_E(TAG, "Failed to create popup");
+    }
+
+    // Dialog
+    if (!easy_flipper_set_dialog_ex(
+            &app->dialog_delete,
+            FlipStoreViewAppDelete,
+            "Delete App",
+            0,
+            0,
+            "Are you sure you want to delete this app?",
+            0,
+            10,
+            "No",
+            "Yes",
+            NULL,
+            dialog_callback,
+            callback_to_app_list,
+            &app->view_dispatcher,
+            app))
+    {
+        return NULL;
+    }
+
+    // Text Input
+    if (!easy_flipper_set_uart_text_input(&app->uart_text_input_ssid, FlipStoreViewTextInputSSID, "Enter SSID", app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_size_ssid, flip_store_text_updated_ssid, callback_to_submenu, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_uart_text_input(&app->uart_text_input_pass, FlipStoreViewTextInputPass, "Enter Password", app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_size_pass, flip_store_text_updated_pass, callback_to_submenu, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+
+    // Variable Item List
+    if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipStoreViewSettings, settings_item_selected, callback_to_submenu, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    app->variable_item_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL);
+    app->variable_item_pass = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL);
+
+    // Submenu
+    if (!easy_flipper_set_submenu(&app->submenu, FlipStoreViewSubmenu, "FlipStore", callback_exit_app, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list, FlipStoreViewAppList, "App Catalog", callback_to_submenu, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_bluetooth, FlipStoreViewAppListBluetooth, "Bluetooth", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_games, FlipStoreViewAppListGames, "Games", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_gpio, FlipStoreViewAppListGPIO, "GPIO", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_infrared, FlipStoreViewAppListInfrared, "Infrared", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_ibutton, FlipStoreViewAppListiButton, "iButton", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_media, FlipStoreViewAppListMedia, "Media", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_nfc, FlipStoreViewAppListNFC, "NFC", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_rfid, FlipStoreViewAppListRFID, "RFID", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_subghz, FlipStoreViewAppListSubGHz, "Sub-GHz", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_tools, FlipStoreViewAppListTools, "Tools", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    if (!easy_flipper_set_submenu(&app->submenu_app_list_usb, FlipStoreViewAppListUSB, "USB", callback_to_app_list, &app->view_dispatcher))
+    {
+        return NULL;
+    }
+    submenu_add_item(app->submenu, "Catalog", FlipStoreSubmenuIndexAppList, callback_submenu_choices, app);
+    submenu_add_item(app->submenu, "About", FlipStoreSubmenuIndexAbout, callback_submenu_choices, app);
+    submenu_add_item(app->submenu, "Settings", FlipStoreSubmenuIndexSettings, callback_submenu_choices, app);
+    //
+    submenu_add_item(app->submenu_app_list, "Bluetooth", FlipStoreSubmenuIndexAppListBluetooth, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "Games", FlipStoreSubmenuIndexAppListGames, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "GPIO", FlipStoreSubmenuIndexAppListGPIO, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "Infrared", FlipStoreSubmenuIndexAppListInfrared, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "iButton", FlipStoreSubmenuIndexAppListiButton, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "Media", FlipStoreSubmenuIndexAppListMedia, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "NFC", FlipStoreSubmenuIndexAppListNFC, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "RFID", FlipStoreSubmenuIndexAppListRFID, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "Sub-GHz", FlipStoreSubmenuIndexAppListSubGHz, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "Tools", FlipStoreSubmenuIndexAppListTools, callback_submenu_choices, app);
+    submenu_add_item(app->submenu_app_list, "USB", FlipStoreSubmenuIndexAppListUSB, callback_submenu_choices, app);
+    //
+    // dont add any items to the app list submenu of each category yet
+
+    // load settings
+    if (load_settings(app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid, app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass))
+    {
+        // Update variable items
+        if (app->variable_item_ssid)
+            variable_item_set_current_value_text(app->variable_item_ssid, app->uart_text_input_buffer_ssid);
+        // do not display the password
+
+        // Copy items into their temp buffers with safety checks
+        if (app->uart_text_input_buffer_ssid && app->uart_text_input_temp_buffer_ssid)
+        {
+            strncpy(app->uart_text_input_temp_buffer_ssid, app->uart_text_input_buffer_ssid, app->uart_text_input_buffer_size_ssid - 1);
+            app->uart_text_input_temp_buffer_ssid[app->uart_text_input_buffer_size_ssid - 1] = '\0';
+        }
+        if (app->uart_text_input_buffer_pass && app->uart_text_input_temp_buffer_pass)
+        {
+            strncpy(app->uart_text_input_temp_buffer_pass, app->uart_text_input_buffer_pass, app->uart_text_input_buffer_size_pass - 1);
+            app->uart_text_input_temp_buffer_pass[app->uart_text_input_buffer_size_pass - 1] = '\0';
+        }
+    }
+
+    // Switch to the main view
+    view_dispatcher_switch_to_view(app->view_dispatcher, FlipStoreViewSubmenu);
+
+    return app;
+}
+
+#endif // FLIP_STORE_I_H

+ 317 - 0
flip_store/flip_store_storage.h

@@ -0,0 +1,317 @@
+#ifndef FLIP_STORE_STORAGE_H
+#define FLIP_STORE_STORAGE_H
+
+#include <furi.h>
+#include <storage/storage.h>
+
+#define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store/settings.bin"
+
+static void save_settings(
+    const char *ssid,
+    const char *password)
+{
+    // Create the directory for saving settings
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_store");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    // Open the settings file
+    File *file = storage_file_alloc(storage);
+    if (!storage_file_open(file, SETTINGS_PATH, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(TAG, "Failed to open settings file for writing: %s", SETTINGS_PATH);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return;
+    }
+
+    // Save the ssid length and data
+    size_t ssid_length = strlen(ssid) + 1; // Include null terminator
+    if (storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, ssid, ssid_length) != ssid_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write SSID");
+    }
+
+    // Save the password length and data
+    size_t password_length = strlen(password) + 1; // Include null terminator
+    if (storage_file_write(file, &password_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, password, password_length) != password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write password");
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+}
+
+static bool load_settings(
+    char *ssid,
+    size_t ssid_size,
+    char *password,
+    size_t password_size)
+{
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    if (!storage_file_open(file, SETTINGS_PATH, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", SETTINGS_PATH);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false; // Return false if the file does not exist
+    }
+
+    // Load the ssid
+    size_t ssid_length;
+    if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size ||
+        storage_file_read(file, ssid, ssid_length) != ssid_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read SSID");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    ssid[ssid_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the password
+    size_t password_length;
+    if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size ||
+        storage_file_read(file, password, password_length) != password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read password");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    password[password_length - 1] = '\0'; // Ensure null-termination
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+// future implenetation because we need the app category
+bool delete_app(const char *app_id, const char *app_category)
+{
+    // Create the directory for saving settings
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps/%s/%s", app_category, app_id);
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    if (!storage_simply_remove_recursive(storage, directory_path))
+    {
+        FURI_LOG_E(TAG, "Failed to delete app: %s", app_id);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    furi_record_close(RECORD_STORAGE);
+    return true;
+}
+
+#define BUFFER_SIZE 64
+#define MAX_KEY_LENGTH 32
+#define MAX_VALUE_LENGTH 64
+
+// Function to parse JSON incrementally from a file
+bool parse_json_incrementally(const char *file_path, const char *target_key, char *value_buffer, size_t value_buffer_size)
+{
+    Storage *_storage = NULL;
+    File *_file = NULL;
+    char buffer[BUFFER_SIZE];
+    size_t bytes_read;
+    bool key_found = false;
+    bool in_string = false;
+    bool is_escaped = false;
+    bool reading_key = false;
+    bool reading_value = false;
+    char current_key[MAX_KEY_LENGTH] = {0};
+    size_t key_index = 0;
+    size_t value_index = 0;
+
+    // Open storage and file
+    _storage = furi_record_open(RECORD_STORAGE);
+    if (!_storage)
+    {
+        FURI_LOG_E("JSON_PARSE", "Failed to open storage.");
+        return false;
+    }
+
+    _file = storage_file_alloc(_storage);
+    if (!_file)
+    {
+        FURI_LOG_E("JSON_PARSE", "Failed to allocate file.");
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    if (!storage_file_open(_file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        FURI_LOG_E("JSON_PARSE", "Failed to open JSON file for reading.");
+        goto cleanup;
+    }
+
+    while ((bytes_read = storage_file_read(_file, buffer, BUFFER_SIZE)) > 0)
+    {
+        for (size_t i = 0; i < bytes_read; ++i)
+        {
+            char c = buffer[i];
+
+            if (is_escaped)
+            {
+                is_escaped = false;
+                if (reading_key)
+                {
+                    if (key_index < MAX_KEY_LENGTH - 1)
+                    {
+                        current_key[key_index++] = c;
+                    }
+                }
+                else if (reading_value)
+                {
+                    if (value_index < value_buffer_size - 1)
+                    {
+                        value_buffer[value_index++] = c;
+                    }
+                }
+                continue;
+            }
+
+            if (c == '\\')
+            {
+                is_escaped = true;
+                continue;
+            }
+
+            if (c == '\"')
+            {
+                in_string = !in_string;
+
+                if (in_string)
+                {
+                    // Start of a string
+                    if (!reading_key && !reading_value)
+                    {
+                        // Possible start of a key
+                        reading_key = true;
+                        key_index = 0;
+                        current_key[0] = '\0';
+                    }
+                }
+                else
+                {
+                    // End of a string
+                    if (reading_key)
+                    {
+                        reading_key = false;
+                        current_key[key_index] = '\0';
+
+                        if (strcmp(current_key, target_key) == 0)
+                        {
+                            key_found = true;
+                        }
+                    }
+                    else if (reading_value)
+                    {
+                        reading_value = false;
+                        value_buffer[value_index] = '\0';
+
+                        if (key_found)
+                        {
+                            // Found the target value
+                            goto success;
+                        }
+                    }
+                }
+                continue;
+            }
+
+            if (in_string)
+            {
+                if (reading_key)
+                {
+                    if (key_index < MAX_KEY_LENGTH - 1)
+                    {
+                        current_key[key_index++] = c;
+                    }
+                }
+                else if (reading_value)
+                {
+                    if (value_index < value_buffer_size - 1)
+                    {
+                        value_buffer[value_index++] = c;
+                    }
+                }
+                continue;
+            }
+
+            if (c == ':' && key_found && !reading_value)
+            {
+                // After colon, start reading the value
+                // Skip whitespace and possible opening quote
+                while (i + 1 < bytes_read && (buffer[i + 1] == ' ' || buffer[i + 1] == '\n' || buffer[i + 1] == '\r'))
+                {
+                    i++;
+                }
+
+                if (i + 1 < bytes_read && buffer[i + 1] == '\"')
+                {
+                    i++; // Move to the quote
+                    in_string = true;
+                    reading_value = true;
+                    value_index = 0;
+                }
+                else
+                {
+                    // Handle non-string values (e.g., numbers, booleans)
+                    reading_value = true;
+                    value_index = 0;
+                }
+                continue;
+            }
+
+            if (reading_value && (c == ',' || c == '}' || c == ']'))
+            {
+                // End of the value
+                reading_value = false;
+                value_buffer[value_index] = '\0';
+
+                if (key_found)
+                {
+                    // Found the target value
+                    goto success;
+                }
+                key_found = false;
+            }
+        }
+    }
+
+success:
+    storage_file_close(_file);
+    storage_file_free(_file);
+    furi_record_close(RECORD_STORAGE);
+    return key_found;
+
+cleanup:
+    if (_file)
+    {
+        storage_file_free(_file);
+    }
+    if (_storage)
+    {
+        furi_record_close(RECORD_STORAGE);
+    }
+    return false;
+}
+
+#endif

+ 1350 - 0
flip_store/flipper_http.h

@@ -0,0 +1,1350 @@
+// flipper_http.h - Flipper HTTP Library (www.github.com/jblanked)
+// Author: JBlanked
+#ifndef FLIPPER_HTTP_H
+#define FLIPPER_HTTP_H
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_serial.h>
+#include <storage/storage.h>
+#include <stdlib.h>
+
+// STORAGE_EXT_PATH_PREFIX is defined in the Furi SDK as /ext
+
+#define HTTP_TAG "FlipStore"              // change this to your app name
+#define http_tag "flip_store"             // change this to your app id
+#define UART_CH (FuriHalSerialIdUsart)    // UART channel
+#define TIMEOUT_DURATION_TICKS (6 * 1000) // 6 seconds
+#define BAUDRATE (115200)                 // UART baudrate
+#define RX_BUF_SIZE 1024                  // UART RX buffer size
+#define RX_LINE_BUFFER_SIZE 5000          // UART RX line buffer size (increase for large responses)
+
+// Forward declaration for callback
+typedef void (*FlipperHTTP_Callback)(const char *line, void *context);
+
+// Functions
+bool flipper_http_init(FlipperHTTP_Callback callback, void *context);
+void flipper_http_deinit();
+//---
+void flipper_http_rx_callback(const char *line, void *context);
+bool flipper_http_send_data(const char *data);
+//---
+bool flipper_http_connect_wifi();
+bool flipper_http_disconnect_wifi();
+bool flipper_http_ping();
+bool flipper_http_scan_wifi();
+bool flipper_http_save_wifi(const char *ssid, const char *password);
+//---
+bool flipper_http_get_request(const char *url);
+bool flipper_http_get_request_with_headers(const char *url, const char *headers);
+bool flipper_http_post_request_with_headers(const char *url, const char *headers, const char *payload);
+bool flipper_http_put_request_with_headers(const char *url, const char *headers, const char *payload);
+bool flipper_http_delete_request_with_headers(const char *url, const char *headers, const char *payload);
+//---
+bool flipper_http_get_request_bytes(const char *url, const char *headers);
+bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload);
+//---
+bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[]);
+char *trim(const char *str);
+
+// State variable to track the UART state
+typedef enum
+{
+    INACTIVE,  // Inactive state
+    IDLE,      // Default state
+    RECEIVING, // Receiving data
+    SENDING,   // Sending data
+    ISSUE,     // Issue with connection
+} SerialState;
+
+// Event Flags for UART Worker Thread
+typedef enum
+{
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+} WorkerEvtFlags;
+
+static bool is_compile_app_request = false; // personal use in flip_store_apps.h
+
+// FlipperHTTP Structure
+typedef struct
+{
+    FuriStreamBuffer *flipper_http_stream; // Stream buffer for UART communication
+    FuriHalSerialHandle *serial_handle;    // Serial handle for UART communication
+    FuriThread *rx_thread;                 // Worker thread for UART
+    // uint8_t rx_buf[RX_BUF_SIZE];            // Buffer for received data
+    FuriThreadId rx_thread_id;              // Worker thread ID
+    FlipperHTTP_Callback handle_rx_line_cb; // Callback for received lines
+    void *callback_context;                 // Context for the callback
+    SerialState state;                      // State of the UART
+
+    // variable to store the last received data from the UART
+    char *last_response;
+
+    // Timer-related members
+    FuriTimer *get_timeout_timer; // Timer for HTTP request timeout
+    char *received_data;          // Buffer to store received data
+
+    bool started_receiving_get; // Indicates if a GET request has started
+    bool just_started_get;      // Indicates if GET data reception has just started
+
+    bool started_receiving_post; // Indicates if a POST request has started
+    bool just_started_post;      // Indicates if POST data reception has just started
+
+    bool started_receiving_put; // Indicates if a PUT request has started
+    bool just_started_put;      // Indicates if PUT data reception has just started
+
+    bool started_receiving_delete; // Indicates if a DELETE request has started
+    bool just_started_delete;      // Indicates if DELETE data reception has just started
+
+    // Buffer to hold the raw bytes received from the UART
+    uint8_t *received_bytes;
+    size_t received_bytes_len; // Length of the received bytes
+
+    // File path to save the bytes received
+    char file_path[256];
+
+    bool save_data; // Flag to save the received data
+
+} FlipperHTTP;
+
+static FlipperHTTP fhttp;
+
+// Function to append received data to file
+static bool append_to_file(const char *file_path, const void *data, size_t data_size)
+{
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    // Open the file in append mode
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_OPEN_APPEND))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for appending: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write the data to the file
+    if (storage_file_write(file, data, data_size) != data_size)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+// Global static array for the line buffer
+static char rx_line_buffer[RX_LINE_BUFFER_SIZE];
+#define FILE_BUFFER_SIZE 512
+static uint8_t file_buffer[FILE_BUFFER_SIZE];
+
+// UART worker thread
+/**
+ * @brief      Worker thread to handle UART data asynchronously.
+ * @return     0
+ * @param      context   The context to pass to the callback.
+ * @note       This function will handle received data asynchronously via the callback.
+ */
+// UART worker thread
+static int32_t flipper_http_worker(void *context)
+{
+    UNUSED(context);
+    size_t rx_line_pos = 0;
+    static size_t file_buffer_len = 0;
+
+    while (1)
+    {
+        uint32_t events = furi_thread_flags_wait(WorkerEvtStop | WorkerEvtRxDone, FuriFlagWaitAny, FuriWaitForever);
+        if (events & WorkerEvtStop)
+            break;
+        if (events & WorkerEvtRxDone)
+        {
+            // Continuously read from the stream buffer until it's empty
+            while (!furi_stream_buffer_is_empty(fhttp.flipper_http_stream))
+            {
+                // Read one byte at a time
+                char c = 0;
+                size_t received = furi_stream_buffer_receive(fhttp.flipper_http_stream, &c, 1, 0);
+
+                if (received == 0)
+                {
+                    // No more data to read
+                    break;
+                }
+
+                // Append the received byte to the file if saving is enabled
+                if (fhttp.save_data)
+                {
+                    // Add byte to the buffer
+                    file_buffer[file_buffer_len++] = c;
+                    // Write to file if buffer is full
+                    if (file_buffer_len >= FILE_BUFFER_SIZE)
+                    {
+                        if (!append_to_file(fhttp.file_path, file_buffer, file_buffer_len))
+                        {
+                            FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+                        }
+                        file_buffer_len = 0;
+                    }
+                }
+
+                // Handle line buffering only if callback is set (text data)
+                if (fhttp.handle_rx_line_cb)
+                {
+                    // Handle line buffering
+                    if (c == '\n' || rx_line_pos >= RX_LINE_BUFFER_SIZE - 1)
+                    {
+                        rx_line_buffer[rx_line_pos] = '\0'; // Null-terminate the line
+
+                        // Invoke the callback with the complete line
+                        fhttp.handle_rx_line_cb(rx_line_buffer, fhttp.callback_context);
+
+                        // Reset the line buffer position
+                        rx_line_pos = 0;
+                    }
+                    else
+                    {
+                        rx_line_buffer[rx_line_pos++] = c; // Add character to the line buffer
+                    }
+                }
+            }
+        }
+    }
+
+    if (fhttp.save_data)
+    {
+        // Write the remaining data to the file
+        if (file_buffer_len > 0)
+        {
+            if (!append_to_file(fhttp.file_path, file_buffer, file_buffer_len))
+            {
+                FURI_LOG_E(HTTP_TAG, "Failed to append remaining data to file");
+            }
+        }
+    }
+
+    // remove [POST/END] and/or [GET/END] from the file
+    if (fhttp.save_data)
+    {
+        char *end = NULL;
+        if ((end = strstr(fhttp.file_path, "[POST/END]")) != NULL)
+        {
+            *end = '\0';
+        }
+        else if ((end = strstr(fhttp.file_path, "[GET/END]")) != NULL)
+        {
+            *end = '\0';
+        }
+    }
+
+    // remove newline from the from the end of the file
+    if (fhttp.save_data)
+    {
+        char *end = NULL;
+        if ((end = strstr(fhttp.file_path, "\n")) != NULL)
+        {
+            *end = '\0';
+        }
+    }
+
+    // Reset the file buffer length
+    file_buffer_len = 0;
+
+    return 0;
+}
+
+// Timer callback function
+/**
+ * @brief      Callback function for the GET timeout timer.
+ * @return     0
+ * @param      context   The context to pass to the callback.
+ * @note       This function will be called when the GET request times out.
+ */
+void get_timeout_timer_callback(void *context)
+{
+    UNUSED(context);
+    FURI_LOG_E(HTTP_TAG, "Timeout reached: 2 seconds without receiving the end.");
+
+    // Reset the state
+    fhttp.started_receiving_get = false;
+    fhttp.started_receiving_post = false;
+    fhttp.started_receiving_put = false;
+    fhttp.started_receiving_delete = false;
+
+    // Free received data if any
+    if (fhttp.received_data)
+    {
+        free(fhttp.received_data);
+        fhttp.received_data = NULL;
+    }
+
+    // Update UART state
+    fhttp.state = ISSUE;
+}
+
+// UART RX Handler Callback (Interrupt Context)
+/**
+ * @brief      A private callback function to handle received data asynchronously.
+ * @return     void
+ * @param      handle    The UART handle.
+ * @param      event     The event type.
+ * @param      context   The context to pass to the callback.
+ * @note       This function will handle received data asynchronously via the callback.
+ */
+static void _flipper_http_rx_callback(FuriHalSerialHandle *handle, FuriHalSerialRxEvent event, void *context)
+{
+    UNUSED(context);
+    if (event == FuriHalSerialRxEventData)
+    {
+        uint8_t data = furi_hal_serial_async_rx(handle);
+        furi_stream_buffer_send(fhttp.flipper_http_stream, &data, 1, 0);
+        furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtRxDone);
+    }
+}
+
+// UART initialization function
+/**
+ * @brief      Initialize UART.
+ * @return     true if the UART was initialized successfully, false otherwise.
+ * @param      callback  The callback function to handle received data (ex. flipper_http_rx_callback).
+ * @param      context   The context to pass to the callback.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_init(FlipperHTTP_Callback callback, void *context)
+{
+    if (!context)
+    {
+        FURI_LOG_E(HTTP_TAG, "Invalid context provided to flipper_http_init.");
+        return false;
+    }
+    if (!callback)
+    {
+        FURI_LOG_E(HTTP_TAG, "Invalid callback provided to flipper_http_init.");
+        return false;
+    }
+    fhttp.flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
+    if (!fhttp.flipper_http_stream)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate UART stream buffer.");
+        return false;
+    }
+
+    fhttp.rx_thread = furi_thread_alloc();
+    if (!fhttp.rx_thread)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate UART thread.");
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    furi_thread_set_name(fhttp.rx_thread, "FlipperHTTP_RxThread");
+    furi_thread_set_stack_size(fhttp.rx_thread, 1024);
+    furi_thread_set_context(fhttp.rx_thread, &fhttp);
+    furi_thread_set_callback(fhttp.rx_thread, flipper_http_worker);
+
+    fhttp.handle_rx_line_cb = callback;
+    fhttp.callback_context = context;
+
+    furi_thread_start(fhttp.rx_thread);
+    fhttp.rx_thread_id = furi_thread_get_id(fhttp.rx_thread);
+
+    // handle when the UART control is busy to avoid furi_check failed
+    if (furi_hal_serial_control_is_busy(UART_CH))
+    {
+        FURI_LOG_E(HTTP_TAG, "UART control is busy.");
+        return false;
+    }
+
+    fhttp.serial_handle = furi_hal_serial_control_acquire(UART_CH);
+    if (!fhttp.serial_handle)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL");
+        // Cleanup resources
+        furi_thread_free(fhttp.rx_thread);
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    // Initialize UART with acquired handle
+    furi_hal_serial_init(fhttp.serial_handle, BAUDRATE);
+
+    // Enable RX direction
+    furi_hal_serial_enable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+
+    // Start asynchronous RX with the callback
+    furi_hal_serial_async_rx_start(fhttp.serial_handle, _flipper_http_rx_callback, &fhttp, false);
+
+    // Wait for the TX to complete to ensure UART is ready
+    furi_hal_serial_tx_wait_complete(fhttp.serial_handle);
+
+    // Allocate the timer for handling timeouts
+    fhttp.get_timeout_timer = furi_timer_alloc(
+        get_timeout_timer_callback, // Callback function
+        FuriTimerTypeOnce,          // One-shot timer
+        &fhttp                      // Context passed to callback
+    );
+
+    if (!fhttp.get_timeout_timer)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer.");
+        // Cleanup resources
+        furi_hal_serial_async_rx_stop(fhttp.serial_handle);
+        furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+        furi_hal_serial_control_release(fhttp.serial_handle);
+        furi_hal_serial_deinit(fhttp.serial_handle);
+        furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop);
+        furi_thread_join(fhttp.rx_thread);
+        furi_thread_free(fhttp.rx_thread);
+        furi_stream_buffer_free(fhttp.flipper_http_stream);
+        return false;
+    }
+
+    // Set the timer thread priority if needed
+    furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+
+    fhttp.file_path[0] = '\0'; // Null-terminate the file path
+    fhttp.received_data = NULL;
+    fhttp.received_bytes = NULL;
+    fhttp.received_bytes_len = 0;
+    return true;
+}
+
+// Deinitialize UART
+/**
+ * @brief      Deinitialize UART.
+ * @return     void
+ * @note       This function will stop the asynchronous RX, release the serial handle, and free the resources.
+ */
+void flipper_http_deinit()
+{
+    if (fhttp.serial_handle == NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?");
+        return;
+    }
+    // Stop asynchronous RX
+    furi_hal_serial_async_rx_stop(fhttp.serial_handle);
+
+    // Release and deinitialize the serial handle
+    furi_hal_serial_disable_direction(fhttp.serial_handle, FuriHalSerialDirectionRx);
+    furi_hal_serial_control_release(fhttp.serial_handle);
+    furi_hal_serial_deinit(fhttp.serial_handle);
+
+    // Signal the worker thread to stop
+    furi_thread_flags_set(fhttp.rx_thread_id, WorkerEvtStop);
+    // Wait for the thread to finish
+    furi_thread_join(fhttp.rx_thread);
+    // Free the thread resources
+    furi_thread_free(fhttp.rx_thread);
+
+    // Free the stream buffer
+    furi_stream_buffer_free(fhttp.flipper_http_stream);
+
+    // Free the timer
+    if (fhttp.get_timeout_timer)
+    {
+        furi_timer_free(fhttp.get_timeout_timer);
+        fhttp.get_timeout_timer = NULL;
+    }
+
+    // Free received data if any
+    if (fhttp.received_data)
+    {
+        free(fhttp.received_data);
+        fhttp.received_data = NULL;
+    }
+
+    // Free the last response
+    if (fhttp.last_response)
+    {
+        free(fhttp.last_response);
+        fhttp.last_response = NULL;
+    }
+
+    // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully.");
+}
+
+// Function to send data over UART with newline termination
+/**
+ * @brief      Send data over UART with newline termination.
+ * @return     true if the data was sent successfully, false otherwise.
+ * @param      data  The data to send over UART.
+ * @note       The data will be sent over UART with a newline character appended.
+ */
+bool flipper_http_send_data(const char *data)
+{
+    size_t data_length = strlen(data);
+    if (data_length == 0)
+    {
+        FURI_LOG_E("FlipperHTTP", "Attempted to send empty data.");
+        return false;
+    }
+
+    // Create a buffer with data + '\n'
+    size_t send_length = data_length + 1; // +1 for '\n'
+    if (send_length > 256)
+    { // Ensure buffer size is sufficient
+        FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP.");
+        return false;
+    }
+
+    char send_buffer[257]; // 256 + 1 for safety
+    strncpy(send_buffer, data, 256);
+    send_buffer[data_length] = '\n';     // Append newline
+    send_buffer[data_length + 1] = '\0'; // Null-terminate
+
+    if (fhttp.state == INACTIVE && ((strstr(send_buffer, "[PING]") == NULL) && (strstr(send_buffer, "[WIFI/CONNECT]") == NULL)))
+    {
+        FURI_LOG_E("FlipperHTTP", "Cannot send data while INACTIVE.");
+        fhttp.last_response = "Cannot send data while INACTIVE.";
+        return false;
+    }
+
+    fhttp.state = SENDING;
+    furi_hal_serial_tx(fhttp.serial_handle, (const uint8_t *)send_buffer, send_length);
+
+    // Uncomment below line to log the data sent over UART
+    // FURI_LOG_I("FlipperHTTP", "Sent data over UART: %s", send_buffer);
+    fhttp.state = IDLE;
+    return true;
+}
+
+// Function to send a PING request
+/**
+ * @brief      Send a PING request to check the connection status.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ * @note       This is best used to check if the Wifi Dev Board is connected.
+ * @note       The state will remain INACTIVE until a PONG is received.
+ */
+bool flipper_http_ping()
+{
+    const char *command = "[PING]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send PING command.");
+        return false;
+    }
+    // set state as INACTIVE to be made IDLE if PONG is received
+    fhttp.state = INACTIVE;
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to scan for WiFi networks
+/**
+ * @brief      Send a command to scan for WiFi networks.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_scan_wifi()
+{
+    const char *command = "[WIFI/SCAN]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi scan command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to save WiFi settings (returns true if successful)
+/**
+ * @brief      Send a command to save WiFi settings.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_save_wifi(const char *ssid, const char *password)
+{
+    if (!ssid || !password)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_save_wifi.");
+        return false;
+    }
+    char buffer[256];
+    int ret = snprintf(buffer, sizeof(buffer), "[WIFI/SAVE]{\"ssid\":\"%s\",\"password\":\"%s\"}", ssid, password);
+    if (ret < 0 || ret >= (int)sizeof(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format WiFi save command.");
+        return false;
+    }
+
+    if (!flipper_http_send_data(buffer))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi save command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to disconnect from WiFi (returns true if successful)
+/**
+ * @brief      Send a command to disconnect from WiFi.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_disconnect_wifi()
+{
+    const char *command = "[WIFI/DISCONNECT]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi disconnect command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to connect to WiFi (returns true if successful)
+/**
+ * @brief      Send a command to connect to WiFi.
+ * @return     true if the request was successful, false otherwise.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_connect_wifi()
+{
+    const char *command = "[WIFI/CONNECT]";
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send WiFi connect command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+
+// Function to send a GET request
+/**
+ * @brief      Send a GET request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the GET request to.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_get_request(const char *url)
+{
+    if (!url)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request.");
+        return false;
+    }
+
+    // Prepare GET request command
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[GET]%s", url);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format GET request command.");
+        return false;
+    }
+
+    // Send GET request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send GET request command.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a GET request with headers
+/**
+ * @brief      Send a GET request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the GET request to.
+ * @param      headers  The headers to send with the GET request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_get_request_with_headers(const char *url, const char *headers)
+{
+    if (!url || !headers)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_with_headers.");
+        return false;
+    }
+
+    // Prepare GET request command with headers
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[GET/HTTP]{\"url\":\"%s\",\"headers\":%s}", url, headers);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers.");
+        return false;
+    }
+
+    // Send GET request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a GET request with headers and return bytes
+/**
+ * @brief      Send a GET request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the GET request to.
+ * @param      headers  The headers to send with the GET request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_get_request_bytes(const char *url, const char *headers)
+{
+    if (!url || !headers)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_get_request_bytes.");
+        return false;
+    }
+
+    // Prepare GET request command with headers
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[GET/BYTES]{\"url\":\"%s\",\"headers\":%s}", url, headers);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format GET request command with headers.");
+        return false;
+    }
+
+    // Send GET request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send GET request command with headers.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a POST request with headers
+/**
+ * @brief      Send a POST request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the POST request to.
+ * @param      headers  The headers to send with the POST request.
+ * @param      payload  The data to send with the POST request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_post_request_with_headers(const char *url, const char *headers, const char *payload)
+{
+    if (!url || !headers || !payload)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_post_request_with_headers.");
+        return false;
+    }
+
+    // Prepare POST request command with headers and data
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[POST/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", url, headers, payload);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data.");
+        return false;
+    }
+
+    // Send POST request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a POST request with headers and return bytes
+/**
+ * @brief      Send a POST request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the POST request to.
+ * @param      headers  The headers to send with the POST request.
+ * @param      payload  The data to send with the POST request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_post_request_bytes(const char *url, const char *headers, const char *payload)
+{
+    if (!url || !headers || !payload)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_post_request_bytes.");
+        return false;
+    }
+
+    // Prepare POST request command with headers and data
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[POST/BYTES]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", url, headers, payload);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format POST request command with headers and data.");
+        return false;
+    }
+
+    // Send POST request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send POST request command with headers and data.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a PUT request with headers
+/**
+ * @brief      Send a PUT request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the PUT request to.
+ * @param      headers  The headers to send with the PUT request.
+ * @param      payload  The data to send with the PUT request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_put_request_with_headers(const char *url, const char *headers, const char *payload)
+{
+    if (!url || !headers || !payload)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_put_request_with_headers.");
+        return false;
+    }
+
+    // Prepare PUT request command with headers and data
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[PUT/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", url, headers, payload);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format PUT request command with headers and data.");
+        return false;
+    }
+
+    // Send PUT request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send PUT request command with headers and data.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to send a DELETE request with headers
+/**
+ * @brief      Send a DELETE request to the specified URL.
+ * @return     true if the request was successful, false otherwise.
+ * @param      url  The URL to send the DELETE request to.
+ * @param      headers  The headers to send with the DELETE request.
+ * @param      payload  The data to send with the DELETE request.
+ * @note       The received data will be handled asynchronously via the callback.
+ */
+bool flipper_http_delete_request_with_headers(const char *url, const char *headers, const char *payload)
+{
+    if (!url || !headers || !payload)
+    {
+        FURI_LOG_E("FlipperHTTP", "Invalid arguments provided to flipper_http_delete_request_with_headers.");
+        return false;
+    }
+
+    // Prepare DELETE request command with headers and data
+    char command[256];
+    int ret = snprintf(command, sizeof(command), "[DELETE/HTTP]{\"url\":\"%s\",\"headers\":%s,\"payload\":%s}", url, headers, payload);
+    if (ret < 0 || ret >= (int)sizeof(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to format DELETE request command with headers and data.");
+        return false;
+    }
+
+    // Send DELETE request via UART
+    if (!flipper_http_send_data(command))
+    {
+        FURI_LOG_E("FlipperHTTP", "Failed to send DELETE request command with headers and data.");
+        return false;
+    }
+
+    // The response will be handled asynchronously via the callback
+    return true;
+}
+// Function to handle received data asynchronously
+/**
+ * @brief      Callback function to handle received data asynchronously.
+ * @return     void
+ * @param      line     The received line.
+ * @param      context  The context passed to the callback.
+ * @note       The received data will be handled asynchronously via the callback and handles the state of the UART.
+ */
+void flipper_http_rx_callback(const char *line, void *context)
+{
+
+    if (!line || !context)
+    {
+        FURI_LOG_E(HTTP_TAG, "Invalid arguments provided to flipper_http_rx_callback.");
+        return;
+    }
+
+    // Trim the received line to check if it's empty
+    char *trimmed_line = trim(line);
+    if (trimmed_line != NULL && trimmed_line[0] != '\0')
+    {
+        fhttp.last_response = (char *)line;
+    }
+    free(trimmed_line); // Free the allocated memory for trimmed_line
+
+    if (fhttp.state != INACTIVE && fhttp.state != ISSUE)
+    {
+        fhttp.state = RECEIVING;
+    }
+
+    // Uncomment below line to log the data received over UART
+    FURI_LOG_I(HTTP_TAG, "Received UART line: %s", line);
+
+    // Check if we've started receiving data from a GET request
+    if (fhttp.started_receiving_get)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[GET/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "GET request completed.");
+            // Stop the timer since we've completed the GET request
+            furi_timer_stop(fhttp.get_timeout_timer);
+
+            if (fhttp.received_data)
+            {
+                // uncomment if you want to save the received data to the external storage
+                flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data);
+                fhttp.started_receiving_get = false;
+                fhttp.just_started_get = false;
+                fhttp.state = IDLE;
+                return;
+            }
+            else
+            {
+                FURI_LOG_E(HTTP_TAG, "No data received.");
+                fhttp.started_receiving_get = false;
+                fhttp.just_started_get = false;
+                fhttp.state = IDLE;
+                return;
+            }
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.received_data == NULL)
+        {
+            fhttp.received_data = (char *)malloc(strlen(line) + 2); // +2 for newline and null terminator
+            if (fhttp.received_data)
+            {
+                strcpy(fhttp.received_data, line);
+                fhttp.received_data[strlen(line)] = '\n';     // Add newline
+                fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator
+            }
+        }
+        else
+        {
+            size_t current_len = strlen(fhttp.received_data);
+            size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator
+            fhttp.received_data = (char *)realloc(fhttp.received_data, new_size);
+            if (fhttp.received_data)
+            {
+                memcpy(fhttp.received_data + current_len, line, strlen(line)); // Copy line at the end of the current data
+                fhttp.received_data[current_len + strlen(line)] = '\n';        // Add newline
+                fhttp.received_data[current_len + strlen(line) + 1] = '\0';    // Null terminator
+            }
+        }
+
+        if (!fhttp.just_started_get)
+        {
+            fhttp.just_started_get = true;
+        }
+        return;
+    }
+
+    // Check if we've started receiving data from a POST request
+    else if (fhttp.started_receiving_post)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[POST/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "POST request completed.");
+            fhttp.save_data = false;
+            // Stop the timer since we've completed the POST request
+            furi_timer_stop(fhttp.get_timeout_timer);
+
+            if (fhttp.received_data)
+            {
+                // uncomment if you want to save the received data to the external storage
+                // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data);
+                fhttp.started_receiving_post = false;
+                fhttp.just_started_post = false;
+                fhttp.state = IDLE;
+                return;
+            }
+            else
+            {
+                FURI_LOG_E(HTTP_TAG, "No data received.");
+                fhttp.started_receiving_post = false;
+                fhttp.just_started_post = false;
+                fhttp.state = IDLE;
+                return;
+            }
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.received_data == NULL)
+        {
+            fhttp.received_data = (char *)malloc(strlen(line) + 2); // +2 for newline and null terminator
+            if (fhttp.received_data)
+            {
+                strcpy(fhttp.received_data, line);
+                fhttp.received_data[strlen(line)] = '\n';     // Add newline
+                fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator
+            }
+        }
+        else
+        {
+            size_t current_len = strlen(fhttp.received_data);
+            size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator
+            fhttp.received_data = (char *)realloc(fhttp.received_data, new_size);
+            if (fhttp.received_data)
+            {
+                memcpy(fhttp.received_data + current_len, line, strlen(line)); // Copy line at the end of the current data
+                fhttp.received_data[current_len + strlen(line)] = '\n';        // Add newline
+                fhttp.received_data[current_len + strlen(line) + 1] = '\0';    // Null terminator
+            }
+        }
+
+        if (!fhttp.just_started_post)
+        {
+            fhttp.just_started_post = true;
+        }
+        return;
+    }
+
+    // Check if we've started receiving data from a PUT request
+    else if (fhttp.started_receiving_put)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[PUT/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "PUT request completed.");
+            // Stop the timer since we've completed the PUT request
+            furi_timer_stop(fhttp.get_timeout_timer);
+
+            if (fhttp.received_data)
+            {
+                // uncomment if you want to save the received data to the external storage
+                // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data);
+                fhttp.started_receiving_put = false;
+                fhttp.just_started_put = false;
+                fhttp.state = IDLE;
+                return;
+            }
+            else
+            {
+                FURI_LOG_E(HTTP_TAG, "No data received.");
+                fhttp.started_receiving_put = false;
+                fhttp.just_started_put = false;
+                fhttp.state = IDLE;
+                return;
+            }
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.received_data == NULL)
+        {
+            fhttp.received_data = (char *)malloc(strlen(line) + 2); // +2 for newline and null terminator
+            if (fhttp.received_data)
+            {
+                strcpy(fhttp.received_data, line);
+                fhttp.received_data[strlen(line)] = '\n';     // Add newline
+                fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator
+            }
+        }
+        else
+        {
+            size_t current_len = strlen(fhttp.received_data);
+            size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator
+            fhttp.received_data = (char *)realloc(fhttp.received_data, new_size);
+            if (fhttp.received_data)
+            {
+                memcpy(fhttp.received_data + current_len, line, strlen(line)); // Copy line at the end of the current data
+                fhttp.received_data[current_len + strlen(line)] = '\n';        // Add newline
+                fhttp.received_data[current_len + strlen(line) + 1] = '\0';    // Null terminator
+            }
+        }
+
+        if (!fhttp.just_started_put)
+        {
+            fhttp.just_started_put = true;
+        }
+        return;
+    }
+
+    // Check if we've started receiving data from a DELETE request
+    else if (fhttp.started_receiving_delete)
+    {
+        // Restart the timeout timer each time new data is received
+        furi_timer_restart(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+
+        if (strstr(line, "[DELETE/END]") != NULL)
+        {
+            FURI_LOG_I(HTTP_TAG, "DELETE request completed.");
+            // Stop the timer since we've completed the DELETE request
+            furi_timer_stop(fhttp.get_timeout_timer);
+
+            if (fhttp.received_data)
+            {
+                // uncomment if you want to save the received data to the external storage
+                // flipper_http_save_received_data(strlen(fhttp.received_data), fhttp.received_data);
+                fhttp.started_receiving_delete = false;
+                fhttp.just_started_delete = false;
+                fhttp.state = IDLE;
+                return;
+            }
+            else
+            {
+                FURI_LOG_E(HTTP_TAG, "No data received.");
+                fhttp.started_receiving_delete = false;
+                fhttp.just_started_delete = false;
+                fhttp.state = IDLE;
+                return;
+            }
+        }
+
+        // Append the new line to the existing data
+        if (fhttp.received_data == NULL)
+        {
+            fhttp.received_data = (char *)malloc(strlen(line) + 2); // +2 for newline and null terminator
+            if (fhttp.received_data)
+            {
+                strcpy(fhttp.received_data, line);
+                fhttp.received_data[strlen(line)] = '\n';     // Add newline
+                fhttp.received_data[strlen(line) + 1] = '\0'; // Null terminator
+            }
+        }
+        else
+        {
+            size_t current_len = strlen(fhttp.received_data);
+            size_t new_size = current_len + strlen(line) + 2; // +2 for newline and null terminator
+            fhttp.received_data = (char *)realloc(fhttp.received_data, new_size);
+            if (fhttp.received_data)
+            {
+                memcpy(fhttp.received_data + current_len, line, strlen(line)); // Copy line at the end of the current data
+                fhttp.received_data[current_len + strlen(line)] = '\n';        // Add newline
+                fhttp.received_data[current_len + strlen(line) + 1] = '\0';    // Null terminator
+            }
+        }
+
+        if (!fhttp.just_started_delete)
+        {
+            fhttp.just_started_delete = true;
+        }
+        return;
+    }
+
+    // Handle different types of responses
+    if (strstr(line, "[SUCCESS]") != NULL || strstr(line, "[CONNECTED]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Operation succeeded.");
+    }
+    else if (strstr(line, "[INFO]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Received info: %s", line);
+
+        if (fhttp.state == INACTIVE && strstr(line, "[INFO] Already connected to Wifi.") != NULL)
+        {
+            fhttp.state = IDLE;
+        }
+    }
+    else if (strstr(line, "[GET/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "GET request succeeded.");
+        fhttp.started_receiving_get = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+        fhttp.received_data = NULL;
+        if (is_compile_app_request)
+        {
+            fhttp.save_data = true;
+        }
+        return;
+    }
+    else if (strstr(line, "[POST/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "POST request succeeded.");
+        fhttp.started_receiving_post = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+        fhttp.received_data = NULL;
+        if (is_compile_app_request)
+        {
+            fhttp.save_data = true;
+        }
+        return;
+    }
+    else if (strstr(line, "[PUT/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "PUT request succeeded.");
+        fhttp.started_receiving_put = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+        fhttp.received_data = NULL;
+        return;
+    }
+    else if (strstr(line, "[DELETE/SUCCESS]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "DELETE request succeeded.");
+        fhttp.started_receiving_delete = true;
+        furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+        fhttp.state = RECEIVING;
+        fhttp.received_data = NULL;
+        return;
+    }
+    else if (strstr(line, "[DISCONNECTED]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "WiFi disconnected successfully.");
+    }
+    else if (strstr(line, "[ERROR]") != NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "Received error: %s", line);
+        fhttp.state = ISSUE;
+        return;
+    }
+    else if (strstr(line, "[PONG]") != NULL)
+    {
+        FURI_LOG_I(HTTP_TAG, "Received PONG response: Wifi Dev Board is still alive.");
+
+        // send command to connect to WiFi
+        if (fhttp.state == INACTIVE)
+        {
+            fhttp.state = IDLE;
+            return;
+        }
+    }
+
+    if (fhttp.state == INACTIVE && strstr(line, "[PONG]") != NULL)
+    {
+        fhttp.state = IDLE;
+    }
+    else if (fhttp.state == INACTIVE && strstr(line, "[PONG]") == NULL)
+    {
+        fhttp.state = INACTIVE;
+    }
+    else
+    {
+        fhttp.state = IDLE;
+    }
+}
+// Function to save received data to a file
+/**
+ * @brief      Save the received data to a file.
+ * @return     true if the data was saved successfully, false otherwise.
+ * @param      bytes_received  The number of bytes received.
+ * @param      line_buffer     The buffer containing the received data.
+ * @note       The data will be saved to a file in the STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt" directory.
+ */
+bool flipper_http_save_received_data(size_t bytes_received, const char line_buffer[])
+{
+    const char *output_file_path = STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag "/received_data.txt";
+
+    // Ensure the directory exists
+    char directory_path[128];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/" http_tag);
+
+    Storage *_storage = NULL;
+    File *_file = NULL;
+    // Open the storage if not opened already
+    // Initialize storage and create the directory if it doesn't exist
+    _storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(_storage, directory_path); // Create directory if it doesn't exist
+    _file = storage_file_alloc(_storage);
+
+    // Open file for writing and append data line by line
+    if (!storage_file_open(_file, output_file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open output file for writing.");
+        storage_file_free(_file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write each line received from the UART to the file
+    if (bytes_received > 0 && _file)
+    {
+        storage_file_write(_file, line_buffer, bytes_received);
+        storage_file_write(_file, "\n", 1); // Add a newline after each line
+    }
+    else
+    {
+        FURI_LOG_E(HTTP_TAG, "No data received.");
+        return false;
+    }
+
+    if (_file)
+    {
+        storage_file_close(_file);
+        storage_file_free(_file);
+        _file = NULL;
+    }
+    if (_storage)
+    {
+        furi_record_close(RECORD_STORAGE);
+        _storage = NULL;
+    }
+
+    return true;
+}
+// Function to trim leading and trailing spaces and newlines from a constant string
+char *trim(const char *str)
+{
+    const char *end;
+    char *trimmed_str;
+    size_t len;
+
+    // Trim leading space
+    while (isspace((unsigned char)*str))
+        str++;
+
+    // All spaces?
+    if (*str == 0)
+        return strdup(""); // Return an empty string if all spaces
+
+    // Trim trailing space
+    end = str + strlen(str) - 1;
+    while (end > str && isspace((unsigned char)*end))
+        end--;
+
+    // Set length for the trimmed string
+    len = end - str + 1;
+
+    // Allocate space for the trimmed string and null terminator
+    trimmed_str = (char *)malloc(len + 1);
+    if (trimmed_str == NULL)
+    {
+        return NULL; // Handle memory allocation failure
+    }
+
+    // Copy the trimmed part of the string into trimmed_str
+    strncpy(trimmed_str, str, len);
+    trimmed_str[len] = '\0'; // Null terminate the string
+
+    return trimmed_str;
+}
+
+#endif // FLIPPER_HTTP_H

+ 863 - 0
flip_store/jsmn.h

@@ -0,0 +1,863 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010 Serge Zaitsev
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+#ifndef JSMN_H
+#define JSMN_H
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#ifdef JSMN_STATIC
+#define JSMN_API static
+#else
+#define JSMN_API extern
+#endif
+
+  /**
+   * JSON type identifier. Basic types are:
+   * 	o Object
+   * 	o Array
+   * 	o String
+   * 	o Other primitive: number, boolean (true/false) or null
+   */
+  typedef enum
+  {
+    JSMN_UNDEFINED = 0,
+    JSMN_OBJECT = 1 << 0,
+    JSMN_ARRAY = 1 << 1,
+    JSMN_STRING = 1 << 2,
+    JSMN_PRIMITIVE = 1 << 3
+  } jsmntype_t;
+
+  enum jsmnerr
+  {
+    /* Not enough tokens were provided */
+    JSMN_ERROR_NOMEM = -1,
+    /* Invalid character inside JSON string */
+    JSMN_ERROR_INVAL = -2,
+    /* The string is not a full JSON packet, more bytes expected */
+    JSMN_ERROR_PART = -3
+  };
+
+  /**
+   * JSON token description.
+   * type		type (object, array, string etc.)
+   * start	start position in JSON data string
+   * end		end position in JSON data string
+   */
+  typedef struct jsmntok
+  {
+    jsmntype_t type;
+    int start;
+    int end;
+    int size;
+#ifdef JSMN_PARENT_LINKS
+    int parent;
+#endif
+  } jsmntok_t;
+
+  /**
+   * JSON parser. Contains an array of token blocks available. Also stores
+   * the string being parsed now and current position in that string.
+   */
+  typedef struct jsmn_parser
+  {
+    unsigned int pos;     /* offset in the JSON string */
+    unsigned int toknext; /* next token to allocate */
+    int toksuper;         /* superior token node, e.g. parent object or array */
+  } jsmn_parser;
+
+  /**
+   * Create JSON parser over an array of tokens
+   */
+  JSMN_API void jsmn_init(jsmn_parser *parser);
+
+  /**
+   * Run JSON parser. It parses a JSON data string into and array of tokens, each
+   * describing
+   * a single JSON object.
+   */
+  JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
+                          jsmntok_t *tokens, const unsigned int num_tokens);
+
+#ifndef JSMN_HEADER
+  /**
+   * Allocates a fresh unused token from the token pool.
+   */
+  static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens,
+                                     const size_t num_tokens)
+  {
+    jsmntok_t *tok;
+    if (parser->toknext >= num_tokens)
+    {
+      return NULL;
+    }
+    tok = &tokens[parser->toknext++];
+    tok->start = tok->end = -1;
+    tok->size = 0;
+#ifdef JSMN_PARENT_LINKS
+    tok->parent = -1;
+#endif
+    return tok;
+  }
+
+  /**
+   * Fills token type and boundaries.
+   */
+  static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type,
+                              const int start, const int end)
+  {
+    token->type = type;
+    token->start = start;
+    token->end = end;
+    token->size = 0;
+  }
+
+  /**
+   * Fills next available token with JSON primitive.
+   */
+  static int jsmn_parse_primitive(jsmn_parser *parser, const char *js,
+                                  const size_t len, jsmntok_t *tokens,
+                                  const size_t num_tokens)
+  {
+    jsmntok_t *token;
+    int start;
+
+    start = parser->pos;
+
+    for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++)
+    {
+      switch (js[parser->pos])
+      {
+#ifndef JSMN_STRICT
+      /* In strict mode primitive must be followed by "," or "}" or "]" */
+      case ':':
+#endif
+      case '\t':
+      case '\r':
+      case '\n':
+      case ' ':
+      case ',':
+      case ']':
+      case '}':
+        goto found;
+      default:
+        /* to quiet a warning from gcc*/
+        break;
+      }
+      if (js[parser->pos] < 32 || js[parser->pos] >= 127)
+      {
+        parser->pos = start;
+        return JSMN_ERROR_INVAL;
+      }
+    }
+#ifdef JSMN_STRICT
+    /* In strict mode primitive must be followed by a comma/object/array */
+    parser->pos = start;
+    return JSMN_ERROR_PART;
+#endif
+
+  found:
+    if (tokens == NULL)
+    {
+      parser->pos--;
+      return 0;
+    }
+    token = jsmn_alloc_token(parser, tokens, num_tokens);
+    if (token == NULL)
+    {
+      parser->pos = start;
+      return JSMN_ERROR_NOMEM;
+    }
+    jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+    token->parent = parser->toksuper;
+#endif
+    parser->pos--;
+    return 0;
+  }
+
+  /**
+   * Fills next token with JSON string.
+   */
+  static int jsmn_parse_string(jsmn_parser *parser, const char *js,
+                               const size_t len, jsmntok_t *tokens,
+                               const size_t num_tokens)
+  {
+    jsmntok_t *token;
+
+    int start = parser->pos;
+
+    /* Skip starting quote */
+    parser->pos++;
+
+    for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++)
+    {
+      char c = js[parser->pos];
+
+      /* Quote: end of string */
+      if (c == '\"')
+      {
+        if (tokens == NULL)
+        {
+          return 0;
+        }
+        token = jsmn_alloc_token(parser, tokens, num_tokens);
+        if (token == NULL)
+        {
+          parser->pos = start;
+          return JSMN_ERROR_NOMEM;
+        }
+        jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+        token->parent = parser->toksuper;
+#endif
+        return 0;
+      }
+
+      /* Backslash: Quoted symbol expected */
+      if (c == '\\' && parser->pos + 1 < len)
+      {
+        int i;
+        parser->pos++;
+        switch (js[parser->pos])
+        {
+        /* Allowed escaped symbols */
+        case '\"':
+        case '/':
+        case '\\':
+        case 'b':
+        case 'f':
+        case 'r':
+        case 'n':
+        case 't':
+          break;
+        /* Allows escaped symbol \uXXXX */
+        case 'u':
+          parser->pos++;
+          for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0';
+               i++)
+          {
+            /* If it isn't a hex character we have an error */
+            if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */
+                  (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */
+                  (js[parser->pos] >= 97 && js[parser->pos] <= 102)))
+            { /* a-f */
+              parser->pos = start;
+              return JSMN_ERROR_INVAL;
+            }
+            parser->pos++;
+          }
+          parser->pos--;
+          break;
+        /* Unexpected symbol */
+        default:
+          parser->pos = start;
+          return JSMN_ERROR_INVAL;
+        }
+      }
+    }
+    parser->pos = start;
+    return JSMN_ERROR_PART;
+  }
+
+  /**
+   * Parse JSON string and fill tokens.
+   */
+  JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len,
+                          jsmntok_t *tokens, const unsigned int num_tokens)
+  {
+    int r;
+    int i;
+    jsmntok_t *token;
+    int count = parser->toknext;
+
+    for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++)
+    {
+      char c;
+      jsmntype_t type;
+
+      c = js[parser->pos];
+      switch (c)
+      {
+      case '{':
+      case '[':
+        count++;
+        if (tokens == NULL)
+        {
+          break;
+        }
+        token = jsmn_alloc_token(parser, tokens, num_tokens);
+        if (token == NULL)
+        {
+          return JSMN_ERROR_NOMEM;
+        }
+        if (parser->toksuper != -1)
+        {
+          jsmntok_t *t = &tokens[parser->toksuper];
+#ifdef JSMN_STRICT
+          /* In strict mode an object or array can't become a key */
+          if (t->type == JSMN_OBJECT)
+          {
+            return JSMN_ERROR_INVAL;
+          }
+#endif
+          t->size++;
+#ifdef JSMN_PARENT_LINKS
+          token->parent = parser->toksuper;
+#endif
+        }
+        token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
+        token->start = parser->pos;
+        parser->toksuper = parser->toknext - 1;
+        break;
+      case '}':
+      case ']':
+        if (tokens == NULL)
+        {
+          break;
+        }
+        type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
+#ifdef JSMN_PARENT_LINKS
+        if (parser->toknext < 1)
+        {
+          return JSMN_ERROR_INVAL;
+        }
+        token = &tokens[parser->toknext - 1];
+        for (;;)
+        {
+          if (token->start != -1 && token->end == -1)
+          {
+            if (token->type != type)
+            {
+              return JSMN_ERROR_INVAL;
+            }
+            token->end = parser->pos + 1;
+            parser->toksuper = token->parent;
+            break;
+          }
+          if (token->parent == -1)
+          {
+            if (token->type != type || parser->toksuper == -1)
+            {
+              return JSMN_ERROR_INVAL;
+            }
+            break;
+          }
+          token = &tokens[token->parent];
+        }
+#else
+        for (i = parser->toknext - 1; i >= 0; i--)
+        {
+          token = &tokens[i];
+          if (token->start != -1 && token->end == -1)
+          {
+            if (token->type != type)
+            {
+              return JSMN_ERROR_INVAL;
+            }
+            parser->toksuper = -1;
+            token->end = parser->pos + 1;
+            break;
+          }
+        }
+        /* Error if unmatched closing bracket */
+        if (i == -1)
+        {
+          return JSMN_ERROR_INVAL;
+        }
+        for (; i >= 0; i--)
+        {
+          token = &tokens[i];
+          if (token->start != -1 && token->end == -1)
+          {
+            parser->toksuper = i;
+            break;
+          }
+        }
+#endif
+        break;
+      case '\"':
+        r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
+        if (r < 0)
+        {
+          return r;
+        }
+        count++;
+        if (parser->toksuper != -1 && tokens != NULL)
+        {
+          tokens[parser->toksuper].size++;
+        }
+        break;
+      case '\t':
+      case '\r':
+      case '\n':
+      case ' ':
+        break;
+      case ':':
+        parser->toksuper = parser->toknext - 1;
+        break;
+      case ',':
+        if (tokens != NULL && parser->toksuper != -1 &&
+            tokens[parser->toksuper].type != JSMN_ARRAY &&
+            tokens[parser->toksuper].type != JSMN_OBJECT)
+        {
+#ifdef JSMN_PARENT_LINKS
+          parser->toksuper = tokens[parser->toksuper].parent;
+#else
+          for (i = parser->toknext - 1; i >= 0; i--)
+          {
+            if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT)
+            {
+              if (tokens[i].start != -1 && tokens[i].end == -1)
+              {
+                parser->toksuper = i;
+                break;
+              }
+            }
+          }
+#endif
+        }
+        break;
+#ifdef JSMN_STRICT
+      /* In strict mode primitives are: numbers and booleans */
+      case '-':
+      case '0':
+      case '1':
+      case '2':
+      case '3':
+      case '4':
+      case '5':
+      case '6':
+      case '7':
+      case '8':
+      case '9':
+      case 't':
+      case 'f':
+      case 'n':
+        /* And they must not be keys of the object */
+        if (tokens != NULL && parser->toksuper != -1)
+        {
+          const jsmntok_t *t = &tokens[parser->toksuper];
+          if (t->type == JSMN_OBJECT ||
+              (t->type == JSMN_STRING && t->size != 0))
+          {
+            return JSMN_ERROR_INVAL;
+          }
+        }
+#else
+      /* In non-strict mode every unquoted value is a primitive */
+      default:
+#endif
+        r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
+        if (r < 0)
+        {
+          return r;
+        }
+        count++;
+        if (parser->toksuper != -1 && tokens != NULL)
+        {
+          tokens[parser->toksuper].size++;
+        }
+        break;
+
+#ifdef JSMN_STRICT
+      /* Unexpected char in strict mode */
+      default:
+        return JSMN_ERROR_INVAL;
+#endif
+      }
+    }
+
+    if (tokens != NULL)
+    {
+      for (i = parser->toknext - 1; i >= 0; i--)
+      {
+        /* Unmatched opened object or array */
+        if (tokens[i].start != -1 && tokens[i].end == -1)
+        {
+          return JSMN_ERROR_PART;
+        }
+      }
+    }
+
+    return count;
+  }
+
+  /**
+   * Creates a new parser based over a given buffer with an array of tokens
+   * available.
+   */
+  JSMN_API void jsmn_init(jsmn_parser *parser)
+  {
+    parser->pos = 0;
+    parser->toknext = 0;
+    parser->toksuper = -1;
+  }
+
+#endif /* JSMN_HEADER */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* JSMN_H */
+
+#ifndef JB_JSMN_EDIT
+#define JB_JSMN_EDIT
+/* Added in by JBlanked on 2024-10-16 for use in Flipper Zero SDK*/
+
+#include <string.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <furi.h>
+
+// Helper function to create a JSON object
+char *jsmn(const char *key, const char *value)
+{
+  int length = strlen(key) + strlen(value) + 8;         // Calculate required length
+  char *result = (char *)malloc(length * sizeof(char)); // Allocate memory
+  if (result == NULL)
+  {
+    return NULL; // Handle memory allocation failure
+  }
+  snprintf(result, length, "{\"%s\":\"%s\"}", key, value);
+  return result; // Caller is responsible for freeing this memory
+}
+
+// Helper function to compare JSON keys
+int jsoneq(const char *json, jsmntok_t *tok, const char *s)
+{
+  if (tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start &&
+      strncmp(json + tok->start, s, tok->end - tok->start) == 0)
+  {
+    return 0;
+  }
+  return -1;
+}
+
+// return the value of the key in the JSON data
+char *get_json_value(char *key, char *json_data, uint32_t max_tokens)
+{
+  // Parse the JSON feed
+  if (json_data != NULL)
+  {
+    jsmn_parser parser;
+    jsmn_init(&parser);
+
+    // Allocate tokens array on the heap
+    jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
+    if (tokens == NULL)
+    {
+      FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+      return NULL;
+    }
+
+    int ret = jsmn_parse(&parser, json_data, strlen(json_data), tokens, max_tokens);
+    if (ret < 0)
+    {
+      // Handle parsing errors
+      FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret);
+      free(tokens);
+      return NULL;
+    }
+
+    // Ensure that the root element is an object
+    if (ret < 1 || tokens[0].type != JSMN_OBJECT)
+    {
+      FURI_LOG_E("JSMM.H", "Root element is not an object.");
+      free(tokens);
+      return NULL;
+    }
+
+    // Loop through the tokens to find the key
+    for (int i = 1; i < ret; i++)
+    {
+      if (jsoneq(json_data, &tokens[i], key) == 0)
+      {
+        // We found the key. Now, return the associated value.
+        int length = tokens[i + 1].end - tokens[i + 1].start;
+        char *value = malloc(length + 1);
+        if (value == NULL)
+        {
+          FURI_LOG_E("JSMM.H", "Failed to allocate memory for value.");
+          free(tokens);
+          return NULL;
+        }
+        strncpy(value, json_data + tokens[i + 1].start, length);
+        value[length] = '\0'; // Null-terminate the string
+
+        free(tokens); // Free the token array
+        return value; // Return the extracted value
+      }
+    }
+
+    // Free the token array if key was not found
+    free(tokens);
+  }
+  else
+  {
+    FURI_LOG_E("JSMM.H", "JSON data is NULL");
+  }
+  FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON.");
+  return NULL; // Return NULL if something goes wrong
+}
+
+// Revised get_json_array_value function
+char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t max_tokens)
+{
+  // Retrieve the array string for the given key
+  char *array_str = get_json_value(key, json_data, max_tokens);
+  if (array_str == NULL)
+  {
+    FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key);
+    return NULL;
+  }
+
+  // Initialize the JSON parser
+  jsmn_parser parser;
+  jsmn_init(&parser);
+
+  // Allocate memory for JSON tokens
+  jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
+  if (tokens == NULL)
+  {
+    FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+    free(array_str);
+    return NULL;
+  }
+
+  // Parse the JSON array
+  int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens);
+  if (ret < 0)
+  {
+    FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret);
+    free(tokens);
+    free(array_str);
+    return NULL;
+  }
+
+  // Ensure the root element is an array
+  if (ret < 1 || tokens[0].type != JSMN_ARRAY)
+  {
+    FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key);
+    free(tokens);
+    free(array_str);
+    return NULL;
+  }
+
+  // Check if the index is within bounds
+  if (index >= (uint32_t)tokens[0].size)
+  {
+    FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %d.", (unsigned long)index, tokens[0].size);
+    free(tokens);
+    free(array_str);
+    return NULL;
+  }
+
+  // Locate the token corresponding to the desired array element
+  int current_token = 1; // Start after the array token
+  for (uint32_t i = 0; i < index; i++)
+  {
+    if (tokens[current_token].type == JSMN_OBJECT)
+    {
+      // For objects, skip all key-value pairs
+      current_token += 1 + 2 * tokens[current_token].size;
+    }
+    else if (tokens[current_token].type == JSMN_ARRAY)
+    {
+      // For nested arrays, skip all elements
+      current_token += 1 + tokens[current_token].size;
+    }
+    else
+    {
+      // For primitive types, simply move to the next token
+      current_token += 1;
+    }
+
+    // Safety check to prevent out-of-bounds
+    if (current_token >= ret)
+    {
+      FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array.");
+      free(tokens);
+      free(array_str);
+      return NULL;
+    }
+  }
+
+  // Extract the array element
+  jsmntok_t element = tokens[current_token];
+  int length = element.end - element.start;
+  char *value = malloc(length + 1);
+  if (value == NULL)
+  {
+    FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element.");
+    free(tokens);
+    free(array_str);
+    return NULL;
+  }
+
+  // Copy the element value to a new string
+  strncpy(value, array_str + element.start, length);
+  value[length] = '\0'; // Null-terminate the string
+
+  // Clean up
+  free(tokens);
+  free(array_str);
+
+  return value;
+}
+
+// Revised get_json_array_values function with correct token skipping
+char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, int *num_values)
+{
+  // Retrieve the array string for the given key
+  char *array_str = get_json_value(key, json_data, max_tokens);
+  if (array_str == NULL)
+  {
+    FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key);
+    return NULL;
+  }
+
+  // Initialize the JSON parser
+  jsmn_parser parser;
+  jsmn_init(&parser);
+
+  // Allocate memory for JSON tokens
+  jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens); // Allocate on the heap
+  if (tokens == NULL)
+  {
+    FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+    free(array_str);
+    return NULL;
+  }
+
+  // Parse the JSON array
+  int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens);
+  if (ret < 0)
+  {
+    FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret);
+    free(tokens);
+    free(array_str);
+    return NULL;
+  }
+
+  // Ensure the root element is an array
+  if (tokens[0].type != JSMN_ARRAY)
+  {
+    FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key);
+    free(tokens);
+    free(array_str);
+    return NULL;
+  }
+
+  // Allocate memory for the array of values (maximum possible)
+  int array_size = tokens[0].size;
+  char **values = malloc(array_size * sizeof(char *));
+  if (values == NULL)
+  {
+    FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values.");
+    free(tokens);
+    free(array_str);
+    return NULL;
+  }
+
+  int actual_num_values = 0;
+
+  // Traverse the array and extract all object values
+  int current_token = 1; // Start after the array token
+  for (int i = 0; i < array_size; i++)
+  {
+    if (current_token >= ret)
+    {
+      FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array.");
+      break;
+    }
+
+    jsmntok_t element = tokens[current_token];
+
+    if (element.type != JSMN_OBJECT)
+    {
+      FURI_LOG_E("JSMM.H", "Array element %d is not an object, skipping.", i);
+      // Skip this element
+      current_token += 1;
+      continue;
+    }
+
+    int length = element.end - element.start;
+
+    // Allocate a new string for the value and copy the data
+    char *value = malloc(length + 1);
+    if (value == NULL)
+    {
+      FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element.");
+      for (int j = 0; j < actual_num_values; j++)
+      {
+        free(values[j]);
+      }
+      free(values);
+      free(tokens);
+      free(array_str);
+      return NULL;
+    }
+
+    strncpy(value, array_str + element.start, length);
+    value[length] = '\0'; // Null-terminate the string
+
+    values[actual_num_values] = value;
+    actual_num_values++;
+
+    // Skip all tokens related to this object to avoid misparsing
+    current_token += 1 + (2 * element.size); // Each key-value pair consumes two tokens
+  }
+
+  *num_values = actual_num_values;
+
+  // Reallocate the values array to actual_num_values if necessary
+  if (actual_num_values < array_size)
+  {
+    char **reduced_values = realloc(values, actual_num_values * sizeof(char *));
+    if (reduced_values != NULL)
+    {
+      values = reduced_values;
+    }
+
+    // Free the remaining values
+    for (int i = actual_num_values; i < array_size; i++)
+    {
+      free(values[i]);
+    }
+  }
+
+  // Clean up
+  free(tokens);
+  free(array_str);
+  return values;
+}
+
+#endif /* JB_JSMN_EDIT */

+ 803 - 0
flip_store/uart_text_input.h

@@ -0,0 +1,803 @@
+// from https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/uart_terminal/uart_text_input.c
+// all credits to xMasterX for the code
+#ifndef UART_TEXT_INPUT_H
+#define UART_TEXT_INPUT_H
+
+#include <gui/elements.h>
+#include "flip_store_icons.h"
+#include <furi.h>
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <core/common_defines.h>
+
+/** Text input anonymous structure */
+typedef struct UART_TextInput UART_TextInput;
+typedef void (*UART_TextInputCallback)(void *context);
+typedef bool (*UART_TextInputValidatorCallback)(const char *text, FuriString *error, void *context);
+
+UART_TextInputValidatorCallback
+uart_text_input_get_validator_callback(UART_TextInput *uart_text_input);
+
+void uart_text_input_reset(UART_TextInput *uart_text_input);
+
+struct UART_TextInput
+{
+    View *view;
+    FuriTimer *timer;
+};
+
+typedef struct
+{
+    const char text;
+    const uint8_t x;
+    const uint8_t y;
+} UART_TextInputKey;
+
+typedef struct
+{
+    const char *header;
+    char *text_buffer;
+    size_t text_buffer_size;
+    bool clear_default_text;
+
+    UART_TextInputCallback callback;
+    void *callback_context;
+
+    uint8_t selected_row;
+    uint8_t selected_column;
+
+    UART_TextInputValidatorCallback validator_callback;
+    void *validator_callback_context;
+    FuriString *validator_text;
+    bool valadator_message_visible;
+} UART_TextInputModel;
+
+static const uint8_t keyboard_origin_x = 1;
+static const uint8_t keyboard_origin_y = 29;
+static const uint8_t keyboard_row_count = 4;
+
+#define mode_AT "Send AT command to UART"
+
+#define ENTER_KEY '\r'
+#define BACKSPACE_KEY '\b'
+
+static const UART_TextInputKey keyboard_keys_row_1[] = {
+    {'{', 1, 0},
+    {'(', 9, 0},
+    {'[', 17, 0},
+    {'|', 25, 0},
+    {'@', 33, 0},
+    {'&', 41, 0},
+    {'#', 49, 0},
+    {';', 57, 0},
+    {'^', 65, 0},
+    {'*', 73, 0},
+    {'`', 81, 0},
+    {'"', 89, 0},
+    {'~', 97, 0},
+    {'\'', 105, 0},
+    {'.', 113, 0},
+    {'/', 120, 0},
+};
+
+static const UART_TextInputKey keyboard_keys_row_2[] = {
+    {'q', 1, 10},
+    {'w', 9, 10},
+    {'e', 17, 10},
+    {'r', 25, 10},
+    {'t', 33, 10},
+    {'y', 41, 10},
+    {'u', 49, 10},
+    {'i', 57, 10},
+    {'o', 65, 10},
+    {'p', 73, 10},
+    {'0', 81, 10},
+    {'1', 89, 10},
+    {'2', 97, 10},
+    {'3', 105, 10},
+    {'=', 113, 10},
+    {'-', 120, 10},
+};
+
+static const UART_TextInputKey keyboard_keys_row_3[] = {
+    {'a', 1, 21},
+    {'s', 9, 21},
+    {'d', 18, 21},
+    {'f', 25, 21},
+    {'g', 33, 21},
+    {'h', 41, 21},
+    {'j', 49, 21},
+    {'k', 57, 21},
+    {'l', 65, 21},
+    {BACKSPACE_KEY, 72, 13},
+    {'4', 89, 21},
+    {'5', 97, 21},
+    {'6', 105, 21},
+    {'$', 113, 21},
+    {'%', 120, 21},
+
+};
+
+static const UART_TextInputKey keyboard_keys_row_4[] = {
+    {'z', 1, 33},
+    {'x', 9, 33},
+    {'c', 18, 33},
+    {'v', 25, 33},
+    {'b', 33, 33},
+    {'n', 41, 33},
+    {'m', 49, 33},
+    {'_', 57, 33},
+    {ENTER_KEY, 64, 24},
+    {'7', 89, 33},
+    {'8', 97, 33},
+    {'9', 105, 33},
+    {'!', 113, 33},
+    {'+', 120, 33},
+};
+
+static uint8_t get_row_size(uint8_t row_index)
+{
+    uint8_t row_size = 0;
+
+    switch (row_index + 1)
+    {
+    case 1:
+        row_size = sizeof(keyboard_keys_row_1) / sizeof(UART_TextInputKey);
+        break;
+    case 2:
+        row_size = sizeof(keyboard_keys_row_2) / sizeof(UART_TextInputKey);
+        break;
+    case 3:
+        row_size = sizeof(keyboard_keys_row_3) / sizeof(UART_TextInputKey);
+        break;
+    case 4:
+        row_size = sizeof(keyboard_keys_row_4) / sizeof(UART_TextInputKey);
+        break;
+    }
+
+    return row_size;
+}
+
+static const UART_TextInputKey *get_row(uint8_t row_index)
+{
+    const UART_TextInputKey *row = NULL;
+
+    switch (row_index + 1)
+    {
+    case 1:
+        row = keyboard_keys_row_1;
+        break;
+    case 2:
+        row = keyboard_keys_row_2;
+        break;
+    case 3:
+        row = keyboard_keys_row_3;
+        break;
+    case 4:
+        row = keyboard_keys_row_4;
+        break;
+    }
+
+    return row;
+}
+
+static char get_selected_char(UART_TextInputModel *model)
+{
+    return get_row(model->selected_row)[model->selected_column].text;
+}
+
+static bool char_is_lowercase(char letter)
+{
+    return (letter >= 0x61 && letter <= 0x7A);
+}
+
+static bool char_is_uppercase(char letter)
+{
+    return (letter >= 0x41 && letter <= 0x5A);
+}
+
+static char char_to_lowercase(const char letter)
+{
+    switch (letter)
+    {
+    case ' ':
+        return 0x5f;
+        break;
+    case ')':
+        return 0x28;
+        break;
+    case '}':
+        return 0x7b;
+        break;
+    case ']':
+        return 0x5b;
+        break;
+    case '\\':
+        return 0x2f;
+        break;
+    case ':':
+        return 0x3b;
+        break;
+    case ',':
+        return 0x2e;
+        break;
+    case '?':
+        return 0x21;
+        break;
+    case '>':
+        return 0x3c;
+        break;
+    }
+    if (char_is_uppercase(letter))
+    {
+        return (letter + 0x20);
+    }
+    else
+    {
+        return letter;
+    }
+}
+
+static char char_to_uppercase(const char letter)
+{
+    switch (letter)
+    {
+    case '_':
+        return 0x20;
+        break;
+    case '(':
+        return 0x29;
+        break;
+    case '{':
+        return 0x7d;
+        break;
+    case '[':
+        return 0x5d;
+        break;
+    case '/':
+        return 0x5c;
+        break;
+    case ';':
+        return 0x3a;
+        break;
+    case '.':
+        return 0x2c;
+        break;
+    case '!':
+        return 0x3f;
+        break;
+    case '<':
+        return 0x3e;
+        break;
+    }
+    if (char_is_lowercase(letter))
+    {
+        return (letter - 0x20);
+    }
+    else
+    {
+        return letter;
+    }
+}
+
+static void uart_text_input_backspace_cb(UART_TextInputModel *model)
+{
+    uint8_t text_length = model->clear_default_text ? 1 : strlen(model->text_buffer);
+    if (text_length > 0)
+    {
+        model->text_buffer[text_length - 1] = 0;
+    }
+}
+
+static void uart_text_input_view_draw_callback(Canvas *canvas, void *_model)
+{
+    UART_TextInputModel *model = _model;
+    // uint8_t text_length = model->text_buffer ? strlen(model->text_buffer) : 0;
+    uint8_t needed_string_width = canvas_width(canvas) - 8;
+    uint8_t start_pos = 4;
+
+    const char *text = model->text_buffer;
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_draw_str(canvas, 2, 7, model->header);
+    elements_slightly_rounded_frame(canvas, 1, 8, 126, 12);
+
+    if (canvas_string_width(canvas, text) > needed_string_width)
+    {
+        canvas_draw_str(canvas, start_pos, 17, "...");
+        start_pos += 6;
+        needed_string_width -= 8;
+    }
+
+    while (text != 0 && canvas_string_width(canvas, text) > needed_string_width)
+    {
+        text++;
+    }
+
+    if (model->clear_default_text)
+    {
+        elements_slightly_rounded_box(
+            canvas, start_pos - 1, 14, canvas_string_width(canvas, text) + 2, 10);
+        canvas_set_color(canvas, ColorWhite);
+    }
+    else
+    {
+        canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 1, 18, "|");
+        canvas_draw_str(canvas, start_pos + canvas_string_width(canvas, text) + 2, 18, "|");
+    }
+    canvas_draw_str(canvas, start_pos, 17, text);
+
+    canvas_set_font(canvas, FontKeyboard);
+
+    for (uint8_t row = 0; row <= keyboard_row_count; row++)
+    {
+        const uint8_t column_count = get_row_size(row);
+        const UART_TextInputKey *keys = get_row(row);
+
+        for (size_t column = 0; column < column_count; column++)
+        {
+            if (keys[column].text == ENTER_KEY)
+            {
+                canvas_set_color(canvas, ColorBlack);
+                if (model->selected_row == row && model->selected_column == column)
+                {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySaveSelected_24x11);
+                }
+                else
+                {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeySave_24x11);
+                }
+            }
+            else if (keys[column].text == BACKSPACE_KEY)
+            {
+                canvas_set_color(canvas, ColorBlack);
+                if (model->selected_row == row && model->selected_column == column)
+                {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspaceSelected_16x9);
+                }
+                else
+                {
+                    canvas_draw_icon(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        &I_KeyBackspace_16x9);
+                }
+            }
+            else
+            {
+                if (model->selected_row == row && model->selected_column == column)
+                {
+                    canvas_set_color(canvas, ColorBlack);
+                    canvas_draw_box(
+                        canvas,
+                        keyboard_origin_x + keys[column].x - 1,
+                        keyboard_origin_y + keys[column].y - 8,
+                        7,
+                        10);
+                    canvas_set_color(canvas, ColorWhite);
+                }
+                else
+                {
+                    canvas_set_color(canvas, ColorBlack);
+                }
+                if (0 == strcmp(model->header, mode_AT))
+                {
+                    canvas_draw_glyph(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        char_to_uppercase(keys[column].text));
+                }
+                else
+                {
+                    canvas_draw_glyph(
+                        canvas,
+                        keyboard_origin_x + keys[column].x,
+                        keyboard_origin_y + keys[column].y,
+                        keys[column].text);
+                }
+            }
+        }
+    }
+    if (model->valadator_message_visible)
+    {
+        canvas_set_font(canvas, FontSecondary);
+        canvas_set_color(canvas, ColorWhite);
+        canvas_draw_box(canvas, 8, 10, 110, 48);
+        canvas_set_color(canvas, ColorBlack);
+        canvas_draw_icon(canvas, 10, 14, &I_WarningDolphin_45x42);
+        canvas_draw_rframe(canvas, 8, 8, 112, 50, 3);
+        canvas_draw_rframe(canvas, 9, 9, 110, 48, 2);
+        elements_multiline_text(canvas, 62, 20, furi_string_get_cstr(model->validator_text));
+        canvas_set_font(canvas, FontKeyboard);
+    }
+}
+
+static void
+uart_text_input_handle_up(UART_TextInput *uart_text_input, UART_TextInputModel *model)
+{
+    UNUSED(uart_text_input);
+    if (model->selected_row > 0)
+    {
+        model->selected_row--;
+        if (model->selected_column > get_row_size(model->selected_row) - 6)
+        {
+            model->selected_column = model->selected_column + 1;
+        }
+    }
+}
+
+static void
+uart_text_input_handle_down(UART_TextInput *uart_text_input, UART_TextInputModel *model)
+{
+    UNUSED(uart_text_input);
+    if (model->selected_row < keyboard_row_count - 1)
+    {
+        model->selected_row++;
+        if (model->selected_column > get_row_size(model->selected_row) - 4)
+        {
+            model->selected_column = model->selected_column - 1;
+        }
+    }
+}
+
+static void
+uart_text_input_handle_left(UART_TextInput *uart_text_input, UART_TextInputModel *model)
+{
+    UNUSED(uart_text_input);
+    if (model->selected_column > 0)
+    {
+        model->selected_column--;
+    }
+    else
+    {
+        model->selected_column = get_row_size(model->selected_row) - 1;
+    }
+}
+
+static void
+uart_text_input_handle_right(UART_TextInput *uart_text_input, UART_TextInputModel *model)
+{
+    UNUSED(uart_text_input);
+    if (model->selected_column < get_row_size(model->selected_row) - 1)
+    {
+        model->selected_column++;
+    }
+    else
+    {
+        model->selected_column = 0;
+    }
+}
+
+static void uart_text_input_handle_ok(
+    UART_TextInput *uart_text_input,
+    UART_TextInputModel *model,
+    bool shift)
+{
+    char selected = get_selected_char(model);
+    uint8_t text_length = strlen(model->text_buffer);
+
+    if (0 == strcmp(model->header, mode_AT))
+    {
+        selected = char_to_uppercase(selected);
+    }
+
+    if (shift)
+    {
+        if (0 == strcmp(model->header, mode_AT))
+        {
+            selected = char_to_lowercase(selected);
+        }
+        else
+        {
+            selected = char_to_uppercase(selected);
+        }
+    }
+
+    if (selected == ENTER_KEY)
+    {
+        if (model->validator_callback &&
+            (!model->validator_callback(
+                model->text_buffer, model->validator_text, model->validator_callback_context)))
+        {
+            model->valadator_message_visible = true;
+            furi_timer_start(uart_text_input->timer, furi_kernel_get_tick_frequency() * 4);
+        }
+        else if (model->callback != 0 && text_length > 0)
+        {
+            model->callback(model->callback_context);
+        }
+    }
+    else if (selected == BACKSPACE_KEY)
+    {
+        uart_text_input_backspace_cb(model);
+    }
+    else
+    {
+        if (model->clear_default_text)
+        {
+            text_length = 0;
+        }
+        if (text_length < (model->text_buffer_size - 1))
+        {
+            model->text_buffer[text_length] = selected;
+            model->text_buffer[text_length + 1] = 0;
+        }
+    }
+    model->clear_default_text = false;
+}
+
+static bool uart_text_input_view_input_callback(InputEvent *event, void *context)
+{
+    UART_TextInput *uart_text_input = context;
+    furi_assert(uart_text_input);
+
+    bool consumed = false;
+
+    // Acquire model
+    UART_TextInputModel *model = view_get_model(uart_text_input->view);
+
+    if ((!(event->type == InputTypePress) && !(event->type == InputTypeRelease)) &&
+        model->valadator_message_visible)
+    {
+        model->valadator_message_visible = false;
+        consumed = true;
+    }
+    else if (event->type == InputTypeShort)
+    {
+        consumed = true;
+        switch (event->key)
+        {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyOk:
+            uart_text_input_handle_ok(uart_text_input, model, false);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+    else if (event->type == InputTypeLong)
+    {
+        consumed = true;
+        switch (event->key)
+        {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyOk:
+            uart_text_input_handle_ok(uart_text_input, model, true);
+            break;
+        case InputKeyBack:
+            uart_text_input_backspace_cb(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+    else if (event->type == InputTypeRepeat)
+    {
+        consumed = true;
+        switch (event->key)
+        {
+        case InputKeyUp:
+            uart_text_input_handle_up(uart_text_input, model);
+            break;
+        case InputKeyDown:
+            uart_text_input_handle_down(uart_text_input, model);
+            break;
+        case InputKeyLeft:
+            uart_text_input_handle_left(uart_text_input, model);
+            break;
+        case InputKeyRight:
+            uart_text_input_handle_right(uart_text_input, model);
+            break;
+        case InputKeyBack:
+            uart_text_input_backspace_cb(model);
+            break;
+        default:
+            consumed = false;
+            break;
+        }
+    }
+
+    // Commit model
+    view_commit_model(uart_text_input->view, consumed);
+
+    return consumed;
+}
+
+void uart_text_input_timer_callback(void *context)
+{
+    furi_assert(context);
+    UART_TextInput *uart_text_input = context;
+
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { model->valadator_message_visible = false; },
+        true);
+}
+
+UART_TextInput *uart_text_input_alloc()
+{
+    UART_TextInput *uart_text_input = malloc(sizeof(UART_TextInput));
+    uart_text_input->view = view_alloc();
+    view_set_context(uart_text_input->view, uart_text_input);
+    view_allocate_model(uart_text_input->view, ViewModelTypeLocking, sizeof(UART_TextInputModel));
+    view_set_draw_callback(uart_text_input->view, uart_text_input_view_draw_callback);
+    view_set_input_callback(uart_text_input->view, uart_text_input_view_input_callback);
+
+    uart_text_input->timer =
+        furi_timer_alloc(uart_text_input_timer_callback, FuriTimerTypeOnce, uart_text_input);
+
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { model->validator_text = furi_string_alloc(); },
+        false);
+
+    uart_text_input_reset(uart_text_input);
+
+    return uart_text_input;
+}
+
+void uart_text_input_free(UART_TextInput *uart_text_input)
+{
+    furi_assert(uart_text_input);
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { furi_string_free(model->validator_text); },
+        false);
+
+    // Send stop command
+    furi_timer_stop(uart_text_input->timer);
+    // Release allocated memory
+    furi_timer_free(uart_text_input->timer);
+
+    view_free(uart_text_input->view);
+
+    free(uart_text_input);
+}
+
+void uart_text_input_reset(UART_TextInput *uart_text_input)
+{
+    furi_assert(uart_text_input);
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->text_buffer_size = 0;
+            model->header = "";
+            model->selected_row = 0;
+            model->selected_column = 0;
+            model->clear_default_text = false;
+            model->text_buffer = NULL;
+            model->text_buffer_size = 0;
+            model->callback = NULL;
+            model->callback_context = NULL;
+            model->validator_callback = NULL;
+            model->validator_callback_context = NULL;
+            furi_string_reset(model->validator_text);
+            model->valadator_message_visible = false;
+        },
+        true);
+}
+
+View *uart_text_input_get_view(UART_TextInput *uart_text_input)
+{
+    furi_assert(uart_text_input);
+    return uart_text_input->view;
+}
+
+void uart_text_input_set_result_callback(
+    UART_TextInput *uart_text_input,
+    UART_TextInputCallback callback,
+    void *callback_context,
+    char *text_buffer,
+    size_t text_buffer_size,
+    bool clear_default_text)
+{
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->callback = callback;
+            model->callback_context = callback_context;
+            model->text_buffer = text_buffer;
+            model->text_buffer_size = text_buffer_size;
+            model->clear_default_text = clear_default_text;
+            if (text_buffer && text_buffer[0] != '\0')
+            {
+                // Set focus on Save
+                model->selected_row = 2;
+                model->selected_column = 8;
+            }
+        },
+        true);
+}
+
+void uart_text_input_set_validator(
+    UART_TextInput *uart_text_input,
+    UART_TextInputValidatorCallback callback,
+    void *callback_context)
+{
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        {
+            model->validator_callback = callback;
+            model->validator_callback_context = callback_context;
+        },
+        true);
+}
+
+UART_TextInputValidatorCallback
+uart_text_input_get_validator_callback(UART_TextInput *uart_text_input)
+{
+    UART_TextInputValidatorCallback validator_callback = NULL;
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { validator_callback = model->validator_callback; },
+        false);
+    return validator_callback;
+}
+
+void *uart_text_input_get_validator_callback_context(UART_TextInput *uart_text_input)
+{
+    void *validator_callback_context = NULL;
+    with_view_model(
+        uart_text_input->view,
+        UART_TextInputModel * model,
+        { validator_callback_context = model->validator_callback_context; },
+        false);
+    return validator_callback_context;
+}
+
+void uart_text_input_set_header_text(UART_TextInput *uart_text_input, const char *text)
+{
+    with_view_model(
+        uart_text_input->view, UART_TextInputModel * model, { model->header = text; }, true);
+}
+
+#endif // UART_TEXT_INPUT_H