628 Commits

Author SHA1 Message Date
Jura Shikin 5979e307b5 Merge branch 'develop' into 'master'
noise - Fix project description yaml file

See merge request raspberry/mobile/ios!266
2023-03-02 17:03:01 +03:00
Jura Shikin c6bb0d3645 noise - Fix project description yaml file 2023-03-02 16:57:07 +03:00
Jura Shikin 39ff0f9b35 Merge branch 'develop' into 'master'
Prepare release 1.4.0

See merge request raspberry/mobile/ios!265
2023-03-02 16:26:46 +03:00
Jura Shikin 0df769f86b Merge branch 'master' into 'develop'
# Conflicts:
#   iOS/project.yml
2023-03-02 13:08:31 +00:00
Никита Чирухин 274a2c5261 Merge branch 'feature/MALINKA-1175/camera_on_cross' into 'develop'
MALINKA-1175: Fix of qr-camera cross behaviour

See merge request raspberry/mobile/ios!264
2023-03-02 15:38:13 +03:00
Elena Nazarova c2a64000b4 MALINKA-1175: Fix of qr-camera cross behaviour 2023-03-02 15:38:13 +03:00
Juraldinio aca9045648 MALINKA-1171 - Fix crash in chat 2023-03-01 20:43:56 +03:00
Juraldinio b53098d3eb MALINKA-498 - cherry pick from 1.5.0 release 2023-03-01 15:29:38 +03:00
Андрей Геращенко 49c4137975 Merge branch 'MALINKA-1170/send_token_loader' into 'develop'
MALINKA-1170: Add quantity of action check

See merge request raspberry/mobile/ios!262
2023-03-01 11:36:51 +03:00
Elena Nazarova 2bc0c83328 MALINKA-1170: Add quantity of action check 2023-03-01 11:36:51 +03:00
Никита Чирухин f86c434d1f Merge branch 'feature/MALINKA-1162/infinite-loading' into 'develop'
MALINKA-1162 - infinite loading

See merge request raspberry/mobile/ios!260
2023-02-28 10:44:13 +03:00
Никита Чирухин b98ca6d496 Merge branch 'feature/MALINKA-1029/Chats' into 'develop'
MALINKA-1029 - fix show chats

See merge request raspberry/mobile/ios!259
2023-02-28 09:33:06 +03:00
Jura Shikin 2ec2fee225 MALINKA-1029 - fix show chats 2023-02-28 09:33:05 +03:00
Juraldinio 9b280ce58c MALINKA-1162 - infinite loading 2023-02-27 16:17:17 +03:00
Jura Shikin 6f9f336bab Merge branch 'release/1.3.2' into 'master'
MALINKA-1161 - add rubles icon to application

See merge request raspberry/mobile/ios!258
2023-02-23 23:47:40 +03:00
Juraldinio 9f29129168 MALINKA-1161 - add rubles icon to application 2023-02-23 23:36:43 +03:00
Juraldinio e862226d7f MALINKA-1137 - remove build stage from MR request 2023-02-21 15:11:32 +03:00
Elena Nazarova 2a43809fac Merge branch 'feature/MALINKA-774/Incorrect_messages_order' into 'develop'
Resolve MALINKA-774 "Feature//incorrect messages order"

See merge request raspberry/mobile/ios!239
2023-02-06 22:49:01 +03:00
Андрей Геращенко 6e71b86fd0 Resolve MALINKA-774 "Feature//incorrect messages order" 2023-02-06 22:49:01 +03:00
Elena Nazarova 056387a79e Merge branch 'feature/MALINKA-1055/breaks_view_of_dialog_when_delete_message' into 'develop'
MALINKA-1055: Breaks the display of the dialog when deleting a message

See merge request raspberry/mobile/ios!232
2023-02-06 16:52:53 +03:00
Никита Чирухин 5488f23461 MALINKA-1055: Breaks the display of the dialog when deleting a message 2023-02-06 16:52:52 +03:00
Jura Shikin 26aa66a52e Merge branch 'feature/MALINKA-1110/Incorrent_price_representation' into 'develop'
MALINKA-1110: Fixed price representation

See merge request raspberry/mobile/ios!245
2023-02-06 16:36:32 +03:00
user 6df69c750b MALINKA-1110: Fixed price representation 2023-02-06 16:09:29 +03:00
Jura Shikin edeaf1f91a Merge branch 'feature/MALINKA-789/Token_transfer_deeplink' into 'develop'
MALINKA-789: Fixed incoming transfer deeplink

See merge request raspberry/mobile/ios!244
2023-02-06 09:07:53 +03:00
Андрей Геращенко d428f84985 MALINKA-789: Fixed incoming transfer deeplink 2023-02-06 09:07:53 +03:00
Андрей Геращенко c8f503268e Merge branch 'feature/MALINKA-1089/Chat_error_fix' into 'develop'
MALINKA-1089: Fixed error representation

See merge request raspberry/mobile/ios!242
2023-02-02 15:27:14 +03:00
user 93133dc17e MALINKA-1089: Fixed error representation 2023-02-02 15:10:15 +03:00
Никита Чирухин 43b81ebf3b Merge branch 'feature/MALINKA-1089/Error_opening_resources_screen' into 'develop'
MALINKA-1089: Fixed problem with opening resources error window and fixed presentation of alerts

See merge request raspberry/mobile/ios!235
2023-02-02 11:11:08 +03:00
user 76640eb4da MALINKA-1089: Fixed problem with opening resources error window and fixed presentation of alerts 2023-01-31 20:57:13 +03:00
Никита Чирухин b5e870050f Merge branch 'feature/MALINKA-1083/Pin_code_change_error' into 'develop'
MALINKA-1083: Fixed pin code issues

See merge request raspberry/mobile/ios!234
2023-01-31 15:39:43 +03:00
Jura Shikin af5516f201 Merge branch 'release/1.3.1' into 'master'
Hot fix

See merge request raspberry/mobile/ios!233
2023-01-31 12:08:00 +03:00
Elena Nazarova 09aa04ff79 Hot fix 2023-01-31 12:08:00 +03:00
user a92549347c MALINKA-1083: Fixed pin code issues 2023-01-31 03:11:42 +03:00
Никита Чирухин 80887fe6c0 Merge branch 'feature/MALINKA-1075/Autocomplete_error' into 'develop'
Resolve MALINKA-1075 "Feature//autocomplete error"

See merge request raspberry/mobile/ios!230
2023-01-30 18:46:33 +03:00
Андрей Геращенко c9fd8f012a Resolve MALINKA-1075 "Feature//autocomplete error" 2023-01-30 18:46:33 +03:00
Elena Nazarova 5a3b3f7f8e Merge branch 'MALINKA-1084/WalletModelAccountAction_refactoring' into 'develop'
MALINKA-1084: WalletModelAccountAction refactoring

See merge request raspberry/mobile/ios!229
2023-01-30 15:15:51 +03:00
Elena Nazarova 636ab46f5b MALINKA-1084: WalletModelAccountAction refactoring 2023-01-30 15:15:50 +03:00
Никита Чирухин d5a2b69f20 Merge branch 'feature/removePassword' into 'develop'
Remove passworf from Accounts service

See merge request raspberry/mobile/ios!227
2023-01-30 14:53:09 +03:00
Никита Чирухин ac9a088829 Merge branch 'feature/MALINKA-874/New_account_selection' into 'develop'
MALINKA-874: Fix selection of new account after its creation

See merge request raspberry/mobile/ios!228
2023-01-30 14:52:36 +03:00
Juraldinio 9c1c2ad2aa Remove passworf from Accounts service 2023-01-30 08:51:28 +03:00
user b2fbc0b83f MALINKA-874: Fix selection of new account after its creation 2023-01-30 02:20:38 +03:00
Jura Shikin 7518417578 Merge branch 'MALINKA-874/activate_accepted_account' into 'develop'
MALINKA-874: Activate accepted account

See merge request raspberry/mobile/ios!226
2023-01-27 16:23:53 +03:00
Elena Nazarova 741861c8f3 MALINKA-874: Activate accepted account 2023-01-27 16:23:53 +03:00
Никита Чирухин 90b936cd47 Merge branch 'feature/MALINKA-1082/Error_on_changing_pin_code' into 'develop'
MALINKA-1082: Fixed tabbar problems

See merge request raspberry/mobile/ios!224
2023-01-27 15:31:06 +03:00
Андрей Геращенко 9a4e02d2f3 MALINKA-1082: Fixed tabbar problems 2023-01-27 15:31:06 +03:00
Juraldinio 64a2313ba9 Remove warnings 2023-01-27 11:23:51 +03:00
Никита Чирухин bc313d7c1a Merge branch 'MALINKA-1071/ui_after_status_update' into 'develop'
MALINKA-1071: Update ui on account status change

See merge request raspberry/mobile/ios!222
2023-01-26 15:19:31 +03:00
Elena Nazarova 60e7c139bc MALINKA-1071: Update ui on account status change 2023-01-26 15:19:31 +03:00
Андрей Геращенко 877c4e2673 Merge branch 'MALINKA-1076/rubcash_history' into 'develop'
MALINKA-1076: Added missing parameters

See merge request raspberry/mobile/ios!220
2023-01-26 15:09:33 +03:00
Elena Nazarova 894f5d5dcc MALINKA-1076: Added missing parameters 2023-01-26 15:09:32 +03:00
Никита Чирухин eac2be21fd Merge branch 'feature/MALINKA-1033/Incorrect_pin_code_flow' into 'develop'
MALINKA-1033: Incorrect reset of pin code flow

See merge request raspberry/mobile/ios!221
2023-01-26 14:03:35 +03:00
Андрей Геращенко e795ec521c MALINKA-1033: Incorrect reset of pin code flow 2023-01-26 14:03:35 +03:00
Никита Чирухин ba8d09ee52 Merge branch 'feature/MALINKA-1020/Resets_default_token' into 'develop'
MALINKA-1020: Attempts to fix problem of reseting default token

See merge request raspberry/mobile/ios!219
2023-01-26 03:03:23 +03:00
Андрей Геращенко 86cd216417 MALINKA-1020: Attempts to fix problem of reseting default token 2023-01-26 03:03:23 +03:00
Никита Чирухин a68977db6f Merge branch 'MALINKA-1050/slow_succeed_alert' into 'develop'
MALINKA-1050: Loader until alert is shown

See merge request raspberry/mobile/ios!218
2023-01-26 03:02:06 +03:00
Elena Nazarova afccab2bd9 MALINKA-1050: Loader until alert is shown 2023-01-26 03:02:06 +03:00
Jura Shikin 6930978c91 Merge branch 'feature/MALINKA-1043/navbar_disappears_after_deleting_account' into 'develop'
MALINKA-1043: The lower bar disappears after deleting the account

See merge request raspberry/mobile/ios!217
2023-01-25 10:56:54 +03:00
Никита Чирухин 6e512a3ff2 MALINKA-1043: The lower bar disappears after deleting the account 2023-01-25 10:56:54 +03:00
Никита Чирухин 788b48f217 Merge branch 'MALINKA-1071/executed_ui' into 'develop'
MALINKA-1071: ui status fixes

See merge request raspberry/mobile/ios!215
2023-01-24 17:51:03 +03:00
Никита Чирухин dfe5b13c68 Merge branch 'MALINKA-1047/textfield_overlay' into 'develop'
MALINKA-1047: textfield overlay fix

See merge request raspberry/mobile/ios!216
2023-01-24 17:50:47 +03:00
Elena Nazarova 8f402d4e15 MALINKA-1047: textfield overlay fix 2023-01-24 13:08:05 +03:00
Elena Nazarova dc81f97030 MALINKA-1071: ui status fixes 2023-01-24 11:44:12 +03:00
Jura Shikin b06d9bc5a1 Merge branch 'feature/Messages' into 'develop'
WiP

See merge request raspberry/mobile/ios!214
2023-01-23 23:15:06 +03:00
Jura Shikin 9b34d78319 WiP 2023-01-23 23:15:06 +03:00
Jura Shikin 9eb99f52ac Merge branch 'MALINKA-1068/icon' into 'develop'
MALINKA-1068: Changed icon

See merge request raspberry/mobile/ios!213
2023-01-23 16:58:07 +03:00
Elena Nazarova 15f3f89189 MALINKA-1068: Changed icon 2023-01-23 16:58:07 +03:00
Jura Shikin ca423995df Merge branch 'feature/MALINKA-874/no_account_selected_after_creation' into 'develop'
MALINKA-874: No account is selected after creation

See merge request raspberry/mobile/ios!205
2023-01-23 15:12:12 +03:00
Никита Чирухин 5f66477a81 MALINKA-874: No account is selected after creation 2023-01-23 15:12:12 +03:00
Jura Shikin 913e5c79a9 Merge branch 'feature/MALINKA-1048/Crash_on_deleting_message' into 'develop'
MALINKA-1048: Fixed crash on deleting message

See merge request raspberry/mobile/ios!212
2023-01-23 12:54:57 +03:00
Андрей Геращенко 9d39001efd MALINKA-1048: Fixed crash on deleting message 2023-01-23 12:54:57 +03:00
Jura Shikin 0f6afd874a Merge branch 'feature/MALINKA-1030/Fix_crash_on_textfield' into 'develop'
MALINKA-1030: Fixed textfield input crash

See merge request raspberry/mobile/ios!210
2023-01-23 12:51:06 +03:00
Андрей Геращенко df2a5d73e5 MALINKA-1030: Fixed textfield input crash 2023-01-23 12:51:06 +03:00
Jura Shikin 42904783c3 Merge branch 'feature/syncPC' into 'develop'
MALINKA-1057 - Sync with paycash changes

See merge request raspberry/mobile/ios!211
2023-01-23 12:50:29 +03:00
Jura Shikin 7f4129ac72 MALINKA-1057 - Sync with paycash changes 2023-01-23 12:50:29 +03:00
Jura Shikin 74efc886cc Merge branch 'feature/MALINKA-1050/update_p2pControllerDashboard_screen_notification_appears_twice' into 'develop'
MALINKA-1050: When you update the Sell and Buy screen notification appears twice

See merge request raspberry/mobile/ios!208
2023-01-20 16:41:11 +03:00
Никита Чирухин a3cbaf6441 MALINKA-1050: When you update the Sell and Buy screen notification appears twice 2023-01-20 16:41:10 +03:00
Jura Shikin 119d53a3f0 Merge branch 'feature/MALINKA-1044/incorrect_display_of_textfield_in_the_dialog' into 'develop'
MALINKA-1044: Incorrect display of the input field in the dialog

See merge request raspberry/mobile/ios!206
2023-01-20 16:40:06 +03:00
Никита Чирухин b6b4ebc1a8 MALINKA-1044: Incorrect display of the input field in the dialog 2023-01-20 16:40:06 +03:00
Никита Чирухин f685698b87 Merge branch 'feature/MALINKA-1027/Fix_authorization_problems' into 'develop'
Resolve MALINKA-1027 "Feature//fix authorization problems"

See merge request raspberry/mobile/ios!204
2023-01-20 14:57:02 +03:00
Андрей Геращенко e8597d65da Resolve MALINKA-1027 "Feature//fix authorization problems" 2023-01-20 14:57:02 +03:00
Никита Чирухин 03903f906e Merge branch 'feature/MALINKA-1047/Chat_input_field_conflict' into 'develop'
MALINKA-1047: Fixed presentation of popup - accessory view no longer covers popup

See merge request raspberry/mobile/ios!209
2023-01-20 14:53:29 +03:00
user f4cd49636b MALINKA-1047: Fixed presentation of popup - accessory view no longer covers popup 2023-01-20 01:51:43 +03:00
Андрей Геращенко 7f85320305 Merge branch 'feature/MALINKA-1049/artifacts_locked_screen' into 'develop'
MALINKA-1049: Artifacts locked screen

See merge request raspberry/mobile/ios!207
2023-01-19 23:25:55 +03:00
Никита Чирухин 3f66b4bcf2 MALINKA-1049: Artifacts locked screen 2023-01-19 23:25:55 +03:00
Jura Shikin 0dfda056f7 Merge branch 'feature/MALINKA-997/double_navigationBar_on_account_resources' into 'develop'
MALINKA-997: double navigationbar on account resources

See merge request raspberry/mobile/ios!203
2023-01-18 18:25:14 +03:00
Никита Чирухин 515d69b821 MALINKA-997: double navigationbar on account resources 2023-01-18 18:25:14 +03:00
Juraldinio bf6f5a761c MALINKA-1030 - fix crash on search 2023-01-18 16:49:39 +03:00
Juraldinio 5e5d9c3faf MALINKA-1031 - remove crash 2023-01-18 16:35:20 +03:00
Juraldinio ca84952391 MALINKA-861 - show enter new pincode after fail 2023-01-17 19:10:42 +03:00
Juraldinio 42ac87962e MALINKA-1028 - fullscreen presentation 2023-01-17 16:08:48 +03:00
Андрей Геращенко e980c64fcd Merge branch 'feature/MALINKA-997/double_navigationBar_on_account_resources' into 'develop'
MALINKA-997: Double NavigationBar after ResourcesControllerSetup from chat

See merge request raspberry/mobile/ios!201
2023-01-17 15:10:50 +03:00
Никита Чирухин bfa6e3ac26 MALINKA-997: Double NavigationBar after ResourcesControllerSetup from chat 2023-01-17 15:10:50 +03:00
Никита Чирухин 3f8a9ccf54 Merge branch 'feature/MALINKA-1004/node_actions_model_unit_tests' into 'develop'
MALINKA-1004 Node actions model unit tests"

See merge request raspberry/mobile/ios!198
2023-01-17 15:09:45 +03:00
Андрей Геращенко edcc3274da MALINKA-1004 Node actions model unit tests" 2023-01-17 15:09:45 +03:00
Juraldinio 81ad8361d8 MALINKA-986 - move from node to hyperion loading 2023-01-17 00:10:17 +03:00
Juraldinio 6ef84a8afb MALINKA-986 - Fix crashes 2023-01-16 23:54:01 +03:00
Juraldinio 3f584acbd0 MALINKA-996 - Fix deletion also remove hude warnings 2023-01-16 22:55:11 +03:00
Juraldinio b9d5305ef6 MALINKA-861 - do not allow close window 2023-01-16 15:08:31 +03:00
Jura Shikin 418f4f373c Merge branch 'feature/MALINKA-970' into 'develop'
MALINKA-970 - Remove twice ask pincode

See merge request raspberry/mobile/ios!200
2023-01-16 14:39:30 +03:00
Jura Shikin 6e7509e3cf MALINKA-970 - Remove twice ask pincode 2023-01-16 14:39:30 +03:00
Jura Shikin dfea0fd2bb Merge branch 'feature/RemoveWallet' into 'develop'
MALINKA-861 - Remove wallet

See merge request raspberry/mobile/ios!199
2023-01-16 11:56:50 +03:00
Jura Shikin 2ffc46c0ec MALINKA-861 - Remove wallet 2023-01-16 11:56:50 +03:00
Jura Shikin b48079d295 Merge branch 'NetworkTests' into 'develop'
Some network tests

See merge request raspberry/mobile/ios!197
2023-01-14 01:40:26 +03:00
Jura Shikin cc0fdf6fe9 Some network tests 2023-01-14 01:40:25 +03:00
Андрей Геращенко ab5d2e3bbc Merge branch 'feature/MALINKA-956/disable_ability_to_select_same_tokens' into 'develop'
MALINKA-956: Disable the ability to select the same tokens in the liquidity deposit

See merge request raspberry/mobile/ios!195
2023-01-13 15:16:23 +03:00
Никита Чирухин 21d35c1f11 MALINKA-956: Disable the ability to select the same tokens in the liquidity deposit 2023-01-13 15:16:22 +03:00
Никита Чирухин 8df85ecec4 Merge branch 'feature/MALINKA-946/greymass_history_request' into 'develop'
MALINKA-946: Moved v1/history/get_actions request from the project to WalletNetwork library

See merge request raspberry/mobile/ios!193
2023-01-13 13:22:58 +03:00
Андрей Геращенко 0015980718 MALINKA-946: Moved v1/history/get_actions request from the project to WalletNetwork library 2023-01-13 13:22:58 +03:00
Juraldinio 13bed53acf MALINKA-1001 - add new options to usernames and backend 2023-01-12 22:41:48 +03:00
Никита Чирухин 258de53fd3 Merge branch 'MALINKA-955/disable_transactions_purchase_2' into 'develop'
MALINKA-955: Transaction purchase button ability

See merge request raspberry/mobile/ios!194
2023-01-12 20:46:38 +03:00
Elena Nazarova ed78569d7a MALINKA-955: Transaction purchase button ability 2023-01-12 20:46:38 +03:00
Андрей Геращенко f8751ba13c Merge branch 'feature/MALINKA-954/warranty_exchange_message' into 'develop'
MALINKA-954: Small text fix

See merge request raspberry/mobile/ios!196
2023-01-12 17:47:23 +03:00
Никита Чирухин dfa5757bff MALINKA-954: Small text fix 2023-01-12 17:47:23 +03:00
Jura Shikin cbf1f05258 Merge branch 'MALINKA-954/warranty_exchange_message_2' into 'develop'
MALINKA-954: removed extra condition

See merge request raspberry/mobile/ios!192
2023-01-12 10:57:22 +03:00
Никита Чирухин f16f4bb8a5 Merge branch 'MALINKA-969/old_tokens' into 'develop'
MALINKA-969: reload balances on viewWillAppear

See merge request raspberry/mobile/ios!191
2023-01-12 04:42:40 +03:00
Elena Nazarova 37ccc8883c MALINKA-954: removed extra condition 2023-01-11 16:12:45 +03:00
Elena Nazarova 3b034daa7c MALINKA-969: reload balances on viewWillAppear 2023-01-11 13:27:16 +03:00
Jura Shikin 6f341a403e Merge branch 'feature/MALINKA-964/Refresh_QR-code_screen' into 'develop'
MALINKA-964: Fixed app crash on changing account, added account refresh for...

See merge request raspberry/mobile/ios!187
2023-01-10 21:16:21 +03:00
Андрей Геращенко 4670cd12c4 MALINKA-964: Fixed app crash on changing account, added account refresh for... 2023-01-10 21:16:21 +03:00
Jura Shikin 4bd0e932f2 Merge branch 'MALINKA-954/warranty_exchange_message' into 'develop'
MALINKA-954: Warranty exchange message

See merge request raspberry/mobile/ios!189
2023-01-10 17:10:23 +03:00
Elena Nazarova 0e76a70430 MALINKA-954: Warranty exchange message 2023-01-10 17:10:23 +03:00
Jura Shikin 9bbd8b102e Merge branch 'feature/MALINKA-962/Crash_on_selecting_account' into 'develop'
MALINKA-962: Replaced unowned self with weak self

See merge request raspberry/mobile/ios!190
2023-01-10 17:08:51 +03:00
user 645ce066e4 MALINKA-962: Replaced unowned self with weak self 2023-01-10 14:01:33 +03:00
Elena Nazarova 8b28dadf18 Merge branch 'feature/MALINKA-911/flashing_screen_then_delete_last_account' into 'develop'
MALINKA-911: Flashing screen You don't have any accounts

See merge request raspberry/mobile/ios!188
2023-01-10 13:31:07 +03:00
Никита Чирухин 74551cebde MALINKA-911: Flashing screen You don't have any accounts 2023-01-10 13:31:07 +03:00
Никита Чирухин 23aac206bf Merge branch 'MALINKA-955/disable_transactions_purchase' into 'develop'
MALINKA-955: Check if there is enough token amount for transaction

See merge request raspberry/mobile/ios!186
2023-01-09 17:14:42 +03:00
Elena Nazarova 9a8161b864 MALINKA-955: Check if there is enough token amount for transaction 2023-01-09 17:14:42 +03:00
Никита Чирухин c24adc4ece Merge branch 'MALINKA-920/resources_loader' into 'develop'
MALINKA-920: Succeed alert

See merge request raspberry/mobile/ios!185
2023-01-09 15:02:46 +03:00
Elena Nazarova c5720accc8 MALINKA-920: Succeed alert 2023-01-09 15:02:46 +03:00
Jura Shikin 4c62dcad48 Merge branch 'MALINKA-944/headers' into 'develop'
MALINKA-944: Headers

See merge request raspberry/mobile/ios!181
2022-12-30 12:13:27 +03:00
Elena Nazarova c681978462 MALINKA-944: Headers 2022-12-30 12:13:27 +03:00
Juraldinio 72c4f0b4c6 Change addresses for WebSocket connection 2022-12-29 23:36:55 +03:00
Juraldinio b8633b3f54 MALINKA-966 - delete push tokien links with wallet 2022-12-29 23:24:39 +03:00
Jura Shikin 6a9bc68faa Merge branch 'feature/MALINKA-959/Infinite_loader' into 'develop'
MALINKA-959: Fixed infinite loader on exchange and buying resources

See merge request raspberry/mobile/ios!184
2022-12-29 20:37:36 +03:00
Jura Shikin 2e1158f790 Merge branch 'feature/MALINKA-726/RAM_Popups' into 'develop'
MALINKA-726: Buying Ram error popups

See merge request raspberry/mobile/ios!180
2022-12-29 20:36:10 +03:00
Андрей Геращенко 77f79a91f0 MALINKA-726: Buying Ram error popups 2022-12-29 20:36:09 +03:00
Jura Shikin 8df87cd766 Merge branch 'MALINKA-949/button_title' into 'develop'
MALINKA-949: Button title fix

See merge request raspberry/mobile/ios!182
2022-12-29 20:31:57 +03:00
Elena Nazarova 25c30715c6 MALINKA-949: Button title fix 2022-12-29 20:31:56 +03:00
Jura Shikin be52cc354f Merge branch 'feature/MALINKA-906/message_field_overlaps_dialog_box' into 'develop'
MALINKA-906: The message input field overlaps the dialog box

See merge request raspberry/mobile/ios!183
2022-12-29 20:25:44 +03:00
Никита Чирухин d7a88aa5f1 MALINKA-906: The message input field overlaps the dialog box 2022-12-29 20:25:44 +03:00
user 1583762844 MALINKA-959: Fixed infinite loader on exchange and buying resources 2022-12-29 19:55:06 +03:00
Jura Shikin 43155fe846 Merge branch 'feature/MALINKA-917/pin_code_requested_twice' into 'develop'
MALINKA-917: A faceID is requested twice

See merge request raspberry/mobile/ios!178
2022-12-29 12:00:48 +03:00
Никита Чирухин 8be55c7ede MALINKA-917: A faceID is requested twice 2022-12-29 12:00:48 +03:00
Juraldinio bd0a776129 Remove infinite reconnection 2022-12-28 17:48:50 +03:00
Juraldinio 3aa7409ac2 MALINKA-811 - add headers and logs to console 2022-12-28 13:29:23 +03:00
Juraldinio 56e24b4a78 MALINKA-950 - fix crash on exchange controller 2022-12-28 12:21:03 +03:00
Juraldinio 43762fafa6 MALINKA-851 - fix loading tokens 2022-12-27 12:57:12 +03:00
Никита Чирухин 73cf64e420 Merge branch 'MALINKA-920/ram_succeed' into 'develop'
MALINKA-920: Fixed ram buying/selling behaviour

See merge request raspberry/mobile/ios!170
2022-12-27 01:23:40 +03:00
Elena Nazarova b93191b335 MALINKA-920: Fixed ram buying/selling behaviour 2022-12-27 01:23:39 +03:00
Juraldinio 2d1ce6b8fd MALINKA-851 - Optimisations working with API 2022-12-26 22:59:04 +03:00
Jura Shikin a48e179ebc Merge branch 'feature/MALINKA-943' into 'develop'
Sync our develop and PayChash develop branches

See merge request raspberry/mobile/ios!179
2022-12-26 19:33:09 +03:00
Jura Shikin 86952acdaa Sync our develop and PayChash develop branches 2022-12-26 19:33:09 +03:00
Juraldinio c436bc254f MALINKA-874 - fixes selectin creating account 2022-12-26 16:33:23 +03:00
Juraldinio 3136bee08f MALINKA-936 - fix checking resource behaviour 2022-12-26 11:21:29 +03:00
Juraldinio 63597b81f3 Increase version to 1.4.0 2022-12-24 00:44:44 +03:00
Jura Shikin 4e6246e269 Merge branch 'merge_master' into 'develop'
Merge master

See merge request raspberry/mobile/ios!177
2022-12-24 00:20:22 +03:00
Jura Shikin 85e63aae6f Merge branch 'merge_master' into 'master'
Merge master

See merge request raspberry/mobile/ios!176
2022-12-23 18:04:26 +03:00
Elena Nazarova 497e9c4b72 removed duplicate 2022-12-23 17:53:43 +03:00
Elena Nazarova 962585298d Merge branch 'master' into merge_master
# Conflicts:
#	iOS/Wallet/Sources/Main/Controller/MainController.swift
#	iOS/Wallet/Sources/Main/Service/MainService.swift
#	iOS/Wallet/Sources/Wallet/Controller/WalletController.swift
#	iOS/project.yml
2022-12-23 17:40:01 +03:00
Jura Shikin 08c01dedd3 Merge branch 'MALINKA-938/headers' into 'develop'
MALINKA-938: Add headers to get_info

See merge request raspberry/mobile/ios!174
2022-12-23 16:10:26 +03:00
Elena Nazarova b455e5a154 MALINKA-938: Add headers to get_info 2022-12-23 16:10:26 +03:00
Никита Чирухин cb7efbe3b3 Merge branch 'MALINKA-809/log_error_request' into 'develop'
MALINKA-809: Added headers to error tracking request

See merge request raspberry/mobile/ios!173
2022-12-23 11:15:19 +03:00
Elena Nazarova 42ea52bcf3 MALINKA-809: Added headers to error tracking request 2022-12-23 11:15:19 +03:00
Juraldinio 3b117a010b MALINKA-914 - fix navigation after creation by private 2022-12-22 23:25:02 +03:00
Juraldinio 0b33ba74de MALINKA-929 - fix hide loader 2022-12-22 22:06:47 +03:00
Juraldinio ccd40b275c MALINKA-927 - fixes with wallet view controller 2022-12-22 19:12:32 +03:00
Juraldinio 6cf8826e0d Revert "MALINKA-923: Remove requests not from api"
This reverts commit 6cd670a9c2.
2022-12-22 14:16:17 +03:00
Jura Shikin d358c85c08 Merge branch 'MALINKA-929/fix' into 'develop'
MALINKA-924: Headers

See merge request raspberry/mobile/ios!171
2022-12-21 20:26:26 +03:00
Elena Nazarova 520e9ae9d3 MALINKA-924: Headers 2022-12-21 20:26:26 +03:00
Juraldinio 27755cce87 MALINKA-928 - fix logic for select wallets 2022-12-21 18:22:56 +03:00
Juraldinio 675ff396a5 MALINKA-928 - correct selecting accounts 2022-12-21 16:48:44 +03:00
Juraldinio 0b5853e069 MALINKA-926 - fix failed build 2022-12-21 15:49:51 +03:00
Juraldinio 221ec59015 MALINKA-926 - Show error for invalid key 2022-12-21 14:17:13 +03:00
Juraldinio a4373d5ec5 MALINKA-916 - fix activation 2022-12-20 21:47:31 +03:00
Jura Shikin 75430ad4fd Merge branch 'MALINKA-809/log_error' into 'develop'
MALINKA-809: Change url for error tracking

See merge request raspberry/mobile/ios!169
2022-12-20 19:46:45 +03:00
Elena Nazarova cbf9fdd3c5 MALINKA-809: Change url for error tracking 2022-12-20 19:46:45 +03:00
Jura Shikin 2ea9701a50 Merge branch 'MALINKA-827/requests_from_api' into 'develop'
MALINKA-923: Remove requests not from api

See merge request raspberry/mobile/ios!166
2022-12-20 19:23:46 +03:00
Elena Nazarova 6cd670a9c2 MALINKA-923: Remove requests not from api 2022-12-20 19:23:46 +03:00
Juraldinio 1efc74a3f9 MALINKA-916 - fix behaviour 2022-12-20 18:29:37 +03:00
Jura Shikin 7c09b215da Merge branch 'MALINKA-857/default_hyperions' into 'develop'
MALINKA-857: Default hyperions

See merge request raspberry/mobile/ios!163
2022-12-20 11:30:56 +03:00
Elena Nazarova 0e251a0a7d MALINKA-857: Default hyperions 2022-12-20 11:30:56 +03:00
Jura Shikin 5a3c10db48 Merge branch 'MALINKA-809/url_to_log_errors' into 'develop'
MALINKA-809: Change url to log errors

See merge request raspberry/mobile/ios!168
2022-12-19 23:54:39 +03:00
Elena Nazarova e8b2b4dca3 MALINKA-809: Change url to log errors 2022-12-19 23:54:39 +03:00
Jura Shikin ba913ade2a Merge branch 'feature/MALINKA-726/RAM_error_popup' into 'develop'
MALINKA-726: Popups for errors on Buy Ram screen

See merge request raspberry/mobile/ios!164
2022-12-19 17:20:47 +03:00
Никита Чирухин 23d37d4a88 MALINKA-726: Popups for errors on Buy Ram screen 2022-12-19 17:20:47 +03:00
Jura Shikin 328b941581 Merge branch 'feature/MALINKA-811/change_URL_request_web_sockets_to_backed_enviroment' into 'develop'
MALINKA-811: Change the URL to request web sockets to the application

See merge request raspberry/mobile/ios!165
2022-12-19 17:18:21 +03:00
Никита Чирухин 60dbf3c641 MALINKA-811: Change the URL to request web sockets to the application 2022-12-19 17:18:21 +03:00
Juraldinio e0318d5999 MALINKA-828 - new default configuration 2022-12-19 16:53:47 +03:00
Jura Shikin 0846342d14 Merge branch 'MALINKA-801/get_hyperions' into 'develop'
MALINKA-801: Add getEosHyperions

See merge request raspberry/mobile/ios!162
2022-12-19 15:18:14 +03:00
Elena Nazarova eed4af91ef MALINKA-801: Add getEosHyperions 2022-12-19 15:18:14 +03:00
Juraldinio e5c18ed350 nodes loading with try 2022-12-19 14:52:15 +03:00
Juraldinio 4d22d97b36 MALINKA-912 - token push notifications 2022-12-19 13:55:38 +03:00
Jura Shikin ce2de4f36c Merge branch 'MALINKA-828/default_raspberry_backend' into 'develop'
MALINKA-828: Raspberry backend by default

See merge request raspberry/mobile/ios!161
2022-12-19 13:06:30 +03:00
Elena Nazarova 7c0b98c3ce MALINKA-828: Raspberry backend by default 2022-12-19 13:06:30 +03:00
Jura Shikin 0532086cb7 Merge branch 'MALINKA-809/change_url_to_log_errors' into 'develop'
MALINKA-809: When changing environments, change the url to log errors

See merge request raspberry/mobile/ios!160
2022-12-19 13:01:21 +03:00
Elena Nazarova 3521bb43b1 MALINKA-809: When changing environments, change the url to log errors 2022-12-19 12:43:37 +03:00
Jura Shikin e9a43c9595 Merge branch 'MALINKA-781/offer_to_buy_ram' into 'develop'
MALINKA-781: Offer to buy ram for eos

See merge request raspberry/mobile/ios!158
2022-12-19 12:36:59 +03:00
Elena Nazarova 9b9c1d9527 MALINKA-781: Offer to buy ram for eos 2022-12-19 12:36:59 +03:00
Jura Shikin fa176eaf9d Merge branch 'MALINKA-760/ram_contract' into 'develop'
MALINKA-760: Ram contract

See merge request raspberry/mobile/ios!159
2022-12-19 12:25:56 +03:00
Elena Nazarova 0e11479be6 MALINKA-760: Ram contract 2022-12-19 12:25:56 +03:00
Juraldinio 723abd03c6 Fix misstyped 2022-12-19 12:13:06 +03:00
Juraldinio a34e0f32d1 Build app 2022-12-18 17:56:34 +03:00
Juraldinio bcd58591ec Add unit tests to main application 2022-12-18 16:22:09 +03:00
Juraldinio 98fa0aa835 Fix crash application when no Hyperions or incorrect version 2022-12-17 23:37:59 +03:00
Juraldinio 029044058d Small improvements for speed in version parsing 2022-12-16 19:41:20 +03:00
Juraldinio 9ecda74db9 MALINKA-901 - fix cancelling and passing biometrics 2022-12-16 18:55:34 +03:00
Jura Shikin a985c7f524 Merge branch 'MALINKA-909/back_to_accounts' into 'develop'
MALINKA-909: Check if is on boarding

See merge request raspberry/mobile/ios!156
2022-12-16 16:20:43 +03:00
Elena Nazarova 5204f69be5 MALINKA-909: Check if is on boarding 2022-12-16 16:20:43 +03:00
Jura Shikin ef66ee2473 Merge branch 'MALINKA-476/fixes' into 'develop'
MALINKA-476: UI fixes

See merge request raspberry/mobile/ios!155
2022-12-16 14:51:14 +03:00
Elena Nazarova 95e9d937da MALINKA-476: UI fixes 2022-12-16 14:51:14 +03:00
Juraldinio f2a9d3ab97 MALINKA-904 - fix flashing resource setup 2022-12-16 13:20:53 +03:00
Juraldinio f118c3db53 MALINKA-896, MALINKA-901 - fix crash on loading app 2022-12-16 13:06:02 +03:00
Никита Чирухин 5a31865355 Merge branch 'MALINKA-868/to_welcome' into 'develop'
MALINKA-868: When no accounts show welcome screen

See merge request raspberry/mobile/ios!154
2022-12-16 11:06:52 +03:00
Elena Nazarova b8d24912a0 MALINKA-868: When no accounts show welcome screen 2022-12-16 11:06:52 +03:00
Juraldinio 80fbb82695 MALINKA-849 - show unaccepted accounts 2022-12-15 13:58:27 +03:00
Juraldinio fb55b3b5ba MALINKA-897 - save statuses 2022-12-15 13:50:52 +03:00
Juraldinio 296bacdf9a MALINKA-477 - add refresh after return to application 2022-12-15 13:14:52 +03:00
Juraldinio b77b486ab9 MALINKA-889 - remove unnecessary code 2022-12-15 12:54:09 +03:00
Juraldinio ccb332c7b6 MALINKA-477 - request on restore and on refresh 5 times 2022-12-15 00:39:42 +03:00
Juraldinio 87d399b6cf MALINKA-849 - fix flow with accounts 2022-12-15 00:32:57 +03:00
Juraldinio 711d3f474a MALINKA-858 - Correct show account type 2022-12-14 23:28:32 +03:00
Juraldinio 437e21b841 Remove Notifications. Use Combine version. 2022-12-14 23:16:59 +03:00
Juraldinio 822f39d103 Remove didChangeAccount notification from applciation 2022-12-14 22:10:49 +03:00
Juraldinio 8715aa5e0e MALINKA-890 - Rollback 2022-12-14 19:47:56 +03:00
Juraldinio 00677ab22c Remove warning 2022-12-14 18:59:43 +03:00
Juraldinio fdc037ad03 MALINKA-851 - reduce api calling 2022-12-14 18:32:58 +03:00
Juraldinio 9db17e992c MALINKA-890 - do not show empty account view 2022-12-14 18:32:58 +03:00
Jura Shikin 57f58be5aa Merge branch 'MALINKA-869/successful_deal' into 'develop'
MALINKA-869: Changed conditions of succesful deal

See merge request raspberry/mobile/ios!151
2022-12-14 18:32:01 +03:00
Elena Nazarova 18852a91a1 MALINKA-869: Changed conditions of succesful deal 2022-12-14 18:32:01 +03:00
Juraldinio 2ac95d779e MALINKA-352 - disable Delete button 2022-12-14 13:53:43 +03:00
Jura Shikin 36fbf8e9f2 Merge branch 'MALINKA-851/load_resources' into 'develop'
MALINKA-851: Load resources fix

See merge request raspberry/mobile/ios!150
2022-12-14 13:25:51 +03:00
Elena Nazarova a88ff0b35a MALINKA-851: Load resources fix 2022-12-14 13:25:51 +03:00
Jura Shikin 27076b15cd Merge branch 'MALINKA-868/welcome' into 'develop'
MALINKA-868: Added logic when account is being removed

See merge request raspberry/mobile/ios!148
2022-12-14 12:19:38 +03:00
Elena Nazarova e8646781b2 MALINKA-868: Added logic when account is being removed 2022-12-14 12:19:38 +03:00
Juraldinio e104b84db5 Restore copy Package.resolved 2022-12-14 01:02:28 +03:00
Juraldinio 5f4d969618 MALINKA-849 - create account 2022-12-14 00:58:40 +03:00
Juraldinio fe11ccf7d1 Remove warnings 2022-12-13 22:51:28 +03:00
Juraldinio 036f185194 Remove lint warnings 2022-12-13 22:46:16 +03:00
Juraldinio be2c6c1961 Remove warnings 2022-12-13 22:30:08 +03:00
Juraldinio 9abb34a4b4 MALINKA-849 - allow update on drag down 2022-12-13 20:28:07 +03:00
Juraldinio 2f47853a72 Remove copy Package.resolved 2022-12-13 20:10:48 +03:00
Juraldinio 5bce821cf0 Fix build Archive 2022-12-13 19:02:44 +03:00
Juraldinio 3885d5099c Fix calling UI changes on background thread 2022-12-13 16:01:41 +03:00
Juraldinio c317b22a93 Fix build script 2022-12-13 14:38:18 +03:00
Juraldinio 2b6277d3c6 Fix build before leaving Ventura OS 2022-12-13 14:32:16 +03:00
Juraldinio b502d2acd2 Correct scripts and restore Test stage 2022-12-13 09:50:37 +03:00
Juraldinio 55ab759b6d disable all lint rules for NetworkModelGraphQL 2022-12-12 21:42:41 +03:00
Juraldinio ada74ee74d Remove lint errors 2022-12-12 21:35:59 +03:00
Juraldinio e00d8feb4c Fix yaml after remove tests 2022-12-12 21:02:47 +03:00
Juraldinio 3480df6e5e Temporary remove tests 2022-12-12 21:01:25 +03:00
Juraldinio daf706eb36 MALINKA-858 - change private key 2022-12-12 19:19:40 +03:00
Juraldinio aae94ddc49 MALINKA-861 - partial. Remove update token after delete all 2022-12-08 16:45:07 +03:00
Juraldinio 967ec6f9d5 MALINKA-862 - Switch account to new created 2022-12-08 16:17:02 +03:00
Juraldinio 2a2bef0946 MALINKA-856 - fix PIN changes 2022-12-08 15:31:06 +03:00
Jura Shikin 3c082b5a97 Merge branch 'MALINKA-845/tabbar_fix' into 'develop'
MALINKA-827: tabbar fix

See merge request raspberry/mobile/ios!146
2022-12-08 13:34:35 +03:00
Elena Nazarova 6a9a488503 MALINKA-827: tabbar fix 2022-12-08 13:11:27 +03:00
Juraldinio c35b01614c MALINKA-849 - Fix check status for account 2022-12-08 12:55:47 +03:00
Juraldinio b61c219095 MALINKA-856 - fix saving private key 2022-12-07 21:06:43 +03:00
Juraldinio dbc7c4c0f7 MALINKA-850 - decrypt chat 2022-12-07 18:02:53 +03:00
Juraldinio 9f26d225c3 MALINKA-847 - fix auth biometrics 2022-12-07 14:33:40 +03:00
Juraldinio f3bf66d565 MALINKA-840 - fix dropping accounts across versions 2022-12-07 13:25:38 +03:00
Jura Shikin 1eff29e3b5 Merge branch 'feature/MALINKA-663/Incorrect_switch_to_free_transactions' into 'develop'
MALINKA-663: Reorganised logic of switching transactions type while buying RAM...

See merge request raspberry/mobile/ios!140
2022-12-06 23:40:16 +03:00
Андрей Геращенко 351e1f7997 MALINKA-663: Reorganised logic of switching transactions type while buying RAM... 2022-12-06 23:40:16 +03:00
Jura Shikin 671eb28e4f Merge branch 'feature/WalletStatus' into 'develop'
WiP

See merge request raspberry/mobile/ios!143
2022-12-06 23:00:21 +03:00
Jura Shikin ab8702c265 Wallet status 2022-12-06 23:00:21 +03:00
Jura Shikin 5fca5e7bab Merge branch 'MALINKA-732/autolayout_fixes' into 'develop'
MALINKA-732: Chat autolayout fix

See merge request raspberry/mobile/ios!141
2022-12-06 11:46:44 +03:00
Elena Nazarova 6b2444bfb4 MALINKA-732: Chat autolayout fix 2022-12-06 11:46:44 +03:00
Андрей Геращенко 5201889c76 Merge branch 'feature/MALINKA-726/RAM_error_popups' into 'develop'
Resolve MALINKA-726 Popups for errors on "Buy Ram" screen

See merge request raspberry/mobile/ios!138
2022-12-02 12:05:49 +03:00
Андрей Геращенко 2c4c5e858b Resolve MALINKA-726 Popups for errors on "Buy Ram" screen 2022-12-02 12:05:49 +03:00
Jura Shikin f474652e47 Merge branch 'feature/MALINKA-744/error_AutoLayout_creating_account' into 'develop'
MALINKA-744: Error with AutoLayout when creating an account

See merge request raspberry/mobile/ios!133
2022-12-01 11:53:49 +03:00
Никита Чирухин 0fbc8833a4 MALINKA-744: Error with AutoLayout when creating an account 2022-12-01 11:53:49 +03:00
Андрей Геращенко e56bab499f Merge branch 'MALINKA-747/release_cpu_autolayout' into 'develop'
MALINKA-747: Release/return cpu autolayout

See merge request raspberry/mobile/ios!135
2022-12-01 11:20:26 +03:00
Elena Nazarova fe531fb6c5 MALINKA-747: Release/return cpu autolayout 2022-12-01 11:20:26 +03:00
Elena Nazarova d9cc0d43c6 Merge branch 'MALINKA-791/cryptocash-release-button-hide' into 'develop'
MALINKA-791: hid cryptocash release button for list tokens

See merge request raspberry/mobile/ios!134
2022-11-30 18:23:50 +03:00
Elena Nazarova cf3872690b MALINKA-791: hid cryptocash release button for list tokens 2022-11-30 18:23:50 +03:00
Андрей Геращенко b02caf33f2 Merge branch 'feature/MALINKA-737/error_with_AutoLayout_account_resources_screen' into 'develop'
MALINKA-737: Error with AutoLayout on account resources screen

See merge request raspberry/mobile/ios!130
2022-11-29 14:03:13 +03:00
Никита Чирухин eb10cd8481 MALINKA-737: Error with AutoLayout on account resources screen 2022-11-29 14:03:12 +03:00
Никита Чирухин 2d3c32709f Merge branch 'feature/MALINKA-726/RAM_error_popups' into 'develop'
Resolve MALINKA-726 Fixed design for errors on bying ram

See merge request raspberry/mobile/ios!127
2022-11-24 21:36:54 +03:00
Андрей Геращенко f4bd507f08 Resolve MALINKA-726 Fixed design for errors on bying ram 2022-11-24 21:36:54 +03:00
Никита Чирухин 3cde524869 Merge branch 'MALINKA-751/autolayout_add_account' into 'develop'
MALINKA-751: Autolayout add token

See merge request raspberry/mobile/ios!129
2022-11-24 21:36:30 +03:00
Elena Nazarova c16d3bdd26 MALINKA-751: Autolayout add token 2022-11-24 21:36:30 +03:00
Jura Shikin e25df20907 Merge branch 'feature/MALINKA-770/URL_card_withdrawal' into 'develop'
Resolve MALINKA-770 "Feature//url card withdrawal"

See merge request raspberry/mobile/ios!128
2022-11-24 11:21:30 +03:00
Андрей Геращенко e27c464216 Resolve MALINKA-770 "Feature//url card withdrawal" 2022-11-24 11:21:30 +03:00
Андрей Геращенко a675ecfff2 Merge branch 'MALINKA-732/autolayout_sell' into 'develop'
MALINKA-732: Fixed autolayout warnings on sell screen

See merge request raspberry/mobile/ios!126
2022-11-24 10:56:21 +03:00
Elena Nazarova 70b04a31d8 MALINKA-732: Fixed autolayout warnings on sell screen 2022-11-24 10:56:21 +03:00
Jura Shikin 0620258f4f Merge branch 'feature/MALINKA-770/New_card_withdrawal' into 'master'
MALINKA-770: Added handle of wurl and wbtn in app settings

See merge request raspberry/mobile/ios!125
2022-11-24 09:36:08 +03:00
Андрей Геращенко fc465325e6 MALINKA-770: Added handle of wurl and wbtn in app settings 2022-11-24 09:36:08 +03:00
Elena Nazarova da3bcecc94 Merge branch 'feature/MALINKA-725/disable_raspberries_warranty_exchange' into 'develop'
MALINKA-725: Disable raspberries exchange

See merge request raspberry/mobile/ios!124
2022-11-22 11:18:06 +03:00
Никита Чирухин 2a6b269316 MALINKA-725: Disable raspberries exchange 2022-11-22 11:18:06 +03:00
Jura Shikin 8f2458b71c Merge branch 'feature/MALINKA-631/errors_with_AutoLayout' into 'develop'
MALINKA-631: Errors with AutoLayout

See merge request raspberry/mobile/ios!119
2022-11-22 08:49:20 +03:00
Никита Чирухин 2fac50e903 MALINKA-631: Errors with AutoLayout 2022-11-22 08:49:20 +03:00
Jura Shikin 2d7181942f Merge branch 'MALINKA-749/token_to_self' into 'develop'
MALINKA-749: Can't send token to self

See merge request raspberry/mobile/ios!123
2022-11-22 08:42:36 +03:00
Elena Nazarova 539484bf59 MALINKA-749: Can't send token to self 2022-11-22 08:42:35 +03:00
Jura Shikin 0b8685b9f9 Merge branch 'feature/MALINKA-726/RAM_error_popups' into 'develop'
Resolve MALINKA-726 "Feature//ram error popups"

See merge request raspberry/mobile/ios!121
2022-11-18 13:23:30 +03:00
Андрей Геращенко 99efa4cbe5 Resolve MALINKA-726 "Feature//ram error popups" 2022-11-18 13:23:29 +03:00
Никита Чирухин 3cc383b101 Merge branch 'feature/MALINKA-727/Sell_correct_ram_amount' into 'develop'
MALINKA-727: Fixed minimal amount of RAM to sell

See merge request raspberry/mobile/ios!120
2022-11-17 13:08:48 +03:00
Андрей Геращенко 37dc41ed18 MALINKA-727: Fixed minimal amount of RAM to sell 2022-11-17 13:08:47 +03:00
Андрей Геращенко 99701611f5 Merge branch 'MALINKA-664/resources_0' into 'develop'
MALINKA-664: fixed titles, better calculations

See merge request raspberry/mobile/ios!113
2022-11-17 12:16:19 +03:00
Elena Nazarova 4d49b7235b MALINKA-664: fixed titles, better calculations 2022-11-17 12:16:19 +03:00
Juraldinio d922c68639 MALINKA-736 - Headers for backend only for Malinka.life 2022-11-16 21:35:58 +03:00
Андрей Геращенко 1031e2b344 Merge branch 'feature/MALINKA-725/disable_raspberries_warranty_exchange' into 'develop'
MALINKA-725: Disable raspberries exchange

See merge request raspberry/mobile/ios!117
2022-11-16 15:24:36 +03:00
Никита Чирухин 6d06f8d928 MALINKA-725: Disable raspberries exchange 2022-11-16 15:24:36 +03:00
Juraldinio 14fb9a7fd5 Pass headers to backend 2022-11-16 13:58:39 +03:00
Jura Shikin 5d5804357e Merge branch 'feature/MALINKA-736/Settings' into 'develop'
MALINKA-736 - Upddate settings in application

See merge request raspberry/mobile/ios!118
2022-11-16 13:11:25 +03:00
Juraldinio e925d8045a MALINKA-736 - Upddate settings in application 2022-11-16 12:57:42 +03:00
Андрей Геращенко 4dc3315f4e Merge branch 'MALINKA-706/locale' into 'develop'
MALINKA-706: localized resources capacity title

See merge request raspberry/mobile/ios!115
2022-11-14 15:06:16 +03:00
Андрей Геращенко 26718f1619 Merge branch 'feature/MALINKA-631/errors_with_AutoLAyout' into 'develop'
MALINKA-631: Errors with AutoLayout

See merge request raspberry/mobile/ios!114
2022-11-14 13:53:17 +03:00
Никита Чирухин 6588a26a27 MALINKA-631: Errors with AutoLayout 2022-11-14 13:53:17 +03:00
Андрей Геращенко 3bde096265 Merge branch 'feature/MALINKA-709/tabbar_background_fix' into 'develop'
MALINKA-709: Tabbar background fix

See merge request raspberry/mobile/ios!116
2022-11-14 11:22:53 +03:00
Никита Чирухин 8d2c261e3b MALINKA-709: Tabbar background fix 2022-11-14 11:22:53 +03:00
Jura Shikin 34b695ed2d Merge branch 'MALINKA-697/resources_no_network_crash' into 'develop'
MALINKA-697: fix crash when no network

See merge request raspberry/mobile/ios!110
2022-11-11 12:24:17 +03:00
Elena Nazarova 812456d2a9 MALINKA-697: fix crash when no network 2022-11-11 12:24:17 +03:00
Elena Nazarova 541b97179e MALINKA-706: localized resources capacity title 2022-11-11 12:14:43 +03:00
Jura Shikin bd5c0f3add Merge branch 'feature/MALINKA-702/Forbid_zero_buy' into 'develop'
MALINKA-702: Fixed opportunity to buy zero RAM

See merge request raspberry/mobile/ios!111
2022-11-10 12:04:40 +03:00
Андрей Геращенко 38cd5a9e19 MALINKA-702: Fixed opportunity to buy zero RAM 2022-11-10 12:04:40 +03:00
Андрей Геращенко 10c65f0bc6 Merge branch 'MALINKA-688/resources_settings' into 'develop'
MALINKA-688: Back to resources setup

See merge request raspberry/mobile/ios!108
2022-11-09 17:50:16 +03:00
Elena Nazarova 5070a64f40 MALINKA-688: Back to resources setup 2022-11-09 17:50:16 +03:00
Никита Чирухин 6b7737895a Merge branch 'MALINKA-689/resources_error_title' into 'develop'
MALINKA-689: fix error title

See merge request raspberry/mobile/ios!109
2022-11-09 16:59:12 +03:00
Никита Чирухин ec04220aa0 Merge branch 'MALINKA-599/tabbar-2' into 'develop'
MALINKA-599: Tabbar fixes

See merge request raspberry/mobile/ios!107
2022-11-09 16:58:20 +03:00
Elena Nazarova 5d10b7b1f3 MALINKA-599: Tabbar fixes 2022-11-09 16:58:20 +03:00
Elena Nazarova bdf31e10f2 MALINKA-689: fix error title 2022-11-09 14:24:18 +03:00
Juraldinio 5345b2884b Improvements in optioal template 2022-11-08 12:28:22 +03:00
Juraldinio 28703c85c2 MALINKA-622 - fix crash
When not selected environment
2022-11-08 12:17:25 +03:00
Jura Shikin f417c23ffb Merge branch 'MALINKA-654/remove_unused_constants' into 'develop'
MALINKA-654: Remove unused constants

See merge request raspberry/mobile/ios!104
2022-11-08 11:58:52 +03:00
Никита Чирухин 00f8b69799 MALINKA-654: Remove unused constants 2022-11-08 11:58:52 +03:00
Juraldinio be6b3cd276 Rename enmironment to environment 2022-11-07 15:16:15 +03:00
Jura Shikin 1d2532b138 Merge branch 'MALINKA-601/chat-notifications' into 'develop'
MALINKA-601: unread messages update fix

See merge request raspberry/mobile/ios!100
2022-11-07 14:56:23 +03:00
Elena Nazarova ad18dcb1a1 MALINKA-601: unread messages update fix 2022-11-07 14:56:23 +03:00
Jura Shikin 58de513bc1 Merge branch 'MALINKA-664/resources' into 'develop'
MALINKA-664: Resources

See merge request raspberry/mobile/ios!103
2022-11-07 14:55:53 +03:00
Elena Nazarova 0e30d0da79 MALINKA-664: Resources 2022-11-07 14:55:53 +03:00
Jura Shikin 307e486e1f Merge branch 'feature/MALINKA-596/bug_with_screen_update' into 'develop'
MALINKA-596: Bug with screen update

See merge request raspberry/mobile/ios!102
2022-11-03 11:27:08 +03:00
Никита Чирухин b35b352abe MALINKA-596: Bug with screen update 2022-11-03 11:27:08 +03:00
Juraldinio ca504cb08b Up version to 1.3.0 2022-11-03 11:22:20 +03:00
Jura Shikin e4a7f8aa0c Merge branch 'feature/MALINKA-646/Memory_leaks' into 'develop'
MALINKA-646: Fixed navigation-affected memory leaks

See merge request raspberry/mobile/ios!97
2022-11-03 10:57:30 +03:00
Андрей Геращенко d782addf9f MALINKA-646: Fixed navigation-affected memory leaks 2022-11-03 10:57:30 +03:00
Jura Shikin b03c63bd9f Merge branch 'develop' into 'master'
Release 1.2.3

See merge request raspberry/mobile/ios!101
2022-11-02 19:27:30 +03:00
Juraldinio 85052c55f1 MALINKA-662 - fix “you deposited” description message 2022-11-02 15:15:09 +03:00
Juraldinio 464f1cc182 Version 1.2.3 2022-11-02 14:03:51 +03:00
Juraldinio cd9be1dbec MALINKA-595 - fix showswitcher for transaction kind 2022-11-02 14:03:01 +03:00
Jura Shikin 2f4c2edbd3 Merge branch 'MALINKA-590/chat-extra-back-arrow' into 'develop'
MALINKA-590: hide navigationItem if there is one already

See merge request raspberry/mobile/ios!96
2022-11-02 12:08:36 +03:00
Elena Nazarova c932f29f2a MALINKA-590: hide navigationItem if there is one already 2022-11-02 12:08:35 +03:00
Juraldinio 5cf6f2190e MALINKA-596 - fix UIRefreshControl behaviour 2022-11-01 22:46:52 +03:00
Juraldinio d4fa122f5d Update versino to 1.3.0 2022-11-01 15:56:38 +03:00
Jura Shikin cc428dcfcc Merge branch 'feature/MALINKA-653' into 'develop'
MALINKA-653 - Unit tests in project

See merge request raspberry/mobile/ios!98
2022-11-01 15:42:20 +03:00
Jura Shikin e3a6643eb0 MALINKA-653 - Unit tests in project 2022-11-01 15:42:20 +03:00
Jura Shikin 1b7a254106 Merge branch 'MALINKA-619/Bug_with_environment' into 'develop'
MALINKA-619: Bug with environment when installing the release version on top of the beta

See merge request raspberry/mobile/ios!95
2022-11-01 13:54:05 +03:00
Никита Чирухин 5a43fb1b79 MALINKA-619: Bug with environment when installing the release version on top of the beta 2022-11-01 13:54:05 +03:00
Jura Shikin 963cf15149 Merge branch 'MALINKA-588/chat-artifacts-update' into 'develop'
MALINKA-588: Chat and wallet cards artifacts

See merge request raspberry/mobile/ios!93
2022-10-31 15:15:01 +03:00
Elena Nazarova 5743654658 MALINKA-588: Chat and wallet cards artifacts 2022-10-31 15:15:01 +03:00
Juraldinio 8a072e714d Fix misstyped in builscript 2022-10-27 19:43:56 +03:00
Jura Shikin a132647daa Merge branch 'develop' into 'master'
Release 1.2.2

See merge request raspberry/mobile/ios!94
2022-10-27 19:26:54 +03:00
Juraldinio dc50893c99 Vestion 1.2.2 2022-10-27 17:46:16 +03:00
Juraldinio 0ee67de6aa MALINKA-626 - fix show tokens icons 2022-10-27 16:48:13 +03:00
Juraldinio 57a1d29dfc Update Realm to 10.32.1 2022-10-27 15:39:22 +03:00
Juraldinio 6b536d86da MALINKA-627 - remove comission 2022-10-27 13:59:26 +03:00
Juraldinio 4c50c2e30e MALINKA-626 - fix show tokens icons 2022-10-26 23:39:28 +03:00
Juraldinio b393626df9 MALINKA-626 - fix show tokens icons 2022-10-26 19:29:04 +03:00
Juraldinio cdf7aa27f6 Update to 1.3.0 version 2022-10-26 18:23:29 +03:00
Juraldinio 411eb9510f Correct builds. 2022-10-26 18:22:50 +03:00
Jura Shikin 55a36f48b7 Merge branch 'develop' into 'master'
Release 1.2.1

See merge request raspberry/mobile/ios!91
2022-10-26 18:07:09 +03:00
Juraldinio 00a8cfe1e5 Fix 1.2.1 correcting request table 2022-10-26 16:42:18 +03:00
Juraldinio 7fea607a40 MALINKA-613 - Correct builds for tag 2022-10-25 22:54:29 +03:00
Juraldinio 001c85357e Add show version in alert 2022-10-25 21:40:11 +03:00
Juraldinio 2db21f4632 Increase version to 1.3.0 2022-10-25 21:31:38 +03:00
Juraldinio 2c94a109c9 MALINKA-608 - When account locked we can’t change it 2022-10-25 21:29:50 +03:00
Jura Shikin dadb0ab265 Merge branch 'develop' into 'master'
Release 1.2.0

See merge request raspberry/mobile/ios!90
2022-10-25 19:22:19 +03:00
Juraldinio 3ef732f1f4 Small improvements on ExchangeCryptocashViewController 2022-10-24 21:17:17 +03:00
Juraldinio 4e22b4dcbf Merge branch 'feature/MALINKA-591' into develop 2022-10-24 19:02:36 +03:00
Juraldinio dc64504e60 MALINKA-591 - fix update info about resources 2022-10-24 19:02:23 +03:00
Juraldinio 98ea0ff49c Fix for build. Local version for Branch.io package. 2022-10-24 11:53:12 +03:00
Juraldinio bfcafdee43 Update Branch.io version dependency 2022-10-23 23:38:25 +03:00
Juraldinio e4e899d4d6 MALINKA-554 - Switch environments by Settings 2022-10-23 20:55:42 +03:00
Juraldinio 4b1062cc49 Forgotten changes in table names 2022-10-19 11:50:12 +03:00
Juraldinio a98e25177d Revert "Revert after check"
This reverts commit b14f23c52a.
2022-10-19 11:03:18 +03:00
Juraldinio b14f23c52a Revert after check 2022-10-19 09:44:03 +03:00
Juraldinio c20ad486c6 Contracts for preprod and small fixes in logic 2022-10-18 23:49:59 +03:00
Juraldinio 76a73b1841 Temporary Build 2022-10-17 18:55:04 +03:00
y.shikin 56f64b2226 Remove double quotes 2022-10-17 17:27:02 +03:00
Igor Lunev 333211f4d6 Update .gitlab-ci.yml file 2022-10-13 22:39:34 +03:00
Jura Shikin 41a2ca49d8 Merge branch 'feature/MALINKA-552/UI_bag_on_sell_buy_screen' into 'develop'
MALINKA-552: UI bug on the Sell/Buy screen

See merge request raspberry/mobile/ios!83
2022-10-13 12:36:27 +03:00
Никита Чирухин 432d814d88 MALINKA-552: UI bug on the Sell/Buy screen 2022-10-13 12:36:26 +03:00
Jura Shikin 6628cf639f Merge branch 'feature/MALINKA-524/error_not_enough_RAM_translat' into 'develop'
MALINKA-524: Error "Not enough RAM" in the Russian version of the application

See merge request raspberry/mobile/ios!81
2022-10-12 11:46:26 +03:00
Никита Чирухин f47a81fa89 MALINKA-524: Error "Not enough RAM" in the Russian version of the application 2022-10-12 11:46:26 +03:00
Juraldinio aeefedeafd Upload debug symbols to Crashlytics 2022-10-12 00:04:28 +03:00
Juraldinio 6211ffd317 MALINKA-548 - fix crashed on send 2022-10-11 15:44:04 +03:00
Juraldinio 76c283263e Change swap1 to swap 2022-10-11 15:09:28 +03:00
Jura Shikin 399c9225a8 Merge branch 'feature/MALINKA-550/empty_view_bag_on_buy_sell_screen' into 'develop'
MALINKA-550: The background on the "Sell" and "Buy" screens does not shift when updating

See merge request raspberry/mobile/ios!82
2022-10-10 15:29:42 +03:00
Никита Чирухин d9e970e0b8 MALINKA-550: The background on the "Sell" and "Buy" screens does not shift when updating 2022-10-10 15:29:42 +03:00
Juraldinio 42d672c84b MALINKA-513 - remove redirection to wockjabber.xyz 2022-10-09 22:25:52 +03:00
Juraldinio fae1e8241c MALINKA-513 - fix crash on buy button pressed 2022-10-07 22:17:45 +03:00
Juraldinio 85c1285ea4 MALINKA-528 - add crashlytics 2022-10-07 21:29:23 +03:00
Juraldinio f6706142a9 Fix crash on start after biometrics 2022-10-07 21:26:07 +03:00
Juraldinio 2d19546787 Fix hide banner buttons 2022-10-07 14:37:55 +03:00
Juraldinio 23b9e05807 Hide navigation bar on show history 2022-10-07 14:06:05 +03:00
Juraldinio ac605de375 Fix crash on USDT 2022-10-07 13:40:58 +03:00
Juraldinio e5b03b38de MALINKA-523 - disable progress statuses 2022-10-07 12:16:32 +03:00
Jura Shikin be92d693e0 Merge branch 'feature/PayCashSync' into 'develop'
Feature/pay cash sync

See merge request raspberry/mobile/ios!79
2022-10-06 16:17:16 +03:00
Jura Shikin 253a974f4a Feature/pay cash sync 2022-10-06 16:17:16 +03:00
Андрей Геращенко 34864ebb7b Merge branch 'MALINKA-505/extra_white_header' into 'develop'
MALINKA-505: Extra white header

See merge request raspberry/mobile/ios!75
2022-10-03 16:06:58 +03:00
Elena Nazarova 6fd7bf4b13 MALINKA-505: Extra white header 2022-10-03 16:06:58 +03:00
Elena Nazarova e6b096f6e6 Merge branch 'feature/MALINKA-503/AccountsList_artifacts' into 'develop'
MALINKA-503: Fixed artifacts on accounts screen opening

See merge request raspberry/mobile/ios!72
2022-10-03 15:17:57 +03:00
Jura Shikin 0fcd756add Merge branch 'MALINKA-508/chat_notifications' into 'develop'
MALINKA-508: if no accounts set message badge to zero

See merge request raspberry/mobile/ios!74
2022-09-30 21:43:54 +03:00
Jura Shikin ebd339af25 Merge branch 'MALINKA-509/remove-dash' into 'develop'
MALINKA-509: removed dash

See merge request raspberry/mobile/ios!73
2022-09-30 21:36:34 +03:00
Elena Nazarova a3b5fed569 MALINKA-508: if no accounts set message badge to zero 2022-09-30 19:39:00 +03:00
Elena Nazarova 973065a9ee MALINKA-509: removed dash 2022-09-30 13:50:18 +03:00
user ca0a0cb494 MALINKA-503: Fixed artifacts on accounts screen opening 2022-09-29 19:17:20 +03:00
Jura Shikin 6d5ce05b98 Merge branch 'MALINKA-479/Failed' into 'develop'
MALINKA-479: Wallet statuses UI

