diff --git a/egym-ios-mwa-reference/Assets.xcassets/100.png b/egym-ios-mwa-reference/Assets.xcassets/100.png new file mode 100644 index 0000000..549108d Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/100.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/102.png b/egym-ios-mwa-reference/Assets.xcassets/102.png new file mode 100644 index 0000000..b2f1bcc Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/102.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/1024.jpeg b/egym-ios-mwa-reference/Assets.xcassets/1024.jpeg new file mode 100644 index 0000000..f7bdc72 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/1024.jpeg differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/108.png b/egym-ios-mwa-reference/Assets.xcassets/108.png new file mode 100644 index 0000000..e75eb8e Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/108.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/114.png b/egym-ios-mwa-reference/Assets.xcassets/114.png new file mode 100644 index 0000000..80b05e3 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/114.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/120.png b/egym-ios-mwa-reference/Assets.xcassets/120.png new file mode 100644 index 0000000..d66ccf7 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/120.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/128.png b/egym-ios-mwa-reference/Assets.xcassets/128.png new file mode 100644 index 0000000..d1a891b Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/128.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/144.png b/egym-ios-mwa-reference/Assets.xcassets/144.png new file mode 100644 index 0000000..7b28033 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/144.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/152.png b/egym-ios-mwa-reference/Assets.xcassets/152.png new file mode 100644 index 0000000..a0f92ee Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/152.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/16.png b/egym-ios-mwa-reference/Assets.xcassets/16.png new file mode 100644 index 0000000..8b4c634 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/16.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/167.png b/egym-ios-mwa-reference/Assets.xcassets/167.png new file mode 100644 index 0000000..8b4c4d6 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/167.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/172.png b/egym-ios-mwa-reference/Assets.xcassets/172.png new file mode 100644 index 0000000..547f6ad Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/172.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/180.png b/egym-ios-mwa-reference/Assets.xcassets/180.png new file mode 100644 index 0000000..bbf1446 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/180.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/196.png b/egym-ios-mwa-reference/Assets.xcassets/196.png new file mode 100644 index 0000000..c85d870 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/196.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/20.png b/egym-ios-mwa-reference/Assets.xcassets/20.png new file mode 100644 index 0000000..05d796f Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/20.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/216.png b/egym-ios-mwa-reference/Assets.xcassets/216.png new file mode 100644 index 0000000..efb5620 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/216.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/234.png b/egym-ios-mwa-reference/Assets.xcassets/234.png new file mode 100644 index 0000000..8611382 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/234.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/256.png b/egym-ios-mwa-reference/Assets.xcassets/256.png new file mode 100644 index 0000000..e14c769 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/256.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/258.png b/egym-ios-mwa-reference/Assets.xcassets/258.png new file mode 100644 index 0000000..1c8ae14 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/258.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/29.png b/egym-ios-mwa-reference/Assets.xcassets/29.png new file mode 100644 index 0000000..9e7e4c4 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/29.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/32.png b/egym-ios-mwa-reference/Assets.xcassets/32.png new file mode 100644 index 0000000..cc89dde Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/32.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/40.png b/egym-ios-mwa-reference/Assets.xcassets/40.png new file mode 100644 index 0000000..5741b13 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/40.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/48.png b/egym-ios-mwa-reference/Assets.xcassets/48.png new file mode 100644 index 0000000..8b935df Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/48.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/50.png b/egym-ios-mwa-reference/Assets.xcassets/50.png new file mode 100644 index 0000000..08a4489 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/50.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/512.png b/egym-ios-mwa-reference/Assets.xcassets/512.png new file mode 100644 index 0000000..74bbea3 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/512.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/55.png b/egym-ios-mwa-reference/Assets.xcassets/55.png new file mode 100644 index 0000000..d2c89c0 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/55.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/57.png b/egym-ios-mwa-reference/Assets.xcassets/57.png new file mode 100644 index 0000000..30ba783 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/57.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/58.png b/egym-ios-mwa-reference/Assets.xcassets/58.png new file mode 100644 index 0000000..9ae33c1 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/58.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/60.png b/egym-ios-mwa-reference/Assets.xcassets/60.png new file mode 100644 index 0000000..3d63ba2 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/60.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/64.png b/egym-ios-mwa-reference/Assets.xcassets/64.png new file mode 100644 index 0000000..574cd9e Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/64.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/66.png b/egym-ios-mwa-reference/Assets.xcassets/66.png new file mode 100644 index 0000000..d7dae7d Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/66.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/72.png b/egym-ios-mwa-reference/Assets.xcassets/72.png new file mode 100644 index 0000000..96505ae Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/72.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/76.png b/egym-ios-mwa-reference/Assets.xcassets/76.png new file mode 100644 index 0000000..a53088b Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/76.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/80.png b/egym-ios-mwa-reference/Assets.xcassets/80.png new file mode 100644 index 0000000..40f153f Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/80.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/87.png b/egym-ios-mwa-reference/Assets.xcassets/87.png new file mode 100644 index 0000000..6875714 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/87.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/88.png b/egym-ios-mwa-reference/Assets.xcassets/88.png new file mode 100644 index 0000000..a5a2fae Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/88.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/92.png b/egym-ios-mwa-reference/Assets.xcassets/92.png new file mode 100644 index 0000000..be4ab06 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/92.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/100.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000..549108d Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/102.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/102.png new file mode 100644 index 0000000..b2f1bcc Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/102.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024 1.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024 1.png new file mode 100644 index 0000000..95b9313 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024 1.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024 2.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024 2.png new file mode 100644 index 0000000..95b9313 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024 2.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..95b9313 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/108.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/108.png new file mode 100644 index 0000000..e75eb8e Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/108.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/114.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..80b05e3 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/120.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..d66ccf7 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/128.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000..d1a891b Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/144.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000..7b28033 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/152.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..a0f92ee Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/16.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000..8b4c634 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/16.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/167.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..8b4c4d6 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/172.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 0000000..547f6ad Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/172.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/180.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..bbf1446 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/196.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 0000000..c85d870 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/196.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/20.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..05d796f Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/216.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 0000000..efb5620 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/216.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/234.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/234.png new file mode 100644 index 0000000..8611382 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/234.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/256.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000..e14c769 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/256.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/258.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/258.png new file mode 100644 index 0000000..1c8ae14 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/258.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/29.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..9e7e4c4 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/32.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 0000000..cc89dde Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/40.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..5741b13 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/48.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 0000000..8b935df Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/48.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/50.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000..08a4489 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/512.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000..74bbea3 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/512.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/55.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 0000000..d2c89c0 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/55.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/57.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..30ba783 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/58.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..9ae33c1 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/60.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..3d63ba2 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/64.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000..574cd9e Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/64.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/66.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/66.png new file mode 100644 index 0000000..d7dae7d Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/66.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/72.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000..96505ae Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/76.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..a53088b Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/80.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..40f153f Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/87.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..6875714 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/88.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 0000000..a5a2fae Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/88.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/92.png b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/92.png new file mode 100644 index 0000000..be4ab06 Binary files /dev/null and b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/92.png differ diff --git a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/Contents.json b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/Contents.json index 2305880..aebbed3 100644 --- a/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/egym-ios-mwa-reference/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,30 +1,345 @@ { "images" : [ { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" }, { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "idiom" : "universal", - "platform" : "ios", + "filename" : "29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "100.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "72.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "72x72" + }, + { + "filename" : "144.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "72x72" + }, + { + "filename" : "76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024 1.png", + "idiom" : "ios-marketing", + "scale" : "1x", "size" : "1024x1024" }, { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "tinted" - } - ], - "idiom" : "universal", - "platform" : "ios", + "filename" : "16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "1024 2.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + }, + { + "filename" : "48.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "24x24", + "subtype" : "38mm" + }, + { + "filename" : "55.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "27.5x27.5", + "subtype" : "42mm" + }, + { + "filename" : "58.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "66.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "33x33", + "subtype" : "45mm" + }, + { + "filename" : "80.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "40x40", + "subtype" : "38mm" + }, + { + "filename" : "88.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "44x44", + "subtype" : "40mm" + }, + { + "filename" : "92.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "46x46", + "subtype" : "41mm" + }, + { + "filename" : "100.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "50x50", + "subtype" : "44mm" + }, + { + "filename" : "102.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "51x51", + "subtype" : "45mm" + }, + { + "filename" : "108.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, + { + "filename" : "172.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "86x86", + "subtype" : "38mm" + }, + { + "filename" : "196.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "98x98", + "subtype" : "42mm" + }, + { + "filename" : "216.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "108x108", + "subtype" : "44mm" + }, + { + "filename" : "234.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "117x117", + "subtype" : "45mm" + }, + { + "filename" : "258.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" + }, + { + "filename" : "1024.png", + "idiom" : "watch-marketing", + "scale" : "1x", "size" : "1024x1024" } ], diff --git a/egym-ios-mwa-reference/Assets.xcassets/Contents.json b/egym-ios-mwa-reference/Assets.xcassets/Contents.json index 73c0059..2aa0709 100644 --- a/egym-ios-mwa-reference/Assets.xcassets/Contents.json +++ b/egym-ios-mwa-reference/Assets.xcassets/Contents.json @@ -2,5 +2,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "compression-type" : "lossless" } } diff --git a/egym-ios-mwa-reference/ConfigView.swift b/egym-ios-mwa-reference/ConfigView.swift new file mode 100644 index 0000000..0aa9e67 --- /dev/null +++ b/egym-ios-mwa-reference/ConfigView.swift @@ -0,0 +1,143 @@ +import SwiftUI +import UniformTypeIdentifiers +import WebKit + +struct ConfigView: View { + @AppStorage("config_clientId") private var clientId: String = "" + @AppStorage("config_memberId") private var memberId: String = "" + @AppStorage("config_privateKey") private var privateKey: String = "" + @AppStorage("config_beurl") private var beurl: String = "" + @AppStorage("config_gymLocationId") private var gymLocationId: String = "" + @AppStorage("config_locale") private var locale: String = "en-US" + + private let availableLocales = [ + "ar-AE", "be-BY", "bg-BG", "ca-ES", "cs-CZ", "cy-GB", "da-DK", + "de-DE", "el-GR", "en-AU", "en-CA", "en-GB", "en-US", "es-419", + "es-ES", "es-MX", "fi-FI", "fr-CA", "fr-FR", "he-IL", "is-IS", + "it-IT", "ja-JP", "ko-KR", "nb-NO", "nl-BE", "nl-NL", "pl-PL", + "pt-PT", "ro-RO", "ru-RU", "sv-SE", "th-TH", "tr-TR", "uk-UA", + "zh-CN", "zh-Hans-CN", "zh-TW" + ] + + @State private var showFileImporter = false + @State private var tokensClearedMessage: String? + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + Form { + Section("Gym Location ID") { + TextField("e.g. 999", text: $gymLocationId) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + } + + Section("Locale") { + Picker("Language", selection: $locale) { + ForEach(availableLocales, id: \.self) { loc in + Text(loc).tag(loc) + } + } + .pickerStyle(.menu) + } + + Section("Member ID") { + TextField("e.g. 22205900226191", text: $memberId) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .keyboardType(.numberPad) + } + + Section("RSA Private Key (PKCS8 PEM)") { + if privateKey.isEmpty { + Text("No key loaded") + .foregroundStyle(.secondary) + } else { + Text("Key loaded (\(privateKey.count) chars)") + .foregroundStyle(.green) + } + + Button("Import .pem / .key file") { + showFileImporter = true + } + + Button("Clear Key", role: .destructive) { + privateKey = "" + } + .disabled(privateKey.isEmpty) + } + + Section("Client ID (optional)") { + TextField("e.g. rmwabrand", text: $clientId) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + } + + Section("Backend URL (optional)") { + TextField("e.g. https://api.example.com", text: $beurl) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .keyboardType(.URL) + } + + Section("Tokens") { + Button("Clear Access & Refresh Tokens + Cookies", role: .destructive) { + let defaults = UserDefaults.standard + let allKeys = defaults.dictionaryRepresentation().keys + for key in allKeys { + let lower = key.lowercased() + if lower.contains("token") || lower.contains("access") || lower.contains("refresh") { + defaults.removeObject(forKey: key) + } + } + defaults.synchronize() + + // Clear HTTPCookieStorage + if let cookies = HTTPCookieStorage.shared.cookies { + for cookie in cookies { + HTTPCookieStorage.shared.deleteCookie(cookie) + } + } + + // Clear WKWebView cookies and website data + let dataStore = WKWebsiteDataStore.default() + let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes() + dataStore.removeData(ofTypes: dataTypes, modifiedSince: .distantPast) { + print("WebView data cleared") + } + + tokensClearedMessage = "Tokens & cookies cleared!" + } + if let message = tokensClearedMessage { + Text(message) + .foregroundStyle(.green) + .font(.footnote) + } + } + + Section { + Button("Done") { dismiss() } + } + } + .navigationTitle("JWT Configuration") + .fileImporter( + isPresented: $showFileImporter, + allowedContentTypes: [.plainText, .data], + allowsMultipleSelection: false + ) { result in + switch result { + case .success(let urls): + guard let url = urls.first else { return } + let accessing = url.startAccessingSecurityScopedResource() + defer { if accessing { url.stopAccessingSecurityScopedResource() } } + if let data = try? Data(contentsOf: url), + let content = String(data: data, encoding: .utf8) { + privateKey = content + } + case .failure(let error): + print("File import error: \(error.localizedDescription)") + } + } + } + } +} diff --git a/egym-ios-mwa-reference/ContentView.swift b/egym-ios-mwa-reference/ContentView.swift index b946497..491a4c4 100644 --- a/egym-ios-mwa-reference/ContentView.swift +++ b/egym-ios-mwa-reference/ContentView.swift @@ -14,83 +14,96 @@ let IS_WEB_DEBUGGABLE = false struct ContentView: View { #if canImport(IonicPortals) - - private static let egymWorkoutsPortal = Portal( - name: "egym-workouts", - startDir: "portals/webdir", - initialContext: [ - "startingRoute": "/workouts/home", - "email": "email@example.com", - "firstName": "Oleksandr", - "lastName": "Usyk", - "dateOfBirth": "1990-01-01", - "gymLocation": "DE01", - "language": "de-DE", - "measurementSystem": "METRIC", - "gender": "MALE", -// "memberId": "jwt", // unique member id signed in JWT - "cardNumber": "000123123123" // 12 digit number with leading zeros - ], - plugins: [.type(PreferencesPlugin.self), .type(CapacitorNFCPassWalletPlugin.self)], - liveUpdateConfig: LiveUpdate( - appId: "851e0894", - channel: "reference", - syncOnAdd: true - ), - ).configuring(\.isWebDebuggable, IS_WEB_DEBUGGABLE) - - private static let egymBioagePortal = Portal( - name: "egym-bioage", - startDir: "portals/webdir", - initialContext: [ - "startingRoute": "/bioage/home", - "email": "email@example.com", - "firstName": "Oleksandr", - "lastName": "Usyk", - "dateOfBirth": "1990-01-01", - "gymLocation": "DE01", - "language": "de-DE", - "measurementSystem": "METRIC", - "gender": "MALE", -// "memberId": "jwt", // unique member id signed in JWT - "cardNumber": "000123123123" // 12 digit number with leading zeros - ], - plugins: [.type(PreferencesPlugin.self), .type(CapacitorNFCPassWalletPlugin.self)], - liveUpdateConfig: LiveUpdate( - appId: "068a3720", - channel: "reference", - syncOnAdd: true - ), - ).configuring(\.isWebDebuggable, IS_WEB_DEBUGGABLE) - private static let egymNfcPortal = Portal( - name: "egym-nfc", - startDir: "portals/webdir", - initialContext: [ - "startingRoute": "/nfc/home", - "email": "email@example.com", - "firstName": "Oleksandr", - "lastName": "Usyk", - "dateOfBirth": "1990-01-01", - "gymLocation": "DE01", - "language": "de-DE", - "measurementSystem": "METRIC", - "gender": "MALE", -// "memberId": "jwt", // unique member id signed in JWT - "cardNumber": "000123123123" // 12 digit number with leading zeros - ], - plugins: [.type(PreferencesPlugin.self), .type(CapacitorNFCPassWalletPlugin.self)], - liveUpdateConfig: LiveUpdate( - appId: "dcbe378a", - channel: "reference", - syncOnAdd: true - ), - ).configuring(\.isWebDebuggable, IS_WEB_DEBUGGABLE) + @AppStorage("config_clientId") private var clientId: String = "" + @AppStorage("config_memberId") private var memberId: String = "" + @AppStorage("config_privateKey") private var privateKey: String = "" + @AppStorage("config_beurl") private var beurl: String = "" + @AppStorage("config_gymLocationId") private var gymLocationId: String = "" + @AppStorage("config_locale") private var locale: String = "en-US" + + private var memberIdJWT: String? { + guard !memberId.isEmpty, !privateKey.isEmpty else { return nil } + return try? JWTGenerator.generateMembershipJWT(memberId: memberId, privateKeyPEM: privateKey) + } + + private func makeContext(startingRoute: String) -> [String: String] { + var ctx: [String: String] = [ + "startingRoute": startingRoute, + "email": "email@example.com", + "firstName": "Oleksandr", + "lastName": "Usyk", + "dateOfBirth": "1990-01-01", + "gymLocation": "DE01", + "language": locale, + "measurementSystem": "METRIC", + "gender": "MALE", + "cardNumber": "000123123123", + "showLogger": "true" + ] + if !clientId.isEmpty { + ctx["clientId"] = clientId + } + if !gymLocationId.isEmpty { + ctx["gymLocation"] = gymLocationId + } + if let jwt = memberIdJWT { + ctx["memberId"] = jwt + } + if !beurl.isEmpty { + ctx["url"] = beurl + } + print("[Portal Context] \(startingRoute): \(ctx)") + return ctx + } + + private func workoutsPortal() -> Portal { + Portal( + name: "egym-workouts", + startDir: "portals/webdir", + initialContext: makeContext(startingRoute: "/workouts/home"), + plugins: [.type(PreferencesPlugin.self), .type(CapacitorNFCPassWalletPlugin.self)], + liveUpdateConfig: LiveUpdate( + appId: "851e0894", + channel: "reference", + syncOnAdd: true + ) + ).configuring(\.isWebDebuggable, IS_WEB_DEBUGGABLE) + } + + private func bioagePortal() -> Portal { + Portal( + name: "egym-bioage", + startDir: "portals/webdir", + initialContext: makeContext(startingRoute: "/bioage/home"), + plugins: [.type(PreferencesPlugin.self), .type(CapacitorNFCPassWalletPlugin.self)], + liveUpdateConfig: LiveUpdate( + appId: "068a3720", + channel: "reference", + syncOnAdd: true + ) + ).configuring(\.isWebDebuggable, IS_WEB_DEBUGGABLE) + } + + private func nfcPortal() -> Portal { + Portal( + name: "egym-nfc", + startDir: "portals/webdir", + initialContext: makeContext(startingRoute: "/nfc/home"), + plugins: [.type(PreferencesPlugin.self), .type(CapacitorNFCPassWalletPlugin.self)], + liveUpdateConfig: LiveUpdate( + appId: "dcbe378a", + channel: "egymdevelop", + syncOnAdd: true + ) + ).configuring(\.isWebDebuggable, IS_WEB_DEBUGGABLE) + } #endif @State private var showWorkoutsWebApp = false @State private var showBioageWebApp = false @State private var showNfcWebApp = false + @State private var showConfig = false @State private var dismissListener: Task? var body: some View { @@ -101,13 +114,18 @@ struct ContentView: View { Text("Hello, world!") #if canImport(IonicPortals) + Button("⚙️ JWT Config") { showConfig = true } + .sheet(isPresented: $showConfig) { + ConfigView() + } + Button("Workouts MWA") { showWorkoutsWebApp = true } .fullScreenCover(isPresented: $showWorkoutsWebApp, onDismiss: { // cleanup when modal goes away dismissListener?.cancel() dismissListener = nil }) { - PortalView(portal: Self.egymWorkoutsPortal) + PortalView(portal: workoutsPortal()) .onAppear { dismissListener?.cancel() dismissListener = Task { @@ -132,7 +150,7 @@ struct ContentView: View { dismissListener?.cancel() dismissListener = nil }) { - PortalView(portal: Self.egymBioagePortal) + PortalView(portal: bioagePortal()) .onAppear { dismissListener?.cancel() dismissListener = Task { @@ -157,7 +175,7 @@ struct ContentView: View { dismissListener?.cancel() dismissListener = nil }) { - PortalView(portal: Self.egymNfcPortal) + PortalView(portal: nfcPortal()) .onAppear { dismissListener?.cancel() dismissListener = Task { diff --git a/egym-ios-mwa-reference/JWTGenerator.swift b/egym-ios-mwa-reference/JWTGenerator.swift new file mode 100644 index 0000000..8105abd --- /dev/null +++ b/egym-ios-mwa-reference/JWTGenerator.swift @@ -0,0 +1,150 @@ +import Foundation +import Security + +struct JWTGenerator { + enum JWTError: Error { + case invalidKey + case signingFailed + } + + /// Generate an RS256 JWT with `sub` = memberId, valid for 24 hours. + static func generateMembershipJWT(memberId: String, privateKeyPEM: String) throws -> String { + let privateKey = try loadPrivateKey(from: privateKeyPEM) + + let header = ["alg": "RS256", "typ": "JWT"] + let now = Date() + let payload: [String: Any] = [ + "sub": memberId, + "iat": Int(now.timeIntervalSince1970), + "exp": Int(now.addingTimeInterval(86400).timeIntervalSince1970) + ] + + let headerData = try JSONSerialization.data(withJSONObject: header) + let payloadData = try JSONSerialization.data(withJSONObject: payload) + + let headerB64 = base64URLEncode(headerData) + let payloadB64 = base64URLEncode(payloadData) + + let signingInput = "\(headerB64).\(payloadB64)" + guard let signingData = signingInput.data(using: .utf8) else { + throw JWTError.signingFailed + } + + let signature = try sign(data: signingData, with: privateKey) + let signatureB64 = base64URLEncode(signature) + + return "\(headerB64).\(payloadB64).\(signatureB64)" + } + + private static func loadPrivateKey(from pem: String) throws -> SecKey { + let stripped = pem + .replacingOccurrences(of: "-----BEGIN PRIVATE KEY-----", with: "") + .replacingOccurrences(of: "-----END PRIVATE KEY-----", with: "") + .replacingOccurrences(of: "-----BEGIN RSA PRIVATE KEY-----", with: "") + .replacingOccurrences(of: "-----END RSA PRIVATE KEY-----", with: "") + .replacingOccurrences(of: "\n", with: "") + .replacingOccurrences(of: "\r", with: "") + .trimmingCharacters(in: .whitespaces) + + guard var keyData = Data(base64Encoded: stripped) else { + throw JWTError.invalidKey + } + + // If the key is PKCS#8 wrapped (BEGIN PRIVATE KEY), strip the ASN.1 header + // to get the raw PKCS#1 RSA key that SecKeyCreateWithData expects. + if pem.contains("BEGIN PRIVATE KEY") { + keyData = try stripPKCS8Header(keyData) + } + + let attributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, + ] + + var error: Unmanaged? + guard let secKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error) else { + if let err = error?.takeRetainedValue() { + print("[JWTGenerator] SecKey creation failed: \(err)") + } + throw JWTError.invalidKey + } + return secKey + } + + /// Strip the PKCS#8 ASN.1 header to extract the inner PKCS#1 RSA private key. + /// PKCS#8 structure: SEQUENCE { INTEGER(version), SEQUENCE { OID, NULL }, OCTET STRING { PKCS#1 key } } + private static func stripPKCS8Header(_ keyData: Data) throws -> Data { + var index = 0 + let bytes = [UInt8](keyData) + + guard bytes.count > 0, bytes[index] == 0x30 else { throw JWTError.invalidKey } + index += 1 + + // Skip the outer SEQUENCE length + index = skipASN1Length(bytes, index: index) + + // Expect INTEGER (version = 0) + guard bytes[index] == 0x02 else { throw JWTError.invalidKey } + index += 1 + let versionLength = Int(bytes[index]) + index += 1 + versionLength + + // Expect SEQUENCE (AlgorithmIdentifier) + guard bytes[index] == 0x30 else { throw JWTError.invalidKey } + index += 1 + let algIdLength = readASN1Length(bytes, index: &index) + index += algIdLength + + // Expect OCTET STRING containing the PKCS#1 key + guard bytes[index] == 0x04 else { throw JWTError.invalidKey } + index += 1 + _ = readASN1Length(bytes, index: &index) + + // The rest is the PKCS#1 RSA private key + return Data(bytes[index...]) + } + + private static func skipASN1Length(_ bytes: [UInt8], index: Int) -> Int { + var idx = index + _ = readASN1Length(bytes, index: &idx) + return idx + } + + private static func readASN1Length(_ bytes: [UInt8], index: inout Int) -> Int { + guard index < bytes.count else { return 0 } + if bytes[index] < 0x80 { + let length = Int(bytes[index]) + index += 1 + return length + } else { + let numBytes = Int(bytes[index] & 0x7F) + index += 1 + var length = 0 + for _ in 0.. Data { + var error: Unmanaged? + guard let signature = SecKeyCreateSignature( + key, + .rsaSignatureMessagePKCS1v15SHA256, + data as CFData, + &error + ) as Data? else { + throw JWTError.signingFailed + } + return signature + } + + private static func base64URLEncode(_ data: Data) -> String { + data.base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } +}