-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
5049 lines (4331 loc) · 163 KB
/
app.js
File metadata and controls
5049 lines (4331 loc) · 163 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import posthog from "posthog-js";
// --- CONFIGURATION ---
const IS_DEV = import.meta.env.DEV
// --- ANALYTICS ---
const POSTHOG_KEY = import.meta.env.VITE_PUBLIC_POSTHOG_KEY;
const POSTHOG_HOST = import.meta.env.VITE_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com';
if (POSTHOG_KEY) {
posthog.init(POSTHOG_KEY, {
api_host: POSTHOG_HOST,
person_profiles: 'identified_only'
});
}
function trackEvent(eventName, properties = {}) {
if (POSTHOG_KEY) {
try {
posthog.capture(eventName, properties);
} catch(e) {
console.error("PostHog tracking failed", e);
}
}
}
// --- AUTH / SUBSCRIPTION CONFIG ---
const BUILDSHIP_BASE_URL = 'https://4tgke4.buildship.run'
const STRIPE_PUBLISHABLE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_live_51R8y3MKszA2slvDX8402H2tJtQkNanGCSeAz8YA5hZ8mmiwAR9ztvhGHvzh2KX1KMZt4vvt6wlh1MUtw8C9kbpkJ00NFSVe4GL'
const STRIPE_PRICE_IDS = {
professional: 'price_1T2ldCKszA2slvDXatdeCpbI',
power: 'price_1T2le9KszA2slvDXR4mPvw7M'
}
const proGateAttachedSet = new WeakSet()
let authState = {
email: null,
sessionToken: null,
isVerified: false,
}
let subscriptionState = {
tier: 'free',
status: 'none',
periodEnd: null,
}
// --- PIPELINE ---
const PIPELINE_ENDPOINT = `${BUILDSHIP_BASE_URL}/service/runpipeline`
// --- IDENTITY RESOLUTION ---
const IDENTITY_COOKIE_KEY = 'bs_identity'
const IDENTITY_SESSION_KEY = 'bs_user_id'
const IDENTITY_ENDPOINT = `${BUILDSHIP_BASE_URL}/authUserCheck`
let identityState = {
userId: null,
status: null, // 'recognized' | 'new' | null
resolved: false,
}
// --- TIER LIMITS ---
const TIER_LIMITS = {
free: 2,
professional: 50,
power: 2000,
}
const FREE_MODEL = 'google/gemini-3.1-pro-preview'
const PRO_MODELS = [
'anthropic/claude-4.6-opus',
'openai/gpt-5.3-codex',
'openrouter/auto',
'openrouter/free',
]
const USAGE_STORAGE_KEY = 'ccc_usage'
// Model Configuration
const PROMPT_ARCHITECT_MODEL = "google/gemini-3.1-pro-preview"
const CODE_REVIEW_MODEL = "google/gemini-3.1-pro-preview"
const FALLBACK_MODEL = "google/gemini-3.1-pro-preview"
// --- DYNAMIC PRICING ---
const BASE_PRICES_AUD = { professional: 11, power: 49 }
const LOCALE_CURRENCY_MAP = {
en_US: 'USD', en_GB: 'GBP', en_AU: 'AUD', en_NZ: 'NZD', en_CA: 'CAD',
en_IN: 'INR', en_SG: 'SGD', en_HK: 'HKD', en_PH: 'PHP', en_ZA: 'ZAR',
en: 'USD',
de: 'EUR', fr: 'EUR', es: 'EUR', it: 'EUR', nl: 'EUR', pt_PT: 'EUR',
pt_BR: 'BRL', pt: 'BRL',
ja: 'JPY', ko: 'KRW', zh_CN: 'CNY', zh_TW: 'TWD', zh: 'CNY',
th: 'THB', vi: 'VND', id: 'IDR', ms_MY: 'MYR', ms: 'MYR',
sv: 'SEK', nb: 'NOK', da: 'DKK', pl: 'PLN', cs: 'CZK',
hu: 'HUF', ro: 'RON', tr: 'TRY',
ar: 'AED', he: 'ILS', ru: 'RUB', uk: 'UAH',
}
const AUD_EXCHANGE_RATES_FALLBACK = {
AUD: 1, USD: 0.65, EUR: 0.60, GBP: 0.52, CAD: 0.88,
NZD: 1.08, JPY: 97, KRW: 870, INR: 54, SGD: 0.87,
HKD: 5.08, BRL: 3.18, CNY: 4.70, TWD: 20.5, THB: 22.5,
VND: 16200, IDR: 10200, MYR: 2.88, SEK: 6.80, NOK: 6.95,
DKK: 4.48, PLN: 2.60, CZK: 15.2, HUF: 238, RON: 2.98,
TRY: 20.9, AED: 2.39, ILS: 2.38, PHP: 36.4, ZAR: 11.8,
RUB: 58, UAH: 26.8, CHF: 0.57, MXN: 11.1, ARS: 580,
CLP: 610, COP: 2700, PEN: 2.44,
}
let AUD_EXCHANGE_RATES = { ...AUD_EXCHANGE_RATES_FALLBACK }
async function fetchAudExchangeRates() {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
try {
const res = await fetch('https://open.er-api.com/v6/latest/AUD', { signal: controller.signal })
if (!res.ok) return
const data = await res.json()
if (data.result !== 'success' || !data.rates) return
const rates = data.rates
const updated = { AUD: 1 }
const allCurrencies = new Set([
...Object.keys(AUD_EXCHANGE_RATES_FALLBACK),
...Object.values(TIMEZONE_CURRENCY_MAP),
...Object.values(LOCALE_CURRENCY_MAP),
])
for (const code of allCurrencies) {
if (code === 'AUD') continue
if (typeof rates[code] === 'number' && rates[code] > 0) {
updated[code] = rates[code]
} else if (AUD_EXCHANGE_RATES_FALLBACK[code]) {
updated[code] = AUD_EXCHANGE_RATES_FALLBACK[code]
}
}
AUD_EXCHANGE_RATES = updated
} catch {
} finally {
clearTimeout(timeout)
}
}
const TIMEZONE_CURRENCY_MAP = {
'America/Sao_Paulo': 'BRL', 'America/Fortaleza': 'BRL', 'America/Recife': 'BRL',
'America/Bahia': 'BRL', 'America/Belem': 'BRL', 'America/Manaus': 'BRL',
'America/Cuiaba': 'BRL', 'America/Campo_Grande': 'BRL', 'America/Araguaina': 'BRL',
'America/Noronha': 'BRL', 'America/Rio_Branco': 'BRL', 'America/Porto_Velho': 'BRL',
'America/Boa_Vista': 'BRL', 'America/Maceio': 'BRL', 'America/Santarem': 'BRL',
'America/Eirunepe': 'BRL',
'Europe/London': 'GBP', 'Europe/Paris': 'EUR', 'Europe/Berlin': 'EUR',
'Europe/Madrid': 'EUR', 'Europe/Rome': 'EUR', 'Europe/Amsterdam': 'EUR',
'Europe/Brussels': 'EUR', 'Europe/Vienna': 'EUR', 'Europe/Lisbon': 'EUR',
'Europe/Dublin': 'EUR', 'Europe/Helsinki': 'EUR', 'Europe/Athens': 'EUR',
'Europe/Bucharest': 'RON', 'Europe/Budapest': 'HUF', 'Europe/Warsaw': 'PLN',
'Europe/Prague': 'CZK', 'Europe/Copenhagen': 'DKK', 'Europe/Stockholm': 'SEK',
'Europe/Oslo': 'NOK', 'Europe/Zurich': 'CHF', 'Europe/Istanbul': 'TRY',
'Europe/Moscow': 'RUB', 'Europe/Kiev': 'UAH', 'Europe/Kyiv': 'UAH',
'Asia/Tokyo': 'JPY', 'Asia/Seoul': 'KRW', 'Asia/Shanghai': 'CNY',
'Asia/Taipei': 'TWD', 'Asia/Hong_Kong': 'HKD', 'Asia/Singapore': 'SGD',
'Asia/Kolkata': 'INR', 'Asia/Calcutta': 'INR', 'Asia/Bangkok': 'THB',
'Asia/Ho_Chi_Minh': 'VND', 'Asia/Jakarta': 'IDR', 'Asia/Kuala_Lumpur': 'MYR',
'Asia/Dubai': 'AED', 'Asia/Jerusalem': 'ILS', 'Asia/Tel_Aviv': 'ILS',
'Asia/Manila': 'PHP',
'Pacific/Auckland': 'NZD',
'Australia/Sydney': 'AUD', 'Australia/Melbourne': 'AUD', 'Australia/Brisbane': 'AUD',
'Australia/Perth': 'AUD', 'Australia/Adelaide': 'AUD', 'Australia/Hobart': 'AUD',
'Australia/Darwin': 'AUD', 'Australia/Lord_Howe': 'AUD',
'America/Toronto': 'CAD', 'America/Vancouver': 'CAD', 'America/Edmonton': 'CAD',
'America/Winnipeg': 'CAD', 'America/Halifax': 'CAD', 'America/St_Johns': 'CAD',
'America/Regina': 'CAD',
'America/New_York': 'USD', 'America/Chicago': 'USD', 'America/Denver': 'USD',
'America/Los_Angeles': 'USD', 'America/Phoenix': 'USD', 'America/Anchorage': 'USD',
'Pacific/Honolulu': 'USD',
'America/Mexico_City': 'MXN', 'America/Cancun': 'MXN', 'America/Tijuana': 'MXN',
'America/Argentina/Buenos_Aires': 'ARS',
'America/Santiago': 'CLP', 'America/Bogota': 'COP', 'America/Lima': 'PEN',
'Africa/Johannesburg': 'ZAR',
}
function detectUserCurrency() {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
if (tz && TIMEZONE_CURRENCY_MAP[tz]) return TIMEZONE_CURRENCY_MAP[tz]
} catch {}
const locale = navigator.language || 'en-US'
const normalized = locale.replace('-', '_')
const exactMatch = LOCALE_CURRENCY_MAP[normalized]
if (exactMatch) return exactMatch
const langOnly = normalized.split('_')[0]
const langMatch = LOCALE_CURRENCY_MAP[langOnly]
if (langMatch) return langMatch
return 'USD'
}
function formatPrice(audAmount, currency) {
const rate = AUD_EXCHANGE_RATES[currency] ?? AUD_EXCHANGE_RATES.USD
const converted = audAmount * rate
const rounded = Math.round(converted * 100) / 100
try {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
minimumFractionDigits: 0,
maximumFractionDigits: rounded >= 100 ? 0 : rounded % 1 === 0 ? 0 : 2,
}).format(rounded)
} catch {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(audAmount * AUD_EXCHANGE_RATES.USD)
}
}
// --- SHARED FLUTTERFLOW CONSTRAINTS TEMPLATE ---
// These constraints are shared across all three pipeline agents to ensure consistency.
// Based on "The Definitive Guide to Integrating Dart Artifacts into FlutterFlow Environments"
const FF_CORE_PHILOSOPHY = `## THE FLUTTERFLOW INTEGRATION PHILOSOPHY
**FlutterFlow is the host organism.** Your Dart must conform to FlutterFlow's boilerplate, parsing rules, and parameter system - not the other way around.
Key principles:
1. **Settings and code must match.** FlutterFlow binds custom code by name/signature. If the UI says the widget/action is \`NeuroRadialGauge\`, your Dart must export that exact class/function name. Name mismatches are a top cause of "mysterious" breakage.
2. **Headers are automatic.** The boilerplate header (with imports) is added automatically at commit time - generated code should be clean (class/function only).
3. **You are responsible for dependencies.** FlutterFlow won't auto-add pubspec packages. If the code imports it, you must add it in Project Dependencies (and sometimes native config).
4. **The Parser Gap is real.** FlutterFlow parses custom code to power the UI (parameter panels, variable pickers). That parser is stricter than Dart itself - valid Dart can still be "invalid" to FlutterFlow.`;
const FF_ARTIFACT_TYPES = `## THE FOUR ARTIFACT SURFACES
### A) Custom Functions (Pure/Sync Logic Silo)
- **Purpose:** Synchronous data manipulation, math calculations, string formatting
- **CRITICAL RESTRICTION:** NO external imports allowed. Stored in \`/lib/flutter_flow/custom_functions.dart\`.
- **Allowed Imports:** Only predefined imports (dart:convert, dart:math, package:flutter/material.dart, google_fonts, intl, timeago, cloud_firestore, etc). NO custom package imports.
- **Returns:** Synchronous value only (String, int, double, bool, List, Map) - NOT Future.
- **Use when:** Pure computation, no side effects, no async.
### B) Custom Actions (Async/Side Effects Silo)
- **Purpose:** API calls, complex logic, third-party libraries
- **Return type:** ALWAYS Future<T>
- **Imports:**
- External packages: include (e.g., \`import 'package:flutter_tts/flutter_tts.dart';\`)
- FlutterFlow imports: DO NOT include - added at commit
- **Use when:** Async operations, external packages.
### C) Custom Widgets (Visual/UI Silo)
- **Purpose:** Custom UI components
- **Imports:**
- External packages: include (e.g., \`import 'package:percent_indicator/percent_indicator.dart';\`)
- FlutterFlow imports: DO NOT include - added at commit
- **Parameters:** Must accept nullable \`width\` and \`height\`.
- **Use when:** Custom UI not in standard library.
### D) Code Files (Classes/Enums/Utilities) - NEW FEATURE
- **Purpose:** Reusable models, enums, utility classes.
- **Location:** \`lib/custom_code/\` (not synced unless in widgets/actions, but managed via UI).
- **Capabilities:** Create custom data types, use properties in UI.
- **Limitations:** No generics, no function-typed fields. Must re-parse in FF after changes.`;
const FF_TYPE_SYSTEM = `## FLUTTERFLOW TYPE SYSTEM (Parameters & State)
### Custom Code Parameter Types
Only these parameter types work in FlutterFlow's Custom Code UI. **ALWAYS Use Simple Types.**
- **Primitives:** String, bool, int, double, Color (nullable), DateTime
- **Lists:** List<String>, List<int>, List<double>, List<bool>, List<ProductStruct>
- **FlutterFlow Structs:** \`SomeNameStruct\` (UpperCamelCase, must exist in FF Data Types)
- **Special types:** DocumentReference, LatLng, FFPlace, FFUploadedFile, Uint8List (Bytes), dynamic (JSON)
- **Action callbacks (widget→FF, data OUT — PREFERRED data return pattern):** Use \`Future Function(ParamType paramName, ...)?\` to pass data from the widget/action back to FlutterFlow. This is the **primary** way to surface data from custom code. Parameters MUST be standard FlutterFlow data types and MUST have names. Example:
\`\`\`dart
final Future Function(
FFUploadedFile? bytes, dynamic jsonObject, String? string)?
onValueChanged;
\`\`\`
Supported param types: \`String\`, \`int\`, \`double\`, \`bool\`, \`Color\`, \`DateTime\`, \`LatLng\`, \`FFPlace\`, \`FFUploadedFile\`, \`dynamic\` (JSON), \`DocumentReference\`, FlutterFlow Structs.
- **Action callbacks (FF→widget, data IN):** Same syntax — \`Future<dynamic> Function(String value)?\` etc. FlutterFlow passes a value from an Action Flow into the widget callback. Same type rules apply.
- **⛔ CRITICAL: Named Callback Parameters Required:** All callback parameters MUST have a name (both directions). FlutterFlow's parser rejects anonymous parameters.
**❌ WRONG — Missing parameter name:**
\`\`\`dart
final Future<dynamic> Function(String)? onDrawingComplete; // ❌ Parser error
\`\`\`
**✅ CORRECT — Parameter has a name:**
\`\`\`dart
final Future<dynamic> Function(String drawing)? onDrawingComplete; // ✅ Works
\`\`\`
All callback parameters must be named: \`Function(String value)\`, \`Function(int index)\`, \`Function(bool isValid)\`, etc.
- **Widget Builder:** \`Widget Function(BuildContext)\`
**FORBIDDEN COMPLEX TYPES:**
- ❌ EdgeInsets (use individual doubles: paddingLeft, paddingRight...)
- ❌ Duration (use int milliseconds)
- ❌ TextStyle (break into properties)
### App State Variable Types (CRITICAL — different from parameter types)
App State variables (global, persistent across pages) support ONLY:
- Integer, Double, String, Boolean, Color
- ImagePath, VideoPath, AudioPath
- DocumentReference, DateTime, JSON, LatLng
- Data Type (FF Structs), Enum, CustomClass, CustomEnum
- Lists of any of the above
**App State does NOT support Bytes/Uint8List/FFUploadedFile.** This is a common pitfall — code that stores raw byte data (image bytes, file bytes, signature data) directly in FFAppState will fail to compile.
### Page State Variable Types
Page State variables (local to a single page) support everything App State does, PLUS:
- ✅ Bytes (Uint8List) — available ONLY in Page State, not App State
### Implications for Code Generation
- When the user needs to store byte data: use a callback to pass bytes back to FlutterFlow (user can store in Page State), or convert to base64 String for App State storage, or upload to storage and store the resulting URL as ImagePath.
- NEVER generate code that writes FFUploadedFile or Uint8List to FFAppState — it will not compile.
**IMPORTANT:** Custom Dart classes for data exchange are now allowed via "Code Files", but Structs are still preferred for parameters visible in the UI builder.`;
const FF_STATE_PATTERNS = `## STATE & DATA: FFAppState Patterns
FlutterFlow's generated \`FFAppState\` is a **global singleton that extends ChangeNotifier**.
**CRITICAL WARNING FOR CODE GENERATION:** The variable names below (myVar, localValue) are EXAMPLES ONLY. Generated code must NEVER assume any specific FFAppState variables exist in the user's project. If a variable is referenced, it MUST be documented as a required user action ("Create App State variable X of type Y in FlutterFlow").
### App State vs Page State: Allowed Types
**App State variables** (global, persist across pages) support ONLY these types:
- Integer, Double, String, Boolean, Color
- ImagePath, VideoPath, AudioPath
- DocumentReference, DateTime, JSON, LatLng
- Data Type (custom FF Structs), Enum
- CustomClass, CustomEnum
- Lists of any of the above
**App State does NOT support:**
- ❌ Bytes / Uint8List / FFUploadedFile — these CANNOT be stored in App State
- ❌ Arbitrary Dart objects or custom classes not registered as FF Data Types
**Page State variables** (local to a single page) support everything App State does, PLUS:
- ✅ Bytes (Uint8List) — available in Page State only
**CRITICAL IMPLICATION:** Code that tries to store raw byte data (e.g., image bytes, file bytes, signature PNG data) in FFAppState will fail. For byte data:
1. Use a callback parameter to pass bytes back to FlutterFlow, and let the user store it in Page State or upload it.
2. If persistence across pages is needed, convert bytes to a base64 String and store that in App State instead.
3. Alternatively, upload the bytes to storage and store the resulting URL (ImagePath) in App State.
### Reading state (non-reactive):
\`\`\`dart
final v = FFAppState().myVar; // 'myVar' must exist in the user's FF project
\`\`\`
### Writing state (reactive across app):
\`\`\`dart
FFAppState().update(() => FFAppState().myVar = newValue);
\`\`\`
This triggers \`notifyListeners()\` and updates all subscribed pages.
### Returning values from Custom Widgets:
FlutterFlow doesn't directly "pull" values out of widgets. Two patterns (in order of preference):
1. **Callbacks (PREFERRED):** Use action callback parameters with standard FF data types — lets the user wire the data flow in FlutterFlow UI without needing specific app state variables. Callbacks can carry data directly as named parameters: \`Future Function(FFUploadedFile? bytes, dynamic jsonObject, String? result)? onValueChanged\`. This is the primary mechanism for surfacing data from custom widgets/actions.
2. **AppState workaround (LAST RESORT):** Store result in FFAppState when callback typing is fragile. NOTE: The variable MUST already exist in the user's project, and the value MUST be one of the allowed App State types listed above:
\`\`\`dart
// REQUIRED: Create App State variable 'localValue' (String) in FlutterFlow first
FFAppState().update(() {
FFAppState().localValue = 'setvalue';
});
\`\`\``;
const FF_FORBIDDEN_PATTERNS = `## FORBIDDEN PATTERNS (Will cause build failures)
- \`void main()\` or \`main()\` function
- \`runApp()\` call
- \`MaterialApp\` or \`Scaffold\` (except rarely)
- Modifying the mandatory header comments or imports ABOVE the "DO NOT REMOVE" line.
- Importing packages without adding them to Project Dependencies (in UI).
- Adding custom imports to Custom Functions (strictly forbidden).
- Using complex parameter types (EdgeInsets, Duration, TextStyle) in Widgets/Actions.
- Using generics or function-typed fields in Code Files.
### ⛔ CRITICAL: RESERVED PARAMETER NAMES (INSTANT COMPILATION FAILURE)
**NEVER name a widget parameter \`key\`. This is THE #1 CAUSE of mysterious build failures.**
**Why it breaks:**
- Flutter widgets inherit a \`Key? key\` property from \`Widget.key\`
- FlutterFlow auto-injects \`super.key\` in widget constructors
- Adding \`this.key\` creates TWO parameters named \`key\` → "Duplicated parameter name" error
- Your custom \`String? key\` conflicts with Flutter's \`Key? key\` → type mismatch error
**WRONG — WILL NOT COMPILE:**
\`\`\`dart
class KeyboardHintWidget extends StatelessWidget {
final String? key; // ❌ CONFLICTS with Widget.key
final String? label;
const KeyboardHintWidget({super.key, this.key, this.label}); // ❌ DUPLICATED
}
\`\`\`
**CORRECT — RENAME THE PARAMETER:**
\`\`\`dart
class KeyboardHintWidget extends StatelessWidget {
final String? keyLabel; // ✅ Renamed from 'key' to 'keyLabel'
final String? label;
const KeyboardHintWidget({super.key, this.keyLabel, this.label}); // ✅ Works
}
\`\`\`
**Alternative names for \`key\` parameter:**
- \`keyLabel\`, \`keyValue\`, \`keyText\`, \`keyName\`, \`keyChar\`, \`keyCode\`
- \`apiKey\`, \`dictKey\`, \`mapKey\`, \`cacheKey\`, \`storageKey\`
- Or describe the purpose: \`buttonLabel\`, \`shortcutKey\`, \`accessKey\`
**⚠️ CONCEPT TRAP (most common mistake):**
When your widget's concept IS "a key" (keyboard key, API key, dictionary key, map key), you will feel tempted to name the parameter \`key\`. **DO NOT DO IT.** The semantic fit is perfect, but it will break compilation. Always rename: a \`KeyboardHintWidget\` uses \`keyLabel\` or \`keyChar\`, never \`key\`.
**Other reserved parameter names:** \`context\`, \`widget\`, \`state\`, \`mounted\`, \`setState\` — these conflict with Flutter framework internals.
### EXTERNAL PACKAGE API SAFETY
- NEVER assume mutable setters exist on controller or configuration objects from external packages.
- Package APIs change between versions — if you are not 100% certain a setter exists, do NOT use it.
- When you need to change controller properties after construction (e.g., in \`didUpdateWidget()\`): dispose the old controller and re-create it with new values. Do NOT attempt to mutate properties directly.
- Example (CORRECT):
\`\`\`dart
void didUpdateWidget(MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.penColor != oldWidget.penColor) {
_controller.dispose();
_initializeController(); // Re-create with new values
}
}
\`\`\`
- Example (WRONG — may not compile if setter doesn't exist):
\`\`\`dart
_controller.penColor = widget.penColor; // Setter may not exist!
\`\`\`
### FFAPPSTATE VARIABLE RULES
- NEVER reference specific FFAppState variable names (e.g., \`FFAppState().uploadedSignature\`, \`FFAppState().myCustomVar\`). You cannot know what variables exist in the user's project.
- Instead of writing to FFAppState directly: use callback parameters (\`Future Function()?\`) to communicate data back to FlutterFlow, letting the user wire it to their own app state in the FlutterFlow UI.
- If storing data in FFAppState is absolutely necessary for the pattern to work, you MUST:
1. Add a clear code comment: \`// REQUIRED: Create an App State variable named 'yourVarName' of type X in FlutterFlow\`
2. Document this in the output as a required user action
3. Prefer the callback pattern over direct FFAppState access whenever possible`;
const FF_REQUIRED_PATTERNS = `## REQUIRED PATTERNS (For FlutterFlow compatibility)
### Headers (MANDATORY)
- **Custom Widgets:** Must start with the widget-specific header (see Artifact Types).
- **Custom Actions:** Must start with the action-specific header (see Artifact Types).
### Null Safety
- 100% null-safe Dart. Use \`??\` or \`?.\` over \`!\`.
### Widget Parameters
- ALWAYS include nullable \`width\` and \`height\`.
- Use simple types only (e.g., \`double? padding\` instead of \`EdgeInsets?\`).
### Callbacks & Actions
- **Signature:** \`final Future Function()? onTap;\` or \`final Future Function(String)? onChanged;\`
- **Invocation:** \`await widget.onTap?.call();\` (ALWAYS await actions).
### Dependencies
- **Widgets/Actions:** Imports go BELOW the "DO NOT REMOVE" line.
- **Project Scope:** Dependencies must be added via FlutterFlow UI (Settings -> Project Dependencies).`;
const FF_INTEGRATION_GAP_TABLE = `## THE INTEGRATION GAP (What AI vs. FF Needs)
| Issue | AI Default | FlutterFlow Requirement |
|-------|------------|--------------------------|
| **Imports** | Normal imports | **MANDATORY Header** with specific imports first |
| **Params** | \`EdgeInsets\` | Individual \`double\`s (paddingTop, etc) |
| **Duration** | \`Duration\` | \`int\` (milliseconds) |
| **Callbacks** | \`VoidCallback\` | \`Future Function()\` (always await) |
| **Colors** | \`Colors.blue\` | \`FlutterFlowTheme.of(context).primary\` |
| **State** | \`State<T>\` | \`FFAppState().update(() {...})\` |`;
const FF_TROUBLESHOOTING_CHECKLIST = `## TROUBLESHOOTING CHECKLIST
1. **Header Mismatch:** Does the file start with the EXACT required boilerplate?
2. **Type Issues:** Are you using EdgeInsets, Duration, or TextStyle? (Forbidden).
3. **Imports:** Are custom imports BELOW the "DO NOT REMOVE" line?
4. **Dependencies:** Did you add packages to Project Dependencies in the UI?
5. **Actions:** Are you awaiting callbacks? (\`await widget.onTap()\`)
6. **State:** Use \`FFAppState().update()\` for reactive changes.`;
// --- NEW SECTIONS COMPLETED BASED ON RESEARCH (DEFINITIVE GUIDE) ---
const FF_PROMPT_PROTOCOL = `## THE "CLEAN ROOM" PROMPT PROTOCOL
Use this preamble for all code generation.
> "Act as a Senior Flutter Developer for FlutterFlow."
> 1. **No Header Needed:** Do NOT include import statements or boilerplate headers - these are added automatically at commit time.
> 2. **Types:** Use ONLY simple types (double, int, String, bool). NO complex Flutter types like EdgeInsets, Duration, TextStyle.
> 3. **Actions:** Callbacks must return \`Future\`. Await them.
> 4. **Theme:** Use \`FlutterFlowTheme.of(context)\`.
> 5. **Null Safety:** Strict. \`width\`/\`height\` are nullable.`;
const FF_WORKFLOW_PROTOCOL = `## TRI-SURFACE INTEGRATION WORKFLOW
### Phase 1: Extraction
1. **Isolate Core Class:** Extract only the main Widget/Action code.
2. **Identify Helpers:** Separate internal data models (convert to Structs).
3. **Capture Imports:** List all external packages (add to Project Dependencies).
### Phase 2: Injection
1. **Generate:** Code is generated WITHOUT headers (clean class/function only).
2. **Commit:** Headers are added AUTOMATICALLY when committing to FlutterFlow.
3. **Refactor Name:** Ensure \`class [WidgetName]\` matches exactly.
4. **Refactor Colors:** Use \`FlutterFlowTheme.of(context).primary\`.
5. **Refactor Logic:** Convert calls to \`Future Function()\` callbacks.`;
// Compose the full shared template
const FF_SHARED_CONSTRAINTS = `${FF_CORE_PHILOSOPHY}
---
${FF_ARTIFACT_TYPES}
---
${FF_TYPE_SYSTEM}
---
${FF_STATE_PATTERNS}
---
${FF_FORBIDDEN_PATTERNS}
---
${FF_REQUIRED_PATTERNS}
---
${FF_INTEGRATION_GAP_TABLE}
---
${FF_PROMPT_PROTOCOL}
---
${FF_WORKFLOW_PROTOCOL}
---
${FF_TROUBLESHOOTING_CHECKLIST}`;
// --- SECURE STORAGE (AES-256-GCM encryption) ---
const STORAGE_KEY_PREFIX = "ccc_api_key_";
const ENCRYPTION_KEY_NAME = "ccc_encryption_key";
// Generate or retrieve encryption key using Web Crypto API
async function getEncryptionKey() {
const storedKey = sessionStorage.getItem(ENCRYPTION_KEY_NAME);
if (storedKey) {
const keyData = JSON.parse(storedKey);
return await crypto.subtle.importKey(
"jwk",
keyData,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
}
// Generate a new key derived from a device fingerprint + random salt
const fingerprint = await generateDeviceFingerprint();
const salt = crypto.getRandomValues(new Uint8Array(16));
// Use PBKDF2 to derive a key from the fingerprint
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
encoder.encode(fingerprint),
"PBKDF2",
false,
["deriveBits", "deriveKey"],
);
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: salt,
iterations: 100000,
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
// Store the key in session storage (clears when browser closes)
const exportedKey = await crypto.subtle.exportKey("jwk", key);
sessionStorage.setItem(ENCRYPTION_KEY_NAME, JSON.stringify(exportedKey));
// Store salt in localStorage for key regeneration
localStorage.setItem(STORAGE_KEY_PREFIX + "salt", arrayBufferToBase64(salt));
return key;
}
// Generate a simple device fingerprint for key derivation
async function generateDeviceFingerprint() {
const components = [
navigator.userAgent,
navigator.language,
screen.width + "x" + screen.height,
new Date().getTimezoneOffset().toString(),
navigator.hardwareConcurrency?.toString() || "unknown",
];
const fingerprint = components.join("|");
const encoder = new TextEncoder();
const data = encoder.encode(fingerprint);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return arrayBufferToBase64(hashBuffer);
}
// Encrypt data using AES-256-GCM
async function encryptData(plaintext) {
const key = await getEncryptionKey();
const encoder = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
key,
encoder.encode(plaintext),
);
// Combine IV + encrypted data
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return arrayBufferToBase64(combined);
}
// Decrypt data using AES-256-GCM
async function decryptData(encryptedBase64) {
try {
const key = await getEncryptionKey();
const combined = base64ToArrayBuffer(encryptedBase64);
const iv = combined.slice(0, 12);
const encrypted = combined.slice(12);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: iv },
key,
encrypted,
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
} catch (error) {
console.error("Decryption failed:", error);
return null;
}
}
// Helper functions for base64 encoding/decoding
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
// --- API KEY MANAGEMENT ---
async function saveApiKey(provider, apiKey) {
if (!apiKey || apiKey.trim() === "") {
localStorage.removeItem(STORAGE_KEY_PREFIX + provider);
return;
}
const encrypted = await encryptData(apiKey.trim());
localStorage.setItem(STORAGE_KEY_PREFIX + provider, encrypted);
}
async function getApiKey(provider) {
// Only check user-stored key - no environment fallback
const encrypted = localStorage.getItem(STORAGE_KEY_PREFIX + provider);
if (encrypted) {
const decrypted = await decryptData(encrypted);
if (decrypted) return decrypted;
// If decryption failed, clean up the stale encrypted data
localStorage.removeItem(STORAGE_KEY_PREFIX + provider);
}
// Return empty string if no user key is configured
return "";
}
function getFlutterFlowEndpoint() {
return (
localStorage.getItem("flutterflow_api_endpoint") ||
FF_API_ENDPOINTS.production
);
}
function setFlutterFlowEndpoint(endpoint) {
localStorage.setItem("flutterflow_api_endpoint", endpoint);
return true;
}
function hasStoredKey(provider) {
const keys = {
flutterflow: flutterflowApiKey,
flutterflow_project_id: flutterflowProjectId,
}
return keys[provider] && keys[provider].length > 0
}
// Get current active API keys (for use in API calls)
let flutterflowApiKey = "";
let flutterflowProjectId = "";
async function initializeApiKeys() {
flutterflowApiKey = await getApiKey("flutterflow");
flutterflowProjectId = await getApiKey("flutterflow_project_id");
updateApiKeyStatusIndicators();
updateDeployButtonVisibility();
}
// --- API KEY UI FUNCTIONS ---
function openApiKeysModal() {
const modal = document.getElementById("api-keys-modal");
modal.classList.add("open");
// Load current keys into inputs (masked)
loadApiKeyInputs();
}
function closeApiKeysModal(event) {
if (event && event.target !== event.currentTarget) return;
const modal = document.getElementById("api-keys-modal");
if (modal) {
modal.classList.remove("open");
}
// Show walkthrough again after closing API keys
const walkthroughModal = document.getElementById("walkthrough-modal");
if (walkthroughModal) {
advanceWalkthrough();
walkthroughModal.classList.add("open");
}
}
let walkthroughStep = 1;
function getWalkthroughSteps() {
const container = document.querySelector('.wt-steps');
if (!container) return [];
return Array.from(container.querySelectorAll('.wt-step-card'));
}
function updateWalkthroughUI() {
const steps = getWalkthroughSteps();
if (!steps.length) return;
steps.forEach((stepEl, idx) => {
const i = idx + 1;
if (i === walkthroughStep) {
stepEl.classList.remove("opacity-60", "bg-gray-50", "border-gray-200");
stepEl.classList.add("bg-blue-50", "border-blue-200");
const numEl = stepEl.querySelector("div:first-child");
if (numEl) {
numEl.classList.remove("bg-gray-400");
numEl.classList.add("bg-blue-500");
numEl.innerHTML = i;
}
} else if (i < walkthroughStep) {
stepEl.classList.remove("opacity-60", "bg-blue-50", "border-blue-200");
stepEl.classList.add("bg-green-50", "border-green-200");
const numEl = stepEl.querySelector("div:first-child");
if (numEl) {
numEl.classList.remove("bg-blue-500", "bg-gray-400");
numEl.classList.add("bg-green-500");
numEl.innerHTML = "✓";
}
} else {
stepEl.classList.add("opacity-60", "bg-gray-50", "border-gray-200");
stepEl.classList.remove(
"bg-blue-50",
"border-blue-200",
"bg-green-50",
"border-green-200",
);
const numEl = stepEl.querySelector("div:first-child");
if (numEl) {
numEl.classList.remove("bg-blue-500", "bg-green-500");
numEl.classList.add("bg-gray-400");
numEl.innerHTML = i;
}
}
});
}
function advanceWalkthrough() {
const totalSteps = getWalkthroughSteps().length;
if (totalSteps > 0 && walkthroughStep <= totalSteps) {
walkthroughStep++;
updateWalkthroughUI();
}
}
function openWalkthroughModal() {
const modal = document.getElementById("walkthrough-modal");
if (modal) {
walkthroughStep = 1;
updateWalkthroughUI();
modal.classList.add("open");
}
}
function closeWalkthroughModal(event) {
if (event && event.target !== event.currentTarget) return;
const modal = document.getElementById("walkthrough-modal");
if (modal) {
modal.classList.remove("open");
}
const dontShow = document.getElementById("walkthrough-dont-show");
if (dontShow && dontShow.checked) {
localStorage.setItem("hasSeenWalkthrough", "true");
}
}
function showWalkthroughIfNeeded() {
const hasSeen = localStorage.getItem("hasSeenWalkthrough");
if (!hasSeen) {
const modal = document.getElementById("walkthrough-modal");
if (modal) {
walkthroughStep = 1;
updateWalkthroughUI();
modal.classList.add("open");
}
}
}
async function loadApiKeyInputs() {
const flutterflowInput = document.getElementById("flutterflow-api-key-input");
const projectIdInput = document.getElementById(
"flutterflow-project-id-input",
);
if (flutterflowApiKey) {
flutterflowInput.value = "";
flutterflowInput.placeholder = "Key saved (enter new to replace)";
} else {
flutterflowInput.placeholder = "Enter your FlutterFlow API key";
}
if (flutterflowProjectId) {
projectIdInput.value = "";
projectIdInput.placeholder = "Project ID saved (enter new to replace)";
} else {
projectIdInput.placeholder = "Enter your FlutterFlow Project ID";
}
updateModalKeyStatuses();
}
function updateModalKeyStatuses() {
updateKeyStatus("flutterflow", "flutterflow-key-status");
updateKeyStatus("flutterflow_project_id", "flutterflow-project-status");
}
function updateKeyStatus(provider, statusElementId) {
const statusEl = document.getElementById(statusElementId);
if (!statusEl) return;
const dot = statusEl.querySelector(".key-status-dot");
const text = statusEl.querySelector("span");
if (hasStoredKey(provider)) {
dot.className = "key-status-dot configured";
text.className = "text-green-600";
text.textContent = "User key configured";
} else {
dot.className = "key-status-dot missing";
text.className = "text-gray-500";
text.textContent = "Not configured";
}
}
function updateDeployButtonVisibility() {
const flutterFlowConfigured =
hasStoredKey("flutterflow") && hasStoredKey("flutterflow_project_id");
const hasGeneratedCode =
pipelineState.step2Result && pipelineState.step2Result.length > 0;
const deployBtn = document.getElementById("btn-deploy-to-ff");
const runBtn = document.getElementById("btn-run-pipeline");
if (flutterFlowConfigured && hasGeneratedCode) {
if (deployBtn) deployBtn.classList.remove("hidden");
if (runBtn) runBtn.classList.add("hidden");
} else {
if (deployBtn) deployBtn.classList.add("hidden");
if (runBtn) runBtn.classList.remove("hidden");
}
}
function updateApiKeyStatusIndicators() {
const container = document.getElementById("api-keys-status");
if (!container) return;
const dots = container.querySelectorAll(".key-status-dot");
const providers = [
"flutterflow",
];
dots.forEach((dot, index) => {
const provider = providers[index];
if (provider === "flutterflow") {
// For FlutterFlow, check both API key and Project ID
if (
hasStoredKey("flutterflow") &&
hasStoredKey("flutterflow_project_id")
) {
dot.className = "key-status-dot configured";
dot.title = "FlutterFlow (Fully configured)";
} else if (
hasStoredKey("flutterflow") ||
hasStoredKey("flutterflow_project_id")
) {
dot.className = "key-status-dot env";
dot.title = "FlutterFlow (Partially configured)";
} else {
dot.className = "key-status-dot missing";
dot.title = "FlutterFlow (Not configured)";
}
} else if (hasStoredKey(provider)) {
dot.className = "key-status-dot configured";
dot.title =
provider.charAt(0).toUpperCase() + provider.slice(1) + " (User key)";
} else {
dot.className = "key-status-dot missing";
dot.title =
provider.charAt(0).toUpperCase() +
provider.slice(1) +
" (Not configured)";
}
});
// Toggle deploy button visibility
updateDeployButtonVisibility();
}
async function saveApiKeys() {
const flutterflowInput = document.getElementById("flutterflow-api-key-input");
const projectIdInput = document.getElementById(
"flutterflow-project-id-input",
);
// Only save if user entered a new value
if (flutterflowInput.value.trim()) {
await saveApiKey("flutterflow", flutterflowInput.value);
}
if (projectIdInput.value.trim()) {
if (!validateFlutterFlowProjectId(projectIdInput.value)) {
showToast("Invalid FlutterFlow Project ID. Must be at least 5 characters (letters, numbers, dashes).", "error");
projectIdInput.focus();
return;
}
await saveApiKey("flutterflow_project_id", projectIdInput.value);
}
// Reinitialize keys
await initializeApiKeys();
// Update UI
loadApiKeyInputs();
// Show confirmation
const btn = document.querySelector("#api-keys-modal .bg-blue-500");
const originalText = btn.textContent;
btn.textContent = "Saved!";
btn.classList.remove("bg-blue-500", "hover:bg-blue-600");
btn.classList.add("bg-green-500");
setTimeout(() => {