See merge request raspberry/mobile/ios!71
2022-09-29 15:09:19 +03:00
Elena Nazarova ee989e4aaa MALINKA-479: Wallet statuses UI 2022-09-29 15:09:19 +03:00
Андрей Геращенко 7c1e70da34 Merge branch 'feature/MALINKA-476/Loader' into 'develop'
MALINKA-476: Add loader before account created

See merge request raspberry/mobile/ios!70
2022-09-26 19:20:54 +03:00
Elena Nazarova 6b87a8fb91 MALINKA-476: Add loader before account created 2022-09-26 19:20:54 +03:00
Jura Shikin e03b2bfb73 Merge branch 'feature/MALINKA-470/artifact_QR_scan_camera' into 'develop'
MALINKA-470: Artifacts when scanning a QR code by the camera

See merge request raspberry/mobile/ios!68
2022-09-26 11:27:45 +03:00
Никита Чирухин 12659f02ba MALINKA-470: Artifacts when scanning a QR code by the camera 2022-09-26 11:27:44 +03:00
Jura Shikin 8af4141ec4 Merge branch 'feature/MALINKA-466/TRX_IN_CHAIN' into 'develop'
Resolve MALINKA-466 "Feature//trx in chain"

See merge request raspberry/mobile/ios!69
2022-09-23 21:35:57 +03:00
Jura Shikin 0ef27447e9 Resolve MALINKA-466 "Feature//trx in chain" 2022-09-23 21:35:57 +03:00
Jura Shikin 47a3c5aac0 Merge branch 'feature/MALINKA-452/searchfield_bug' into 'develop'
MALINKA-452: textfield resigns on changing selected section

See merge request raspberry/mobile/ios!66
2022-09-20 19:00:11 +03:00
Elena Nazarova 5468e093be MALINKA-452: textfield resigns on changing selected section 2022-09-20 19:00:11 +03:00
Jura Shikin 83feaac990 Merge branch 'feature/MALINKA-458/glitch_transfer_of_funds' into 'develop'
MALINKA-458: Artifacts when navigating from the Wallet screen

See merge request raspberry/mobile/ios!65
2022-09-19 19:20:00 +03:00
Никита Чирухин ea7b5f5fbd MALINKA-458: Artifacts when navigating from the Wallet screen 2022-09-19 19:20:00 +03:00
Jura Shikin 7d96161bd7 Merge branch 'feature/MALINKA-457/glitch_add_tokens' into 'develop'
MALINKA-457: Artefacts on the "Add Tokens" screen

See merge request raspberry/mobile/ios!64
2022-09-19 15:03:05 +03:00
Никита Чирухин 09944289f3 MALINKA-457: Artefacts on the "Add Tokens" screen 2022-09-19 15:03:05 +03:00
y.shikin 5129fad5ae Increase app version to 1.2.0 2022-09-15 17:55:49 +03:00
Jura Shikin e1f10f4044 Merge branch 'feature/MALINKA-419/animation_bag_my_applications' into 'develop'
MALINKA-419: Animation bug on My applications screen

See merge request raspberry/mobile/ios!63
2022-09-15 15:13:41 +03:00
Никита Чирухин 29b5f0ad80 MALINKA-419: Animation bug on My applications screen 2022-09-15 15:13:41 +03:00
Jura Shikin 6ec3a8ae1d Merge branch 'feature/MALINKA-366/loader_assemble_malinka' into 'develop'
MALINKA-366: hide loader then collect mlnk

See merge request raspberry/mobile/ios!59
2022-09-15 11:59:37 +03:00
Никита Чирухин bf9d0ec9bc MALINKA-366: hide loader then collect mlnk 2022-09-15 11:59:37 +03:00
Jura Shikin 8f1314c87a Merge branch 'feature/MALINKA-398/glitch_navigation_resources' into 'develop'
MALINKA-398: Artifacts on the "Resources" screen

See merge request raspberry/mobile/ios!57
2022-09-10 10:49:14 +03:00
Никита Чирухин df450b50b4 MALINKA-398: Artifacts on the "Resources" screen 2022-09-10 10:49:14 +03:00
Jura Shikin 18de060a8f Merge branch 'feature/MALINKA-399/glitch_navigation_currency_exchange' into 'develop'
MALINKA-399: Artifacts on the "Currency exchange" screen

See merge request raspberry/mobile/ios!58
2022-09-10 10:48:20 +03:00
Никита Чирухин 7939ab13cc MALINKA-399: Artifacts on the "Currency exchange" screen 2022-09-10 10:48:19 +03:00
Jura Shikin 5ef3124fa3 Merge branch 'feature/MALINKA-397/glitch_navigation_my_account' into 'develop'
MALINKA-397: Artifacts on the "My Accounts" screen

See merge request raspberry/mobile/ios!54
2022-09-09 21:58:23 +03:00
Никита Чирухин f0bdcbd69c MALINKA-397: Artifacts on the "My Accounts" screen 2022-09-09 21:58:23 +03:00
Jura Shikin 5a5477b852 Merge branch 'feature/MALINKA-389/error_text_fix' into 'develop'
MALINKA-389: Notes on the text of the error

See merge request raspberry/mobile/ios!53
2022-09-07 12:27:37 +03:00
Никита Чирухин 885dbe3067 MALINKA-389: Notes on the text of the error 2022-09-07 12:27:37 +03:00
Jura Shikin b73e135aa2 Merge branch 'develop' into 'master'
small fixes in yaml file

See merge request raspberry/mobile/ios!50
2022-08-31 15:29:53 +03:00
y.shikin 66aa113dd4 small fixes in yaml file 2022-08-31 15:28:43 +03:00
Jura Shikin 9dfa80bfb3 Merge branch 'develop' into 'master'
Release 1.1.0

See merge request raspberry/mobile/ios!49
2022-08-31 14:58:39 +03:00
y.shikin fe6656c3ed Fixes yaml 2022-08-31 14:50:10 +03:00
y.shikin c8b1372f6c Add notifications for release 2022-08-31 14:46:17 +03:00
Jura Shikin c748281e5e Merge branch 'develop' into 'master'
Release 1.1.0

See merge request raspberry/mobile/ios!48
2022-08-31 14:42:14 +03:00
y.shikin 13063262cb MALINKA-220 - fix localizations 2022-08-31 14:21:25 +03:00
y.shikin 47e79bc85c MALINKA-220 - correct texts 2022-08-31 14:21:25 +03:00
y.shikin a721bf8416 MALINKA-385 - disable button for prevent double navigate 2022-08-31 14:21:25 +03:00
Juraldinio 4feaba4305 Add device token field 2022-08-31 14:21:25 +03:00
Juraldinio 21a73e8da3 Add change environment in settings 2022-08-31 14:21:25 +03:00
Juraldinio e90cf92749 MALINKA-382 - add call device for check device status 2022-08-31 14:21:25 +03:00
Juraldinio a6dbebf835 MALINKA-380 - preprod and prduction environments 2022-08-31 14:21:25 +03:00
Juraldinio c830ffcf57 MALINKA-354 - add network layer for accountOrder request 2022-08-31 14:21:25 +03:00
Juraldinio e8764b76e2 Statuses for CreateAccount 2022-08-31 14:21:25 +03:00
Juraldinio bb91cdd763 Fix show settings only for beta, not prod 2022-08-31 14:21:25 +03:00
Juraldinio b272f32c55 add property to model 2022-08-31 14:21:25 +03:00
Juraldinio 7c7cd6d0ee Rewrite working with Keychain in application 2022-08-31 14:21:25 +03:00
Juraldinio 63f317dd70 Fix working with token on Simulators 2022-08-31 14:21:25 +03:00
Juraldinio ec51ecaab5 Add helper class for operate with Settings 2022-08-31 14:21:25 +03:00
Juraldinio 38a6d36c91 Some changes 2022-08-31 14:21:25 +03:00
Juraldinio 4a25a43792 Add ignorance while Create account 2022-08-31 14:21:25 +03:00
Juraldinio d03bd5353a Add device ignorance option 2022-08-31 14:21:25 +03:00
Juraldinio 34f67d4c0f Add settings for get device identity 2022-08-31 14:21:25 +03:00
Juraldinio 414c5bcb15 MALINKA-353 - handle order.id response. Prepare for make queries for check status. 2022-08-31 14:21:25 +03:00
Juraldinio e083211591 Draft for key operations 2022-08-31 14:21:25 +03:00
Juraldinio 0aea752053 Remove WalletKeys from project 2022-08-31 14:21:25 +03:00
Juraldinio 58b40ee976 Refactoring Wallet holder to Bank 2022-08-31 14:21:25 +03:00
Juraldinio 0af7900e7b Create wallet flow 2022-08-31 14:21:25 +03:00
Juraldinio 8f05b7ca62 Worked flow for create Account 2022-08-31 14:21:25 +03:00
Juraldinio 13a5d74cae Remove unused classes 2022-08-31 14:21:25 +03:00
Juraldinio cd3e0fa724 WiP with Wallet operations 2022-08-31 14:21:25 +03:00
Juraldinio aecf4c2547 Private and public keys and check wallet name 2022-08-31 14:21:25 +03:00
Juraldinio 89950e3024 Error message and flow for insert private keys 2022-08-31 14:21:25 +03:00
Juraldinio 0ac42cd017 Check device, validate token and show popup 2022-08-31 14:21:25 +03:00
Juraldinio 9181a8dfcb API for create wallet and check device 2022-08-31 14:21:25 +03:00
Juraldinio 70540cd7a2 Build main application 2022-08-31 14:21:25 +03:00
Juraldinio b2a41d5472 Create worked Package 2022-08-31 14:21:25 +03:00
Juraldinio 1672372e19 packages commits 2022-08-31 14:21:25 +03:00
Juraldinio 98598b6af1 Add local eosio-swift with 1.0.0 version 2022-08-31 14:21:24 +03:00
Андрей Геращенко 8ee24471de MALINKA-210: Changes design of wallet creation screen, added wallet creation... 2022-08-31 14:21:24 +03:00
Juraldinio 52a0ee9d01 Autobuild and Wallet API checker and creation 2022-08-31 14:21:24 +03:00
Juraldinio 2305b9cab2 MALINKA-336 - Register device service 2022-08-31 14:21:24 +03:00
Juraldinio 5b0260f004 MALINKA-336 - fix build 2022-08-31 14:21:24 +03:00
Juraldinio 83033ed052 MALINKA-336 - Device creation and validation 2022-08-31 14:21:24 +03:00
Андрей Геращенко 9b54501b19 MALINKA-344: Refactoring for realm 2022-08-31 14:21:24 +03:00
Juraldinio c2afd848bb Remove temporary debug strings 2022-08-31 14:21:24 +03:00
Juraldinio 23bd0102a0 Remove duplications after develop rebase 2022-08-31 14:21:24 +03:00
Juraldinio 940f64eaec Remove OnBoard navigation controller 2022-08-31 14:21:24 +03:00
Juraldinio c2783aff4c MALINKA-341 - wip 2022-08-31 14:21:24 +03:00
Juraldinio 08321a2e3d MALINKA-216 - create wallet 2022-08-31 14:21:24 +03:00
Juraldinio b34585a487 MALINKA-257: Fixed package paths
# Conflicts:
#	iOS/Packages/WalletFoundation/Package.swift
2022-08-31 14:21:24 +03:00
Juraldinio 3a3db75331 MALINKA-257: Added KeyChainAccess package
# Conflicts:
#	iOS/Packages/WalletFoundation/Sources/FCUUID/DeviceUUID.swift
2022-08-31 14:21:24 +03:00
Juraldinio 32d667d55a Change version to 1.1.0
# Conflicts:
#	iOS/project.yml
2022-08-31 14:21:24 +03:00
Juraldinio bba1824a08 MALINKA-340 - fix infinite call load info 2022-08-31 14:21:24 +03:00
Juraldinio 1b8fefdf5a Fixes for working with UUID in cloud
# Conflicts:
#	toolchain/generate.swift

# Conflicts:
#	iOS/project.yml
2022-08-31 14:21:24 +03:00
Juraldinio 85176b43f7 MALINKA-257: Fixed package paths
# Conflicts:
#	iOS/Packages/WalletFoundation/Package.swift
2022-08-31 14:21:24 +03:00
Juraldinio 6c45ec02f8 MALINKA-257: Added KeyChainAccess package
# Conflicts:
#	iOS/Packages/WalletFoundation/Sources/FCUUID/DeviceUUID.swift
2022-08-31 14:21:24 +03:00
Juraldinio 01c2aacc13 Increase marketing version
# Conflicts:
#	iOS/project.yml
2022-08-31 14:21:24 +03:00
Juraldinio 0676bcf49c MALINKA-340 - fix infinite call load info 2022-08-31 14:21:24 +03:00
Elena Nazarova 8f0568d2c6 MALINKA-317: update titles 2022-08-31 14:21:24 +03:00
Juraldinio aa3d10bd6b Try fix deploying 2022-08-31 14:21:24 +03:00
Juraldinio 73eb93c14a Remove warnings 2022-08-31 14:21:24 +03:00
Juraldinio 2b01f7f731 MALINKA-309 - show error for failed registration 2022-08-31 14:21:24 +03:00
Juraldinio 0a74026258 MALINKA-308 - Deeplinks refactoring. Open link after auth if exists. 2022-08-31 14:21:24 +03:00
Juraldinio a030c4d871 Remove temporary debug code 2022-08-31 14:21:24 +03:00
Juraldinio 19b064ae08 Update build yaml file for fix build 2022-08-31 14:21:24 +03:00
Juraldinio f62e58dc66 Fix generate UUID and add organization name 2022-08-31 14:21:24 +03:00
Juraldinio 3a69245723 Close setters to private 2022-08-31 14:21:24 +03:00
Juraldinio 911bfe19e9 REfactoring and commenting 2022-08-31 14:21:24 +03:00
Juraldinio 05e545d8f4 Huge refactoring 2022-08-31 14:21:24 +03:00
y.shikin ff4677166d Fixes for working with UUID in cloud
# Conflicts:
#	toolchain/generate.swift
2022-08-31 14:21:23 +03:00
user d97d742997 MALINKA-257: Added test uuid functionality 2022-08-31 14:20:08 +03:00
user 361b1cc036 MALINKA-257: Fixes for DeviceUUID and added icloud preferences into entitlements 2022-08-31 14:20:08 +03:00
user a7be38c96e MALINKA-257: Minor fixes 2022-08-31 14:20:08 +03:00
user 507f7fdf77 MALINKA-257: Fixed deprecation warnings 2022-08-31 14:20:08 +03:00
user 01b82ef107 MALINKA-257: Fixed dependencies 2022-08-31 14:20:08 +03:00
user 6177c88ff7 MALINKA-257: Fixed package paths 2022-08-31 14:20:08 +03:00
user a722dfb167 MALINKA-257: Added KeyChainAccess package 2022-08-31 14:20:08 +03:00
user 71927e48f5 MALINKA-257: Small refactoring and tiny fixes 2022-08-31 14:20:08 +03:00
user cdb5128eaf MALINKA-257: Added new keychain lib support 2022-08-31 14:20:08 +03:00
user afddc4d85c MALINKA-257: Added FCUUID class 2022-08-31 14:20:08 +03:00
Juraldinio 6cdad7b683 MALINKA-267 - GraphQL call registerDevice 2022-08-31 14:20:08 +03:00
Juraldinio 02bd64b558 Fix GraphQL models 2022-08-31 14:20:08 +03:00
Juraldinio 47beb2abcc Clean up warnings 2022-08-31 14:20:08 +03:00
Juraldinio fe941babc8 Remove unused code and fix DeviceCheckToken struct 2022-08-31 14:20:08 +03:00
Juraldinio 031fe5bf12 WiP 2022-08-31 14:20:08 +03:00
Juraldinio 638c3a072a WiP 2022-08-31 14:20:08 +03:00
Juraldinio 4cb2ed9ac1 WiP 2022-08-31 14:20:08 +03:00
Elena Nazarova db419691eb MALINKA-262: generating device token 2022-08-31 14:20:08 +03:00
Juraldinio 8310a9ff01 DeviceIdentity draft objc version 2022-08-31 14:20:08 +03:00
Juraldinio 5846017b26 Added SPM packages Foundation, Kit and Network 2022-08-31 14:20:08 +03:00
Juraldinio 5f96a45770 Rename to paycash 2022-08-31 14:20:08 +03:00
Juraldinio d39caa511d Add malinka property to servers 2022-08-31 14:20:08 +03:00
Juraldinio e1a1fa6307 Rename server links 2022-08-31 14:20:08 +03:00
Андрей Геращенко 19b95bc1d9 MALINKA-210: Added "Create wallet" screen 2022-08-31 14:20:08 +03:00
Juraldinio d85d7ae03d Apollo attach 2022-08-31 14:20:08 +03:00
Elena Nazarova 4ddd0b47f3 MALINKA-215: bug dismissing popup after successed transfer 2022-08-31 14:20:08 +03:00
Андрей Геращенко c0e6946740 MALINKA-199: Исправлено открытие контроллера с историей операций по криптотокену 2022-08-31 14:20:08 +03:00
Juraldinio b9bf6bcb8c WiP Package separation 2022-08-31 14:20:08 +03:00
y.shikin dcf25e6106 Change version to 1.1.0
# Conflicts:
#	iOS/project.yml
2022-08-31 14:20:07 +03:00
Juraldinio edfb491f3d For make release 2022-08-31 14:19:13 +03:00
Juraldinio 5c09962dbd Remove line for force new build 2022-08-16 12:07:08 +03:00
Juraldinio e2ef60e5fa Remove exit 2022-08-16 10:07:34 +03:00
Juraldinio 8feec718b1 Fix build for beta
# Conflicts:
#	toolchain/generate.swift
2022-08-16 10:02:33 +03:00
Juraldinio 3435a05910 fix echo yml 2022-08-16 10:01:27 +03:00
Juraldinio 13af51a1c1 Echo fix 2022-08-16 10:01:22 +03:00
Juraldinio e31978d529 Fix yml 2022-08-16 10:01:19 +03:00
Juraldinio da7af84c8f Add version for gitlab logs 2022-08-16 10:01:15 +03:00
Juraldinio ebc519cbaf Choose schedule 2022-08-16 10:01:10 +03:00
Juraldinio 0aa9cfae03 Add schedule for iOS build 2022-08-16 10:01:01 +03:00
Juraldinio 623a9df19d Add loader and hide how news 2022-08-16 09:53:13 +03:00
Juraldinio 82348e8f06 MALINKA-340 - fix infinite call load info
# Conflicts:
#	iOS/project.yml
2022-08-15 23:10:51 +03:00
Juraldinio bf7b2997b4 Merge branch 'release102' 2022-08-09 12:16:31 +03:00
Juraldinio 023793926c Develop and master - manual, tag - auto 2022-07-28 10:55:50 +03:00
Juraldinio ef96484dbb Upload to upstore manual 2022-07-28 10:50:44 +03:00
Juraldinio 914c7a2c1f MALINKA-236 - enable spaw and set 1.0.2 version 2022-07-28 10:33:56 +03:00
Juraldinio 6e5f0aacd1 Increase marketing version
# Conflicts:
#	iOS/project.yml
2022-07-28 10:33:54 +03:00
Juraldinio bb588d8e71 Update version to 1.0.1 2022-07-25 15:04:45 +03:00
Juraldinio ca2a6888f7 Remove warnings 2022-07-25 11:26:14 +03:00
Juraldinio 624abcf187 Add malinka.life domain to secure 2022-07-25 11:26:14 +03:00
Jura Shikin 14e5797425 Merge branch 'feature/MALINKA-193/localization_eng' into 'develop'
MALINKA-193: eng localization for working with card recognition

See merge request raspberry/ios!35
2022-07-22 11:06:41 +03:00
Elena Nazarova d07e38b571 MALINKA-193: eng localization for working with card recognition 2022-07-22 11:06:41 +03:00
Jura Shikin e064f72e53 Merge branch 'feature/MALINKA-194/Correct_translation' into 'develop'
MALINKA-194: Поправил локализацию

See merge request raspberry/ios!34
2022-07-22 11:06:12 +03:00
Андрей Геращенко 2e358c49cc MALINKA-194: Поправил локализацию 2022-07-22 11:06:11 +03:00
Juraldinio 4d62bd64f8 Change application ID in AppStore + Linting 2022-07-22 10:47:18 +03:00
Jura Shikin 4c1df2c6ab Merge branch 'feature/MALINKA-179/hide_swap_inheritance' into 'develop'
MALINKA-179: hide SWAP segment in Inheritance

See merge request raspberry/ios!29
2022-07-21 13:47:49 +03:00
Elena Nazarova 518fe9194c MALINKA-179: hide SWAP segment in Inheritance 2022-07-21 13:47:49 +03:00
Juraldinio 7db887675e Update vps working 2022-07-21 11:42:59 +03:00
Juraldinio 6da443dbdc Upload with VPN 2022-07-21 11:04:47 +03:00
Elena Nazarova a4c3f85e9d Merge branch 'feature/MALINKA-176/Hide_swap_buttons' into 'develop'
MALINKA-176: Added functionality for hiding swap button on Wallet screens

See merge request raspberry/ios!33
2022-07-21 10:46:42 +03:00
Андрей Геращенко fde17328ff MALINKA-176: Added functionality for hiding swap button on Wallet screens 2022-07-21 10:46:42 +03:00
Juraldinio 7eec4ed44b MALINKA-181 - notification 2022-07-21 09:41:07 +03:00
Juraldinio de2f4d8e8d MALINKA-181 - Update notification config 2022-07-20 19:53:29 +03:00
Jura Shikin 5b88e2c763 Merge branch 'feature/MALINKA-180/Appsettings_2' into 'develop'
MALINKA-180: Переделал получение параметра is_swap_active

See merge request raspberry/ios!32
2022-07-20 15:46:56 +03:00
Андрей Геращенко feb95933f0 MALINKA-180: Переделал получение параметра is_swap_active 2022-07-20 15:46:55 +03:00
Jura Shikin c4632cd65b Merge branch 'feature/MALINKA-177/hide_currency_exchange' into 'develop'
MALINKA-177: hide currency exchange

See merge request raspberry/ios!30
2022-07-20 15:37:37 +03:00
Elena Nazarova 05bcdfadfc MALINKA-177: hide currency exchange 2022-07-20 15:37:37 +03:00
Jura Shikin 9586acb576 Merge branch 'feature/MALINKA-178/hide_more_options' into 'develop'
MALINKA-178: hide 2 options from 'more'

See merge request raspberry/ios!28
2022-07-20 12:38:14 +03:00
Elena Nazarova 8a8cece7bc MALINKA-178: hide 2 options from 'more' 2022-07-20 12:38:14 +03:00
Elena Nazarova 8b2cb5ea53 Merge branch 'feature/MALINKA-180/Appsettings_isSwapActive' into 'develop'
MALINKA-180: Added swap functionality switching option

See merge request raspberry/ios!31
2022-07-20 12:15:40 +03:00
Андрей Геращенко 7f6df3c484 MALINKA-180: Added swap functionality switching option 2022-07-20 12:15:40 +03:00
Jura Shikin 85da277713 Merge branch 'feature/slack_emoji' into 'develop'
avocado -> raspberry

See merge request raspberry/ios!27
2022-07-19 15:18:17 +03:00
Juraldinio 68f466c6c0 Correct APPLICATION_SCHEME for build 2022-07-19 15:09:34 +03:00
Elena Nazarova d071af0536 avocado -> raspberry 2022-07-19 15:08:43 +03:00
Juraldinio c81e365ab6 Fix sign identity 2022-07-19 14:58:12 +03:00
Juraldinio 83f59087cc MALINKA-172 - Fix application scheme 2022-07-19 14:52:52 +03:00
Jura Shikin 713409aa54 Merge branch 'feature/MALINKA-174/about_app_text' into 'develop'
MALINKA-174: change about app text

See merge request raspberry/ios!26
2022-07-19 14:50:30 +03:00
Elena Nazarova edfa8e16b0 MALINKA-174: change about app text 2022-07-19 14:50:30 +03:00
Juraldinio 753cb4ac60 MALINKA-172 - Fix xcodeproj name 2022-07-19 14:45:20 +03:00
Juraldinio e14baf9589 MALINKA-172 - add BETA build type 2022-07-19 14:41:55 +03:00
Juraldinio a08b9e8eec MALINKA-172 - Change credentials for build app 2022-07-19 13:05:16 +03:00
Juraldinio b6758613b0 MALINKA-102 - fixes 2022-06-19 20:45:28 +03:00
Jura Shikin 97c68b96bf Merge branch 'feature/ios/MALINKA-73/Search_translation' into 'develop'
MALINKA-73: Исправил перевод в поиске по учетным записям

See merge request raspberry/ios!25
2022-06-15 11:01:30 +03:00
Андрей Геращенко b504642824 MALINKA-73: Исправил перевод в поиске по учетным записям 2022-06-15 11:01:30 +03:00
Juraldinio 5d7ebab22c Revert "Kalinka instead Malinka"
This reverts commit a51048aaa6.
2022-06-14 16:14:19 +03:00
Juraldinio a51048aaa6 Kalinka instead Malinka 2022-06-14 14:59:41 +03:00
Jura Shikin 1cc76eb17b Merge branch 'feature/ios/MALINKA-96/dev_14_06' into 'develop'
Resolve MALINKA-96 Правки с дева 14.06

See merge request raspberry/ios!24
2022-06-14 12:56:24 +03:00
Андрей Геращенко 89e7eb651f Resolve MALINKA-96 Правки с дева 14.06 2022-06-14 12:56:24 +03:00
Jura Shikin a78c3a0734 Merge branch 'feature/ios/MALINKA-84/Missed_commit' into 'develop'
MALINKA-84: Утраченные правки

See merge request raspberry/ios!23
2022-06-14 12:18:03 +03:00
Андрей Геращенко 84a3388d7c MALINKA-84: Утраченные правки 2022-06-14 12:18:03 +03:00
Jura Shikin 40e0d840d2 Merge branch 'feature/ios/MALINKA-84/dev_10_06' into 'develop'
MALINKA-84: Синхронизация веток сторонних разработчиков

See merge request raspberry/ios!22
2022-06-10 22:46:45 +03:00
Андрей Геращенко a1fc40f5dd MALINKA-84: Синхронизация веток сторонних разработчиков 2022-06-10 22:46:45 +03:00
Jura Shikin 9aebbdc611 Merge branch 'feature/MALINKA-70/Confirmation_title' into 'develop'
MALINKA-70: Allocating to another account titles

See merge request raspberry/ios!21
2022-06-08 10:59:16 +03:00
Elena Nazarova cac0c6bccd MALINKA-70: Allocating to another account titles 2022-06-08 10:59:16 +03:00
Juraldinio 4383d39d60 Hack for prevent infinite loading 2022-06-07 20:22:34 +03:00
Juraldinio 86a75f9b31 Remove cycle updating balance 2022-06-07 14:19:15 +03:00
Jura Shikin 2c90971314 Merge branch 'feature/MALINKA-72/Fix_add_card_button' into 'develop'
Resolve MALINKA-72 Исправлено поведение кнопки добавления банковской карточки

See merge request raspberry/ios!20
2022-06-03 21:00:57 +03:00
Андрей Геращенко 52fcd915bc Resolve MALINKA-72 Исправлено поведение кнопки добавления банковской карточки 2022-06-03 21:00:57 +03:00
Juraldinio 327d0393ad misstyping 2022-06-03 18:40:08 +03:00
Juraldinio c713149e06 Add defaults 2022-06-03 18:39:12 +03:00
Juraldinio a96e49181e Build with sniffing rights 2022-06-03 18:09:21 +03:00
Jura Shikin eed8f9c719 Merge branch 'feature/MALINKA-62/Remove_Paycash_mentions' into 'develop'
MALINKA-62: Удалил упоминания и логотипы пейкеша

See merge request raspberry/ios!19
2022-06-03 16:55:25 +03:00
Андрей Геращенко cb24ac2b29 MALINKA-62: Удалил упоминания и логотипы пейкеша 2022-06-03 16:55:25 +03:00
Jura Shikin 3dcee588b1 Merge branch 'feature/MALINKA-72/Исправить_перевод' into 'develop'
MALINKA-72: Исправил перевод

See merge request raspberry/ios!17
2022-06-02 14:45:28 +03:00
Андрей Геращенко 15ca62f7eb MALINKA-72: Исправил перевод 2022-06-02 14:45:28 +03:00
Андрей Геращенко 7da567d728 Merge branch 'feature/MALINKA-70/Confirmation_title' into 'develop'
MALINKA-70: Confirmation titles

See merge request raspberry/ios!15
2022-06-02 11:21:29 +03:00
Elena Nazarova 94f657c1e0 MALINKA-70: Confirmation titles 2022-06-02 11:21:29 +03:00
Jura Shikin ba6952712a Merge branch 'fetature/MALINKA-67/updateToDev_2' into 'develop'
MALINKA-67: Перенес коммиты поверх дева

See merge request raspberry/ios!16
2022-06-02 10:36:43 +03:00
Андрей Геращенко 235fadcc4c MALINKA-67: Перенес коммиты поверх дева 2022-06-02 10:36:43 +03:00
Андрей Геращенко 98f4b316fb Merge branch 'feature/MALINKA-68/Welcome_text' into 'develop'
MALINKA-68: Welcome titles

See merge request raspberry/ios!14
2022-06-01 13:58:01 +03:00
Elena Nazarova 75007f8c29 MALINKA-68: Welcome titles 2022-06-01 13:58:01 +03:00
Андрей Геращенко 43c1d95a6b Merge branch 'feature/MALINKA-16/About_app' into 'develop'
MALINKA-16: Paycash -> Malinka

See merge request raspberry/ios!13
2022-06-01 13:23:34 +03:00
Elena Nazarova 523ceff41e MALINKA-16: Paycash -> Malinka 2022-06-01 13:23:34 +03:00
Juraldinio 29086ab27a Change branch key for beta 2022-05-31 17:40:58 +03:00
Jura Shikin 436f284a8d Merge branch 'feature/MALINKA-11/Hide_buying_wallet' into 'develop'
MALINKA-11: Buying functionality is hidden

See merge request raspberry/ios!12
2022-05-31 17:36:11 +03:00
Jura Shikin 4f9328cb9e Merge branch 'feature/MALINKA-3/Цвета' into 'develop'
MALINKA-3: Цвета

See merge request raspberry/ios!11
2022-05-31 17:24:19 +03:00
Elena Nazarova fc9e3f18f2 MALINKA-3: Цвета 2022-05-31 17:24:19 +03:00
Андрей Геращенко d239e1dbf0 MALINKA-11: Buying functionality is hidden 2022-05-31 16:27:48 +03:00
Juraldinio 4bf39173be MALINKA-53 - Change hadrcoded link 2022-05-31 12:09:22 +03:00
Juraldinio 87f68e0ce5 After merge fixes 2022-05-31 12:02:09 +03:00
Juraldinio 73e1fad281 Sync develop at 30052022. 2022-05-30 23:45:32 +03:00
Juraldinio 0a69e0cbc9 Update gitignore 2022-05-30 14:53:56 +03:00
Juraldinio 4ea440f084 Add new Branch.io 2022-05-30 13:24:28 +03:00
Juraldinio 3f5c20dc18 Remove and add ignores 2022-05-30 12:57:35 +03:00
2334 changed files with 397646 additions and 49875 deletions
+5
View File
@@ -93,4 +93,9 @@ iOSInjectionProject/
Info.plist
/PayCash.entitlements
/PayCash.xcodeproj
/ResultIPA/
*.pbxproj
*.xcscheme
*.xcodeproj
*.entitlements
/iOS/Assets/Settings.bundle
+34 -18
View File
@@ -1,5 +1,6 @@
# Execute always
before_script:
- echo $CI_PIPELINE_IID
- echo $GITLAB_USER_ID
- echo ${CI_COMMIT_REF_SLUG}
- id
@@ -9,13 +10,12 @@ variables:
stages:
- lint
# - test
- test
- build
- deploy
- notification
# Linting stage
.linting: &linting
tags:
- malinka
@@ -34,6 +34,25 @@ Lint:
- /^feature/
- merge_requests
# Testing stage
.testing: &testing
tags:
- malinka
stage: test
when: always
allow_failure: false
script:
- chmod +x ./toolchain/testing.sh
- ./toolchain/testing.sh
Test:
<<: *testing
only:
- develop
- /^bugfix/
- /^feature/
- merge_requests
# Build IPA
.BuildIPA: &BuildIPA
tags:
@@ -44,23 +63,20 @@ Lint:
script:
- rm -rf ./ResultIPA
- chmod +x ./toolchain/build_iOS.sh
# - ./toolchain/build_iOS.sh PRODUCTION
- ./toolchain/build_iOS.sh BETA
- ./toolchain/build_iOS.sh ${APPLICATION_DEPLOY_TYPE} ${APPLICATION_NAME} ${APPLICATION_SCHEME_NAME}
# Build stage
build:
stage: build
<<: *BuildIPA
only:
- master
- develop
only:
- tags
- merge_requests
- develop
artifacts:
paths:
- ./ResultIPA
expire_in: 3 days
expire_in: 2 days
buildLeaf:
stage: build
@@ -85,7 +101,7 @@ buildLeaf:
IPA_PATH: ./ResultIPA
script:
- chmod +x ./toolchain/deploy_iOS.sh
- ./toolchain/deploy_iOS.sh ${IPA_DEPLOY_TARGET} ${IPA_PATH}
- ./toolchain/deploy_iOS.sh ${IPA_DEPLOY_TARGET} ${IPA_PATH} ${APPLICATION_SCHEME_NAME}
needs:
- job: build
artifacts: true
@@ -95,9 +111,8 @@ deploy:
<<: *DeployIPA
when: on_success
only:
- master
- develop
- tags
- develop
needs:
- job: build
artifacts: true
@@ -122,28 +137,29 @@ deployLeaf:
script:
- chmod +x ./toolchain/slack_notification.sh
- ./toolchain/slack_notification.sh ${BUILD_RESULT}
needs:
- job: deploy
# Develop
FailureNotification:
variables:
BUILD_RESULT: "FAILURE"
when: on_failure
only:
- master
- develop
- tags
- develop
<<: *slacknotification
needs:
- job: deploy
SuccessNotification:
variables:
BUILD_RESULT: "SUCCESS"
when: on_success
only:
- master
- develop
- tags
- develop
<<: *slacknotification
needs:
- job: deploy
# Merge checks
+32
View File
@@ -0,0 +1,32 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "WalletFoundation",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "WalletFoundation",
targets: ["WalletFoundation"]),
],
dependencies: [
.package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "WalletFoundation",
dependencies: ["KeyChainAccess"],
path: "./Sources"),
.testTarget(
name: "WalletFoundationTests",
dependencies: ["WalletFoundation"],
path: "Tests"//, // Test files
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
)
]
)
@@ -0,0 +1,85 @@
//
// ApplicationSettings.swift
//
//
// Created by Juraldinio on 8/28/22.
//
import Foundation
fileprivate enum ApplicationSettingsKeys {
// TODO: - Need remove after 1.4.0 version
static let ignoreDeviceSatusKeyOld = "Settings.device_ignore"
static let ignoreCreateAccountSatusKeyOld = "Status.createAccount_ignore"
static let isNetworkLogEnabledKeyOld = "Settings.network_copy"
static let isDeviceTokenCopyEnabledKeyOld = "Settings.device_token_copy_enable"
static let apiEnvironmentKeyOld = "Settings.api_environment"
static let apiPaycashEnvironmentKeyOld = "Settings.api_paycash_environment"
static let deviceDescriptionKeyOld = "Settings.device_id"
static let deviceTokenKeyOld = "Settings.device_token"
/// Ignore device status on register flow
static let ignoreDeviceSatusKey = "Settings.application.device_ignore"
/// Ignore create account status on register flow
static let ignoreCreateAccountSatusKey = "Status.application.createAccount_ignore"
/// log all network activity
static let isNetworkLogEnabledKey = "Settings.application.network_copy"
/// Copy device token to Settings fields
static let isDeviceTokenCopyEnabledKey = "Settings.application.device_token_copy_enable"
/// Field in Settings for device description
static let deviceDescriptionKey = "Settings.application.device_id"
/// Field in Settings for device token
static let deviceTokenKey = "Settings.application.device_token"
static let apiUsernameEnvironmentKey = "Settings.application.environment.usernames"
static let apiBackendEnvironmentKey = "Settings.application.environment.backend"
static let otherEnvironmentKey = "Settings.application.environment.other"
static let smartEnvironmentKey = "Settings.application.environment.smart"
}
public enum ApplicationSettings {
public static var ignoreDeviceSatus: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKey) }
public static var ignoreCreateAccountSatus: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKey) }
public static var isNetworkLogEnabled: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKey) }
public static var isDeviceTokenCopyEnabled: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKey) }
public static func device(description: String) { UserDefaults.standard.set(description, forKey: ApplicationSettingsKeys.deviceDescriptionKey) }
public static func device(token: String) { UserDefaults.standard.set(token, forKey: ApplicationSettingsKeys.deviceTokenKey) }
public static var apiUsernameEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.apiUsernameEnvironmentKey) }
public static var apiBackendEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.apiBackendEnvironmentKey) }
public static var otherEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.otherEnvironmentKey) }
public static var smartsEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.smartEnvironmentKey) }
public static func clearApiEmvironment() {
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceDescriptionKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceTokenKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiUsernameEnvironmentKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiBackendEnvironmentKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.otherEnvironmentKey)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.smartEnvironmentKey)
// TODO: - Need remove after 1.4.0 version
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiEnvironmentKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiPaycashEnvironmentKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceDescriptionKeyOld)
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceTokenKeyOld)
}
}
@@ -0,0 +1,26 @@
//
// CommonKey.swift
//
//
// Created by Juraldinio on 11/25/22.
//
import Foundation
public struct CommonKey: RawRepresentable, Hashable {
public var rawValue: String
public init?(rawValue: String) {
self.rawValue = rawValue
}
public init(_ rawValue: String) {
self.rawValue = rawValue
}
public func with(_ suffix: String) -> Self { .init(rawValue + "." + suffix) }
public static func key(_ key: String) -> Self { .init(key) }
public static func key(_ key: String, suffix: String) -> Self { Self.key(key).with(suffix) }
}
@@ -0,0 +1,191 @@
//
// WalletKeychain.swift
//
//
// Created by Juraldinio on 11/27/22.
//
import Foundation
import LocalAuthentication
extension String {
fileprivate static let password = "PWD"
fileprivate static let biometric = "BIO"
}
final public class WalletKeychain {
public typealias Key = CommonKey
public static let instance = WalletKeychain()
// MARK: - Init
private init() { }
// MARK: - Interface
public func exist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(.password)) }
public func bioExist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(.biometric) ) }
public subscript(biometric key: Key) -> String? {
self.loadBiometricProtected(key: key.with(.biometric))
.map({ String(data: $0, encoding: .utf8) }) ?? nil
}
public subscript(_ key: Key, password password: String) -> String? {
get { loadPassProtected(key: key.with(.password), password: password).map { String(data: $0, encoding: .utf8) } ?? nil }
set { update(key, password: password, newValue: newValue) }
}
// MARK: - Private
private func getPwdSecAccessControl() -> SecAccessControl {
var access: SecAccessControl?
var error: Unmanaged<CFError>?
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .applicationPassword, &error)
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
return access! // swiftlint:disable:this force_unwrapping
}
@discardableResult
private func setPassProtected(key: Key, data: String, password: String) -> Bool {
let context = LAContext()
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
let query = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key.rawValue,
kSecAttrAccessControl as String: getPwdSecAccessControl(),
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
kSecUseAuthenticationContext: context
] as CFDictionary
let status: OSStatus = SecItemAdd(query, nil)
if status == errSecSuccess {
return true
} else if status == errSecDuplicateItem {
if removeProtected(key: key) {
return setPassProtected(key: key, data: data, password: password)
} else {
return false
}
} else {
return false
}
}
private func loadPassProtected(key: Key, password: String) -> Data? {
let context = LAContext()
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecReturnData as String: kCFBooleanTrue!,
kSecAttrAccessControl as String: getPwdSecAccessControl(),
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationContext as String: context,
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
]
var dataTypeRef: AnyObject?
let result = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if result == noErr,
let value = dataTypeRef as? Data {
return value
}
return nil
}
// MARK: - Biometric entries
private func getBiometricSecAccessControl() -> SecAccessControl {
var access: SecAccessControl?
var error: Unmanaged<CFError>?
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, &error)
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
return access! // swiftlint:disable:this force_unwrapping
}
@discardableResult
private func setBiometricEntry(key: Key, data: String) -> Bool {
let query = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: key.rawValue,
kSecAttrAccessControl as String: getBiometricSecAccessControl(),
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
] as CFDictionary
let status: OSStatus = SecItemAdd(query, nil)
if status == errSecSuccess {
return true
} else if status == errSecDuplicateItem {
if removeProtected(key: key) {
return setBiometricEntry(key: key, data: data)
} else {
return false
}
} else {
return false
}
}
@discardableResult
private func loadBiometricProtected(key: Key) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecReturnData as String: kCFBooleanTrue as Any,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseOperationPrompt as String: "Access your data"
]
var dataTypeRef: AnyObject?
return SecItemCopyMatching(query as CFDictionary, &dataTypeRef) == noErr ? dataTypeRef as? Data : nil
}
// MARK: -
private func update(_ key: Key, password: String, newValue: String?) {
if let value = newValue {
setPassProtected(key: key.with(.password), data: value, password: password)
setBiometricEntry(key: key.with(.biometric), data: value)
} else {
removeProtected(key: key.with(.password))
removeProtected(key: key.with(.biometric))
}
}
@discardableResult
private func removeProtected(key: Key) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue
]
return SecItemDelete(query as CFDictionary) == noErr
}
private func checkProtectedExist(key: Key) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key.rawValue,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
]
let status = SecItemCopyMatching(query as CFDictionary, nil)
switch status {
case errSecSuccess, errSecInteractionNotAllowed: return true
case errSecItemNotFound: return false
default: return false
}
}
}
@@ -0,0 +1,54 @@
//
// Settings.swift
//
//
// Created by Juraldinio on 11/29/22.
//
import Foundation
final public class Settings {
public static let shared = Settings()
private init() {}
public func contains(_ key: CommonKey) -> Bool { defaults.value(forKey: key.rawValue) != nil }
public subscript<T>(_ key: CommonKey) -> T? {
get { defaults.value(forKey: key.rawValue) as? T }
set { set(value: newValue, for: key) }
}
public subscript<T: Codable>(_ key: CommonKey) -> [T]? {
get {
let decoder = JSONDecoder()
guard let data = defaults.value(forKey: key.rawValue) as? Data,
let value = try? decoder.decode([T].self, from: data) else { return nil }
return value
}
set {
let encoder = JSONEncoder()
guard let value = newValue,
let data = try? encoder.encode(value) else { return }
set(value: data, for: key)
}
}
public subscript(_ key: CommonKey) -> Bool {
get { defaults.bool(forKey: key.rawValue) }
set { set(value: newValue, for: key) }
}
public subscript(_ key: CommonKey) -> Int {
get { (defaults.value(forKey: key.rawValue) as? Int) ?? 0 }
set { set(value: newValue, for: key) }
}
private func set(value: Any?, for key: CommonKey) {
defaults.setValue(value, forKey: key.rawValue)
defaults.synchronize()
}
private var defaults: UserDefaults { UserDefaults.standard }
}
@@ -0,0 +1,55 @@
//
// DeviceToken.swift
//
//
// Created by NUT.Tech on 02.08.2022.
//
import Foundation
import DeviceCheck
import UIKit
public enum DeviceCheckTokenError: Error {
case notSupported
case generation
}
public struct DeviceCheckToken {
public let token: String
public let isSimulator: Bool
public static func generate() async -> Result<DeviceCheckToken, DeviceCheckTokenError> {
let token: String
let isSimulator: Bool
#if targetEnvironment(simulator)
isSimulator = true
token = tokenConstant
#else
let device = DCDevice.current
guard device.isSupported else { return .failure(.notSupported) }
guard let data = try? await device.generateToken() else { return .failure(.generation) }
isSimulator = false
token = data.base64EncodedString()
#endif
if ApplicationSettings.isDeviceTokenCopyEnabled {
ApplicationSettings.device(token: token)
}
return .success(DeviceCheckToken(token: token, isSimulator: isSimulator))
}
}
#if targetEnvironment(simulator)
fileprivate let tokenConstant = """
AgAAAIArgRs5gVfPAEHSra0ejd0EUNk0+me89vLfv5ZingpyOOkgXXXyjPzYTzWmWSu+BYqcD47byirLZ++3dJccpF99hWppT7G5xAuU+y56WpSYsATWySfuxbSSMT9JSoOWz4QtiDmmVmUHbzCfHbTP3Tr3hsG+86KBBaoSqHtDRy+dtKlV32kDRbuuniBy3nseZsZoCggAAKTXDFKWHDZ55Ya1Cp+8s+dGOyi4C08v0P/8rbQHcjjkOphpLUqKaBZCykAaf5Ue1c1ul57OKeoyaDy9ShXGvwIKIcZrvZBBds3wwEFQuZBNPTG1ZvpIZ3npXscHWaKRd228V/bboEKarukYi3+lsxafZj+R8laD0Ex3nP3WZaIU4990oerdiu6wbdELlaNyrVF6SRJcZQhhflfnnyXHMqw6c41Hk3toMHsUd9wzdzcYmZB1PI5rsgSfnWrxAbBy5rYpH+ZkGkhcHFJqQIO4TB8ZKU0KSjmS8mBnVDT/1VPgiNDz/qI+KWiZ1xcuEwwvaEmD+Fk6Pt9GMRsfxI+SvldTgk8REqr9dBXt69xwM2FHBHP9k4okkaMbsU1qpZPKCwsnTBrkbvLn0zVn2+tfTukLx0O+uCPRMluXn3EXDrQ0aRHFbUtgoMqOx0JP+7tj/BaOAKkc3C2GaCRfEes2YmqZkM4pMve4Qh1jAsIwYtGcDz7AeGQ61kVrc4CFj3xj2OgC+IbBi7naOZqNvr1Be1YPKt8Vpig9YF5GueY9p4DRGlOG6UX2RY4wsZTt38Lxw5uBBkWuINjRyqsiN5obPZ3xLagfzfNsFDBYgBsGrib8nURVfSgAqIgrozOl+cppRB7xoN9QRti+HAJqKNd6sqpsPkXzqnDWPBD2Jdg2WCJE2bjJhTqyJ6L3lHFguOdIPc2P6F1CGx4bn1GtegOLlFxexOjMzfU86gJOhYjkVGHt8GD96ohRl75/fsv1reCI16pWt2x8p+Bbh4kko8wP3FtXiun+i6gPDMBhE30Ye5ATsIUIHFZjHOA8UfntaEyCSAngQebQ0X4UERcue+GKY4RqVfPhuVqJa3RHt35Ci/2g7VWllNs1NKYsPofAgTNO2n/kwGrnnIL+gQPNsO6XjnqDOjjT/eDfZX88eK4k6+8+MnW3+l8IAZOIh5JT++JZvBadrNZPV7G/2ME6G1FCIAZ4icCidbMzpj4sGc8dlJOg/B3457/Vt8CHLulhajQIsXQuqGDotgzirELTVQ+Z2eh3a8W/Pu+g8iNIUL1MCEzHg7RefM6tUescNDH6uPEuEeXKmrsoVbHdUvhuLIVbQGHMCjuGbmxmczcfIcRAPWkjwuVQOKBpoaD/Zep2gM71inpz6056bmvUuTMM9MV6sM87Fqrv6TvIRT/ch/i7Flmv56ERn/NGryafdcvDIu+JMs5U3yvb3STTSZmbh8RXVGIJjOZ1FJYLREUxxK7eEGM2JLCD+CxR7LuLRuN4AtpI4GIhFrbdVdDkywqNpvEY6aGEOFnD9NP6neBuHRhK/AzqpDE83uFf+1JiPPY7aHYVoQhCxkPs8ex0qJnjHaveKiWfSmaZ6JfY/vVByzJNr5XD6ZSQlJQJ3+xjRb+blTR0XcZ5BHI9ovQQAmGQljWpGPnD5CZQ7ah5kVoK1SbPqtxY6J5zQUTjtTpSe3l6By/nopXH6HSQXJGotOzb+eMOOHFhDC9ypq3urHY+Q1jXB18eR/xkXEIlZQsPBmwCLhoNltD57faLzlqgiwinHjqslntnvfsMkoIpQnWFwLYKh0biW9KM5ZWv7CMIxcc+Wjl1FVnyUrMzhz/IIW6WshcNmZWFSMpaKzozxIyFQZ/IjZHPrE1SI33PwGn/Tro8ZRSf9mpKJbA3uidrygVz6WQlJJwR3ujdgZ9aJ9WZQWVehVYj5mMg5P5MB/BG1G/TQNTLn72Root26hSB+8WMCAo/EEY3L5Qox+JabfDV+kBsHjFtDfyo7ghp/AOvkVzudzzk+F3ruc1bIDJlSfOQ5WRTkjmQGhKDIfDvLIi4Mt8bQTzZ3KPkLaU+hwuFE7m9c/MgzmLWK24oDsZ5nZ4oBeUNc+lThUuz7qnWENE1sIXij3zFljsoH+HOPb+zSt8m7iwszxMZ6T84LvqxqWVGEkI4Il2s41ti5QH74nOzTaci+5dYRVWOnY/hAwI51HE1sb3Pe+NsSHcfgtDW0E4Xx+Wx0uLdqbTlriXRnUIOoi9PVNR6XOdJ4nDOxyiOMMhnUooQ7lRqm0hCxw9nAEw5+PvYWWFXxPaupUfeVsjH+9dXW6bzosCGzTbVpHcDjPWie70r+Nma+oOwA4ARKHmGsbKcoO43xos6sqfbZTCCP9BIPQnZ8XbUen9G7eMs9ESigKoynKKVGNmsBXK4lU8xM/qLXXudViMdSPOZ6mghjJCNK0yA1v9l/ipZRHiTPFOttELH6Ip+fKDtfqdeEqCiPrSnVtWzehUKOUhlNJtexkOZcb2Dtq4L7JlJ0GkJg80vCvEYvArM2JpPqKDVr8hCBNC87u6zk9T3E+L2dfL30aiNVAGTl44Qw0pPerIr1a6m9Jkj690Pi3OI7UAgWaoQjxYm2my7DZMqtkL6CrT0NW9KnihXw701ngJysdKcZ0JkMDT2LzP+2Nj10WIOwkLxASexSQgSoyGk7yTYLUAyvwN1rhRtspXaiyOcfyzDwgTIU9Sn/jMbC6fv7GPReCsiFR8Xa6VCj37eFPXgBiOpAYtj/zMz/3S/io3LTqs7QG1M14CX31xSxu21tASOzaRhbd2RB2QCHXgpqv4593psE5EPjbRZt5DN2toQ4XJJ1A1/EcyDkEJ8+1gu34aVrqC6ejm/07/MQ7ISmUuPrJyCaPIW+PbkxF0VpYU5lJ9HP+LD7WggPwi8NVz8zFWNtyTM6aUuTNL69sBHpWlYeqCwpkJ+EcJFuaTnT27N4pFvwA==
"""
#endif
@@ -0,0 +1,346 @@
//
// DeviceUUID.swift
//
//
// Created Nut.Tech on 02.08.2022.
//
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
import KeyChainAccess
/// Class allow retrive UUID with different life cycle.
final public class DeviceUUID {
private enum Constants {
enum Keys {
static let installationUUIDKey = "installationUUIDKey"
static let deviceUUIDKey = "deviceUUIDKey"
static let devicesUUIDsKey = "devicesUUIDsKey"
static let devicesUUIDsToggleKey = "devicesUUIDsToggleKey"
static let devicesUUIDs = "devicesUUIDs"
}
enum Locals {
static let DevicesUUIDsDidChangeNotification = NSNotification.Name(rawValue: "DevicesUUIDsDidChangeNotification")
}
}
/// Shared instance for class
static public var shared = DeviceUUID()
/// Changes each time the app gets launched (persistent to session).
lazy private(set) public var session: String = {
self.generateUuid()
}()
/// Changes each time the app gets installed (persistent to installation).
lazy private(set) public var installation: String = {
self.getValue(forKey: Constants.Keys.installationUUIDKey,
defaultValue: nil,
keychain: false,
synchronizable: false)
}()
/// Changes each time all the apps of the same vendor are uninstalled (this works exactly as identifierForVendor).
lazy private(set) public var vendor: String? = {
#if os(iOS)
UIDevice.current
.identifierForVendor?
.uuidString
.lowercased()
.replacingOccurrences(of: "-", with: "")
#elseif os(macOS)
return nil
#endif
}()
/// Changes only on system reset, this is the best replacement to the good old udid (persistent to device)
lazy private(set) public var device: String = {
self.getValue(forKey: Constants.Keys.deviceUUIDKey,
defaultValue: nil,
synchronizable: false)
}()
/// List of all uuidForDevice of the same user.
/// In this way it's possible manage guest accounts across multiple devices easily
lazy private(set) public var devices: [String] = {
let devicesString = self.getValue(forKey: Constants.Keys.devicesUUIDsKey,
defaultValue: self.device)
return devicesString.components(separatedBy: "|")
}()
/// Changes each time (no persistent), but allows to keep in memory more temporary uuids.
public func uuid(forKey key: String) -> String {
guard let uuid = self.uuids[key] else {
let value = self.generateUuid()
self.uuids[key] = value
return value
}
return uuid
}
/// Changes each time (no persistent)
public func generateUuid() -> String {
let uuidRef = CFUUIDCreate(nil)
let uuidStringRef = CFUUIDCreateString(nil, uuidRef)
return ((uuidStringRef as? String) ?? "")
.lowercased()
.replacingOccurrences(of: "-", with: "")
}
// MARK: - Private
private var uuids = [String: String]()
private var isCloudAvailable = false
// MARK: - Init
init() {
self.initCloudUUIDsDevices()
}
// MARK: - Internal
func getValue(forKey key: String,
defaultValue: String? = nil,
userDefaults: Bool = true,
keychain: Bool = true,
service: String? = nil,
accessGroup: String? = nil,
synchronizable: Bool = true) -> String {
if let newValue = Self.getValue(forKey: key,
userDefaults:
userDefaults,
keychain: keychain,
service: service,
accessGroup: accessGroup) {
return newValue
} else {
let value = defaultValue ?? self.generateUuid()
Self.setValue(value,
forKey: key,
userDefaults: userDefaults,
keychain: keychain,
service: service,
accessGroup: accessGroup,
synchronizable: synchronizable)
return value
}
}
public func uuidForDeviceMigratingValue(forKey key: String,
service: String? = nil,
accessGroup: String? = nil,
commitMigration: Bool) -> String? {
if let uuidToMigrate = Self.getValue(forKey: key,
service: service,
accessGroup: accessGroup) {
return self.uuid(forDeviceMigratingValue: uuidToMigrate, commitMigration: commitMigration)
}
return nil
}
func updateUUIDDevice(value: String) {
self.device = value
Self.setValue(value,
forKey: Constants.Keys.deviceUUIDKey,
synchronizable: false)
}
func uuid(forDeviceMigratingValue value: String, commitMigration: Bool) -> String? {
if self.isValidUUID(value) {
let oldValue = self.device
let newValue = value
guard oldValue != newValue else { return oldValue }
if commitMigration {
self.updateUUIDDevice(value: newValue)
let deviceSet = NSMutableOrderedSet(array: self.devices)
deviceSet.add(newValue)
deviceSet.remove(oldValue)
if let uuidsArray = deviceSet.array as? [String] {
self.updateUUIDsDevices(with: uuidsArray)
}
self.syncCloudUUIDsDevices()
return self.device
} else {
return oldValue
}
} else {
let exception = NSException(name: NSExceptionName(rawValue: "Invalid uuid to migrate"),
reason: "uuid value should be a string of 32 or 36 characters.")
exception.raise()
return nil
}
}
private func initCloudUUIDsDevices() {
self.isCloudAvailable = false
guard FileManager.default.ubiquityIdentityToken != nil else { return }
self.isCloudAvailable = true
NotificationCenter.default.addObserver(self,
selector: #selector(self.changesCloudUUIDsDevicesNotification),
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
object: nil)
self.syncCloudUUIDsDevices()
}
private func syncCloudUUIDsDevices() {
if self.isCloudAvailable {
let iCloud = NSUbiquitousKeyValueStore.default
//if keychain contains more device identifiers than icloud, maybe that icloud has been empty, so re-write these identifiers to iCloud
for uuidOfUserDevice in self.devices {
let uuidOfUserDeviceAsKey = "\(Constants.Keys.deviceUUIDKey)_\(uuidOfUserDevice)"
if iCloud.string(forKey: uuidOfUserDeviceAsKey) != uuidOfUserDevice {
iCloud.set(uuidOfUserDevice, forKey: uuidOfUserDeviceAsKey)
}
}
//toggle a boolean value to force notification on other devices, useful for debug
let uuidsOfUserDevicesToggler = !iCloud.bool(forKey: Constants.Keys.devicesUUIDsToggleKey)
iCloud.set(uuidsOfUserDevicesToggler, forKey: Constants.Keys.devicesUUIDsToggleKey)
iCloud.synchronize()
}
}
private func updateUUIDsDevices(with value: [String]) {
self.devices = value
Self.setValue(value.joined(separator: "|"),
forKey: Constants.Keys.devicesUUIDsKey)
}
private func isValidUUID(_ value: String) -> Bool {
let pattern = "^[0-9a-f]{32}|[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$"
guard let regExp = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
return false
}
let uuidValueRange = NSRange(location: 0, length: value.count)
let matchRange = regExp.rangeOfFirstMatch(in: value, options: [], range: uuidValueRange)
var matchValue: String?
if !NSEqualRanges(matchRange, NSRange(location: NSNotFound, length: 0)) {
matchValue = (value as NSString).substring(with: matchRange)
return matchValue == value ? true : false
} else {
return false
}
}
@objc
private func changesCloudUUIDsDevicesNotification(_ notification: Notification?) {
if self.isCloudAvailable {
let uuidsSet = NSMutableOrderedSet(array: self.devices)
let uuidsCount = uuidsSet.count
let iCloud = NSUbiquitousKeyValueStore.default
let iCloudDict = iCloud.dictionaryRepresentation as NSDictionary
iCloudDict.enumerateKeysAndObjects { key, obj, stop in
let uuidKey = key as? NSString
if uuidKey?.range(of: Constants.Keys.deviceUUIDKey).location == 0 {
if let uuidValue = obj as? String {
if uuidKey?.range(of: uuidValue).location != NSNotFound,
self.isValidUUID(uuidValue) {
uuidsSet.add(uuidValue)
} else {
print("invalid uuid")
}
}
}
}
if uuidsSet.count > uuidsCount,
let uuidsArray = uuidsSet.array as? [String] {
self.updateUUIDsDevices(with: uuidsArray)
let userInfo = [Constants.Keys.devicesUUIDs: self.devices]
NotificationCenter.default.post(name: Constants.Locals.DevicesUUIDsDidChangeNotification,
object: self,
userInfo: userInfo)
}
}
}
// MARK: - Static
static private func getValue(forKey key: String,
userDefaults: Bool = true,
keychain: Bool = true,
service: String? = nil,
accessGroup: String? = nil) -> String? {
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup)
var value = try? keychainStore.getString(key)
if userDefaults,
!value.isExist {
value = UserDefaults.standard.string(forKey: key)
}
return value
}
@discardableResult
static private func setValue(_ value: String?,
forKey key: String,
userDefaults: Bool = true,
keychain: Bool = true,
service: String? = nil,
accessGroup: String? = nil,
synchronizable: Bool = true) -> Error? {
if let value = value,
userDefaults {
UserDefaults.standard.set(value, forKey: key)
UserDefaults.standard.synchronize()
}
if let value = value, keychain {
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup).synchronizable(synchronizable)
do {
try keychainStore.set(value, key: key)
} catch {
return error
}
}
return nil
}
static private func makeKeychain(service: String? = nil, accessGroup: String? = nil) -> Keychain {
if let service = service {
if let accessGroup = accessGroup {
return Keychain(service: service, accessGroup: accessGroup)
} else {
return Keychain(service: service)
}
} else if let accessGroup = accessGroup {
return Keychain(accessGroup: accessGroup)
}
return Keychain()
}
}
@@ -0,0 +1,16 @@
//
// Array+Extension.swift
//
//
// Created by Juraldinio on 9/15/22.
//
import Foundation
public extension Array {
subscript(safe index: Index) -> Element? {
return (self.startIndex..<self.endIndex) ~= index ? self[index] : nil
}
}
@@ -0,0 +1,15 @@
//
// Data+Extension.swift
//
//
// Created by Juraldinio on 12/6/22.
//
import Foundation
public extension Data {
func jsonDecoded<T: Decodable>(type: T.Type) -> T? { try? JSONDecoder().decode(type, from: self) }
func jsonDecoded<T: Decodable>(type: T.Type) -> [T]? { try? JSONDecoder().decode([T].self, from: self) }
}
@@ -0,0 +1,15 @@
//
// Dictionary+Extension.swift
//
//
// Created by NUT.Tech on 27.01.2023.
//
import Foundation
public extension Dictionary where Key == String, Value == Any {
func jsonSerialized(options: JSONSerialization.WritingOptions = []) -> Data? {
try? JSONSerialization.data(withJSONObject: self, options: options)
}
}
@@ -0,0 +1,16 @@
//
// Encodable+Extension.swift
//
//
// Created by Juraldinio on 12/6/22.
//
import Foundation
public extension Encodable {
func jsonData() -> Data? {
let encoder = JSONEncoder()
return try? encoder.encode(self)
}
}
@@ -0,0 +1,32 @@
//
// Optional+Extension.swift
//
//
// Created by Juraldinio on 08.08.2022.
//
import Foundation
public extension Optional {
var isExist: Bool {
if case .some = self {
return true
}
return false
}
func orCreate(_ creation: @autoclosure () -> Wrapped) -> Wrapped {
switch self {
case let .some(value): return value
case .none: return creation()
}
}
func orTypedCreate<Element: RawRepresentable>(_ creation: @autoclosure () -> Element) -> Element where Element.RawValue == Wrapped {
switch self {
case let .some(value): return Element(rawValue: value) ?? creation()
case .none: return creation()
}
}
}
@@ -0,0 +1,43 @@
//
// Publisher+Extension.swift
//
//
// Created by Juraldinio on 12/20/22.
//
import Foundation
import Combine
public extension Publisher {
/// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is optional.
/// The first time the upstream publisher emits an element, the previous element will be `nil`.
///
/// let range = (1...5)
/// cancellable = range.publisher
/// .withPrevious()
/// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
/// // Prints: "(nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5) ".
///
/// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> {
scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) }
.compactMap { $0 }
.eraseToAnyPublisher()
}
/// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is not optional.
/// The first time the upstream publisher emits an element, the previous element will be the `initialPreviousValue`.
///
/// let range = (1...5)
/// cancellable = range.publisher
/// .withPrevious(0)
/// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
/// // Prints: "(0, 1) (1, 2) (2, 3) (3, 4) (4, 5) ".
///
/// - Parameter initialPreviousValue: The initial value to use as the "previous" value when the upstream publisher emits for the first time.
/// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
func withPrevious(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> {
scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher()
}
}
@@ -0,0 +1,24 @@
//
// File.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
public extension String {
func trimCompact() -> String {
let value = self
.replacingOccurrences(of: "\n", with: "")
.replacingOccurrences(of: "\r", with: "")
let regex = try! NSRegularExpression(pattern: "[ ]{2,}", options: .caseInsensitive)
return regex.stringByReplacingMatches(in: value,
options: [],
range: NSRange(0..<value.utf16.count),
withTemplate: " ")
}
}
@@ -0,0 +1,43 @@
//
// Either.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
public enum Either<T: Decodable, U: Decodable>: Decodable {
case firstType(T)
case secondType(U)
public func unwrap() -> Any {
switch self {
case .firstType(let objectOfTypeT): return objectOfTypeT
case .secondType(let objectOfTypeU): return objectOfTypeU
}
}
public func map<V>(firstTypeTransform: (T) -> V, secondTypeTransform: (U) -> V) -> V {
switch self {
case .firstType(let value):
return firstTypeTransform(value)
case .secondType(let value):
return secondTypeTransform(value)
}
}
public init(from decoder: Decoder) throws {
if let value = try? T(from: decoder) {
self = .firstType(value)
} else if let value = try? U(from: decoder) {
self = .secondType(value)
} else {
let context = DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription:
"Cannot decode \(T.self) or \(U.self)")
throw DecodingError.dataCorrupted(context)
}
}
}
@@ -0,0 +1,53 @@
//
// Operations.swift
// Jura
//
// Created by Jura on 8/14/19.
// Copyright © 2019 Jura. All rights reserved.
//
import Foundation
precedencegroup MonadicPrecedence {
associativity: left
higherThan: BitwiseShiftPrecedence
}
infix operator >>- : MonadicPrecedence
@inline(__always)
@discardableResult
public func >>-<T, U>(a: T?, f: (T) throws -> U?) rethrows -> U? {
switch a {
case .some(let x):
return try f(x)
case .none:
return nil
}
}
// MARK: <<< / >>>
precedencegroup FunctionApplicationPrecedenceLeft {
lowerThan: AssignmentPrecedence
associativity: left
}
infix operator >>> : FunctionApplicationPrecedenceLeft
@inline(__always)
public func >>><T, U>(x: T, f: (T) throws -> U) rethrows -> U {
return try f(x)
}
precedencegroup FunctionApplicationPrecedenceRight {
lowerThan: AssignmentPrecedence
associativity: right
}
infix operator <<< : FunctionApplicationPrecedenceRight
@inline(__always)
public func <<<<T, U>(f: (T) throws -> U, x: T) rethrows -> U {
return try f(x)
}
@@ -0,0 +1,37 @@
//
// ArrayTests.swift
//
//
// Created by Juraldinio on 31.10.2022.
//
import Foundation
import XCTest
@testable
import WalletFoundation
final class ArrayTests: XCTestCase {
func testOuter() {
let test = [0, 1, 2]
XCTAssertNil(test[safe: 5])
XCTAssertNil(test[safe: -1])
}
func testInner() {
let test = [1, 3, 5]
var value = test[safe: 1]
XCTAssertNotNil(value)
XCTAssertEqual(value!, 3)
value = test[safe: 2]
XCTAssertNotNil(value)
XCTAssertEqual(value!, 5)
value = test[safe: 0]
XCTAssertNotNil(value)
XCTAssertEqual(value!, 1)
}
}
@@ -0,0 +1,58 @@
//
// OptionalsTests.swift
//
//
// Created by Juraldinio on 11/8/22.
//
import Foundation
import XCTest
@testable
import WalletFoundation
final class OptionalsTests: XCTestCase {
func testIsExists() {
var value: Int?
XCTAssertFalse(value.isExist)
value = 42
XCTAssertTrue(value.isExist)
}
func testOrCreate() {
var value: String?
XCTAssertEqual(value.orCreate("Hello"), "Hello")
value = "world"
XCTAssertEqual(value.orCreate("Hello"), "world")
}
func testOrTypedCreate() {
enum TestCases: String {
case first
case second
}
var value: String?
// Create new value because nil
XCTAssertEqual(value.orTypedCreate(TestCases.first), .first)
XCTAssertNotEqual(value.orTypedCreate(TestCases.first), .second)
value = "Hello"
// Create new value because not matched
XCTAssertEqual(value.orTypedCreate(TestCases.second), .second)
XCTAssertNotEqual(value.orTypedCreate(TestCases.second), .first)
value = "first"
// Not create new value and just match
XCTAssertEqual(value.orTypedCreate(TestCases.second), .first)
XCTAssertNotEqual(value.orTypedCreate(TestCases.second), .second)
}
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
+35
View File
@@ -0,0 +1,35 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "WalletKit",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "WalletKit",
targets: ["WalletKit"]),
],
dependencies: [
.package(name: "eosswift", path: "../../Vendors/spm/eos-swift"),
.package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess"),
.package(name: "WalletFoundation", path: "../WalletFoundation"),
.package(name: "WalletNetwork", path: "../WalletNetwork")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "WalletKit",
dependencies: ["eosswift", "KeyChainAccess", "WalletFoundation", "WalletNetwork"],
path: "./Sources"),
.testTarget(
name: "WalletKitTests",
dependencies: ["WalletKit"],
path: "Tests"//, // Test files
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
)
]
)
@@ -0,0 +1,53 @@
//
// DeepLink.swift
//
//
// Created by Juraldinio on 8/9/22.
//
import Foundation
public struct DeepLink {
public typealias DataParams = [AnyHashable: Any]
public typealias InfoParams = [String: Any]
public let action: DeepLinkAction
let data: Data?
public let info: [String: Any]?
public let url: URL?
public static func create(params: DataParams?) -> DeepLink? {
guard let params = params,
let rawAction = params["action"] as? String,
let action = DeepLinkAction(rawValue: rawAction),
let data = (params["params"] as? String)?.data(using: .utf8)
?? (try? JSONSerialization.data(withJSONObject: params["params"] as? [String: Any], options: [])) else {
return nil
}
return DeepLink(action: action, data: data, info: nil, url: URL(string: params["r"] as? String ?? ""))
}
public static func create(info: DataParams?) -> DeepLink? {
guard let info = info as? [String: Any],
let rawAction = info["action"] as? String,
let action = DeepLinkAction(rawValue: rawAction) else {
return nil
}
return DeepLink(action: action, data: nil, info: info, url: nil)
}
public func get<T: Decodable>(_ model: T.Type) -> T? {
guard let data = self.data else { return nil }
return try? JSONDecoder().decode(T.self, from: data)
}
public func dictionary() -> [String: Any] {
guard let data = self.data else { return [:] }
return (try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:]
}
}
@@ -1,14 +1,13 @@
//
// CommonModelDeepLink.swift
// PayCash
// DeepLinkAction.swift
//
//
// Created by Igor on 15.10.2020.
// Copyright © 2020 List. All rights reserved.
// Created by Juraldinio on 8/9/22.
//
import Foundation
enum DeepLinkAction: String {
public enum DeepLinkAction: String {
case connect = "connect_accounts"
case chat = "chat"
case transfer = "transfer"
@@ -17,17 +16,17 @@ enum DeepLinkAction: String {
case approveBuy = "approve_buy"
case emission = "emission"
case chatMessage = "chat.message"
case transactionTransfer = "transactions.incoming_transfer"
case transactionTransfer = "transaction.incoming_transfer"
case transactionInheritance = "transactions.incoming_inheritance"
case transactionEmission = "transactions.emission"
case p2pDealNew = "orders.new_deal"
case p2pDealComplete = "orders.completed_deal"
case p2pDealCancel = "orders.cancelled_deal"
case p2pDealDispute = "orders.dispute_deal"
case cashDealNew = "guarantee_orders.new_deal"
case cashDealConfirm = "guarantee_orders.confirmation_deal"
case cashDealCancel = "guarantee_orders.cancelled_deal"
case cashDealComplete = "guarantee_orders.completed_deal"
// case cashDealNew = "guarantee_orders.new_deal"
// case cashDealConfirm = "guarantee_orders.confirmation_deal"
// case cashDealCancel = "guarantee_orders.cancelled_deal"
// case cashDealComplete = "guarantee_orders.completed_deal"
case news = "news"
case competitivePrice = "competitive_price"
}
@@ -0,0 +1,147 @@
//
// Village.swift
//
//
// Created by Juraldinio on 15.08.2022.
//
import Foundation
import WalletFoundation
import WalletNetwork
public enum VillageError: Error {
case passwordNotMatch
}
final public class Village {
private enum Constants {
static let passwordKey = CommonKey("Account.Service.password")
}
private static var villages = [Village]()
private let environment: NetworkEnvironment
private var houses = [VillageHouse]()
private var foreigner: Foreigner? {
didSet {
guard let foreigner = self.foreigner else { return }
ApplicationSettings.device(description: "\(foreigner)")
}
}
// MARK: - Init
private init(environment: NetworkEnvironment) {
self.environment = environment
}
// MARK: - Public
/// Create instance of Village.
public static func get(with environment: NetworkEnvironment) -> Village {
if let village = Self.villages.first(where: { $0.environment.isEquals(other: environment) }) {
return village
}
let village = Village(environment: environment)
Self.villages.append(village)
return village
}
/// Get device instance.
public func device(force: Bool = false) async throws -> Device {
if force { self.foreigner = nil }
if let foreigner = self.foreigner { return foreigner }
if !force,
let foreigner = Foreigner.restore() {
do {
try await foreigner.retrievStatus(using: self.environment)
self.foreigner = foreigner
return foreigner
} catch { }
}
do {
let foreigner = try await Foreigner.create(for: DeviceUUID.shared.device, using: self.environment)
foreigner.save()
self.foreigner = foreigner
return foreigner
} catch {
throw DeviceError.create
}
}
public func attestate(device: Device) async throws -> CertifiedDevice {
try await Resident.permit(for: device, using: self.environment)
}
/// Create wallet key.
public func createWalletKey() throws -> WalletKey {
try WalletKey.create()
}
/// Create wallet case for hold wallet.
public func createWalletCase(with name: String, key: WalletKey? = nil) async throws -> WalletCase {
let walletKey: WalletKey
if let key = key {
walletKey = key
} else {
walletKey = try WalletKey.create()
}
return try await VillagerCoat.create(name: name, using: self.environment, key: walletKey)
}
/// Create bank that contain Wallets.
public func getBank(with password: String, on device: Device) -> Bank {
if let bank = self.houses.first(where: { $0.isEquals(password: password, device: device, environment: self.environment) }) {
return bank
}
if Self.password != password {
Self.password = password
}
let house = VillageHouse.create(for: password, on: device, environment: self.environment)
self.houses.append(house)
return house
}
public static func reset() {
VillageHouse.removeAllVillagers()
Self.password = nil
}
public static var isPasswordExists: Bool { WalletKeychain.instance.exist(Constants.passwordKey) }
public static func getPassword(password: String?) -> String? {
if let password {
return WalletKeychain.instance[Constants.passwordKey, password: password]
} else {
return WalletKeychain.instance[biometric: Constants.passwordKey]
}
}
// TODO: Remove this function after refactoring passwords
public static func updateCommonPassword(password: String, old: String) throws {
if WalletKeychain.instance[Constants.passwordKey, password: old] != nil {
Self.password = password
} else {
throw VillageError.passwordNotMatch
}
}
private static var password: String? {
get { WalletKeychain.instance[biometric: Constants.passwordKey] }
set { WalletKeychain.instance[Constants.passwordKey, password: newValue ?? ""] = newValue }
}
}
@@ -0,0 +1,93 @@
//
// Tractor.swift
//
//
// Created by Juraldinio on 15.08.2022.
//
import Foundation
import Combine
import WalletFoundation
import WalletNetwork
import KeyChainAccess
final class Foreigner: Device, Codable {
private enum Constant {
static let saveKey = "village.tractor.instance"
static let service = CodingUserInfoKey(rawValue: "service")!
}
// MARK: - Codable
private enum CodingKeys: String, CodingKey {
case uuid
case id
}
// MARK: - Init
private init(uuid: String, id: String, isTrusted: Bool) {
self.uuid = uuid
self.id = id
self.isTrusted = isTrusted
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.uuid = try container.decode(String.self, forKey: .uuid)
self.id = try container.decode(String.self, forKey: .id)
}
// MARK: - Device
let uuid: String
let id: String
private(set) var isTrusted: Bool = false
private(set) var availableAccounts: Int?
// MARK: - CustomStringConvertible
var description: String { "uuid: \(self.uuid), id: \(self.id)" }
// MARK: - Methods
func save() {
guard let rawData = try? JSONEncoder().encode(self) else { return }
let keychain = Keychain()
try? keychain.set(rawData, key: Constant.saveKey)
}
func retrievStatus(using environment: NetworkEnvironment) async throws {
let service = DeviceService(environment: environment)
do {
let status = try await service.stateDevice(id: self.id)
self.availableAccounts = status.availableAccounts
self.isTrusted = status.isTrusted
return
} catch {
throw DeviceError.status
}
}
// MARK: - Static
static func create(for cid: String, using environment: NetworkEnvironment) async throws -> Foreigner {
let service = DeviceService(environment: environment)
do {
let device = try await service.createDevice(uuid: cid)
return Foreigner(uuid: device.uuid, id: device.id, isTrusted: device.isTrusted)
} catch {
throw DeviceError.create
}
}
static func restore() -> Foreigner? {
let keychain = Keychain()
guard let rawData = try? keychain.getData(Constant.saveKey) else { return nil }
let decoder = JSONDecoder()
return try? decoder.decode(Foreigner.self, from: rawData)
}
}
@@ -0,0 +1,47 @@
//
// Resident.swift
//
//
// Created by Juraldinio on 25.08.2022.
//
import Foundation
import WalletFoundation
import WalletNetwork
struct Resident: CertifiedDevice {
let device: Device
let status: DeviceStatus
static func permit(for device: Device, using environment: NetworkEnvironment) async throws -> CertifiedDevice {
if device.isTrusted { return Resident(device: device, status: .valid) }
let result = await DeviceCheckToken.generate()
guard let deviceToken = try? result.get() else {
throw CertifiedDeviceError.token
}
let service = DeviceService(environment: environment)
do {
let status = try await service.checkDevice(id: device.id, token: deviceToken.token)
return Resident(device: device, status: status == .valid ? .valid : .invalid )
} catch let NetworkServiceError.gqlApplication(error) {
let deviceError: CertifiedDeviceError
switch error {
case "Invalid": deviceError = .invalid
case "DeviceNotFoundError": deviceError = .notFound
case "DecryptError": deviceError = .decrypt
case "UnexpectedDeviceKind": deviceError = .kind
case "UnexpectedError": deviceError = .unknown
default: deviceError = .unknown
}
throw deviceError
} catch {
throw CertifiedDeviceError.unknown
}
}
}
@@ -0,0 +1,290 @@
//
// File.swift
//
//
// Created by Juraldinio on 8/27/22.
//
import Foundation
import Combine
import WalletFoundation
import WalletNetwork
final class VillageHouse: Bank {
private enum Constants {
static let oldKey = "Account.Service.collection"
static let key = "Wallet.bank.service"
enum Keys {
static let current = CommonKey("Account.Service.current")
static let collection = CommonKey("Account.Service.collection")
}
}
// MARK: - Properties
private var password: String
private let device: Device
private let environment: NetworkEnvironment
private let activeSubject: CurrentValueSubject<Villager?, Never>
private let villagersSubject: CurrentValueSubject<[Villager], Never>
private var cancelables = Set<AnyCancellable>()
// MARK: - Init
private init(password: String, device: Device, environment: NetworkEnvironment) {
self.password = password
self.device = device
self.environment = environment
let collection = Self.restore()
self.villagersSubject = CurrentValueSubject(collection)
let active = Self.active(in: collection)
self.activeSubject = CurrentValueSubject(active)
self.villagersSubject
.sink { [weak self] villagers in
self?.save(villagers: villagers)
}
.store(in: &self.cancelables)
self.migrate(using: self.password)
}
// MARK: - Bank
var active: Wallet? { self.activeSubject.value }
var wallets: [Wallet] { self.villagersSubject.value }
lazy var activePublisher: AnyPublisher<Wallet?, Never> = self.activeSubject.map { $0 }.eraseToAnyPublisher()
lazy var walletsPublisher: AnyPublisher<[Wallet], Never> = self.villagersSubject.map { $0 }.eraseToAnyPublisher()
func accept(password: String) -> Bool { self.password == password }
func remove(wallet: Wallet) throws {
var villagers = self.villagersSubject.value
guard let villager = self.villager(by: wallet),
let index = villagers.firstIndex(where: { $0 == villager }) else {
throw BankError.notOwned
}
villagers.remove(at: index)
villager.clear()
self.villagersSubject.value = villagers
if let active = self.active,
villager.isEquals(other: active) {
try self.activate(wallet: nil)
}
}
func activate(wallet: Wallet?) throws {
guard let wallet else {
Settings.shared[Constants.Keys.current] = Data()
self.activeSubject.value = nil
return
}
guard let villager = self.villager(by: wallet) else {
throw BankError.notOwned
}
// We can save only wallet in accepted state.
if case .accepted = villager.state {
Settings.shared[Constants.Keys.current] = villager.jsonData()
}
self.activeSubject.value = villager
}
func add(using walletCase: WalletCase) async throws -> Wallet {
let villager = try await Villager.create(walletCase: walletCase, on: self.device, using: self.environment)
villager.updatePrivateKey(using: self.password)
let villagers = self.villagersSubject.value
self.villagersSubject.value = villagers + [villager]
return villager
}
func add(using purses: [Purse]) throws {
let wallets = purses
.filter { $0.bank?.isEquals(other: self) ?? false }
.filter { purse in
!self.wallets.contains(where: { $0.name == purse.name && $0.keyType.rawValue == purse.permission.permName })
}
.map { Villager.create(purse: $0) }
wallets
.filter { $0.key.privateKey.isExist }
.forEach { $0.updatePrivateKey(using: self.password) }
let villagers = self.villagersSubject.value
self.villagersSubject.value = villagers + wallets
}
func restore(using keys: WalletKey) async throws -> PurseHolder {
let holder = PurseHolder()
let service = AccountHyperionService(environment: self.environment)
let eosService = EOSService(environment: self.environment)
return try await holder.load(using: service, eosService: eosService, keys: keys, in: self)
}
func refreshStatus(wallet: Wallet?) async throws {
let villagers: [Villager]
if let wallet,
let villager = self.villager(by: wallet) {
villagers = [villager]
} else {
villagers = self.villagersSubject.value
}
_ = await withTaskGroup(of: Void.self) { group in
villagers.forEach { villager in
switch villager.state {
case .pending,
.creating:
group.addTask {
try? await villager.refresh(using: self.environment)
}
case .accepted,
.declined:
return
}
}
await group.waitForAll()
self.save(villagers: villagers)
}
}
func isEquals(other: Bank) -> Bool {
guard let house = other as? VillageHouse else { return false }
return self.password == house.password &&
self.device.isEquals(other: house.device) &&
self.environment.isEquals(other: house.environment)
}
func switchPassword(_ password: String, old: String) throws {
guard self.accept(password: old) else {
throw BankError.passwordNotMatch
}
self.password = password
old >>- { old in self.villagersSubject.value.forEach { $0.update(password: password, old: old) } }
}
func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult] {
// TODO: - After move EOS to frameworks!
/*guard password == self.password else { throw BankError.passwordNotMatch }
let villagers = keyUpdates.compactMap({ self.villager(by: $0.wallet) })
guard villagers.count != keyUpdates.count else { throw BankError.notOwned }
guard let privateKey = self.privateKey(password) else {
throw WalletError.privateKeyNotExists
}
return WalletKeyUpdate(wallet: self, oldPrivateKey: "", transitionId: "")
return try await villager.update(key: key, password: self.password, using: self.environment)*/
return []
}
// MARK: - Internal
func isEquals(password: String, device: Device, environment: NetworkEnvironment) -> Bool {
return self.password == password &&
self.device.isEquals(other: device) &&
self.environment.isEquals(other: environment)
}
// MARK: - Private
private func save(villagers: [Villager]) {
Settings.shared[Constants.Keys.collection] = villagers.jsonData()
}
private static func restore() -> [Villager] {
if let data: Data = Settings.shared[Constants.Keys.collection],
let villagers: [Villager] = data.jsonDecoded(type: Villager.self) {
return villagers
}
return []
}
private func migrate(using password: String) {
let migrateWallets: [Villager]
if let data = UserDefaults.standard.value(forKey: Constants.oldKey) as? Data,
let collection = try? JSONDecoder().decode([Villager.OldVillager].self, from: data) {
migrateWallets = collection.compactMap { $0.covert(password: password) }
} else {
migrateWallets = []
}
print(migrateWallets)
// Delete previous key
// UserDefaults.standard.set(nil, forKey: Constants.oldKey)
}
private func villager(by wallet: Wallet) -> Villager? {
guard let villager = wallet as? Villager,
self.villagersSubject.value.contains(where: { $0 == villager }) else {
return nil
}
return villager
}
// MARK: - Static
static func create(for password: String, on device: Device, environment: NetworkEnvironment) -> VillageHouse {
VillageHouse(password: password, device: device, environment: environment)
}
private static func active(in collection: [Villager]) -> Villager? {
if let data: Data = Settings.shared[Constants.Keys.current],
let villager: Villager = data.jsonDecoded(type: Villager.self),
collection.contains(where: { $0 == villager }) {
return villager
}
guard let rawValue: String = Settings.shared[Constants.Keys.current] else { return nil }
let name = rawValue.components(separatedBy: "@").first
let keyTypeString = rawValue.components(separatedBy: "@").last ?? ""
let keyType = WalletKeyType(rawValue: keyTypeString) ?? WalletKeyType.active
return collection.first(where: { $0.name == name && $0.keyType == keyType })
}
static func removeAllVillagers() {
Settings.shared[Constants.Keys.collection] = Data()
}
}
@@ -0,0 +1,240 @@
//
// Villager.swift
//
//
// Created by Juraldinio on 16.08.2022.
//
import Foundation
import Combine
import WalletFoundation
import WalletNetwork
extension String {
fileprivate static let privateKey = "privateKey"
}
final class Villager: Wallet, Codable, CustomStringConvertible {
struct OldVillager: Codable {
var username: String
var publicKey: String
var keyType: String
func covert(password: String) -> Villager? {
guard let keyType = WalletKeyType(rawValue: self.keyType),
let key = try? WalletKey.restore(using: self.username, type: keyType, publicKey: self.publicKey, password: password) else {
return nil
}
return Villager(name: self.username, key: key, keyType: keyType, state: .accepted)
}
}
private let stateSubject: CurrentValueSubject<WalletState, Never>
// MARK: - Init
private init(walletCase: WalletCase, keyType: WalletKeyType, state: WalletState) {
self.name = walletCase.name
self.key = walletCase.key
self.keyType = keyType
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
}
private init(name: String, key: WalletKey, keyType: WalletKeyType, state: WalletState) {
self.name = name
self.key = key
self.keyType = keyType
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
}
// MARK: - Codable
private enum CodingKeys: String, CodingKey {
// Old values
case username
case publicKey
//
case name
case key
case keyType
case state
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// For save old format with username field!
let username = try container.decodeIfPresent(String.self, forKey: .username)
if let username {
self.name = username
} else {
self.name = try container.decode(String.self, forKey: .name)
}
self.keyType = try container.decode(WalletKeyType.self, forKey: .keyType)
let state = try container.decode(WalletState.self, forKey: .state)
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
// If first version we hold key on flat structure!
if let key = try? WalletKey(from: decoder) {
self.key = key
} else {
self.key = try container.decode(WalletKey.self, forKey: .key)
}
// TODO: - NEED COMPLETETASK
// self.key = try WalletKey.restore(using: self.name, type: self.keyType, publicKey: publicKey, password: "")
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.key, forKey: .key)
try container.encode(self.keyType, forKey: .keyType)
try container.encode(self.stateSubject.value, forKey: .state)
}
// MARK: - Wallet
let name: String
private(set) var key: WalletKey
let keyType: WalletKeyType
var state: WalletState { self.stateSubject.value }
lazy var statePublisher: AnyPublisher<WalletState, Never> = self.stateSubject.eraseToAnyPublisher()
// MARK: - Equatable
static func == (lhs: Villager, rhs: Villager) -> Bool {
return lhs.name == rhs.name
&& lhs.key == rhs.key
&& lhs.keyType == rhs.keyType
&& lhs.state == rhs.state
&& lhs.name == rhs.name
}
// MARK: - CustomStringConvertible
var description: String { "(name: \(name), keyType: \(keyType), state: \(state), public: \(key.publicKey)" }
// MARK: - Internal
func refresh(using environment: NetworkEnvironment) async throws {
let service = AccountService(environment: environment)
let state: WalletState
switch self.state {
case .creating(let orderId),
.pending(let orderId):
do {
let order = try await service.order(with: orderId)
state = WalletState(order: order)
} catch {
throw WalletError.network(error)
}
default:
state = self.state
}
self.stateSubject.value = state
}
func update(key: WalletKey, using passrod: String) -> Wallet {
self.key = key
self.updatePrivateKey(using: passrod)
return self
}
func privateKey(_ value: String?) -> String? {
let privateKey: String?
if let password = value {
privateKey = WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: password]
} else {
privateKey = WalletKeychain.instance[biometric: .key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)]
}
return privateKey
}
func updatePrivateKey(using password: String) {
guard let privateKey = self.key.privateKey else { return }
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey),
password: password] = privateKey
}
// TODO: - Need review
func migrate(password: String) {
let privateKey = WalletKeychain.instance[.key(self.name, suffix: .privateKey), password: password]
WalletKeychain.instance[.key(self.name, suffix: .privateKey), password: ""] = nil
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: password] = privateKey
}
func update(password: String, old: String) {
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey),
password: password] = self.privateKey(old)
}
func clear() {
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: ""] = nil
}
// MARK: - Static
static func create(purse: Purse) -> Villager {
Villager(name: purse.name, key: purse.key, keyType: purse.keyType, state: .accepted)
}
static func create(walletCase: WalletCase, on device: Device, using environment: NetworkEnvironment) async throws -> Villager {
let service = AccountService(environment: environment)
do {
// return Villager(walletCase: walletCase, state: .pending)
let order = try await service.create(username: walletCase.name,
pubKey: walletCase.key.publicKey,
deviceId: device.id)
let state: WalletState
switch order.status {
case .creating: state = .creating(order.id)
case .active, .executed: state = .pending(order.id)
case .expired, .failed: state = .declined
case .completed: state = .accepted
}
return Villager(walletCase: walletCase, keyType: .owner, state: state)
} catch let NetworkServiceError.gqlApplication(error) {
if ApplicationSettings.ignoreCreateAccountSatus {
return Villager(walletCase: walletCase, keyType: .owner, state: .creating("HELLO"))
}
let walletError: WalletError
switch error {
case "invalid EOS public key": walletError = .invalidKey
case "INVALID DEVICE ID": walletError = .invalidDevice
case "UNKNOWN DEVICE": walletError = .device
case "UNCHECKED": walletError = .unchecked
case "UNTRUSTED": walletError = .untrusted
case "DEVICE LIMIT REACHED": walletError = .deviceLimit
case "DAILY LIMIT REACHED": walletError = .dailyLimit
case "UNAVAILABLE": walletError = .unavailable
case "ACCOUNT ALREADY EXISTS": walletError = .exists
default: walletError = .unavailable
}
throw walletError
} catch {
throw WalletError.unavailable
}
}
static func create(walletKey: WalletKey, using environment: NetworkEnvironment) async throws -> Villager {
// TODO: - Add logic
throw WalletError.unavailable
}
}
@@ -0,0 +1,52 @@
//
// VillagerCoat.swift
//
//
// Created by Juraldinio on 16.08.2022.
//
import Foundation
import WalletNetwork
final class VillagerCoat: WalletCase {
// MARK: - Init
private init(name: String, key: WalletKey) {
self.name = name
self.key = key
}
// MARK: - WalletCase
let name: String
private(set) var key: WalletKey
@discardableResult
func update(key: WalletKey) -> WalletKey {
self.key = key
return self.key
}
@discardableResult
func generateKey() -> WalletKey {
if let key = try? WalletKey.create() {
self.key = key
}
return self.key
}
// MARK: - Static
static func create(name: String, using environment: NetworkEnvironment, key: WalletKey) async throws -> VillagerCoat {
let walletName = name.lowercased()
let service = AccountService(environment: environment)
let isAvailable = await service.isAvailable(username: walletName)
switch isAvailable {
case .available: return VillagerCoat(name: walletName, key: key)
case .alreadyTaken: throw WalletCaseError.exists(name: walletName)
case .system: throw WalletCaseError.system
}
}
}
@@ -0,0 +1,48 @@
//
// Bank.swift
//
//
// Created by Juraldinio on 8/27/22.
//
import Foundation
import Combine
import WalletNetwork
public enum BankError: Error {
case empty
case notOwned
case passwordNotMatch
}
/// Bank that contain wallets.
public protocol Bank: AnyObject {
/// Current active wallet.
var active: Wallet? { get }
/// Publisher for observe changes.
var activePublisher: AnyPublisher<Wallet?, Never> { get }
/// Wallets in bank.
var wallets: [Wallet] { get }
/// Publisher for observe wallet changes.
var walletsPublisher: AnyPublisher<[Wallet], Never> { get }
/// Check password
func accept(password: String) -> Bool
/// Activate wallet
func activate(wallet: Wallet?) throws
/// Create new wallet in bank.
func add(using walletCase: WalletCase) async throws -> Wallet
/// Add Purse to bank with converting to Wallet
func add(using purses: [Purse]) throws
/// Restore wallet by key.
func restore(using keys: WalletKey) async throws -> PurseHolder
/// Remove wallet from bank.
func remove(wallet: Wallet) throws
/// Check wallet status
func refreshStatus(wallet: Wallet?) async throws
/// Update WalletKey for Wallet
func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult]
/// Compare two banks
func isEquals(other: Bank) -> Bool
/// Switch password
func switchPassword(_ password: String, old: String) throws
}
@@ -0,0 +1,40 @@
//
// CertifiedDevice.swift
//
//
// Created by Juraldinio on 25.08.2022.
//
import Foundation
public enum CertifiedDeviceError: Error {
/// Unavailable generate token for check
case token
/// Device not fount on server
case notFound
/// Invalid token
case invalid
/// Token decription error
case decrypt
/// Unexpected device kind
case kind
/// Unknown error
case unknown
/// Already in progress
case progress
}
/// Device status
public enum DeviceStatus {
/// Valid
case valid
/// Invalid
case invalid
}
public protocol CertifiedDevice {
/// Device
var device: Device { get }
/// Device status
var status: DeviceStatus { get }
}
@@ -0,0 +1,40 @@
//
// Device.swift
//
//
// Created by Juraldinio on 15.08.2022.
//
import Foundation
import Combine
/// Errors for Device
public enum DeviceError: Error {
/// Failed while restore
case restore
/// Failed create device on server side
case create
/// Failed retrieve status
case status
}
public protocol Device: CustomStringConvertible {
/// Device generated Identity UUID
var uuid: String { get }
/// Server generated Identity
var id: String { get }
/// Is device trusted
var isTrusted: Bool { get }
func isEquals(other: Device) -> Bool
}
// MARK: - Equatable
extension Device {
func isEquals(other: Device) -> Bool {
return self.uuid == other.uuid &&
self.id == other.id &&
self.isTrusted == other.isTrusted
}
}
@@ -0,0 +1,12 @@
//
// ErrorDomain.swift
//
//
// Created by user on 31.10.2022.
//
import Foundation
enum ErrorDomain: Error {
case noEnvironment
}
@@ -0,0 +1,101 @@
//
// PurseHolder.swift
//
//
// Created by Juraldinio on 12/4/22.
//
import Foundation
import WalletFoundation
import WalletNetwork
import EosioSwift
import Combine
public struct Purse {
public let name: String
public let key: WalletKey
public let keyType: WalletKeyType
let permission: Permission
var bank: Bank?
}
public final class PurseHolder {
public enum PurseError: Error {
case empty
}
public private(set) var purses: [Purse] = []
private var count: Int = 0
func load(using service: AccountHyperionService, eosService: EOSService, keys: WalletKey, in bank: Bank) async throws -> PurseHolder {
let publicKey = keys.publicKey
let collection = await service.fetchNamesHyperion(publicKey: publicKey)
guard !collection.isEmpty else {
throw PurseError.empty
}
let _ = await withCheckedContinuation { continuation in
self.count = collection.count
collection.forEach { name in
DispatchQueue.global().async {
eosService.getAccount(name) { response in
self.count -= 1
switch response {
case let .success(accountInfo):
let permissions = accountInfo.permissions
.filter { permission in permission.requiredAuth.keys.contains(where: { $0.key == publicKey }) }
.sorted { $0.permName > $1.permName }
permissions.forEach { permission in
if (permission.permName == WalletKeyType.owner.rawValue) {
self.purses.append(Purse(name: name,
key: keys,
keyType: .owner,
permission: permission,
bank: bank))
}
let hasAddedOwnerKey = self.purses.contains(where: { account in
account.name == name &&
account.permission.requiredAuth.keys.first?.key == permission.requiredAuth.keys.first?.key
})
if let type = WalletKeyType(rawValue: permission.permName),
type != WalletKeyType.owner,
!hasAddedOwnerKey {
self.purses.append(Purse(name: name,
key: keys,
keyType: type,
permission: permission,
bank: bank))
}
}
default: break
}
if self.count == 0 {
continuation.resume(returning: self.purses)
}
}
}
}
}
return self
}
}
@@ -0,0 +1,171 @@
//
// Wallet.swift
//
//
// Created by Juraldinio on 15.08.2022.
//
import Foundation
import WalletNetwork
import Combine
/// Errors for wallet creation
public enum WalletError: Error {
/// Invalid EOS public key
case invalidKey
/// Device not found
case invalidDevice
/// Device not found
case device
/// Device does not do check phase
case unchecked
/// Device not trusted
case untrusted
/// Device limit reached
case deviceLimit
/// Daily limit reached
case dailyLimit
/// Unknown error
case unavailable
/// Network error
case network(Error)
/// Private key does not exists
case privateKeyNotExists
/// Wallet with such name already exists.
case exists
}
/// Wallet state
public enum WalletState: RawRepresentable {
/// Transaction sent to EOS.
case creating(String)
/// Creation in progress
case pending(String)
/// Created
case accepted
/// Creation failed
case declined
private enum Constants {
static let preffixCreate = "C"
static let preffixPending = "P"
}
// TODO:- remove public
public init(order: WalletOrder) {
switch order.status {
case .completed:
self = .accepted
case .creating:
self = .creating(order.id)
case .executed, .active:
self = .pending(order.id)
case .failed, .expired:
self = .declined
}
}
// MARK: - RawRepresentable
public init?(rawValue: String) {
switch rawValue {
case "accepted": self = .accepted
case "declined": self = .declined
default:
let value = String(rawValue.dropFirst())
let preffix = rawValue.first?.uppercased()
if preffix == Constants.preffixPending {
self = .pending(value)
} else {
self = .creating(value)
}
}
}
public var rawValue: String {
switch self {
case .accepted: return "accepted"
case .declined: return "declined"
case let .pending(value): return "\(Constants.preffixPending)\(value)"
case let .creating(value): return "\(Constants.preffixCreate)\(value)"
}
}
}
extension WalletState: Codable { }
public enum WalletKeyType: String, Codable, CustomStringConvertible {
case owner
case active
public var description: String { self.rawValue }
}
public struct WalletKeyUpdateResult {
public let wallet: Wallet
public let oldPrivateKey: String
public let transitionId: String
}
public struct WalletKeyUpdate {
public let wallet: Wallet
public let key: WalletKey
public init(wallet: Wallet, key: WalletKey) {
self.wallet = wallet
self.key = key
}
}
/// Wallet
public protocol Wallet: AnyObject {
/// Name
var name: String { get }
/// Keys
var key: WalletKey { get }
/// Type
var keyType: WalletKeyType { get }
/// State
var state: WalletState { get }
var statePublisher: AnyPublisher<WalletState, Never> { get }
@discardableResult
func update(key: WalletKey, using passrod: String) -> Wallet
func privateKey(_ value: String?) -> String?
/// Compare two Wallets
func isEquals(other: Wallet) -> Bool
}
public extension Wallet {
var isOnCreationState: Bool {
switch self.state {
case .declined,
.accepted:
return false
case .creating,
.pending:
return true
}
}
var isOnActiveState: Bool {
switch self.state {
case .creating,
.accepted:
return true
case .declined,
.pending:
return false
}
}
func isEquals(other: Wallet) -> Bool {
self.name == other.name &&
self.key == other.key &&
self.keyType == other.keyType &&
self.state == other.state
}
}
@@ -0,0 +1,30 @@
//
// WalletCase.swift
//
//
// Created by Juraldinio on 17.08.2022.
//
import Foundation
/// Wallet Case errors
public enum WalletCaseError: Error {
/// System error
case system
/// Wallet with name already exists
case exists(name: String)
}
/// Wallet case. This mean case that can contain wallet.
public protocol WalletCase: AnyObject {
/// Name
var name: String { get }
/// Key
var key: WalletKey { get }
/// Update key
@discardableResult
func update(key: WalletKey) -> WalletKey
/// Generate new key
@discardableResult
func generateKey() -> WalletKey
}
@@ -0,0 +1,142 @@
//
// WalletKey.swift
//
//
// Created by Juraldinio on 17.08.2022.
//
import Foundation
import eosswift
/// Errors for WalletKey
public enum WalletKeyError: Error {
/// System error
case system
/// Invalid public key
case invalidPublic
/// Invalid private key
case invalidPrivate
/// Invalid pair public and private keys
case invalidPair
///
case unlock
}
/// Represents Wallet key.
/// This keys must pass checks.
public enum WalletKey: Codable, Equatable {
/// Public key
case publicKey(String)
/// Public and Private key
case bunch(publicKey: String, privateKey: String)
public var publicKey: String {
switch self {
case let .publicKey(value): return value
case let .bunch(publicKey: value, privateKey: _): return value
}
}
public var privateKey: String? {
switch self {
case let .bunch(publicKey: _, privateKey: value): return value
default: return nil
}
}
func save() {
}
// MARK: - Codable
private enum CodingKeys: String, CodingKey {
case publicKey
case privateKey
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let publicKey = try container.decode(String.self, forKey: .publicKey)
let privateKey = try container.decodeIfPresent(String.self, forKey: .privateKey)
if let privateKey {
self = .bunch(publicKey: publicKey, privateKey: privateKey)
} else {
self = .publicKey(publicKey)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .publicKey(value):
try container.encode(value, forKey: .publicKey)
case let .bunch(publicKey: publicKey, privateKey: privateKey):
try container.encode(publicKey, forKey: .publicKey)
try container.encode(privateKey, forKey: .privateKey)
}
}
// MARK: - Public static
public static func create(public: String, private: String) throws -> WalletKey {
let privateKey: EOSPrivateKey
do {
privateKey = try EOSPrivateKey(base58: `private`)
} catch {
throw WalletKeyError.invalidPrivate
}
let publicKey: EOSPublicKey
do {
publicKey = try EOSPublicKey(base58: `public`)
} catch {
throw WalletKeyError.invalidPublic
}
guard privateKey.publicKey.base58 == publicKey.base58 else {
throw WalletKeyError.invalidPair
}
return .bunch(publicKey: publicKey.base58, privateKey: privateKey.base58)
}
public static func create(public: String) throws -> WalletKey {
let publicKey: EOSPublicKey
do {
publicKey = try EOSPublicKey(base58: `public`)
} catch {
throw WalletKeyError.invalidPublic
}
return .publicKey(publicKey.base58)
}
public static func create(private: String) throws -> WalletKey {
let privateKey: EOSPrivateKey
do {
privateKey = try EOSPrivateKey(base58: `private`)
} catch {
throw WalletKeyError.invalidPrivate
}
return .bunch(publicKey: privateKey.publicKey.base58, privateKey: privateKey.base58)
}
/// Create key for wallet
public static func create() throws -> WalletKey {
guard let key = try? EOSPrivateKey() else { throw WalletKeyError.system }
return .bunch(publicKey: key.publicKey.base58, privateKey: key.base58)
}
// MARK: - Internal static
static func restore(using name: String, type: WalletKeyType, publicKey: String, password: String) throws -> WalletKey {
throw WalletKeyError.unlock
}
}
@@ -0,0 +1,58 @@
//
// DeepLinkActionTests.swift
//
//
// Created by Juraldinio on 31.10.2022.
//
import Foundation
import XCTest
import WalletKit
final class DeepLinkActionTests: XCTestCase {
func testConvertFromString() {
XCTAssertEqual(DeepLinkAction.connect.rawValue, "connect_accounts")
XCTAssertEqual(DeepLinkAction.chat.rawValue, "chat")
XCTAssertEqual(DeepLinkAction.transfer.rawValue, "transfer")
XCTAssertEqual(DeepLinkAction.walletAuth.rawValue, "wallet_auth")
XCTAssertEqual(DeepLinkAction.tokenization.rawValue, "accept_tokenization")
XCTAssertEqual(DeepLinkAction.approveBuy.rawValue, "approve_buy")
XCTAssertEqual(DeepLinkAction.emission.rawValue, "emission")
XCTAssertEqual(DeepLinkAction.chatMessage.rawValue, "chat.message")
XCTAssertEqual(DeepLinkAction.transactionTransfer.rawValue, "transaction.incoming_transfer")
XCTAssertEqual(DeepLinkAction.transactionInheritance.rawValue, "transactions.incoming_inheritance")
XCTAssertEqual(DeepLinkAction.transactionEmission.rawValue, "transactions.emission")
XCTAssertEqual(DeepLinkAction.p2pDealNew.rawValue, "orders.new_deal")
XCTAssertEqual(DeepLinkAction.p2pDealComplete.rawValue, "orders.completed_deal")
XCTAssertEqual(DeepLinkAction.p2pDealCancel.rawValue, "orders.cancelled_deal")
XCTAssertEqual(DeepLinkAction.p2pDealDispute.rawValue, "orders.dispute_deal")
XCTAssertEqual(DeepLinkAction.news.rawValue, "news")
XCTAssertEqual(DeepLinkAction.competitivePrice.rawValue, "competitive_price")
}
func testConvertToString() {
XCTAssertEqual(DeepLinkAction(rawValue: "connect_accounts"), .connect)
XCTAssertEqual(DeepLinkAction(rawValue: "chat"), .chat)
XCTAssertEqual(DeepLinkAction(rawValue: "transfer"), .transfer)
XCTAssertEqual(DeepLinkAction(rawValue: "wallet_auth"), .walletAuth)
XCTAssertEqual(DeepLinkAction(rawValue: "accept_tokenization"), .tokenization)
XCTAssertEqual(DeepLinkAction(rawValue: "approve_buy"), .approveBuy)
XCTAssertEqual(DeepLinkAction(rawValue: "emission"), .emission)
XCTAssertEqual(DeepLinkAction(rawValue: "chat.message"), .chatMessage)
XCTAssertEqual(DeepLinkAction(rawValue: "transaction.incoming_transfer"), .transactionTransfer)
XCTAssertEqual(DeepLinkAction(rawValue: "transactions.incoming_inheritance"), .transactionInheritance)
XCTAssertEqual(DeepLinkAction(rawValue: "transactions.emission"), .transactionEmission)
XCTAssertEqual(DeepLinkAction(rawValue: "orders.new_deal"), .p2pDealNew)
XCTAssertEqual(DeepLinkAction(rawValue: "orders.completed_deal"), .p2pDealComplete)
XCTAssertEqual(DeepLinkAction(rawValue: "orders.cancelled_deal"), .p2pDealCancel)
XCTAssertEqual(DeepLinkAction(rawValue: "orders.dispute_deal"), .p2pDealDispute)
XCTAssertEqual(DeepLinkAction(rawValue: "news"), .news)
XCTAssertEqual(DeepLinkAction(rawValue: "competitive_price"), .competitivePrice)
}
func testConvertFailed() {
XCTAssertNil(DeepLinkAction(rawValue: "news1"))
}
}
@@ -0,0 +1,21 @@
//
// DeepLinkTests.swift
//
//
// Created by Juraldinio on 31.10.2022.
//
import Foundation
import XCTest
@testable
import WalletKit
final class DeepLinkTests: XCTestCase {
func testCreateWithParamsFailed() {
XCTAssertNil(DeepLink.create(params: nil))
XCTAssertNil(DeepLink.create(params: [:]))
}
}
+36
View File
@@ -0,0 +1,36 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "WalletNetwork",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "WalletNetwork",
targets: ["WalletNetwork"]),
],
dependencies: [
.package(name: "eosswift", path: "../../Vendors/spm/eos-swift"),
.package(name: "EosioSwift", path: "../Vendors/spm/eosio-swift-1.0.0"),
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.2")),
.package(name: "WalletFoundation", path: "../WalletFoundation"),
.package(name: "Mocker", path: "../../Vendors/spm/Mocker-3.0.1")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "WalletNetwork",
dependencies: ["EosioSwift", "eosswift", "Alamofire", "WalletFoundation"],
path: "./Sources"),
.testTarget(
name: "WalletNetworkTests",
dependencies: ["WalletNetwork", "Mocker"],
resources: [
.process("Resources")
])
]
)
@@ -0,0 +1,52 @@
//
// AccountHyperionService.swift
//
//
// Created by Juraldinio on 12/4/22.
//
import Foundation
import Alamofire
import WalletFoundation
public struct AccountHyperionService: NetworkService {
private struct Response: Codable {
let accountNames: [String]
enum CodingKeys: String, CodingKey {
case accountNames = "account_names"
}
}
let environment: NetworkEnvironment
public init(environment: NetworkEnvironment) {
self.environment = environment
}
// Methods
public func fetchNamesHyperion(publicKey: String) async -> [String] {
guard var components = URLComponents(url: self.environment.hyperion.url(), resolvingAgainstBaseURL: false) else {
return []
}
components.path = "/v2/state/get_key_accounts"
components.queryItems = [URLQueryItem(name: "public_key", value: publicKey),
URLQueryItem(name: "skip", value: "0"),
URLQueryItem(name: "limit", value: "5000")]
guard let url = components.url else { return [] }
let response = try? await AF.request(
url,
method: .get,
headers: self.environment.usernames.httpHeaders)
.serializingDecodable(Response.self)
.value
guard let response = response else { return [] }
return response.accountNames
}
}
@@ -0,0 +1,156 @@
//
// AccountService.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
import Alamofire
import WalletFoundation
public struct AccountService: NetworkService {
public enum ServiceError: Error {
case notificationEmptyAccount
case notificationEmptyToken
case notificationEmptyLanguage
}
let environment: NetworkEnvironment
let session: Session
public init(environment: NetworkEnvironment) {
self.environment = environment
if let configuration = environment.configuration {
configuration.headers = HTTPHeaders.default
self.session = Session(configuration: configuration)
} else {
self.session = AF
}
}
// Methods
public func create(username: String, pubKey: String, deviceId: String) async throws -> WalletOrder {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let variables = AccountCreateRequest.Variables(username: username, pubKey: pubKey, deviceId: deviceId)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: AccountCreateRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
.responseString { print(">>>>>> \($0)") }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<AccountCreateResponse>.self, decoder: decoder)
.value
switch result?.result {
case let .firstType(response): return response.order
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
public func order(with id: String) async throws -> WalletOrder {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let variables = AccountOrderRequest.Variables(uid: id)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: AccountOrderRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
.responseString { print(">>>>>> \($0)") }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<AccountOrderResponse>.self, decoder: decoder)
.value
switch result?.result {
case let .firstType(response): return response.order
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
public enum AvailableType {
case available
case alreadyTaken
case system
}
public func isAvailable(username: String) async -> AvailableType {
guard !username.isEmpty else { return .system }
let variables = AccountCheckNameRequest.Variables(username: username)
let response = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: AccountCheckNameRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders)
.responseString { print("checkWalletName: \($0)") }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<AccountCheckNameResponse>.self)
.value
guard let response = response else { return .system }
switch response.result {
case .firstType: return .available
case let .secondType(type):
switch type {
case let .application(error: errorType) where errorType == "ALREADY_TAKEN":
return .alreadyTaken
default: return .system
}
}
}
public func updateNotification(token: String, accounts: [String], language: String) async throws {
if accounts.isEmpty { throw ServiceError.notificationEmptyAccount }
if token.isEmpty { throw ServiceError.notificationEmptyToken }
if language.isEmpty { throw ServiceError.notificationEmptyLanguage }
let variables = AccountNotificationTokenRequest.Variables(
deviceToken: token,
deviceType: "IOS",
eosAccounts: accounts,
langCode: language,
application: "MALINKA"
)
let result = try? await self.session.request(
self.environment.backend.url,
method: .post,
parameters: AccountNotificationTokenRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.backend.httpHeaders)
// .responseString { print("updateNotification: \($0)") }
.serializingDecodable(GraphQLResult<AccountNotificationTokenResponse>.self)
.value
print(result ?? "")
}
}
@@ -0,0 +1,50 @@
//
// AccountNodeService.swift
//
//
// Created by Nut.Tech on 16.01.2023.
//
import Alamofire
import Foundation
public struct NodeService: NetworkService {
let environment: NetworkEnvironment
let session: Session
public init(environment: NetworkEnvironment) {
self.environment = environment
if let configuration = environment.configuration {
configuration.headers = HTTPHeaders.default
self.session = Session(configuration: configuration)
} else {
self.session = AF
}
}
public func fetchActions(account: String,
limit: Int,
skip: Int? = nil) async throws -> [NodeAction] {
guard let url = URL(string: "https://eos.greymass.com") else { return [] } //self.environment.hyperion.url()
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return [] }
components.path = "/v1/history/get_actions"
components.queryItems = [URLQueryItem(name: "account_name", value: account),
URLQueryItem(name: "offset", value: "\(-limit)")]
if let skip {
components.queryItems?.append(URLQueryItem(name: "pos", value: "\(skip)"))
}
do {
let result = try await self.session.request(
components,
method: .get
).serializingDecodable(NodeResponse.self)
.value
return result.actions
} catch {
throw error
}
}
}
@@ -0,0 +1,31 @@
//
// AccountCheckNameRequest.swift
//
//
// Created by Juraldinio on 11.08.2022.
//
import Foundation
struct AccountCheckNameRequest: Encodable {
struct Variables: Encodable {
let username: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "checkWalletName"
let query: String = #"""
query checkWalletName(
$username: String!
) {
checkWalletName(username: $username) { errors }
}
"""#.trimCompact()
}
struct AccountCheckNameResponse: GraphQLResponse {
static let node = "checkWalletName"
}
@@ -0,0 +1,37 @@
//
// AccountCreateRequest.swift
//
//
// Created by Juraldinio on 24.08.2022.
//
import Foundation
struct AccountCreateRequest: Encodable {
struct Variables: Encodable {
let username: String
let pubKey: String
let deviceId: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "CreateAccount"
let query: String = #"""
mutation CreateAccount($username: String!, $pubKey: String!, $deviceId: String!) {
createAccount(username: $username, pubKey: $pubKey, deviceId: $deviceId) {
errors, order { timeout, id, modified, status }, id
}
}
"""#.trimCompact()
}
struct AccountCreateResponse: GraphQLResponse {
static let node = "createAccount"
let id: Int
let order: WalletOrder
}
@@ -1,24 +1,25 @@
//
// AddPushNotificationDeviceTokenGraphQLRequest.swift
// PayCash
// AccountNotificationTokenRequest.swift
//
//
// Created by Saveliy Stavitsky on 7/30/21.
// Copyright © 2021 AM. All rights reserved.
// Created by Juraldinio on 01.08.2022.
//
import Foundation
struct UpdatePushNotificationDeviceTokenGraphQLRequest: Encodable {
struct ResponseData: Decodable { }
struct AccountNotificationTokenRequest: Encodable {
struct Variables: Encodable {
let deviceToken: String
let deviceType: String
let eosAccounts: [String]
let langCode: String
let application: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "updatePushNotificationDeviceToken"
let query: String = #"""
@@ -27,15 +28,22 @@ struct UpdatePushNotificationDeviceTokenGraphQLRequest: Encodable {
$deviceType: DeviceTypeEnum,
$eosAccounts: [String]!,
$langCode: Lang!
$application: ApplicationEnum
) {
updatePushNotificationDeviceToken(
deviceToken: $deviceToken,
deviceType: $deviceType,
eosAccounts: $eosAccounts,
langCode: $langCode
langCode: $langCode,
application: $application
) {
errors
}
}
"""#
"""#.trimCompact()
}
struct AccountNotificationTokenResponse: GraphQLResponse {
static let node = "updatePushNotificationDeviceToken"
}
@@ -0,0 +1,45 @@
//
// AccountOrderRequest.swift
//
//
// Created by Juraldinio on 29.08.2022.
//
import Foundation
struct AccountOrderRequest: Encodable {
struct Variables: Encodable {
let uid: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "accountOrder"
let query: String = #"""
query accountOrder(
$uid: String!
) {
accountOrder(uid: $uid) { timeout, id, modified, status, created }
}
"""#.trimCompact()
}
struct AccountOrderResponse: GraphQLResponse {
private enum Common: String, CodingKey {
case order = "accountOrder"
}
static let node = ""
let order: WalletOrder
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Common.self)
self.order = try container.decode(WalletOrder.self, forKey: .order)
}
}
@@ -0,0 +1,31 @@
//
// File.swift
//
//
// Created by Juraldinio on 8/28/22.
//
import Foundation
public struct WalletOrder: Codable {
public enum Status: String, Codable {
/// Order created.
case active = "ACTIVE"
/// Order expired.
case expired = "EXPIRED"
/// Sent to blockchain.
case executed = "EXECUTED"
/// Accepting EOS transaction. Already sent, but not all blocks created.
case creating = "TRX_IN_CHAIN"
/// Failed.
case failed = "FAILED"
/// Created and completed.
case completed = "COMPLETED"
}
public let timeout: Date
public let modified: Date
public let status: Status
public let id: String
}
@@ -0,0 +1,15 @@
//
// NodeAct.swift
//
//
// Created by Nut.Tech on 10.01.2023.
//
import Foundation
public struct NodeAct: Decodable {
public let data: NodeData
public let name: String
public let authorization: [NodeActAuthorization]
public let account: String
}
@@ -0,0 +1,13 @@
//
// NodeActAuthorization.swift
//
//
// Created by Nut.Tech on 10.01.2023.
//
import Foundation
public struct NodeActAuthorization: Codable {
public let actor: String
public let permission: String
}
@@ -0,0 +1,22 @@
//
// NodeAction.swift
//
//
// Created by Nut.Tech on 10.01.2023.
//
import Foundation
public struct NodeAction: Decodable {
public let accountActionSeq: Int
public let actionTrace: NodeActionTrace
public let globalActionSeq: Int
public let irreversible: Bool
enum CodingKeys: String, CodingKey {
case accountActionSeq = "account_action_seq"
case actionTrace = "action_trace"
case globalActionSeq = "global_action_seq"
case irreversible
}
}
@@ -0,0 +1,24 @@
//
// NodeActionTrace.swift
//
//
// Created by Nut.Tech on 11.01.2023.
//
import Foundation
public struct NodeActionTrace: Decodable {
public let act: NodeAct
public let blockNum: Int
public let blockTime: String
public let receiver: String?
public let trxId: String
enum CodingKeys: String, CodingKey {
case blockNum = "block_num"
case blockTime = "block_time"
case trxId = "trx_id"
case receiver
case act
}
}
@@ -0,0 +1,47 @@
//
// NodeData.swift
//
// Created by NUT.TECH on 10.01.2023.
//
import Foundation
public struct NodeData: Decodable {
public let data: [String: Any]
public init(from decoder: Decoder) throws {
// Create a decoding container using DynamicCodingKeys
// The container will contain all the JSON first level key
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
var tempData = [String: Any]()
// Loop through each key
for key in container.allKeys {
if let value = try? container.decode(NodeData.self, forKey: key) {
tempData[key.stringValue] = value.data
} else if let value = try? container.decode(String.self, forKey: key) {
tempData[key.stringValue] = value
} else if let value = try? container.decode(Int.self, forKey: key) {
tempData[key.stringValue] = value
}
}
self.data = tempData
}
private struct DynamicCodingKeys: CodingKey {
// Use for string-keyed dictionary
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
}
@@ -0,0 +1,12 @@
//
// NodeResponse.swift
//
//
// Created by Nut.Tech on 11.01.2023.
//
import Foundation
public struct NodeResponse: Decodable {
public let actions: [NodeAction]
}
@@ -0,0 +1,41 @@
//
// CommonEnvironment.swift
//
//
// Created by Juraldinio on 04.08.2022.
//
import Foundation
public struct CommonEnvironment: NetworkEnvironment {
public let configuration: URLSessionConfiguration?
public let usernames: NetworkStaticAPIRecord
public let hyperion: NetworkDynamicAPIRecord
public let backend: NetworkStaticAPIRecord
public let node: NetworkDynamicAPIRecord
public init(usernames: NetworkStaticAPIRecord,
backend: NetworkStaticAPIRecord,
hyperion: NetworkDynamicAPIRecord,
node: NetworkDynamicAPIRecord) {
self.configuration = nil
self.usernames = usernames
self.backend = backend
self.hyperion = hyperion
self.node = node
}
public init(usernames: URL,
backend: URL,
hyperion: @escaping NetworkDynamicAPIRecord.Dynamic,
node: @escaping NetworkDynamicAPIRecord.Dynamic,
headers: [String: String] = [:]) {
let userRecord = NetworkStaticAPIRecord(url: usernames, headers: headers)
let backendRecord = NetworkStaticAPIRecord(url: backend, headers: headers)
let hyperionRecord = NetworkDynamicAPIRecord(url: hyperion, headers: headers)
let nodeRecord = NetworkDynamicAPIRecord(url: node, headers: headers)
self.init(usernames: userRecord, backend: backendRecord, hyperion: hyperionRecord, node: nodeRecord)
}
}
@@ -0,0 +1,106 @@
//
// GraphQLResult.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
import WalletFoundation
protocol GraphQLResponse: Decodable {
static var node: String { get }
}
struct GraphQLError: Decodable {
let message: String
}
enum GraphQLResponseError: Decodable {
enum Place: CustomStringConvertible {
case node
case failed
var description: String {
switch self {
case .node: return "node"
case .failed: return "failed"
}
}
}
case system(errors: [GraphQLError])
case application(error: String)
case decode(Place)
init(from decoder: Decoder) throws {
self = .system(errors: [])
}
// MARK: -
var networkServiceError: NetworkServiceError {
switch self {
case let .system(errors: errors): return .gqlSystem(errors.map { $0.message })
case let .application(error: error): return .gqlApplication(error)
case let .decode(place): return .gqlDecode(place.description)
}
}
}
let KEY_ERRORS = "errors"
struct GraphQLResult<Resp: GraphQLResponse>: Decodable {
let result: Either<Resp, GraphQLResponseError>
private enum Common: String, CodingKey {
case data
case errors
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Common.self)
// System error catch
if let errors = try? container.decode([GraphQLError].self, forKey: .errors) {
self.result = .secondType(.system(errors: errors))
return
}
// If node is empty
guard !Resp.node.isEmpty else {
if let response = try? container.decode(Resp.self, forKey: .data) {
self.result = .firstType(response)
} else {
self.result = .secondType(.decode(.failed))
}
return
}
let dataContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .data)
guard let key = DynamicCodingKeys(stringValue: Resp.node) else {
self.result = .secondType(.decode(.node))
return
}
if let responseContainer = try? dataContainer.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: key),
let errorKey = DynamicCodingKeys(stringValue: KEY_ERRORS),
let error = try? responseContainer.decode(String.self, forKey: errorKey) {
self.result = .secondType(.application(error: error))
} else if let response = try? dataContainer.decode(Resp.self, forKey: key) {
self.result = .firstType(response)
} else {
self.result = .secondType(.decode(.failed))
}
}
}
@@ -0,0 +1,84 @@
//
// NetworkService.swift
//
//
// Created by Juraldinio on 04.08.2022.
//
import Foundation
import Alamofire
public struct NetworkStaticAPIRecord {
public typealias Headers = [String: String]
public let url: URL
public let headers: Headers?
public init(url: URL, headers: Headers?) {
self.url = url
self.headers = headers
}
var httpHeaders: HTTPHeaders? {
guard let headers = self.headers else { return nil }
return HTTPHeaders(headers.compactMap { key, value in HTTPHeader(name: key, value: value) })
}
}
public struct NetworkDynamicAPIRecord {
public typealias Headers = [String: String]
public typealias Dynamic = () -> URL
public let url: Dynamic
public let headers: Headers?
public init(url: @escaping Dynamic, headers: Headers?) {
self.url = url
self.headers = headers
}
var httpHeaders: HTTPHeaders? {
guard let headers = self.headers else { return nil }
return HTTPHeaders(headers.compactMap { key, value in HTTPHeader(name: key, value: value) })
}
}
public protocol NetworkEnvironment {
var configuration: URLSessionConfiguration? { get }
var usernames: NetworkStaticAPIRecord { get }
var backend: NetworkStaticAPIRecord { get }
var hyperion: NetworkDynamicAPIRecord { get }
var node: NetworkDynamicAPIRecord { get }
func isEquals(other: NetworkEnvironment) -> Bool
}
public protocol NetworkService {
init(environment: NetworkEnvironment)
}
public enum NetworkServiceError: Error {
case invalidUrl
case gqlSystem([String])
case gqlApplication(String)
case gqlDecode(String)
case uncatched
}
// MARK: - Equatable
extension NetworkEnvironment {
public func isEquals(other: NetworkEnvironment) -> Bool {
return self.configuration == other.configuration &&
self.usernames.url == other.usernames.url &&
self.usernames.headers == other.usernames.headers &&
self.backend.url == other.backend.url &&
self.backend.headers == other.backend.headers &&
self.hyperion.url() == other.hyperion.url() &&
self.hyperion.headers == other.hyperion.headers &&
self.node.url() == other.node.url() &&
self.node.headers == other.node.headers
}
}
@@ -0,0 +1,97 @@
//
// DeviceService.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
import Alamofire
public struct DeviceService: NetworkService {
let environment: NetworkEnvironment
let session: Session
public init(environment: NetworkEnvironment) {
self.environment = environment
if let configuration = environment.configuration {
configuration.headers = HTTPHeaders.default
self.session = Session(configuration: configuration)
} else {
self.session = AF
}
}
public func createDevice(uuid: String) async throws -> Device {
let variables = RegisterDeviceRequest.Variables(cid: uuid)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: RegisterDeviceRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
// .responseString { print($0) }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<RegisterDeviceResponse>.self)
.value
switch result?.result {
case let .firstType(device): return Device(uuid: uuid, id: device.uid, isTrusted: device.isTrustedNow)
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
public func stateDevice(id: String) async throws -> StateDevice {
let variables = StateDeviceRequest.Variables(uid: id)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: StateDeviceRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
// .responseString { print(">>>>>> \($0)") }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<StateDeviceResponse>.self)
.value
switch result?.result {
case let .firstType(state): return StateDevice(availableAccounts: state.availableAccounts,
isTrusted: state.isTrustedNow)
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
public func checkDevice(id: String, token: String) async throws -> DeviceStatus {
let variables = CheckDeviceRequest.Variables(uid: id, token: token)
let result = try? await self.session.request(
self.environment.usernames.url,
method: .post,
parameters: CheckDeviceRequest(variables: variables),
encoder: JSONParameterEncoder.default,
headers: self.environment.usernames.httpHeaders
)
// .responseString { print($0) }
// .cURLDescription { print($0) }
.serializingDecodable(GraphQLResult<CheckDeviceResponse>.self)
.value
switch result?.result {
case let .firstType(status): return status.status
case let .secondType(error): throw error.networkServiceError
default: throw NetworkServiceError.uncatched
}
}
}
@@ -0,0 +1,35 @@
//
// CheckDeviceRequest.swift
//
//
// Created by Juraldinio on 24.08.2022.
//
import Foundation
struct CheckDeviceRequest: Encodable {
struct Variables: Encodable {
let uid: String
let token: String
}
// MARK: - GraphQL
let operationName = "CheckDevice"
let query: String = #"""
mutation CheckDevice($uid: String!, $token: String!) {
checkDevice(uid: $uid, token: $token) {
status
}
}
"""#.trimCompact()
let variables: Variables
}
struct CheckDeviceResponse: GraphQLResponse {
static let node = "checkDevice"
let status: DeviceStatus
}
@@ -0,0 +1,36 @@
//
// RegisterDeviceRequest.swift
//
//
// Created by Juraldinio on 04.08.2022.
//
import Foundation
struct RegisterDeviceRequest: Encodable {
struct Variables: Encodable {
let cid: String
}
// MARK: - GraphQL
let operationName = "RegisterDevice"
let query: String = #"""
mutation RegisterDevice($cid: String) {
registerDevice(kind: IOS, cid: $cid) {
isTrustedNow
uid
}
}
"""#.trimCompact()
let variables: Variables
}
struct RegisterDeviceResponse: GraphQLResponse {
static let node = "registerDevice"
let isTrustedNow: Bool
let uid: String
}
@@ -0,0 +1,34 @@
//
// StateDeviceRequest.swift
//
//
// Created by Juraldinio on 29.08.2022.
//
import Foundation
struct StateDeviceRequest: Encodable {
struct Variables: Encodable {
let uid: String
}
// MARK: - GraphQL
let variables: Variables
let operationName = "device"
let query: String = #"""
query device(
$uid: String!
) {
device(uid: $uid) { availableAccounts, isTrustedNow }
}
"""#.trimCompact()
}
struct StateDeviceResponse: GraphQLResponse {
static let node = "device"
let availableAccounts: Int
let isTrustedNow: Bool
}
@@ -0,0 +1,18 @@
//
// Device.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
/// Model represent device
public struct Device {
/// Device generated Identity UUID
public let uuid: String
/// Server generated Identity
public let id: String
/// Is device trusted
public let isTrusted: Bool
}
@@ -0,0 +1,14 @@
//
// DeviceStatus.swift
//
//
// Created by Juraldinio on 23.08.2022.
//
import Foundation
public enum DeviceStatus: String, Decodable {
case valid = "Valid"
case invalid = "Invalid"
case unexpected = "UnexpectedError"
}
@@ -0,0 +1,13 @@
//
// StateDevice.swift
//
//
// Created by Juraldinio on 29.08.2022.
//
import Foundation
public struct StateDevice {
public let availableAccounts: Int
public let isTrusted: Bool
}
@@ -0,0 +1,68 @@
//
// EOSService.swift
//
//
// Created by Juraldinio on 31.07.2022.
//
import Foundation
import Alamofire
import WalletFoundation
import EosioSwift
import eosswift
public struct EOSService: NetworkService {
let environment: NetworkEnvironment
public init(environment: NetworkEnvironment) {
self.environment = environment
}
// Methods
public func requireNodes() async -> [String] {
let result = try? await AF.request(
self.environment.backend.url,
method: .post,
parameters: EOSNodeRequest(),
encoder: JSONParameterEncoder.default,
headers: self.environment.backend.httpHeaders)
// .responseString(completionHandler: { print(">>>~~~>>> \($0)") })
.serializingDecodable(GraphQLResult<EOSNodesResponse>.self)
.value
switch result?.result {
case let .firstType(value):
return value.nodes.map { $0.url }
default:
return []
}
}
public func requireHyperions() async -> [String] {
let result = try? await AF.request(
self.environment.backend.url,
method: .post,
parameters: EOSHyperionsRequest(),
encoder: JSONParameterEncoder.default,
headers: self.environment.backend.httpHeaders)
.serializingDecodable(GraphQLResult<EOSHyperionsResponse>.self)
.value
switch result?.result {
case let .firstType(value):
return value.hyperions.map { $0.url }
default:
return []
}
}
public func getAccount(_ account: String, completion: @escaping (EosioResult<EosioRpcAccountResponse, EosioError>) -> Void) {
EosioRpcProvider(endpoint: self.environment.node.url(), headers: self.environment.node.headers)
.getAccount(requestParameters: EosioRpcAccountRequest(accountName: account), completion: completion)
}
}
@@ -0,0 +1,35 @@
//
// EOSHyperionsRequest.swift
//
//
// Created by NUT.Tech on 01.08.2022.
//
import Foundation
struct EOSHyperionsRequest: Encodable {
// MARK: - GraphQL
let query: String = #"""
query {
getEOSHyperions {
hyperions {
url
}
}
}
"""#.trimCompact()
}
struct EOSHyperionsResponse: GraphQLResponse {
static let node = "getEOSHyperions"
struct Hyperion: Decodable {
let url: String
}
let hyperions: [Hyperion]
}
@@ -0,0 +1,37 @@
//
// EOSNodesQueryRequest.swift
//
//
// Created by Juraldinio on 01.08.2022.
//
import Foundation
struct EOSNodeRequest: Encodable {
// MARK: - GraphQL
let operationName = "getEOSNodes"
let query: String = #"""
query getEOSNodes {
getEOSNodes {
nodes {
url
}
}
}
"""#.trimCompact()
let variables: [String: String]? = nil
}
struct EOSNodesResponse: GraphQLResponse {
static let node = "getEOSNodes"
struct Node: Decodable {
let url: String
}
let nodes: [Node]
}
@@ -0,0 +1,311 @@
//
// DeviceServiceTests.swift
//
//
// Created by Juraldinio on 13.01.2023.
//
import Foundation
import XCTest
import WalletFoundation
@testable
import WalletNetwork
import Mocker
final class DeviceServiceTests: XCTestCase {
override func tearDown() {
Mocker.removeAll()
}
// MARK: - CreateDevice
// When receive incorrect status we must throw uncatched exception.
func testCreateDeviceFailUncatched() async {
let url = URL(string: "https://malinka.life/create")!
let uuid = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 500,
data: [ .post: Data() ]
).register()
do {
_ = try await service.createDevice(uuid: uuid)
XCTFail("Require to fail")
} catch NetworkServiceError.uncatched {
XCTAssertTrue(true)
} catch {
XCTFail("Require '\(NetworkServiceError.uncatched)' exception")
}
}
// When we receive incorrect data that we can't decode.
func testCreateDeviceFailDecode() async {
let url = URL(string: "https://malinka.life/create")!
let uuid = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.createDeviceDecodeFailed ]
).register()
do {
_ = try await service.createDevice(uuid: uuid)
} catch NetworkServiceError.gqlDecode(let message) {
XCTAssertEqual(message, "failed")
} catch {
XCTFail("Catch exceprion '\(error)' but require NetworkServiceError.gqlDecode")
}
}
// When we successed create device.
func testCreateDeviceSuccessTrusted() async {
let url = URL(string: "https://malinka.life/create")!
let uuid = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.createDeviceTrusted ]
).register()
do {
let device = try await service.createDevice(uuid: uuid)
XCTAssertTrue(device.isTrusted)
XCTAssertEqual(device.id, "a88c4532a09024c245e9")
XCTAssertEqual(device.uuid, uuid)
} catch {
XCTFail("Catch exceprion \(error)")
}
}
// Test send correct request.
func testCreateDeviceSuccessRequest() async {
let url = URL(string: "https://malinka.life/query")!
let uuid = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
let requestExpectation = XCTestExpectation(description: "Request")
let completionExpectation = XCTestExpectation(description: "Completion")
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(httpBodyType: [String: Either<String, [String: String]>].self) { request, object in
XCTAssertEqual(url, mock.request.url)
XCTAssertEqual(request.method, .post)
XCTAssertNotNil(object)
let registerDevice = RegisterDeviceRequest(variables: RegisterDeviceRequest.Variables(cid: uuid))
XCTAssertNotNil(object!["query"])
object!["query"]!.map(firstTypeTransform: {
XCTAssertEqual($0, registerDevice.query)
}, secondTypeTransform: {
XCTFail("Require 'String' type but receive \($0)")
})
XCTAssertNotNil(object!["operationName"])
object!["operationName"]!.map(firstTypeTransform: {
XCTAssertEqual($0, registerDevice.operationName)
}, secondTypeTransform: {
XCTFail("Require 'String' type but receive \($0)")
})
XCTAssertNotNil(object!["variables"])
object!["variables"]!.map(firstTypeTransform: {
XCTFail("Require '[String: String]' type but receive \($0)")
}, secondTypeTransform: {
XCTAssertNotNil($0["cid"])
XCTAssertEqual($0["cid"], registerDevice.variables.cid)
})
requestExpectation.fulfill()
}
mock.completion = {
completionExpectation.fulfill()
}
mock.register()
_ = try? await service.createDevice(uuid: uuid)
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
}
// MARK: - StateDevice
// Receive null values in fields - we must throw exception.
func testQueryStateDeviceException() async {
let url = URL(string: "https://malinka.life/query")!
let id = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.queryDeviceStateNull ]
).register()
do {
_ = try await service.stateDevice(id: id)
XCTFail("Require catch exceprion NetworkServiceError.gqlDecode")
} catch {
XCTAssertTrue(true)
}
}
// Receive untrusted status.
func testQueryStateDeviceUntrusted() async {
let url = URL(string: "https://malinka.life/query")!
let id = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.queryDeviceStateUntrusted ]
).register()
do {
let state = try await service.stateDevice(id: id)
XCTAssertFalse(state.isTrusted)
XCTAssertEqual(state.availableAccounts, 0)
} catch {
XCTFail("Catch exceprion \(error)")
}
}
// Receive trusted status.
func testQueryStateDeviceTrusted() async {
let url = URL(string: "https://malinka.life/query")!
let id = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
Mock(url: url,
dataType: .json,
statusCode: 200,
data: [ .post: DeviceServiceData.queryDeviceStateTrusted ]
).register()
do {
let state = try await service.stateDevice(id: id)
XCTAssertTrue(state.isTrusted)
XCTAssertEqual(state.availableAccounts, 2)
} catch {
XCTFail("Catch exceprion \(error)")
}
}
// Check sent correct request.
func testQueryStateDeviceRequest() async {
let url = URL(string: "https://malinka.life/query")!
let id = "1234-0987-4567"
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
let service = DeviceService(environment: env)
let requestExpectation = XCTestExpectation(description: "Request")
let completionExpectation = XCTestExpectation(description: "Completion")
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(httpBodyType: [String: Either<String, [String: String]>].self) { request, object in
XCTAssertEqual(url, mock.request.url)
XCTAssertEqual(request.method, .post)
XCTAssertNotNil(object)
let stateRequest = StateDeviceRequest(variables: StateDeviceRequest.Variables(uid: id))
XCTAssertNotNil(object!["query"])
object!["query"]!.map(firstTypeTransform: {
XCTAssertEqual($0, stateRequest.query)
}, secondTypeTransform: {
XCTFail("Require 'String' type but receive \($0)")
})
XCTAssertNotNil(object!["operationName"])
object!["operationName"]!.map(firstTypeTransform: {
XCTAssertEqual($0, stateRequest.operationName)
}, secondTypeTransform: {
XCTFail("Require 'String' type but receive \($0)")
})
XCTAssertNotNil(object!["variables"])
object!["variables"]!.map(firstTypeTransform: {
XCTFail("Require '[String: String]' type but receive \($0)")
}, secondTypeTransform: {
XCTAssertNotNil($0["uid"])
XCTAssertEqual($0["uid"], stateRequest.variables.uid)
})
requestExpectation.fulfill()
}
mock.completion = {
completionExpectation.fulfill()
}
mock.register()
_ = try? await service.stateDevice(id: id)
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
}
// MARK: - CheckDevice
}
@@ -0,0 +1,28 @@
//
// DeviceServiceData.swift
//
//
// Created by Juraldinio on 13.01.2023.
//
import Foundation
final class DeviceServiceData {
static let createDeviceTrusted = try! Data(contentsOf: Bundle.module.url(forResource: "CreateDeviceSuccessTrusted",
withExtension: "json")!)
static let createDeviceDecodeFailed = try! Data(contentsOf: Bundle.module.url(forResource: "CreateDeviceSuccessNull",
withExtension: "json")!)
static let queryDeviceStateNull = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessNull",
withExtension: "json")!)
static let queryDeviceStateUntrusted = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessUntrusted",
withExtension: "json")!)
static let queryDeviceStateTrusted = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessTrusted",
withExtension: "json")!)
static let nodeActionsExample = try! Data(contentsOf: Bundle.module.url(forResource: "v1historyTestmalinka1",
withExtension: "json")!)
}
@@ -0,0 +1,44 @@
//
// NetworkEnvironmentImpl.swift
//
//
// Created by Juraldinio on 13.01.2023.
//
import Foundation
import WalletNetwork
struct NetworkEnvironmentImpl: NetworkEnvironment {
let configuration: URLSessionConfiguration?
let usernames: NetworkStaticAPIRecord
let backend: NetworkStaticAPIRecord
let hyperion: NetworkDynamicAPIRecord
let node: NetworkDynamicAPIRecord
static func createStubUsernames(configuration: URLSessionConfiguration,
url: URL,
headers: NetworkStaticAPIRecord.Headers? = nil) -> NetworkEnvironment {
let record = NetworkStaticAPIRecord(url: url, headers: headers)
let dumpUrl = URL(string: "Empty")!
return NetworkEnvironmentImpl(configuration: configuration,
usernames: record,
backend: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
hyperion: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil),
node: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil))
}
static func createStubNodes(configuration: URLSessionConfiguration,
url: URL,
headers: NetworkStaticAPIRecord.Headers? = nil) -> NetworkEnvironment {
let record = NetworkDynamicAPIRecord(url: { url }, headers: headers)
let dumpUrl = URL(string: "Empty")!
return NetworkEnvironmentImpl(configuration: configuration,
usernames: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
backend: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
hyperion: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil),
node: record)
}
}
@@ -0,0 +1,120 @@
//
// NodeActionModelsTests.swift
//
//
// Created by Nut.Tech on 13.01.2023.
//
import WalletNetwork
import Foundation
import XCTest
final class NodeActionModelsTests: XCTestCase {
func testNodeActAuthorization() {
let data = """
{
"actor": "testmalinka1",
"permission": "owner"
}
""".data(using: .utf8)!
let nodeAct = try? JSONDecoder().decode(NodeActAuthorization.self, from: data)
XCTAssertNotNil(nodeAct, "Error parsing NodeActAuthorization object")
XCTAssertEqual(nodeAct?.actor, "testmalinka1", "Error parsing NodeActAuthorization.actor value")
XCTAssertEqual(nodeAct?.permission, "owner", "Error parsing NodeActAuthorization.owner value")
}
func testNodeData() {
let data = """
{
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
}
""".data(using: .utf8)!
let nodeData = try? JSONDecoder().decode(NodeData.self, from: data)
XCTAssertNotNil(nodeData, "Error parsing NodeData object")
guard let parsedData = nodeData?.data else {
XCTFail("Error parsing data value")
return
}
XCTAssertEqual(parsedData["from"] as? String, "testmalinka1", "Error parsing data value")
XCTAssertEqual(parsedData["memo"] as? String, "buyram:testmalinka1", "Error parsing data value")
XCTAssertEqual(parsedData["quantity"] as? String, "0.1000 EOS", "Error parsing data value")
XCTAssertEqual(parsedData["to"] as? String, "malinkawallt", "Error parsing data value")
}
func testNodeAct() {
let data = """
{
"account": "eosio.token",
"authorization": [
{
"actor": "testmalinka1",
"permission": "owner"
}
],
"data": {
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
},
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
"name": "transfer"
}
""".data(using: .utf8)!
let nodeAct = try? JSONDecoder().decode(NodeAct.self, from: data)
XCTAssertNotNil(nodeAct, "Error parsing NodeAct object")
XCTAssertNotNil(nodeAct?.data, "Error parsing NodeAct.data object")
XCTAssertNotNil(nodeAct?.authorization, "buyram:testmalinka1", file: "Error parsing NodeAct.authorization objects")
XCTAssertEqual(nodeAct?.authorization.count, 1, "Error parsing NodeAct.authorization objects")
XCTAssertEqual(nodeAct?.name, "transfer", "Error parsing NodeAct.name value")
XCTAssertEqual(nodeAct?.account, "eosio.token", "Error parsing NodeAct.account value")
}
func testNodeActionTrace() {
guard let url = Bundle.module.url(forResource: "NodeActionTrace", withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(NodeActionTrace.self, from: data)
else {
XCTFail("Error reading JSON file")
return
}
XCTAssertNotNil(result.act, "Error parsing NodeActionTrace.act object")
XCTAssertEqual(result.blockNum, 288713481, "Error parsing NodeActionTrace.blockNum value")
XCTAssertEqual(result.blockTime, "2023-01-12T15:27:01.500", "Error parsing NodeActionTrace.blockTime value")
XCTAssertEqual(result.receiver, "malinkawallt", "Error parsing NodeActionTrace.receiver value")
XCTAssertEqual(result.trxId, "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c", "Error parsing NodeActionTrace.trxId value")
}
func testNodeAction() {
guard let url = Bundle.module.url(forResource: "NodeAction", withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(NodeAction.self, from: data)
else {
XCTFail("Error reading JSON file")
return
}
XCTAssertNotNil(result.actionTrace, "Error parsing NodeAction.actionTrace value")
XCTAssertEqual(result.accountActionSeq, 2858, "Error parsing NodeAction.accountActionSeq value")
XCTAssertEqual(result.globalActionSeq, 357585823156, "Error parsing NodeAction.globalActionSeq value")
XCTAssertTrue(result.irreversible, "Error parsing NodeAction.irreversible value")
}
func testNodeResponse() {
guard let url = Bundle.module.url(forResource: "NodeResponse", withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(NodeResponse.self, from: data)
else {
XCTFail("Error reading JSON file")
return
}
XCTAssertNotNil(result.actions, "Error parsing NodeResponse")
XCTAssertEqual(result.actions.count, 1, "Error parsing NodeResponse.actions")
}
}
@@ -0,0 +1,68 @@
//
// NodeServiceTests.swift
//
//
// Created by Nut.Tech on 16.01.2023.
//
import XCTest
@testable import WalletNetwork
import Mocker
final class NodeServiceTests: XCTestCase {
override func tearDown() {
Mocker.removeAll()
}
func testFetchNodeActions() async {
let accountName = "testmalinka3"
let offset = -100
let url = URL(string: "https://eos.greymass.com/v1/history/get_actions?account_name=\(accountName)&offset=\(offset)")!
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let env = NetworkEnvironmentImpl.createStubNodes(configuration: configuration, url: url)
let service = NodeService(environment: env)
Mock(url: url,
ignoreQuery: true,
dataType: .json,
statusCode: 200,
data: [ .get: DeviceServiceData.nodeActionsExample ]
).register()
do {
let state = try await service.fetchActions(account: accountName,
limit: offset)
guard let firstAction = state.first else {
XCTFail("Error parsing NodeActions")
return
}
XCTAssertEqual(state.count, 100, "Incorrect number of node actions")
XCTAssertEqual(firstAction.accountActionSeq, 2759, "Error parsing NodeAction.accountActionSeq")
XCTAssertEqual(firstAction.globalActionSeq, 357563493447, "Error parsing NodeAction.globalActionSeq")
XCTAssertTrue(firstAction.irreversible, "Error parsing NodeAction.irreversible")
XCTAssertNotNil(firstAction.actionTrace, "Error parsing NodeAction.actionTrace")
XCTAssertEqual(firstAction.actionTrace.blockTime, "2023-01-09T13:55:06.000", "Error parsing NodeAction.actionTrace.blockTime")
XCTAssertEqual(firstAction.actionTrace.blockNum, 288184258, "Error parsing NodeAction.actionTrace.blockNum")
XCTAssertEqual(firstAction.actionTrace.trxId, "a4dfd44c82cf0a3be1930bf6e4ec6ea51c1b331a4e77864a0d2cae4d06e8683d", "Error parsing NodeAction.actionTrace.trxId")
XCTAssertEqual(firstAction.actionTrace.receiver, "testmalinka1", "Error parsing NodeAction.actionTrace.receiver")
let act = firstAction.actionTrace.act
XCTAssertEqual(act.account, "eosio.token", "Error parsing NodeActionTrace.act.account")
XCTAssertEqual(act.name, "transfer", "Error parsing NodeActionTrace.act.name")
XCTAssertEqual(act.authorization.count, 1, "Error parsing NodeActionTrace.act.authorization")
XCTAssertNotNil(act.data, "Error parsing NodeActionTrace.act.data")
guard let authorization = act.authorization.first else {
XCTFail("Error parsing NodeAction.actionTrace.act.authorization")
return
}
XCTAssertEqual(authorization.permission, "owner", "Error parsing NodeActionTrace.act.authorization.permission")
XCTAssertEqual(authorization.actor, "testmalinka1", "Error parsing NodeActionTrace.act.authorization.actor")
} catch {
XCTFail("Catch exceprion \(error)")
}
}
}
@@ -0,0 +1 @@
{"data": {"registerDevice": {"isTrustedNow": false, "uid": null}}}
@@ -0,0 +1 @@
{"data": {"registerDevice": {"isTrustedNow": true, "uid": "a88c4532a09024c245e9"}}}
@@ -0,0 +1,51 @@
{
"account_action_seq": 2858,
"action_trace": {
"account_ram_deltas": [],
"act": {
"account": "eosio.token",
"authorization": [
{
"actor": "testmalinka1",
"permission": "owner"
}
],
"data": {
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
},
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
"name": "transfer"
},
"action_ordinal": 4,
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"closest_unnotified_ancestor_action_ordinal": 2,
"context_free": false,
"creator_action_ordinal": 2,
"elapsed": 2,
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
"receipt": {
"abi_sequence": 4,
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
"auth_sequence": [
[
"testmalinka1",
2192
]
],
"code_sequence": 4,
"global_sequence": 357585823156,
"receiver": "malinkawallt",
"recv_sequence": 14905
},
"receiver": "malinkawallt",
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
},
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"global_action_seq": 357585823156,
"irreversible": true
}
@@ -0,0 +1,44 @@
{
"account_ram_deltas": [],
"act": {
"account": "eosio.token",
"authorization": [
{
"actor": "testmalinka1",
"permission": "owner"
}
],
"data": {
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
},
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
"name": "transfer"
},
"action_ordinal": 4,
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"closest_unnotified_ancestor_action_ordinal": 2,
"context_free": false,
"creator_action_ordinal": 2,
"elapsed": 2,
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
"receipt": {
"abi_sequence": 4,
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
"auth_sequence": [
[
"testmalinka1",
2192
]
],
"code_sequence": 4,
"global_sequence": 357585823156,
"receiver": "malinkawallt",
"recv_sequence": 14905
},
"receiver": "malinkawallt",
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
}
@@ -0,0 +1,57 @@
{
"actions": [
{
"account_action_seq": 2858,
"action_trace": {
"account_ram_deltas": [],
"act": {
"account": "eosio.token",
"authorization": [
{
"actor": "testmalinka1",
"permission": "owner"
}
],
"data": {
"from": "testmalinka1",
"memo": "buyram:testmalinka1",
"quantity": "0.1000 EOS",
"to": "malinkawallt"
},
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
"name": "transfer"
},
"action_ordinal": 4,
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"closest_unnotified_ancestor_action_ordinal": 2,
"context_free": false,
"creator_action_ordinal": 2,
"elapsed": 2,
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
"receipt": {
"abi_sequence": 4,
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
"auth_sequence": [
[
"testmalinka1",
2192
]
],
"code_sequence": 4,
"global_sequence": 357585823156,
"receiver": "malinkawallt",
"recv_sequence": 14905
},
"receiver": "malinkawallt",
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
},
"block_num": 288713481,
"block_time": "2023-01-12T15:27:01.500",
"global_action_seq": 357585823156,
"irreversible": true
}
],
"head_block_num": 288838483,
"last_irreversible_block": 288838158
}
@@ -0,0 +1,8 @@
{
"data": {
"device": {
"availableAccounts": null,
"isTrustedNow": null
}
}
}
@@ -0,0 +1,8 @@
{
"data": {
"device": {
"availableAccounts": 2,
"isTrustedNow": true
}
}
}
@@ -0,0 +1,8 @@
{
"data": {
"device": {
"availableAccounts": 0,
"isTrustedNow": false
}
}
}
-72
View File
@@ -1,72 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>JMalinkaWallet.ipa</key>
<array>
<dict>
<key>architectures</key>
<array>
<string>arm64</string>
</array>
<key>bitcode</key>
<false/>
<key>buildNumber</key>
<string>12</string>
<key>certificate</key>
<dict>
<key>SHA1</key>
<string>35C003923C51D78DF01DBCBFF8DAC6666C09412D</string>
<key>dateExpires</key>
<string>8/4/22</string>
<key>type</key>
<string>iOS Distribution</string>
</dict>
<key>entitlements</key>
<dict>
<key>application-identifier</key>
<string>GENPCTDS3G.com.juraldinio.wallet</string>
<key>aps-environment</key>
<string>production</string>
<key>beta-reports-active</key>
<true/>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:paycashonline.test-app.link</string>
<string>applinks:paycashonline-alternate.test-app.link</string>
<string>applinks:paycashonline-alternate.app.link</string>
<string>applinks:paycashonline.app.link</string>
</array>
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
<key>com.apple.developer.team-identifier</key>
<string>GENPCTDS3G</string>
<key>get-task-allow</key>
<false/>
</dict>
<key>name</key>
<string>JMalinkaWallet.app</string>
<key>profile</key>
<dict>
<key>UUID</key>
<string>215599e9-99cb-4fbc-832f-12be30a2bcb0</string>
<key>dateExpires</key>
<string>8/4/22</string>
<key>name</key>
<string>MalinkaWallet</string>
</dict>
<key>symbols</key>
<true/>
<key>team</key>
<dict>
<key>id</key>
<string>GENPCTDS3G</string>
<key>name</key>
<string></string>
</dict>
<key>versionNumber</key>
<string>1.0.0</string>
</dict>
</array>
</dict>
</plist>
-29
View File
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>generateAppStoreInformation</key>
<false/>
<key>manageAppVersionAndBuildNumber</key>
<true/>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>com.juraldinio.wallet</key>
<string>MalinkaWallet</string>
</dict>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>signingStyle</key>
<string>manual</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>GENPCTDS3G</string>
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "KeyChainAccess",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "KeyChainAccess",
targets: ["KeyChainAccess"]),
],
dependencies: [
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "KeyChainAccess",
dependencies: [],
path: "./Sources"),
]
)
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Mocker",
platforms: [
.macOS(.v10_15),
.iOS(.v11),
.tvOS(.v12),
.watchOS(.v6)],
products: [
.library(name: "Mocker", targets: ["Mocker"])
],
targets: [
.target(
name: "Mocker"
),
.testTarget(
name: "MockerTests",
dependencies: ["Mocker"],
resources: [
.process("Resources")
]
)
],
swiftLanguageVersions: [.v5])
+400
View File
@@ -0,0 +1,400 @@
<p align="center">
<img width="900px" src="Assets/artwork.jpg">
</p>
<p align="center">
<img src="https://api.travis-ci.org/WeTransfer/Mocker.svg?branch=master"/>
<img src="https://img.shields.io/cocoapods/v/Mocker.svg?style=flat"/>
<img src="https://img.shields.io/cocoapods/l/Mocker.svg?style=flat"/>
<img src="https://img.shields.io/cocoapods/p/Mocker.svg?style=flat"/>
<img src="https://img.shields.io/badge/language-swift4.2-f48041.svg?style=flat"/>
<img src="https://img.shields.io/badge/carthage-compatible-4BC51D.svg?style=flat"/>
<img src="https://img.shields.io/badge/spm-compatible-4BC51D.svg?style=flat"/>
<img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat"/>
</p>
Mocker is a library written in Swift which makes it possible to mock data requests using a custom `URLProtocol`.
- [Features](#features)
- [Requirements](#requirements)
- [Usage](#usage)
- [Activating the Mocker](#activating-the-mocker)
- [Custom URLSessions](#custom-urlsessions)
- [Alamofire](#alamofire)
- [Register Mocks](#register-mocks)
- [Create your mocked data](#create-your-mocked-data)
- [JSON Requests](#json-requests)
- [File extensions](#file-extensions)
- [Custom HEAD and GET response](#custom-head-and-get-response)
- [Delayed responses](#delayed-responses)
- [Redirect responses](#redirect-responses)
- [Ignoring URLs](#ignoring-urls)
- [Mock callbacks](#mock-callbacks)
- [Unregister Mocks](#unregister-mocks)
- [Clear all registered mocks](#clear-all-registered-mocks)
- [Communication](#communication)
- [Installation](#installation)
- [Release Notes](#release-notes)
- [License](#license)
## Features
_Run all your data request unit tests offline_ 🎉
- [x] Create mocked data requests based on an URL
- [x] Create mocked data requests based on a file extension
- [x] Works with `URLSession` using a custom protocol class
- [x] Supports popular frameworks like `Alamofire`
## Usage
Unit tests are written for the `Mocker` which can help you to see how it works.
### Activating the Mocker
The mocker will automatically be activated for the default URL loading system like `URLSession.shared` after you've registered your first `Mock`.
##### Custom URLSessions
To make it work with your custom `URLSession`, the `MockingURLProtocol` needs to be registered:
```swift
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockingURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
```
##### Alamofire
Quite similar like registering on a custom `URLSession`.
```swift
let configuration = URLSessionConfiguration.af.default
configuration.protocolClasses = [MockingURLProtocol.self]
let sessionManager = Alamofire.Session(configuration: configuration)
```
### Register Mocks
##### Create your mocked data
It's recommended to create a class with all your mocked data accessible. An example of this can be found in the unit tests of this project:
```swift
public final class MockedData {
public static let botAvatarImageResponseHead: Data = try! Data(contentsOf: Bundle(for: MockedData.self).url(forResource: "Resources/Responses/bot-avatar-image-head", withExtension: "data")!)
public static let botAvatarImageFileUrl: URL = Bundle(for: MockedData.self).url(forResource: "wetransfer_bot_avater", withExtension: "png")!
public static let exampleJSON: URL = Bundle(for: MockedData.self).url(forResource: "Resources/JSON Files/example", withExtension: "json")!
}
```
##### JSON Requests
``` swift
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
let mock = Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
.get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
])
mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
guard let data = data, let jsonDictionary = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
return
}
// jsonDictionary contains your JSON sample file data
// ..
}.resume()
```
##### Empty Responses
``` swift
let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
var request = URLRequest(url: originalURL)
request.httpMethod = "PUT"
let mock = Mock(request: request, statusCode: 204)
mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
// ....
}.resume()
```
##### Ignoring the query
Some URLs like authentication URLs contain timestamps or UUIDs in the query. To mock these you can ignore the Query for a certain URL:
``` swift
/// Would transform to "https://www.example.com/api/authentication" for example.
let originalURL = URL(string: "https://www.example.com/api/authentication?oauth_timestamp=151817037")!
let mock = Mock(url: originalURL, ignoreQuery: true, contentType: .json, statusCode: 200, data: [
.get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
])
mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
guard let data = data, let jsonDictionary = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
return
}
// jsonDictionary contains your JSON sample file data
// ..
}.resume()
```
##### File extensions
```swift
let imageURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
.get: try! Data(contentsOf: MockedData.botAvatarImageFileUrl)
]).register()
URLSession.shared.dataTask(with: imageURL) { (data, response, error) in
let botAvatarImage: UIImage = UIImage(data: data!)! // This is the image from your resources.
}.resume()
```
##### Custom HEAD and GET response
```swift
let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!
Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
.head: try! Data(contentsOf: MockedData.headResponse),
.get: try! Data(contentsOf: MockedData.exampleJSON)
]).register()
URLSession.shared.dataTask(with: exampleURL) { (data, response, error) in
// data is your mocked data
}.resume()
```
##### Custom DataType
In addition to the already build in static `DataType` implementations it is possible to create custom ones that will be used as the value to the `Content-Type` header key.
```swift
let xmlURL = URL(string: "https://www.wetransfer.com/sample-xml.xml")!
Mock(fileExtensions: "png", contentType: .init(name: "xml", headerValue: "text/xml"), statusCode: 200, data: [
.get: try! Data(contentsOf: MockedData.sampleXML)
]).register()
URLSession.shared.dataTask(with: xmlURL) { (data, response, error) in
let sampleXML: Data = data // This is the xml from your resources.
}.resume(
```
##### Delayed responses
Sometimes you want to test if the cancellation of requests is working. In that case, the mocked request should not finish immediately and you need a delay. This can be added easily:
```swift
let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!
var mock = Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
.head: try! Data(contentsOf: MockedData.headResponse),
.get: try! Data(contentsOf: MockedData.exampleJSON)
])
mock.delay = DispatchTimeInterval.seconds(5)
mock.register()
```
##### Redirect responses
Sometimes you want to mock short URLs or other redirect URLs. This is possible by saving the response and mocking the redirect location, which can be found inside the response:
```
Date: Tue, 10 Oct 2017 07:28:33 GMT
Location: https://wetransfer.com/redirect
```
By creating a mock for the short URL and the redirect URL, you can mock redirect and test this behavior:
```swift
let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.redirectGET)]).register()
Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.exampleJSON)]).register()
```
##### Ignoring URLs
As the Mocker catches all URLs by default when registered, you might end up with a `fatalError` thrown in cases you don't need a mocked request. In that case, you can ignore the URL:
```swift
let ignoredURL = URL(string: "www.wetransfer.com")!
Mocker.ignore(ignoredURL)
```
However, if you need the Mocker to catch only mocked URLs and ignore every other URL, you can set the `mode` attribute to `.optin`.
```swift
Mocker.mode = .optin
```
If you want to set the original mode back, you have just to set it to `.optout`.
```swift
Mocker.mode = .optout
```
##### Mock errors
You can request a `Mock` to return an error, allowing testing of error handling.
```swift
Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()],
requestError: TestExampleError.example).register()
URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, err) in
XCTAssertNil(data)
XCTAssertNil(urlresponse)
XCTAssertNotNil(err)
if let err = err {
// there's not a particularly elegant way to verify an instance
// of an error, but this is a convenient workaround for testing
// purposes
XCTAssertEqual("example", String(describing: err))
}
expectation.fulfill()
}.resume()
```
##### Mock callbacks
You can register on `Mock` callbacks to make testing easier.
```swift
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(httpBodyType: [[String:String]].self, callback: { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments)
onRequestExpectation.fulfill()
})
mock.completion = {
endpointIsCalledExpectation.fulfill()
}
mock.register()
```
##### Mock expectations
Instead of setting the `completion` and `onRequest` you can also make use of expectations:
```swift
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
let requestExpectation = expectationForRequestingMock(&mock)
let completionExpectation = expectationForCompletingMock(&mock)
mock.register()
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
```
### Unregister Mocks
##### Clear all registered mocks
You can clear all registered mocks:
```swift
Mocker.removeAll()
```
## Communication
- If you **found a bug**, open an issue.
- If you **have a feature request**, open an issue.
- If you **want to contribute**, submit a pull request.
## Installation
### Carthage
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
You can install Carthage with [Homebrew](http://brew.sh/) using the following command:
```bash
$ brew update
$ brew install carthage
```
To integrate Mocker into your Xcode project using Carthage, specify it in your `Cartfile`:
```ogdl
github "WeTransfer/Mocker" ~> 2.3.0
```
Run `carthage update` to build the framework and drag the built `Mocker.framework` into your Xcode project.
### Swift Package Manager
The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. Its integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
#### Manifest File
Add Mocker as a package to your `Package.swift` file and then specify it as a dependency of the Target in which you wish to use it.
```swift
import PackageDescription
let package = Package(
name: "MyProject",
platforms: [
.macOS(.v10_15)
],
dependencies: [
.package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "2.3.0"))
],
targets: [
.target(
name: "MyProject",
dependencies: ["Mocker"]),
.testTarget(
name: "MyProjectTests",
dependencies: ["MyProject"]),
]
)
```
#### Xcode
To add Mocker as a [dependency](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) to your Xcode project, select *File > Swift Packages > Add Package Dependency* and enter the repository URL.
#### Resolving Build Errors
If you get the following error: *cannot find auto-link library XCTest and XCTestSwiftSupport*, set the following property under Build Options from No to Yes.
ENABLE_TESTING_SEARCH_PATHS to YES
### Manually
If you prefer not to use any of the aforementioned dependency managers, you can integrate Mocker into your project manually.
#### Embedded Framework
- Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository:
```bash
$ git init
```
- Add Mocker as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command:
```bash
$ git submodule add https://github.com/WeTransfer/Mocker.git
```
- Open the new `Mocker ` folder, and drag the `Mocker.xcodeproj` into the Project Navigator of your application's Xcode project.
> It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.
- Select the `Mocker.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target.
- Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar.
- In the tab bar at the top of that window, open the "General" panel.
- Click on the `+` button under the "Embedded Binaries" section.
- Select `Mocker.framework`.
- And that's it!
> The `Mocker.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.
---
## Release Notes
See [CHANGELOG.md](https://github.com/WeTransfer/Mocker/blob/master/Changelog.md) for a list of changes.
## License
Mocker is available under the MIT license. See the LICENSE file for more info.
@@ -0,0 +1,35 @@
//
// Mock+DataType.swift
// Mocker
//
// Created by Weiß, Alexander on 26.07.22.
// Copyright © 2022 WeTransfer. All rights reserved.
//
import Foundation
extension Mock {
/// The types of content of a request. Will be used as Content-Type header inside a `Mock`.
public struct DataType {
/// Name of the data type.
public let name: String
/// The header value of the data type.
public let headerValue: String
public init(name: String, headerValue: String) {
self.name = name
self.headerValue = headerValue
}
}
}
extension Mock.DataType {
public static let json = Mock.DataType(name: "json", headerValue: "application/json; charset=utf-8")
public static let html = Mock.DataType(name: "html", headerValue: "text/html; charset=utf-8")
public static let imagePNG = Mock.DataType(name: "imagePNG", headerValue: "image/png")
public static let pdf = Mock.DataType(name: "pdf", headerValue: "application/pdf")
public static let mp4 = Mock.DataType(name: "mp4", headerValue: "video/mp4")
public static let zip = Mock.DataType(name: "zip", headerValue: "application/zip")
}
@@ -0,0 +1,341 @@
//
// Mock.swift
// Rabbit
//
// Created by Antoine van der Lee on 04/05/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
// Mocker is only used for tests. In tests we don't even check on this SwiftLint warning, but Mocker is available through Rabbit for usage out of Rabbit. Disable for this case.
// swiftlint:disable force_unwrapping
import Foundation
import XCTest
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// A Mock which can be used for mocking data requests with the `Mocker` by calling `Mocker.register(...)`.
public struct Mock: Equatable {
/// HTTP method definitions.
///
/// See https://tools.ietf.org/html/rfc7231#section-4.3
public enum HTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case trace = "TRACE"
case connect = "CONNECT"
}
public typealias OnRequest = (_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void
/// The type of the data which designates the Content-Type header.
@available(*, deprecated, message: "Calling this property is unsafe after migrating to the `contentType` initializers, and will be removed in an upcoming release. Use `contentType` instead.")
public var dataType: DataType {
return contentType!
}
/// The type of the data which designates the Content-Type header. If set to `nil`, no Content-Type header is added to the headers.
public let contentType: DataType?
/// If set, the error that URLProtocol will report as a result rather than returning data from the mock
public let requestError: Error?
/// The headers to send back with the response.
public let headers: [String: String]
/// The HTTP status code to return with the response.
public let statusCode: Int
/// The URL value generated based on the Mock data. Force unwrapped on purpose. If you access this URL while it's not set, this is a programming error.
public var url: URL {
if urlToMock == nil && !data.keys.contains(.get) {
assertionFailure("For non GET mocks you should use the `request` property so the HTTP method is set.")
}
return urlToMock ?? generatedURL
}
/// The URL to mock as set implicitely from the init.
private let urlToMock: URL?
/// The URL generated from all the data set on this mock.
private let generatedURL: URL
/// The `URLRequest` to use if you did not set a specific URL.
public let request: URLRequest
/// If `true`, checking the URL will ignore the query and match only for the scheme, host and path.
public let ignoreQuery: Bool
/// The file extensions to match for.
public let fileExtensions: [String]?
/// The data which will be returned as the response based on the HTTP Method.
private let data: [HTTPMethod: Data]
/// Add a delay to a certain mock, which makes the response returned later.
public var delay: DispatchTimeInterval?
/// Allow response cache.
public var cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed
/// The callback which will be executed everytime this `Mock` was completed. Can be used within unit tests for validating that a request has been executed. The callback must be set before calling `register`.
public var completion: (() -> Void)?
/// The callback which will be executed everytime this `Mock` was started. Can be used within unit tests for validating that a request has been started. The callback must be set before calling `register`.
@available(*, deprecated, message: "Use `onRequestHandler` instead.")
public var onRequest: OnRequest? {
set {
onRequestHandler = OnRequestHandler(legacyCallback: newValue)
}
get {
onRequestHandler?.legacyCallback
}
}
/// The on request handler which will be executed everytime this `Mock` was started. Can be used within unit tests for validating that a request has been started. The handler must be set before calling `register`.
public var onRequestHandler: OnRequestHandler?
/// Can only be set internally as it's used by the `expectationForRequestingMock(_:)` method.
var onRequestExpectation: XCTestExpectation?
/// Can only be set internally as it's used by the `expectationForCompletingMock(_:)` method.
var onCompletedExpectation: XCTestExpectation?
private init(url: URL? = nil, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], requestError: Error? = nil, additionalHeaders: [String: String] = [:], fileExtensions: [String]? = nil) {
guard data.count > 0 else {
preconditionFailure("At least one entry is required in the data dictionary")
}
self.urlToMock = url
let generatedURL = URL(string: "https://mocked.wetransfer.com/\(contentType?.name ?? "no-content")/\(statusCode)/\(data.keys.first!.rawValue)")!
self.generatedURL = generatedURL
var request = URLRequest(url: url ?? generatedURL)
request.httpMethod = data.keys.first!.rawValue
self.request = request
self.ignoreQuery = ignoreQuery
self.requestError = requestError
self.contentType = contentType
self.statusCode = statusCode
self.data = data
self.cacheStoragePolicy = cacheStoragePolicy
var headers = additionalHeaders
if let contentType = contentType {
headers["Content-Type"] = contentType.headerValue
}
self.headers = headers
self.fileExtensions = fileExtensions?.map({ $0.replacingOccurrences(of: ".", with: "") })
}
/// Creates a `Mock` for the given data type. The mock will be automatically matched based on a URL created from the given parameters.
///
/// - Parameters:
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
@available(*, deprecated, renamed: "init(contentType:statusCode:data:additionalHeaders:)")
public init(dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: dataType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Creates a `Mock` for the given content type. The mock will be automatically matched based on a URL created from the given parameters.
///
/// - Parameters:
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
public init(contentType: DataType?, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: contentType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Creates a `Mock` for the given URL.
///
/// - Parameters:
/// - url: The URL to match for and to return the mocked data for.
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
@available(*, deprecated, renamed: "init(url:ignoreQuery:cacheStoragePolicy:contentType:statusCode:data:additionalHeaders:requestError:)")
public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
self.init(
url: url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: dataType,
statusCode: statusCode,
data: data,
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Creates a `Mock` for the given URL.
///
/// - Parameters:
/// - url: The URL to match for and to return the mocked data for.
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
self.init(
url: url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: contentType,
statusCode: statusCode,
data: data,
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
///
/// - Parameters:
/// - fileExtensions: The file extension to match for.
/// - dataType: The type of the data which designates the Content-Type header.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
@available(*, deprecated, renamed: "init(fileExtensions:contentType:statusCode:data:additionalHeaders:)")
public init(fileExtensions: String..., dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: dataType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: fileExtensions
)
}
/// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
///
/// - Parameters:
/// - fileExtensions: The file extension to match for.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response based on the HTTP Method.
/// - additionalHeaders: Additional headers to be added to the response.
public init(fileExtensions: String..., contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
self.init(
url: nil,
contentType: contentType,
statusCode: statusCode,
data: data,
additionalHeaders: additionalHeaders,
fileExtensions: fileExtensions
)
}
/// Creates a `Mock` for the given `URLRequest`.
///
/// - Parameters:
/// - request: The URLRequest, from which the URL and request method is used to match for and to return the mocked data for.
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
/// - statusCode: The HTTP status code to return with the response.
/// - data: The data which will be returned as the response. Defaults to an empty `Data` instance.
/// - additionalHeaders: Additional headers to be added to the response.
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
public init(request: URLRequest, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: Data = Data(), additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else {
preconditionFailure("Unexpected http method")
}
self.init(
url: request.url,
ignoreQuery: ignoreQuery,
cacheStoragePolicy: cacheStoragePolicy,
contentType: contentType,
statusCode: statusCode,
data: [requestHTTPMethod: data],
requestError: requestError,
additionalHeaders: additionalHeaders,
fileExtensions: nil
)
}
/// Registers the mock with the shared `Mocker`.
public func register() {
Mocker.register(self)
}
/// Returns `Data` based on the HTTP Method of the passed request.
///
/// - Parameter request: The request to match data for.
/// - Returns: The `Data` which matches the request. Will be `nil` if no data is registered for the request `HTTPMethod`.
func data(for request: URLRequest) -> Data? {
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else { return nil }
return data[requestHTTPMethod]
}
/// Used to compare the Mock data with the given `URLRequest`.
static func == (mock: Mock, request: URLRequest) -> Bool {
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else { return false }
if let fileExtensions = mock.fileExtensions {
// If the mock contains a file extension, this should always be used to match for.
guard let pathExtension = request.url?.pathExtension else { return false }
return fileExtensions.contains(pathExtension)
} else if mock.ignoreQuery {
return mock.request.url!.baseString == request.url?.baseString && mock.data.keys.contains(requestHTTPMethod)
}
return mock.request.url!.absoluteString == request.url?.absoluteString && mock.data.keys.contains(requestHTTPMethod)
}
public static func == (lhs: Mock, rhs: Mock) -> Bool {
let lhsHTTPMethods: [String] = lhs.data.keys.compactMap { $0.rawValue }
let rhsHTTPMethods: [String] = rhs.data.keys.compactMap { $0.rawValue }
if let lhsFileExtensions = lhs.fileExtensions, let rhsFileExtensions = rhs.fileExtensions, (!lhsFileExtensions.isEmpty || !rhsFileExtensions.isEmpty) {
/// The mocks are targeting file extensions specifically, check on those.
return lhsFileExtensions == rhsFileExtensions && lhsHTTPMethods == rhsHTTPMethods
}
return lhs.request.url!.absoluteString == rhs.request.url!.absoluteString && lhsHTTPMethods == rhsHTTPMethods
}
}
extension URL {
/// Returns the base URL string build with the scheme, host and path. "https://www.wetransfer.com/v1/test?param=test" would be "https://www.wetransfer.com/v1/test".
var baseString: String? {
guard let scheme = scheme, let host = host else { return nil }
return scheme + "://" + host + path
}
}
@@ -0,0 +1,142 @@
//
// Mocker.swift
// Rabbit
//
// Created by Antoine van der Lee on 04/05/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// Can be used for registering Mocked data, returned by the `MockingURLProtocol`.
public struct Mocker {
private struct IgnoredRule: Equatable {
let urlToIgnore: URL
let ignoreQuery: Bool
/// Checks if the passed URL should be ignored.
///
/// - Parameter url: The URL to check for.
/// - Returns: `true` if it should be ignored, `false` if the URL doesn't correspond to ignored rules.
func shouldIgnore(_ url: URL) -> Bool {
if ignoreQuery {
return urlToIgnore.baseString == url.baseString
}
return urlToIgnore.absoluteString == url.absoluteString
}
}
public enum HTTPVersion: String {
case http1_0 = "HTTP/1.0"
case http1_1 = "HTTP/1.1"
case http2_0 = "HTTP/2.0"
}
/// The way Mocker handles unregistered urls
public enum Mode {
/// The default mode: only URLs registered with the `ignore(_ url: URL)` method are ignored for mocking.
///
/// - Registered mocked URL: Mocked.
/// - Registered ignored URL: Ignored by Mocker, default process is applied as if the Mocker doesn't exist.
/// - Any other URL: Raises an error.
case optout
/// Only registered mocked URLs are mocked, all others pass through.
///
/// - Registered mocked URL: Mocked.
/// - Any other URL: Ignored by Mocker, default process is applied as if the Mocker doesn't exist.
case optin
}
/// The mode defines how unknown URLs are handled. Defaults to `optout` which means requests without a mock will fail.
public static var mode: Mode = .optout
/// The shared instance of the Mocker, can be used to register and return mocks.
internal static var shared = Mocker()
/// The HTTP Version to use in the mocked response.
public static var httpVersion: HTTPVersion = HTTPVersion.http1_1
/// The registrated mocks.
private(set) var mocks: [Mock] = []
/// URLs to ignore for mocking.
public var ignoredURLs: [URL] {
ignoredRules.map { $0.urlToIgnore }
}
private var ignoredRules: [IgnoredRule] = []
/// For Thread Safety access.
private let queue = DispatchQueue(label: "mocker.mocks.access.queue", attributes: .concurrent)
private init() {
// Whenever someone is requesting the Mocker, we want the URL protocol to be activated.
_ = URLProtocol.registerClass(MockingURLProtocol.self)
}
/// Register new Mocked data. If a mock for the same URL and HTTPMethod exists, it will be overwritten.
///
/// - Parameter mock: The Mock to be registered for future requests.
public static func register(_ mock: Mock) {
shared.queue.async(flags: .barrier) {
/// Delete the Mock if it was already registered.
shared.mocks.removeAll(where: { $0 == mock })
shared.mocks.append(mock)
}
}
/// Register an URL to ignore for mocking. This will let the URL work as if the Mocker doesn't exist.
///
/// - Parameter url: The URL to mock.
/// - Parameter ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
public static func ignore(_ url: URL, ignoreQuery: Bool = false) {
shared.queue.async(flags: .barrier) {
let rule = IgnoredRule(urlToIgnore: url, ignoreQuery: ignoreQuery)
shared.ignoredRules.append(rule)
}
}
/// Checks if the passed URL should be handled by the Mocker. If the URL is registered to be ignored, it will not handle the URL.
///
/// - Parameter url: The URL to check for.
/// - Returns: `true` if it should be mocked, `false` if the URL is registered as ignored.
public static func shouldHandle(_ request: URLRequest) -> Bool {
switch mode {
case .optout:
guard let url = request.url else { return false }
return shared.queue.sync {
!shared.ignoredRules.contains(where: { $0.shouldIgnore(url) })
}
case .optin:
return mock(for: request) != nil
}
}
/// Removes all registered mocks. Use this method in your tearDown function to make sure a Mock is not used in any other test.
public static func removeAll() {
shared.queue.sync(flags: .barrier) {
shared.mocks.removeAll()
shared.ignoredRules.removeAll()
}
}
/// Retrieve a Mock for the given request. Matches on `request.url` and `request.httpMethod`.
///
/// - Parameter request: The request to search for a mock.
/// - Returns: A mock if found, `nil` if there's no mocked data registered for the given request.
static func mock(for request: URLRequest) -> Mock? {
shared.queue.sync {
/// First check for specific URLs
if let specificMock = shared.mocks.first(where: { $0 == request && $0.fileExtensions == nil }) {
return specificMock
}
/// Second, check for generic file extension Mocks
return shared.mocks.first(where: { $0 == request })
}
}
}
@@ -0,0 +1,110 @@
//
// MockingURLProtocol.swift
// Rabbit
//
// Created by Antoine van der Lee on 04/05/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// The protocol which can be used to send Mocked data back. Use the `Mocker` to register `Mock` data
open class MockingURLProtocol: URLProtocol {
enum Error: Swift.Error, LocalizedError, CustomDebugStringConvertible {
case missingMockedData(url: String)
case explicitMockFailure(url: String)
var errorDescription: String? {
return debugDescription
}
var debugDescription: String {
switch self {
case .missingMockedData(let url):
return "Missing mock for URL: \(url)"
case .explicitMockFailure(url: let url):
return "Induced error for URL: \(url)"
}
}
}
private var responseWorkItem: DispatchWorkItem?
/// Returns Mocked data based on the mocks register in the `Mocker`. Will end up in an error when no Mock data is found for the request.
override public func startLoading() {
guard
let mock = Mocker.mock(for: request),
let response = HTTPURLResponse(url: mock.request.url!, statusCode: mock.statusCode, httpVersion: Mocker.httpVersion.rawValue, headerFields: mock.headers),
let data = mock.data(for: request)
else {
print("\n\n 🚨 No mocked data found for url \(String(describing: request.url?.absoluteString)) method \(String(describing: request.httpMethod)). Did you forget to use `register()`? 🚨 \n\n")
client?.urlProtocol(self, didFailWithError: Error.missingMockedData(url: String(describing: request.url?.absoluteString)))
return
}
if let onRequestHandler = mock.onRequestHandler {
onRequestHandler.handleRequest(request)
}
mock.onRequestExpectation?.fulfill()
guard let delay = mock.delay else {
finishRequest(for: mock, data: data, response: response)
return
}
self.responseWorkItem = DispatchWorkItem(block: { [weak self] in
guard let self = self else { return }
self.finishRequest(for: mock, data: data, response: response)
})
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).asyncAfter(deadline: .now() + delay, execute: responseWorkItem!)
}
private func finishRequest(for mock: Mock, data: Data, response: HTTPURLResponse) {
if let redirectLocation = data.redirectLocation {
self.client?.urlProtocol(self, wasRedirectedTo: URLRequest(url: redirectLocation), redirectResponse: response)
} else if let requestError = mock.requestError {
self.client?.urlProtocol(self, didFailWithError: requestError)
} else {
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: mock.cacheStoragePolicy)
self.client?.urlProtocol(self, didLoad: data)
self.client?.urlProtocolDidFinishLoading(self)
}
mock.completion?()
mock.onCompletedExpectation?.fulfill()
}
/// Implementation does nothing, but is needed for a valid inheritance of URLProtocol.
override public func stopLoading() {
responseWorkItem?.cancel()
}
/// Simply sends back the passed request. Implementation is needed for a valid inheritance of URLProtocol.
override public class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
/// Overrides needed to define a valid inheritance of URLProtocol.
override public class func canInit(with request: URLRequest) -> Bool {
return Mocker.shouldHandle(request)
}
}
private extension Data {
/// Returns the redirect location from the raw HTTP response if exists.
var redirectLocation: URL? {
let locationComponent = String(data: self, encoding: String.Encoding.utf8)?.components(separatedBy: "\n").first(where: { (value) -> Bool in
return value.contains("Location:")
})
guard let redirectLocationString = locationComponent?.components(separatedBy: "Location:").last, let redirectLocation = URL(string: redirectLocationString.trimmingCharacters(in: NSCharacterSet.whitespaces)) else {
return nil
}
return redirectLocation
}
}
@@ -0,0 +1,129 @@
//
// OnRequestHandler.swift
//
//
// Created by Antoine van der Lee on 03/11/2022.
// Copyright © 2022 WeTransfer. All rights reserved.
//
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
/// A handler for verifying outgoing requests.
public struct OnRequestHandler {
public typealias OnRequest<HTTPBody> = (_ request: URLRequest, _ httpBody: HTTPBody?) -> Void
private let internalCallback: (_ request: URLRequest) -> Void
let legacyCallback: Mock.OnRequest?
/// Creates a new request handler using the given `HTTPBody` type, which can be any `Decodable`.
/// - Parameters:
/// - httpBodyType: The decodable type to use for parsing the request body.
/// - callback: The callback which will be called just before the request executes.
public init<HTTPBody: Decodable>(httpBodyType: HTTPBody.Type?, callback: @escaping OnRequest<HTTPBody>) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let decodedObject = try? JSONDecoder().decode(HTTPBody.self, from: httpBody)
else {
callback(request, nil)
return
}
callback(request, decodedObject)
}
legacyCallback = nil
}
/// Creates a new request handler using the given callback to call on request without parsing the body arguments.
/// - Parameter requestCallback: The callback which will be executed just before the request executes, containing the request.
public init(requestCallback: @escaping (_ request: URLRequest) -> Void) {
self.internalCallback = requestCallback
legacyCallback = nil
}
/// Creates a new request handler using the given callback to call on request without parsing the body arguments and without passing the request.
/// - Parameter callback: The callback which will be executed just before the request executes.
public init(callback: @escaping () -> Void) {
self.internalCallback = { _ in
callback()
}
legacyCallback = nil
}
/// Creates a new request handler using the given callback to call on request.
/// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Dictionary.
public init(jsonDictionaryCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void)) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
else {
jsonDictionaryCallback(request, nil)
return
}
jsonDictionaryCallback(request, jsonObject)
}
self.legacyCallback = nil
}
/// Creates a new request handler using the given callback to call on request.
/// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Array.
public init(jsonArrayCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [[String: Any]]?) -> Void)) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [[String: Any]]
else {
jsonArrayCallback(request, nil)
return
}
jsonArrayCallback(request, jsonObject)
}
self.legacyCallback = nil
}
init(legacyCallback: Mock.OnRequest?) {
self.internalCallback = { request in
guard
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
else {
legacyCallback?(request, nil)
return
}
legacyCallback?(request, jsonObject)
}
self.legacyCallback = legacyCallback
}
func handleRequest(_ request: URLRequest) {
internalCallback(request)
}
}
private extension URLRequest {
/// We need to use the http body stream data as the URLRequest once launched converts the `httpBody` to this stream of data.
func httpBodyStreamData() -> Data? {
guard let bodyStream = self.httpBodyStream else { return nil }
bodyStream.open()
// Will read 16 chars per iteration. Can use bigger buffer if needed
let bufferSize: Int = 16
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
var data = Data()
while bodyStream.hasBytesAvailable {
let readData = bodyStream.read(buffer, maxLength: bufferSize)
data.append(buffer, count: readData)
}
buffer.deallocate()
bodyStream.close()
return data
}
}
@@ -0,0 +1,24 @@
//
// XCTest+Mocker.swift
// Mocker
//
// Created by Antoine van der Lee on 27/05/2020.
// Copyright © 2020 WeTransfer. All rights reserved.
//
import Foundation
import XCTest
public extension XCTestCase {
func expectationForRequestingMock(_ mock: inout Mock) -> XCTestExpectation {
let mockExpectation = expectation(description: "\(mock) should be requested")
mock.onRequestExpectation = mockExpectation
return mockExpectation
}
func expectationForCompletingMock(_ mock: inout Mock) -> XCTestExpectation {
let mockExpectation = expectation(description: "\(mock) should be finishing")
mock.onCompletedExpectation = mockExpectation
return mockExpectation
}
}
@@ -0,0 +1,35 @@
//
// MockTests.swift
//
//
// Created by Antoine van der Lee on 21/04/2021.
//
import Foundation
import XCTest
@testable import Mocker
final class MockTests: XCTestCase {
override func setUp() {
super.setUp()
Mocker.mode = .optout
}
override func tearDown() {
Mocker.removeAll()
Mocker.mode = .optout
super.tearDown()
}
/// It should match two file extension mocks correctly.
func testFileExtensionMocksComparing() {
let mock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
let secondMock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
let mock400 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [.put: Data()])
let mockJPEG = Mock(fileExtensions: "jpeg", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
XCTAssertEqual(mock200, secondMock200)
XCTAssertEqual(mock200, mock400)
XCTAssertNotEqual(mock200, mockJPEG)
}
}
@@ -0,0 +1,29 @@
//
// MockedData.swift
// Mocker
//
// Created by Antoine van der Lee on 11/08/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
import Foundation
/// Contains all available Mocked data.
public final class MockedData {
public static let botAvatarImageFileUrl: URL = Bundle.module.url(forResource: "wetransfer_bot_avatar", withExtension: "png")!
public static let exampleJSON: URL = Bundle.module.url(forResource: "example", withExtension: "json")!
public static let redirectGET: URL = Bundle.module.url(forResource: "sample-redirect-get", withExtension: "data")!
}
extension Bundle {
#if !SWIFT_PACKAGE
static let module = Bundle(for: MockedData.self)
#endif
}
internal extension URL {
/// Returns a `Data` representation of the current `URL`. Force unwrapping as it's only used for tests.
var data: Data {
return try! Data(contentsOf: self)
}
}
@@ -0,0 +1,636 @@
//
// MockerTests.swift
// MockerTests
//
// Created by Antoine van der Lee on 11/08/2017.
// Copyright © 2017 WeTransfer. All rights reserved.
//
import XCTest
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
@testable import Mocker
final class MockerTests: XCTestCase {
struct Framework {
let name: String?
let owner: String?
init(jsonDictionary: [String: Any]) {
name = jsonDictionary["name"] as? String
owner = jsonDictionary["owner"] as? String
}
}
override func setUp() {
super.setUp()
Mocker.mode = .optout
}
override func tearDown() {
Mocker.removeAll()
Mocker.mode = .optout
super.tearDown()
}
/// It should returned the register mocked image data as response.
func testImageURLDataRequest() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://avatars3.githubusercontent.com/u/26250426?v=4&s=400")!
let mockedData = MockedData.botAvatarImageFileUrl.data
let mock = Mock(url: originalURL, contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
])
mock.register()
URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should returned the register mocked image data as response for register file types.
func testImageExtensionDataRequest() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")
let mockedData = MockedData.botAvatarImageFileUrl.data
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
]).register()
URLSession.shared.dataTask(with: originalURL!) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should ignore file extension mocks if a specific URL is mocked.
func testSpecificURLOverGenericMocks() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [
.get: Data()
]).register()
let mockedData = MockedData.botAvatarImageFileUrl.data
Mock(url: originalURL, ignoreQuery: true, contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
]).register()
URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should correctly ignore queries if set.
func testIgnoreQueryMocking() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png?width=200&height=200")!
let mockedData = MockedData.botAvatarImageFileUrl.data
Mock(url: originalURL, ignoreQuery: true, contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
]).register()
/// Make it different compared to the mocked URL.
let customURL = URL(string: originalURL.absoluteString + "&" + UUID().uuidString)!
URLSession.shared.dataTask(with: customURL) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should return the mocked JSON.
func testJSONRequest() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
.get: MockedData.exampleJSON.data
]).register()
URLSession.shared.dataTask(with: originalURL) { (data, _, _) in
guard let data = data else {
XCTFail("Data is nil")
return
}
guard let jsonDictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
XCTFail("Wrong data response \(String(describing: data))")
expectation.fulfill()
return
}
let framework = Framework(jsonDictionary: jsonDictionary)
XCTAssertEqual(framework.name, "Mocker")
XCTAssertEqual(framework.owner, "WeTransfer")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// No Content-Type should be included in the headers
func testNoContentType() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
var request = URLRequest(url: originalURL)
request.httpMethod = "PUT"
Mock(request: request, statusCode: 202).register()
URLSession.shared.dataTask(with: request) { (data, response, _) in
guard let response = response as? HTTPURLResponse else {
XCTFail("Unexpected response")
return
}
// data is only nil if there is an error
XCTAssertEqual(data, Data())
XCTAssertNil(response.allHeaderFields["Content-Type"])
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should return the additional headers.
func testAdditionalHeaders() {
let expectation = self.expectation(description: "Data request should succeed")
let headers = ["Testkey": "testvalue"]
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: headers)
mock.register()
URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
XCTAssertNil(error)
XCTAssertEqual(((response as? HTTPURLResponse)?.allHeaderFields["Testkey"] as? String), "testvalue", "Additional headers should be added.")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should override existing mocks.
func testMockOverriding() {
let expectation = self.expectation(description: "Data request should succeed")
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["testkey": "testvalue"])
mock.register()
let newMock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["Newkey": "newvalue"])
newMock.register()
URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
XCTAssertNil(error)
XCTAssertEqual(((response as? HTTPURLResponse)?.allHeaderFields["Newkey"] as? String), "newvalue", "Additional headers should be added.")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should work with a custom URLSession.
func testCustomURLSession() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")
let mockedData = MockedData.botAvatarImageFileUrl.data
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
.get: mockedData
]).register()
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [MockingURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
urlSession.dataTask(with: originalURL!) { (data, _, error) in
XCTAssertNil(error)
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should be possible to test cancellation of requests with a delayed mock.
func testDelayedMockCancelation() {
let expectation = self.expectation(description: "Data request should be cancelled")
var mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
mock.delay = DispatchTimeInterval.seconds(5)
mock.register()
let task = URLSession.shared.dataTask(with: mock.request) { (_, _, error) in
XCTAssertEqual(error?._code, NSURLErrorCancelled)
expectation.fulfill()
}
task.resume()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
task.cancel()
})
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should correctly handle redirect responses.
func testRedirectResponse() throws {
#if os(Linux)
throw XCTSkip("The URLSession swift-corelibs-foundation implementation doesn't currently handle redirects directly")
#endif
let expectation = self.expectation(description: "Data request should be cancelled")
let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: MockedData.redirectGET.data]).register()
Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: MockedData.exampleJSON.data]).register()
URLSession.shared.dataTask(with: urlWhichRedirects) { (data, _, _) in
guard let data = data else {
XCTFail("Data is nil")
return
}
guard let jsonDictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
XCTFail("Wrong data response \(String(describing: data))")
expectation.fulfill()
return
}
let framework = Framework(jsonDictionary: jsonDictionary)
XCTAssertEqual(framework.name, "Mocker")
XCTAssertEqual(framework.owner, "WeTransfer")
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should be possible to ignore URLs and not let them be handled.
func testIgnoreURLs() {
let ignoredURL = URL(string: "www.wetransfer.com")!
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
Mocker.ignore(ignoredURL)
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
}
/// It should be possible to ignore URLs and not let them be handled.
func testIgnoreURLsIgnoreQueries() {
let ignoredURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
let ignoredURLQueries = URL(string: "https://www.wetransfer.com/sample-image.png?width=200&height=200")!
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLQueries)))
Mocker.ignore(ignoredURL, ignoreQuery: true)
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLQueries)))
}
/// It should be possible to compose a url relative to a base and still have it match the full url
func testComposedURLMatch() {
let composedURL = URL(fileURLWithPath: "resource", relativeTo: URL(string: "https://host.com/api/"))
let simpleURL = URL(string: "https://host.com/api/resource")
let mock = Mock(url: composedURL, contentType: .json, statusCode: 200, data: [.get: MockedData.exampleJSON.data])
let urlRequest = URLRequest(url: simpleURL!)
XCTAssertEqual(composedURL.absoluteString, simpleURL?.absoluteString)
XCTAssert(mock == urlRequest)
}
/// It should call the onRequest and completion callbacks when a `Mock` is used and completed in the right order.
func testMockCallbacks() {
let onRequestExpectation = expectation(description: "Data request should start")
let completionExpectation = expectation(description: "Data request should succeed")
var mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
mock.onRequest = { _, _ in
onRequestExpectation.fulfill()
}
mock.completion = {
completionExpectation.fulfill()
}
mock.register()
URLSession.shared.dataTask(with: mock.request).resume()
wait(for: [onRequestExpectation, completionExpectation], timeout: 2.0, enforceOrder: true)
}
/// It should report post body arguments if they exist.
func testOnRequestLegacyPostBodyParameters() throws {
let onRequestExpectation = expectation(description: "Data request should start")
let expectedParameters = ["test": "value"]
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequest = { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments as? [String: String])
onRequestExpectation.fulfill()
}
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
func testOnRequestDecodablePostBodyParameters() throws {
struct RequestParameters: Codable, Equatable {
let name: String
}
let onRequestExpectation = expectation(description: "Data request should start")
let expectedParameters = RequestParameters(name: UUID().uuidString)
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
request.httpBody = try JSONEncoder().encode(expectedParameters)
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = .init(httpBodyType: RequestParameters.self, callback: { request, postBodyDecodable in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyDecodable)
onRequestExpectation.fulfill()
})
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
func testOnRequestJSONDictionaryPostBodyParameters() throws {
let onRequestExpectation = expectation(description: "Data request should start")
let expectedParameters = ["test": "value"]
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = .init(jsonDictionaryCallback: { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments as? [String: String])
onRequestExpectation.fulfill()
})
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
func testOnRequestCallbackWithoutRequestAndParameters() throws {
let onRequestExpectation = expectation(description: "Data request should start")
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = .init(callback: {
onRequestExpectation.fulfill()
})
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
/// It should report post body arguments with top level collection type if they exist.
func testOnRequestPostBodyParametersWithTopLevelCollectionType() throws {
let onRequestExpectation = expectation(description: "Data request should start")
let expectedParameters = [["test": "value"], ["test": "value"]]
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
request.httpMethod = Mock.HTTPMethod.post.rawValue
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
mock.onRequestHandler = OnRequestHandler(jsonArrayCallback: { request, postBodyArguments in
XCTAssertEqual(request.url, mock.request.url)
XCTAssertEqual(expectedParameters, postBodyArguments as? [[String: String]])
onRequestExpectation.fulfill()
})
mock.register()
URLSession.shared.dataTask(with: request).resume()
wait(for: [onRequestExpectation], timeout: 2.0)
}
/// It should call the mock after a delay.
func testDelayedMock() {
let nonDelayExpectation = expectation(description: "Data request should succeed")
let delayedExpectation = expectation(description: "Data request should succeed")
var delayedMock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
delayedMock.delay = DispatchTimeInterval.seconds(1)
delayedMock.completion = {
delayedExpectation.fulfill()
}
delayedMock.register()
var nonDelayMock = Mock(contentType: .json, statusCode: 200, data: [.post: Data()])
nonDelayMock.completion = {
nonDelayExpectation.fulfill()
}
nonDelayMock.register()
XCTAssertNotEqual(delayedMock.request.url, nonDelayMock.request.url)
URLSession.shared.dataTask(with: delayedMock.request).resume()
URLSession.shared.dataTask(with: nonDelayMock.request).resume()
wait(for: [nonDelayExpectation, delayedExpectation], timeout: 2.0, enforceOrder: true)
}
/// It should remove all registered mocks correctly.
func testRemoveAll() {
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
mock.register()
Mocker.removeAll()
XCTAssertTrue(Mocker.shared.mocks.isEmpty)
}
/// It should correctly add two mocks for the same URL if the HTTP method is different.
func testDifferentHTTPMethodSameURL() {
let url = URL(string: "https://www.fakeurl.com/\(UUID().uuidString)")!
Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()]).register()
Mock(url: url, contentType: .json, statusCode: 200, data: [.put: Data()]).register()
var request = URLRequest(url: url)
request.httpMethod = Mock.HTTPMethod.get.rawValue
XCTAssertNotNil(Mocker.mock(for: request))
request.httpMethod = Mock.HTTPMethod.put.rawValue
XCTAssertNotNil(Mocker.mock(for: request))
}
/// It should call the on request expectation.
func testOnRequestExpectation() {
let url = URL(string: "https://www.fakeurl.com")!
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
let expectation = expectationForRequestingMock(&mock)
mock.register()
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
wait(for: [expectation], timeout: 2.0)
}
/// It should call the on completion expectation.
func testOnCompletionExpectation() {
let url = URL(string: "https://www.fakeurl.com")!
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
let expectation = expectationForCompletingMock(&mock)
mock.register()
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
wait(for: [expectation], timeout: 2.0)
}
/// it should return the error we requested from the mock when we pass in an Error.
func testMockReturningError() {
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
enum TestExampleError: Error, LocalizedError {
case example
var errorDescription: String { "example" }
}
Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()], requestError: TestExampleError.example).register()
URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, error) in
XCTAssertNil(data)
XCTAssertNil(urlresponse)
XCTAssertNotNil(error)
if let error = error {
#if os(Linux)
XCTAssertEqual(error as? TestExampleError, .example)
#else
// there's not a particularly elegant way to verify an instance
// of an error, but this is a convenient workaround for testing
// purposes
XCTAssertTrue(String(describing: error).contains("TestExampleError"))
#endif
}
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should cache response
func testMockCachePolicy() throws {
#if os(Linux)
throw XCTSkip("URLSessionTask in swift-corelibs-foundation doesn't cache response for custom protocols")
#endif
let expectation = self.expectation(description: "Data request should succeed")
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
Mock(url: originalURL, cacheStoragePolicy: .allowed,
contentType: .json, statusCode: 200,
data: [.get: MockedData.exampleJSON.data],
additionalHeaders: ["Cache-Control": "public, max-age=31557600, immutable"]
).register()
let configuration = URLSessionConfiguration.default
#if !os(Linux)
configuration.urlCache = URLCache()
#endif
configuration.protocolClasses = [MockingURLProtocol.self]
let urlSession = URLSession(configuration: configuration)
urlSession.dataTask(with: originalURL) { (_, _, error) in
XCTAssertNil(error)
let cachedResponse = configuration.urlCache?.cachedResponse(for: URLRequest(url: originalURL))
XCTAssertNotNil(cachedResponse)
XCTAssertEqual(cachedResponse!.data, MockedData.exampleJSON.data)
expectation.fulfill()
}.resume()
waitForExpectations(timeout: 10.0, handler: nil)
}
/// It should process unknown URL
func testMockerOptoutMode() {
Mocker.mode = .optout
let mockedURL = URL(string: "www.google.com")!
let ignoredURL = URL(string: "www.wetransfer.com")!
let unknownURL = URL(string: "www.netflix.com")!
// Mocking
Mock(url: mockedURL, contentType: .json, statusCode: 200, data: [.get: Data()])
.register()
// Ignoring
Mocker.ignore(ignoredURL)
// Checking mocked URL are processed by Mocker
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: mockedURL)))
// Checking ignored URL are not processed by Mocker
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
// Checking unknown URL are processed by Mocker (.optout mode)
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: unknownURL)))
}
/// It should not process unknown URL
func testMockerOptinMode() {
Mocker.mode = .optin
let mockedURL = URL(string: "www.google.com")!
let ignoredURL = URL(string: "www.wetransfer.com")!
let unknownURL = URL(string: "www.netflix.com")!
// Mocking
Mock(url: mockedURL, contentType: .json, statusCode: 200, data: [.get: Data()])
.register()
// Ignoring
Mocker.ignore(ignoredURL)
// Checking mocked URL are processed by Mocker
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: mockedURL)))
// Checking ignored URL are not processed by Mocker
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
// Checking unknown URL are not processed by Mocker (.optin mode)
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: unknownURL)))
}
/// Default mode should be .optout
func testDefaultMode() {
/// Checking default mode
XCTAssertEqual(.optout, Mocker.mode)
}
}
@@ -0,0 +1,4 @@
{
"name": "Mocker",
"owner": "WeTransfer"
}
@@ -0,0 +1,14 @@
HTTP/1.1 302 Moved Temporarily
Content-Type: text/html;charset=utf-8
Content-Length: 0
Cache-Control: public
Date: Tue, 10 Oct 2017 07:28:33 GMT
Location: https://wetransfer.com/redirect
Server: nginx/1.12.0
X-Content-Type-Options: nosniff
X-Request-Id: 8c43587ec891b2f1f72c61ecec2e96db
X-XSS-Protection: 1; mode=block
X-Cache: Miss from cloudfront
Via: 1.1 72f202fb973968c0cfdb028ab6f36fac.cloudfront.net (CloudFront)
X-Amz-Cf-Id: tU8eVZ9jWBJzd3aEB-4gyym_VxcPKskWFByEvXapy5WrdDkV-35-KA==
Connection: Keep-alive

Some files were not shown because too many files have changed in this diff Show More